From d23d25c6b77e01e985f1b5624f6198fce2c10599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Wed, 30 Apr 2025 21:03:17 +0200 Subject: [PATCH 0001/1175] Add units of measurement for Home Connect counter entities (#143982) --- .../components/home_connect/strings.json | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index ca79ec56ee4..19d7cc06046 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1551,31 +1551,39 @@ } }, "coffee_counter": { - "name": "Coffees" + "name": "Coffees", + "unit_of_measurement": "coffees" }, "powder_coffee_counter": { - "name": "Powder coffees" + "name": "Powder coffees", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]" }, "hot_water_counter": { "name": "Hot water" }, "hot_water_cups_counter": { - "name": "Hot water cups" + "name": "Hot water cups", + "unit_of_measurement": "cups" }, "hot_milk_counter": { - "name": "Hot milk cups" + "name": "Hot milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "frothy_milk_counter": { - "name": "Frothy milk cups" + "name": "Frothy milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "milk_counter": { - "name": "Milk cups" + "name": "Milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "coffee_and_milk_counter": { - "name": "Coffee and milk cups" + "name": "Coffee and milk cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "ristretto_espresso_counter": { - "name": "Ristretto espresso cups" + "name": "Ristretto espresso cups", + "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, "battery_level": { "name": "Battery level" From ad0209a4a0aa29efc1fead5a058ce0d8c34d98d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Apr 2025 21:44:19 +0200 Subject: [PATCH 0002/1175] Bump version to 2025.6.0dev0 (#143983) --- .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 e10bc607258..656b75eb054 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ env: CACHE_VERSION: 12 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 9 - HA_SHORT_VERSION: "2025.5" + HA_SHORT_VERSION: "2025.6" DEFAULT_PYTHON: "3.13" ALL_PYTHON_VERSIONS: "['3.13']" # 10.3 is the oldest supported version diff --git a/homeassistant/const.py b/homeassistant/const.py index b73aed1b8b9..f0615e7415b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 -MINOR_VERSION: Final = 5 +MINOR_VERSION: Final = 6 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 5fbf00bae8a..fcfe8e3448d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.5.0.dev0" +version = "2025.6.0.dev0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From b92f718e082c82db369ff5eea56ee8d039272a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Wed, 30 Apr 2025 22:09:29 +0200 Subject: [PATCH 0003/1175] Matter Cooktop fixture (#143984) --- tests/components/matter/conftest.py | 1 + .../matter/fixtures/nodes/cooktop.json | 308 ++++++++++++++++++ .../matter/snapshots/test_select.ambr | 58 ++++ .../matter/snapshots/test_sensor.ambr | 52 +++ .../matter/snapshots/test_switch.ambr | 96 ++++++ 5 files changed, 515 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/cooktop.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index e180b9e9363..04aeba4546f 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -76,6 +76,7 @@ async def integration_fixture( "air_purifier", "air_quality_sensor", "color_temperature_light", + "cooktop", "dimmable_light", "dimmable_plugin_unit", "door_lock", diff --git a/tests/components/matter/fixtures/nodes/cooktop.json b/tests/components/matter/fixtures/nodes/cooktop.json new file mode 100644 index 00000000000..f32322b6cb7 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/cooktop.json @@ -0,0 +1,308 @@ +{ + "node_id": 3, + "date_commissioned": "2025-04-29T15:54:11.963738", + "last_interview": "2025-04-29T15:54:11.963750", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 3 + } + ], + "0/29/1": [29, 31, 40, 43, 45, 48, 49, 51, 54, 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, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 1, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "Mock", + "0/40/2": 65521, + "0/40/3": "Mock Cooktop", + "0/40/4": 32768, + "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/17": true, + "0/40/18": "8854D258EF79CBAE", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 4, + "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, 17, 18, 19, 21, + 22, 24, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/45/0": 0, + "0/45/1": [0, 1, 2], + "0/45/65532": 1, + "0/45/65533": 2, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 2, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkIN/v6b", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZAP/YMcX0yMLQ==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 23, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65532, 65533, 65528, 65529, 65531 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": 3, + "0/54/3": null, + "0/54/4": null, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65532, 65533, 65528, 65529, + 65531 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 1, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRAxgkBwEkCAEwCUEE1B4lA2AYRzpeBC9EizUv1FilsHNIEbFdH0c0o1NCiMMsdkxMJ/MnyXholb/76NUBLrq0tFMXYMa8TjIcHh915zcKNQEoARgkAgE2AwQCBAEYMAQUgfoxJi2HOriuKa6K2cbtp49/SYIwBRRqGquZZYwbDAaOinVVrS9sWTozoBgwC0DCxbisQiHwqDX9s2aGsCUz+6/8evG3EOMGOU0tG1DuXY4kd5TTxmIAjk51GwIszElOMBsfQV5ZAB1KbSKgaUrwGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyvr+z4yBxEDoiyCFg+i408LqC3j0UMvTszBv1051g2EMrAzBkj+0RZFsSl3eQ3D2c7mTcH6GERtlk4BqGvC1qDcKNQEpARgkAmAwBBRqGquZZYwbDAaOinVVrS9sWTozoDAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQCIuoikQZU9LkDKw7dcTVVXBDlTyBol3w070PIIw8BbaQD5qCeIv/3cI5/X5sAYTmemRq0ZPMjAw1dsN+wodzm8Y", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BIPshBqc9a7nNK00eRrviEzHfe/cfATY9VngqKv17+uAUpy3XujhZBjkAQyhYAaSKxVzSfVttY4FVQkpXIHZFlA=", + "2": 4939, + "3": 2, + "4": 3, + "5": "Maison", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEg+yEGpz1ruc0rTR5Gu+ITMd979x8BNj1WeCoq/Xv64BSnLde6OFkGOQBDKFgBpIrFXNJ9W21jgVVCSlcgdkWUDcKNQEpARgkAmAwBBRPkvAMbwLEubfgETM7L7icezGlHzAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQIKyooBXllxj1uo4Zn4CBbZqECNdO3wwzlhl7ZEygrWa04gBa5rVqgg+JahrvXD6HPHu4XldWIULtqTCPPIm4OsY" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 5, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "1/6/0": true, + "1/6/65532": 4, + "1/6/65533": 6, + "1/6/65528": [], + "1/6/65529": [0], + "1/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 120, + "1": 1 + } + ], + "1/29/1": [3, 6, 29], + "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, 65532, 65533, 65528, 65529, 65531], + "2/6/0": true, + "2/6/65532": 4, + "2/6/65533": 6, + "2/6/65528": [], + "2/6/65529": [0], + "2/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "2/29/0": [ + { + "0": 119, + "1": 1 + } + ], + "2/29/1": [6, 29, 86, 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, 65532, 65533, 65528, 65529, 65531], + "2/86/4": 1, + "2/86/5": ["Low", "Medium", "High"], + "2/86/65532": 2, + "2/86/65533": 1, + "2/86/65528": [], + "2/86/65529": [0], + "2/86/65531": [4, 5, 65532, 65533, 65528, 65529, 65531], + "2/1026/0": 18000, + "2/1026/1": null, + "2/1026/2": null, + "2/1026/65532": 0, + "2/1026/65533": 4, + "2/1026/65528": [], + "2/1026/65529": [], + "2/1026/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 5222dda1ab5..f4b86271a56 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -117,6 +117,64 @@ 'state': 'previous', }) # --- +# name: test_selects[cooktop][select.mock_cooktop_temperature_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_cooktop_temperature_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_level', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-TemperatureControlSelectedTemperatureLevel-86-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[cooktop][select.mock_cooktop_temperature_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Cooktop Temperature level', + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'context': , + 'entity_id': 'select.mock_cooktop_temperature_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Medium', + }) +# --- # name: test_selects[dimmable_light][select.mock_dimmable_light_led_color-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 2c6ef8ad51b..550c9edd160 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -1219,6 +1219,58 @@ 'state': '189.0', }) # --- +# name: test_sensors[cooktop][sensor.mock_cooktop_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_cooktop_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': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-TemperatureSensor-1026-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[cooktop][sensor.mock_cooktop_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Cooktop Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_cooktop_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '180.0', + }) +# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index d60a2933e6f..f7d0b66c5f1 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -1,4 +1,100 @@ # serializer version: 1 +# name: test_switches[cooktop][switch.mock_cooktop_power_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_cooktop_power_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': 'Power (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[cooktop][switch.mock_cooktop_power_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Cooktop Power (1)', + }), + 'context': , + 'entity_id': 'switch.mock_cooktop_power_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[cooktop][switch.mock_cooktop_power_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_cooktop_power_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': 'Power (2)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[cooktop][switch.mock_cooktop_power_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Cooktop Power (2)', + }), + 'context': , + 'entity_id': 'switch.mock_cooktop_power_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switches[door_lock][switch.mock_door_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 7d89804a87b06b5e4cf9cf81ca6880809a794b9d Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 30 Apr 2025 22:56:04 +0200 Subject: [PATCH 0004/1175] Bump pylamarzocco to 2.0.0b7 (#143989) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 7d554214fee..ab5a77cad4c 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.0b6"] + "requirements": ["pylamarzocco==2.0.0b7"] } diff --git a/requirements_all.txt b/requirements_all.txt index aae49abd837..671ccf6b99f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b6 +pylamarzocco==2.0.0b7 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0788977826f..73f265248c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b6 +pylamarzocco==2.0.0b7 # homeassistant.components.lastfm pylast==5.1.0 From c4eddc8d11d251663f44f9e8cef8f6cf4842a8ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 Apr 2025 16:57:02 -0400 Subject: [PATCH 0005/1175] Ensure legacy TTS providers are hidden if entity exists (#143992) --- homeassistant/components/cloud/tts.py | 10 +++-- homeassistant/components/tts/__init__.py | 3 ++ homeassistant/components/tts/legacy.py | 1 + homeassistant/components/tts/media_source.py | 21 +++++++---- tests/components/tts/test_init.py | 39 ++++++++++++++++++++ tests/components/tts/test_media_source.py | 7 ++++ 6 files changed, 71 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index ca3e0719998..85ca599fa87 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity): language=language, voice=options.get( ATTR_VOICE, - self._voice - if language == self._language - else DEFAULT_VOICES[language], + ( + self._voice + if language == self._language + else DEFAULT_VOICES[language] + ), ), gender=options.get(ATTR_GENDER), ), @@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity): class CloudProvider(Provider): """Home Assistant Cloud speech API provider.""" + has_entity = True + def __init__(self, cloud: Cloud[CloudClient]) -> None: """Initialize cloud provider.""" self.cloud = cloud diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 44badaa73d2..b279af31803 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1212,6 +1212,9 @@ def websocket_list_engines( if entity.platform: entity_domains.add(entity.platform.platform_name) for engine_id, provider in hass.data[DATA_TTS_MANAGER].providers.items(): + if provider.has_entity: + continue + provider_info = { "engine_id": engine_id, "name": provider.name, diff --git a/homeassistant/components/tts/legacy.py b/homeassistant/components/tts/legacy.py index 6f0541734d1..877ecc034d6 100644 --- a/homeassistant/components/tts/legacy.py +++ b/homeassistant/components/tts/legacy.py @@ -207,6 +207,7 @@ class Provider: hass: HomeAssistant | None = None name: str | None = None + has_entity: bool = False @property def default_language(self) -> str | None: diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index 97d2ab549bc..d3c0998bb77 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -145,13 +145,20 @@ class TTSMediaSource(MediaSource): return self._engine_item(engine, params) # Root. List providers. - children = [ - self._engine_item(engine) - for engine in self.hass.data[DATA_TTS_MANAGER].providers - ] + [ - self._engine_item(entity.entity_id) - for entity in self.hass.data[DATA_COMPONENT].entities - ] + children = sorted( + [ + self._engine_item(engine_id) + for engine_id, provider in self.hass.data[ + DATA_TTS_MANAGER + ].providers.items() + if not provider.has_entity + ] + + [ + self._engine_item(entity.entity_id) + for entity in self.hass.data[DATA_COMPONENT].entities + ], + key=lambda x: x.title, + ) return BrowseMediaSource( domain=DOMAIN, identifier=None, diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 45424be8481..ea281506f3a 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1522,6 +1522,45 @@ async def test_fetching_in_async( ) +@pytest.mark.parametrize( + ("setup", "engine_id"), + [ + ("mock_setup", "test"), + ], + indirect=["setup"], +) +async def test_ws_list_engines_filter_deprecated( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup: str, + engine_id: str, +) -> None: + """Test listing tts engines and supported languages.""" + client = await hass_ws_client() + + await client.send_json_auto_id({"type": "tts/engine/list"}) + + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "providers": [ + { + "name": "Test", + "engine_id": engine_id, + "supported_languages": ["de_CH", "de_DE", "en_GB", "en_US"], + } + ] + } + + hass.data[tts.DATA_TTS_MANAGER].providers[engine_id].has_entity = True + + await client.send_json_auto_id({"type": "tts/engine/list"}) + + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"providers": []} + + @pytest.mark.parametrize( ("setup", "engine_id", "extra_data"), [ diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index 4ff0a44a4bb..c9d70c7f43e 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -114,6 +114,13 @@ async def test_legacy_resolving( await mock_setup(hass, mock_provider) mock_get_tts_audio = mock_provider.get_tts_audio + mock_provider.has_entity = True + root = await media_source.async_browse_media(hass, "media-source://tts") + assert len(root.children) == 0 + mock_provider.has_entity = False + root = await media_source.async_browse_media(hass, "media-source://tts") + assert len(root.children) == 1 + mock_get_tts_audio.reset_mock() media_id = "media-source://tts/test?message=Hello%20World" media = await media_source.async_resolve_media(hass, media_id, None) From b0345cce68759294758771426639af072b7281a7 Mon Sep 17 00:00:00 2001 From: Megamind Date: Wed, 30 Apr 2025 14:04:56 -0700 Subject: [PATCH 0006/1175] Bump pushover-complete to 1.2.0 (#143966) Co-authored-by: Joostlek --- homeassistant/components/pushover/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index d086321c088..e13a254c423 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/pushover", "iot_class": "cloud_push", "loggers": ["pushover_complete"], - "requirements": ["pushover_complete==1.1.1"] + "requirements": ["pushover_complete==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 671ccf6b99f..235605bad07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1726,7 +1726,7 @@ pulsectl==23.5.2 pushbullet.py==0.11.0 # homeassistant.components.pushover -pushover_complete==1.1.1 +pushover_complete==1.2.0 # homeassistant.components.pvoutput pvo==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73f265248c2..75c80f5180f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ psutil==7.0.0 pushbullet.py==0.11.0 # homeassistant.components.pushover -pushover_complete==1.1.1 +pushover_complete==1.2.0 # homeassistant.components.pvoutput pvo==2.2.1 From 6e76ca0fb392bcec759b22028338f177788558e2 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 30 Apr 2025 23:13:04 +0200 Subject: [PATCH 0007/1175] Add translations for "energy_distance" and "wind_direction" in `random` (#143994) * Add translations for "energy_distance" and "wind_direction" in `random` * Comma --- homeassistant/components/random/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index bacd6dd5a17..af0efb823b9 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -98,6 +98,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", @@ -134,6 +135,7 @@ "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } } From ba712ed5140ae97a09eeb467e655abe77bc5f05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 1 May 2025 00:26:10 +0300 Subject: [PATCH 0008/1175] Move huawei_lte sensor icons to icons.json where applicable (#143999) --- .../components/huawei_lte/icons.json | 131 ++++++++++++++++++ homeassistant/components/huawei_lte/sensor.py | 45 +----- 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/huawei_lte/icons.json b/homeassistant/components/huawei_lte/icons.json index 22eb345eba5..862daa47cde 100644 --- a/homeassistant/components/huawei_lte/icons.json +++ b/homeassistant/components/huawei_lte/icons.json @@ -37,6 +37,137 @@ "default": "mdi:antenna" } }, + "sensor": { + "uptime": { + "default": "mdi:timer-outline" + }, + "wan_ip_address": { + "default": "mdi:ip" + }, + "wan_ipv6_address": { + "default": "mdi:ip" + }, + "cell_id": { + "default": "mdi:antenna" + }, + "cqi0": { + "default": "mdi:speedometer" + }, + "cqi1": { + "default": "mdi:speedometer" + }, + "enodeb_id": { + "default": "mdi:antenna" + }, + "lac": { + "default": "mdi:map-marker" + }, + "nei_cellid": { + "default": "mdi:antenna" + }, + "nrcqi0": { + "default": "mdi:speedometer" + }, + "nrcqi1": { + "default": "mdi:speedometer" + }, + "pci": { + "default": "mdi:antenna" + }, + "rac": { + "default": "mdi:map-marker" + }, + "tac": { + "default": "mdi:map-marker" + }, + "sms_unread": { + "default": "mdi:email-arrow-left" + }, + "current_day_transfer": { + "default": "mdi:arrow-up-down-bold" + }, + "current_month_download": { + "default": "mdi:download" + }, + "current_month_upload": { + "default": "mdi:upload" + }, + "wifi_clients_connected": { + "default": "mdi:wifi" + }, + "primary_dns_server": { + "default": "mdi:ip" + }, + "primary_ipv6_dns_server": { + "default": "mdi:ip" + }, + "secondary_dns_server": { + "default": "mdi:ip" + }, + "secondary_ipv6_dns_server": { + "default": "mdi:ip" + }, + "current_connection_duration": { + "default": "mdi:timer-outline" + }, + "current_connection_download": { + "default": "mdi:download" + }, + "current_download_rate": { + "default": "mdi:download" + }, + "current_connection_upload": { + "default": "mdi:upload" + }, + "current_upload_rate": { + "default": "mdi:upload" + }, + "total_connected_duration": { + "default": "mdi:timer-outline" + }, + "total_download": { + "default": "mdi:download" + }, + "total_upload": { + "default": "mdi:upload" + }, + "sms_deleted_device": { + "default": "mdi:email-minus" + }, + "sms_drafts_device": { + "default": "mdi:email-arrow-right-outline" + }, + "sms_inbox_device": { + "default": "mdi:email" + }, + "sms_capacity_device": { + "default": "mdi:email" + }, + "sms_outbox_device": { + "default": "mdi:email-arrow-right" + }, + "sms_unread_device": { + "default": "mdi:email-arrow-left" + }, + "sms_drafts_sim": { + "default": "mdi:email-arrow-right-outline" + }, + "sms_inbox_sim": { + "default": "mdi:email" + }, + "sms_capacity_sim": { + "default": "mdi:email" + }, + "sms_outbox_sim": { + "default": "mdi:email-arrow-right" + }, + "sms_unread_sim": { + "default": "mdi:email-arrow-left" + }, + "sms_messages_sim": { + "default": "mdi:email-arrow-left" + } + }, "switch": { "mobile_data": { "default": "mdi:signal-off", diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e9270dfd6ff..003ba1f9823 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -138,7 +138,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "uptime": HuaweiSensorEntityDescription( key="uptime", translation_key="uptime", - icon="mdi:timer-outline", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, entity_category=EntityCategory.DIAGNOSTIC, @@ -146,14 +145,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "WanIPAddress": HuaweiSensorEntityDescription( key="WanIPAddress", translation_key="wan_ip_address", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), "WanIPv6Address": HuaweiSensorEntityDescription( key="WanIPv6Address", translation_key="wan_ipv6_address", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), }, @@ -181,19 +178,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "cell_id": HuaweiSensorEntityDescription( key="cell_id", translation_key="cell_id", - icon="mdi:antenna", entity_category=EntityCategory.DIAGNOSTIC, ), "cqi0": HuaweiSensorEntityDescription( key="cqi0", translation_key="cqi0", - icon="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC, ), "cqi1": HuaweiSensorEntityDescription( key="cqi1", translation_key="cqi1", - icon="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC, ), "dl_mcs": HuaweiSensorEntityDescription( @@ -230,7 +224,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "enodeb_id": HuaweiSensorEntityDescription( key="enodeb_id", translation_key="enodeb_id", - icon="mdi:antenna", entity_category=EntityCategory.DIAGNOSTIC, ), "ims": HuaweiSensorEntityDescription( @@ -241,7 +234,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "lac": HuaweiSensorEntityDescription( key="lac", translation_key="lac", - icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "ltedlfreq": HuaweiSensorEntityDescription( @@ -279,7 +271,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "nei_cellid": HuaweiSensorEntityDescription( key="nei_cellid", translation_key="nei_cellid", - icon="mdi:antenna", entity_category=EntityCategory.DIAGNOSTIC, ), "nrbler": HuaweiSensorEntityDescription( @@ -290,13 +281,11 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "nrcqi0": HuaweiSensorEntityDescription( key="nrcqi0", translation_key="nrcqi0", - icon="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC, ), "nrcqi1": HuaweiSensorEntityDescription( key="nrcqi1", translation_key="nrcqi1", - icon="mdi:speedometer", entity_category=EntityCategory.DIAGNOSTIC, ), "nrdlbandwidth": HuaweiSensorEntityDescription( @@ -376,7 +365,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "pci": HuaweiSensorEntityDescription( key="pci", translation_key="pci", - icon="mdi:antenna", entity_category=EntityCategory.DIAGNOSTIC, ), "plmn": HuaweiSensorEntityDescription( @@ -387,7 +375,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "rac": HuaweiSensorEntityDescription( key="rac", translation_key="rac", - icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "rrc_status": HuaweiSensorEntityDescription( @@ -458,7 +445,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "tac": HuaweiSensorEntityDescription( key="tac", translation_key="tac", - icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), "tdd": HuaweiSensorEntityDescription( @@ -522,7 +508,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "UnreadMessage": HuaweiSensorEntityDescription( key="UnreadMessage", translation_key="sms_unread", - icon="mdi:email-arrow-left", ), }, ), @@ -536,7 +521,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_day_transfer", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:arrow-up-down-bold", state_class=SensorStateClass.TOTAL, last_reset_item="CurrentDayDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, @@ -546,7 +530,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_month_download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", state_class=SensorStateClass.TOTAL, last_reset_item="MonthDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, @@ -556,7 +539,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_month_upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", state_class=SensorStateClass.TOTAL, last_reset_item="MonthDuration", last_reset_format_fn=format_last_reset_elapsed_seconds, @@ -580,32 +562,27 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "CurrentWifiUser": HuaweiSensorEntityDescription( key="CurrentWifiUser", translation_key="wifi_clients_connected", - icon="mdi:wifi", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "PrimaryDns": HuaweiSensorEntityDescription( key="PrimaryDns", translation_key="primary_dns_server", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "PrimaryIPv6Dns": HuaweiSensorEntityDescription( key="PrimaryIPv6Dns", translation_key="primary_ipv6_dns_server", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "SecondaryDns": HuaweiSensorEntityDescription( key="SecondaryDns", translation_key="secondary_dns_server", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), "SecondaryIPv6Dns": HuaweiSensorEntityDescription( key="SecondaryIPv6Dns", translation_key="secondary_ipv6_dns_server", - icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), }, @@ -618,14 +595,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_connection_duration", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, - icon="mdi:timer-outline", ), "CurrentDownload": HuaweiSensorEntityDescription( key="CurrentDownload", translation_key="current_connection_download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), "CurrentDownloadRate": HuaweiSensorEntityDescription( @@ -633,7 +608,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_download_rate", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:download", state_class=SensorStateClass.MEASUREMENT, ), "CurrentUpload": HuaweiSensorEntityDescription( @@ -641,7 +615,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_connection_upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), "CurrentUploadRate": HuaweiSensorEntityDescription( @@ -649,7 +622,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="current_upload_rate", native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, device_class=SensorDeviceClass.DATA_RATE, - icon="mdi:upload", state_class=SensorStateClass.MEASUREMENT, ), "TotalConnectTime": HuaweiSensorEntityDescription( @@ -657,7 +629,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="total_connected_duration", native_unit_of_measurement=UnitOfTime.SECONDS, device_class=SensorDeviceClass.DURATION, - icon="mdi:timer-outline", state_class=SensorStateClass.TOTAL_INCREASING, ), "TotalDownload": HuaweiSensorEntityDescription( @@ -665,7 +636,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="total_download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:download", state_class=SensorStateClass.TOTAL_INCREASING, ), "TotalUpload": HuaweiSensorEntityDescription( @@ -673,7 +643,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { translation_key="total_upload", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, - icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), }, @@ -719,62 +688,50 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "LocalDeleted": HuaweiSensorEntityDescription( key="LocalDeleted", translation_key="sms_deleted_device", - icon="mdi:email-minus", ), "LocalDraft": HuaweiSensorEntityDescription( key="LocalDraft", translation_key="sms_drafts_device", - icon="mdi:email-arrow-right-outline", ), "LocalInbox": HuaweiSensorEntityDescription( key="LocalInbox", translation_key="sms_inbox_device", - icon="mdi:email", ), "LocalMax": HuaweiSensorEntityDescription( key="LocalMax", translation_key="sms_capacity_device", - icon="mdi:email", ), "LocalOutbox": HuaweiSensorEntityDescription( key="LocalOutbox", translation_key="sms_outbox_device", - icon="mdi:email-arrow-right", ), "LocalUnread": HuaweiSensorEntityDescription( key="LocalUnread", translation_key="sms_unread_device", - icon="mdi:email-arrow-left", ), "SimDraft": HuaweiSensorEntityDescription( key="SimDraft", translation_key="sms_drafts_sim", - icon="mdi:email-arrow-right-outline", ), "SimInbox": HuaweiSensorEntityDescription( key="SimInbox", translation_key="sms_inbox_sim", - icon="mdi:email", ), "SimMax": HuaweiSensorEntityDescription( key="SimMax", translation_key="sms_capacity_sim", - icon="mdi:email", ), "SimOutbox": HuaweiSensorEntityDescription( key="SimOutbox", translation_key="sms_outbox_sim", - icon="mdi:email-arrow-right", ), "SimUnread": HuaweiSensorEntityDescription( key="SimUnread", translation_key="sms_unread_sim", - icon="mdi:email-arrow-left", ), "SimUsed": HuaweiSensorEntityDescription( key="SimUsed", translation_key="sms_messages_sim", - icon="mdi:email-arrow-left", ), }, ), @@ -870,7 +827,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): """Return icon for sensor.""" if self.entity_description.icon_fn: return self.entity_description.icon_fn(self.state) - return self.entity_description.icon + return super().icon @property def device_class(self) -> SensorDeviceClass | None: From 93f4f14b2a8f0e6f7c3ba4c177dcf3fcd5127e63 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 30 Apr 2025 23:45:41 +0200 Subject: [PATCH 0009/1175] Default backup encryption to true when updating only location retention (#143997) --- homeassistant/components/backup/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/backup/config.py b/homeassistant/components/backup/config.py index 75576105e92..0c8a5c82f7c 100644 --- a/homeassistant/components/backup/config.py +++ b/homeassistant/components/backup/config.py @@ -202,7 +202,7 @@ class BackupConfig: if agent_id not in self.data.agents: old_agent_retention = None self.data.agents[agent_id] = AgentConfig( - protected=agent_config.get("protected", False), + protected=agent_config.get("protected", True), retention=new_agent_retention, ) else: From 5250590b17ae4528982cc7bf2eab154ab97a8276 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 1 May 2025 09:28:25 +0200 Subject: [PATCH 0010/1175] Remove deprecated action `api_call` from Habitica integration (#143978) --- homeassistant/components/habitica/const.py | 11 +--- homeassistant/components/habitica/services.py | 62 +------------------ .../components/habitica/services.yaml | 16 ----- .../components/habitica/strings.json | 22 ------- tests/components/habitica/test_init.py | 51 +-------------- 5 files changed, 5 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 7a5677cb687..f9874c711f0 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -1,6 +1,6 @@ """Constants for the habitica integration.""" -from homeassistant.const import APPLICATION_NAME, CONF_PATH, __version__ +from homeassistant.const import APPLICATION_NAME, __version__ CONF_API_USER = "api_user" @@ -13,15 +13,6 @@ HABITICANS_URL = "https://habitica.com/static/img/home-main@3x.ffc32b12.png" DOMAIN = "habitica" -# service constants -SERVICE_API_CALL = "api_call" -ATTR_PATH = CONF_PATH -ATTR_ARGS = "args" - -# event constants -EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" -ATTR_DATA = "data" - MANUFACTURER = "HabitRPG, Inc." NAME = "Habitica" diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py index bcbd6caa7a7..8ef12a38f1c 100644 --- a/homeassistant/components/habitica/services.py +++ b/homeassistant/components/habitica/services.py @@ -29,7 +29,7 @@ import voluptuous as vol from homeassistant.components.todo import ATTR_RENAME from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_DATE, ATTR_NAME, CONF_NAME +from homeassistant.const import ATTR_DATE, ATTR_NAME from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -38,28 +38,24 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.selector import ConfigEntrySelector from homeassistant.util import dt as dt_util from .const import ( ATTR_ADD_CHECKLIST_ITEM, ATTR_ALIAS, - ATTR_ARGS, ATTR_CLEAR_DATE, ATTR_CLEAR_REMINDER, ATTR_CONFIG_ENTRY, ATTR_COST, ATTR_COUNTER_DOWN, ATTR_COUNTER_UP, - ATTR_DATA, ATTR_DIRECTION, ATTR_FREQUENCY, ATTR_INTERVAL, ATTR_ITEM, ATTR_KEYWORD, ATTR_NOTES, - ATTR_PATH, ATTR_PRIORITY, ATTR_REMINDER, ATTR_REMOVE_CHECKLIST_ITEM, @@ -78,10 +74,8 @@ from .const import ( ATTR_UNSCORE_CHECKLIST_ITEM, ATTR_UP_DOWN, DOMAIN, - EVENT_API_CALL_SUCCESS, SERVICE_ABORT_QUEST, SERVICE_ACCEPT_QUEST, - SERVICE_API_CALL, SERVICE_CANCEL_QUEST, SERVICE_CAST_SKILL, SERVICE_CREATE_DAILY, @@ -106,14 +100,6 @@ from .coordinator import HabiticaConfigEntry _LOGGER = logging.getLogger(__name__) -SERVICE_API_CALL_SCHEMA = vol.Schema( - { - vol.Required(ATTR_NAME): str, - vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), - vol.Optional(ATTR_ARGS): dict, - } -) - SERVICE_CAST_SKILL_SCHEMA = vol.Schema( { vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}), @@ -266,46 +252,6 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry: def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Set up services for Habitica integration.""" - async def handle_api_call(call: ServiceCall) -> None: - async_create_issue( - hass, - DOMAIN, - "deprecated_api_call", - breaks_in_ha_version="2025.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_api_call", - ) - _LOGGER.warning( - "Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0" - ) - - name = call.data[ATTR_NAME] - path = call.data[ATTR_PATH] - entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN) - - api = None - for entry in entries: - if entry.data[CONF_NAME] == name: - api = await entry.runtime_data.habitica.habitipy() - break - if api is None: - _LOGGER.error("API_CALL: User '%s' not configured", name) - return - try: - for element in path: - api = api[element] - except KeyError: - _LOGGER.error( - "API_CALL: Path %s is invalid for API on '{%s}' element", path, element - ) - return - kwargs = call.data.get(ATTR_ARGS, {}) - data = await api(**kwargs) - hass.bus.async_fire( - EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} - ) - async def cast_skill(call: ServiceCall) -> ServiceResponse: """Skill action.""" entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY]) @@ -928,12 +874,6 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 schema=SERVICE_CREATE_TASK_SCHEMA, supports_response=SupportsResponse.ONLY, ) - hass.services.async_register( - DOMAIN, - SERVICE_API_CALL, - handle_api_call, - schema=SERVICE_API_CALL_SCHEMA, - ) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index 3fb25e2b4b7..e7f4b4207b0 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,20 +1,4 @@ # Describes the format for Habitica service -api_call: - fields: - name: - required: true - example: "xxxNotAValidNickxxx" - selector: - text: - path: - required: true - example: '["tasks", "user", "post"]' - selector: - object: - args: - example: '{"text": "Use API from Home Assistant", "type": "todo"}' - selector: - object: cast_skill: fields: config_entry: &config_entry diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 695eb1576fe..5b03d8662cb 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -526,31 +526,9 @@ "deprecated_entity": { "title": "The Habitica {name} entity is deprecated", "description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue." - }, - "deprecated_api_call": { - "title": "The Habitica action habitica.api_call is deprecated", - "description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities." } }, "services": { - "api_call": { - "name": "API name", - "description": "Calls Habitica API.", - "fields": { - "name": { - "name": "[%key:common::config_flow::data::name%]", - "description": "Habitica's username to call for." - }, - "path": { - "name": "[%key:common::config_flow::data::path%]", - "description": "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks." - }, - "args": { - "name": "Args", - "description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint." - } - } - }, "cast_skill": { "name": "Cast a skill", "description": "Uses a skill or spell from your Habitica character on a specific task to affect its progress or status.", diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index e953ec254d6..e904ccc890d 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -8,17 +8,9 @@ from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.habitica.const import ( - ATTR_ARGS, - ATTR_DATA, - ATTR_PATH, - DOMAIN, - EVENT_API_CALL_SUCCESS, - SERVICE_API_CALL, -) +from homeassistant.components.habitica.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_NAME -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import HomeAssistant from .conftest import ( ERROR_BAD_REQUEST, @@ -27,13 +19,7 @@ from .conftest import ( ERROR_TOO_MANY_REQUESTS, ) -from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed - - -@pytest.fixture -def capture_api_call_success(hass: HomeAssistant) -> list[Event]: - """Capture api_call events.""" - return async_capture_events(hass, EVENT_API_CALL_SUCCESS) +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("habitica") @@ -53,37 +39,6 @@ async def test_entry_setup_unload( assert config_entry.state is ConfigEntryState.NOT_LOADED -@pytest.mark.usefixtures("habitica") -async def test_service_call( - hass: HomeAssistant, - config_entry: MockConfigEntry, - capture_api_call_success: list[Event], -) -> None: - """Test integration setup, service call and unload.""" - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state is ConfigEntryState.LOADED - - assert len(capture_api_call_success) == 0 - - TEST_SERVICE_DATA = { - ATTR_NAME: "test-user", - ATTR_PATH: ["tasks", "user", "post"], - ATTR_ARGS: {"text": "Use API from Home Assistant", "type": "todo"}, - } - await hass.services.async_call( - DOMAIN, SERVICE_API_CALL, TEST_SERVICE_DATA, blocking=True - ) - - assert len(capture_api_call_success) == 1 - captured_data = capture_api_call_success[0].data - captured_data[ATTR_ARGS] = captured_data[ATTR_DATA] - del captured_data[ATTR_DATA] - assert captured_data == TEST_SERVICE_DATA - - @pytest.mark.parametrize( ("exception"), [ERROR_BAD_REQUEST, ERROR_TOO_MANY_REQUESTS, ClientError], From c2079ddf6f8033c185707d6ca01f280d6df4b969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 09:49:25 +0200 Subject: [PATCH 0011/1175] Remove unused client param at Home Connect diagnostics (#144017) --- homeassistant/components/home_connect/diagnostics.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/home_connect/diagnostics.py b/homeassistant/components/home_connect/diagnostics.py index fd74277a815..59856999ec7 100644 --- a/homeassistant/components/home_connect/diagnostics.py +++ b/homeassistant/components/home_connect/diagnostics.py @@ -4,8 +4,6 @@ from __future__ import annotations from typing import Any -from aiohomeconnect.client import Client as HomeConnectClient - from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry @@ -14,7 +12,7 @@ from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry async def _generate_appliance_diagnostics( - client: HomeConnectClient, appliance: HomeConnectApplianceData + appliance: HomeConnectApplianceData, ) -> dict[str, Any]: return { **appliance.info.to_dict(), @@ -31,9 +29,7 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" return { - appliance.info.ha_id: await _generate_appliance_diagnostics( - entry.runtime_data.client, appliance - ) + appliance.info.ha_id: await _generate_appliance_diagnostics(appliance) for appliance in entry.runtime_data.data.values() } @@ -45,6 +41,4 @@ async def async_get_device_diagnostics( ha_id = next( (identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN), ) - return await _generate_appliance_diagnostics( - entry.runtime_data.client, entry.runtime_data.data[ha_id] - ) + return await _generate_appliance_diagnostics(entry.runtime_data.data[ha_id]) From dd8d714c94f9c0995ea5d428b89223926b0facbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 09:49:49 +0200 Subject: [PATCH 0012/1175] Remove `_attr_should_poll` from Home Connect base entity (#144016) --- homeassistant/components/home_connect/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index facb3b14a9b..a3368ce550c 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -32,7 +32,6 @@ _LOGGER = logging.getLogger(__name__) class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]): """Generic Home Connect entity (base class).""" - _attr_should_poll = False _attr_has_entity_name = True def __init__( From 5ddc449247659541cf2ac98042df13710c62050e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 09:57:17 +0200 Subject: [PATCH 0013/1175] Remove default brightness values from Home Connect light entities (#144019) --- homeassistant/components/home_connect/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index de55a60bd43..b4ea57c63f6 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -39,11 +39,11 @@ PARALLEL_UPDATES = 1 class HomeConnectLightEntityDescription(LightEntityDescription): """Light entity description.""" - brightness_key: SettingKey | None = None + brightness_key: SettingKey + brightness_scale: tuple[float, float] color_key: SettingKey | None = None enable_custom_color_value_key: str | None = None custom_color_key: SettingKey | None = None - brightness_scale: tuple[float, float] = (0.0, 100.0) LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = ( From f441f4d7c05da454c68f4a92e99f22d41c5977a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 09:57:30 +0200 Subject: [PATCH 0014/1175] Remove translation key for battery level in Home Connect sensor (#144020) --- homeassistant/components/home_connect/sensor.py | 1 - homeassistant/components/home_connect/strings.json | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 0f0161971a2..2872c4a95d3 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -159,7 +159,6 @@ SENSORS = ( HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_BATTERY_LEVEL, device_class=SensorDeviceClass.BATTERY, - translation_key="battery_level", ), HomeConnectSensorEntityDescription( key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE, diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 19d7cc06046..9c0da723b04 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1585,9 +1585,6 @@ "name": "Ristretto espresso cups", "unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]" }, - "battery_level": { - "name": "Battery level" - }, "camera_state": { "name": "Camera state", "state": { From 17360ede282cb6fddb37913d8fa5f2677610cd44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 09:57:42 +0200 Subject: [PATCH 0015/1175] Use common percentage const at Home Connect (#144021) --- homeassistant/components/home_connect/number.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/number.py b/homeassistant/components/home_connect/number.py index 1bb793f4015..790036d26f8 100644 --- a/homeassistant/components/home_connect/number.py +++ b/homeassistant/components/home_connect/number.py @@ -11,6 +11,7 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -79,7 +80,7 @@ NUMBERS = ( NumberEntityDescription( key=SettingKey.COOKING_HOOD_COLOR_TEMPERATURE_PERCENT, translation_key="color_temperature_percent", - native_unit_of_measurement="%", + native_unit_of_measurement=PERCENTAGE, ), NumberEntityDescription( key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL, From bc47049d42079e52c2df656c4335bc191fa5bba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 10:18:32 +0200 Subject: [PATCH 0016/1175] Remove non required Home Connect tests (#144024) --- .../home_connect/test_binary_sensor.py | 12 --------- tests/components/home_connect/test_button.py | 13 --------- .../home_connect/test_coordinator.py | 27 ------------------- tests/components/home_connect/test_init.py | 12 --------- tests/components/home_connect/test_light.py | 12 --------- tests/components/home_connect/test_number.py | 12 --------- tests/components/home_connect/test_select.py | 12 --------- tests/components/home_connect/test_sensor.py | 12 --------- tests/components/home_connect/test_switch.py | 13 --------- tests/components/home_connect/test_time.py | 12 --------- 10 files changed, 137 deletions(-) diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 509003ad931..46da0fc0d8f 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -40,18 +40,6 @@ def platforms() -> list[str]: return [Platform.BINARY_SENSOR] -async def test_binary_sensors( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test binary sensor entities.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index c96fe840238..cb3a16fa76c 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -32,19 +32,6 @@ def platforms() -> list[str]: return [Platform.BUTTON] -async def test_buttons( - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test button entities.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 31bb6d8d6a7..36ca5cf0879 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -79,33 +79,6 @@ def platforms() -> list[str]: return [Platform.SENSOR, Platform.SWITCH] -async def test_coordinator_update( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test that the coordinator can update.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - -async def test_coordinator_update_failing_get_appliances( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client_with_exception: MagicMock, -) -> None: - """Test that the coordinator raises ConfigEntryNotReady when it fails to get appliances.""" - client_with_exception.get_home_appliances.return_value = None - client_with_exception.get_home_appliances.side_effect = HomeConnectError() - - assert config_entry.state == ConfigEntryState.NOT_LOADED - await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.SETUP_RETRY - - @pytest.mark.usefixtures("setup_credentials") @pytest.mark.parametrize("platforms", [("binary_sensor",)]) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 2147d9b170a..93a3caa6361 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -57,18 +57,6 @@ async def test_entry_setup( assert config_entry.state == ConfigEntryState.NOT_LOADED -async def test_exception_handling( - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, - setup_credentials: None, - client_with_exception: MagicMock, -) -> None: - """Test exception handling.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("token_expiration_time", [12345]) async def test_token_refresh_success( hass: HomeAssistant, diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index 298eead1737..4894f223d4e 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -53,18 +53,6 @@ def platforms() -> list[str]: return [Platform.LIGHT] -async def test_light( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test switch entities.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Hood"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index 7e89f66683b..4de7d662cc3 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -58,18 +58,6 @@ def platforms() -> list[str]: return [Platform.NUMBER] -async def test_number( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test number entity.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 6d8c090571e..4791b93332f 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -62,18 +62,6 @@ def platforms() -> list[str]: return [Platform.SELECT] -async def test_select( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test select entity.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index d48befcf73f..a810de049ed 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -89,18 +89,6 @@ def platforms() -> list[str]: return [Platform.SENSOR] -async def test_sensors( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test sensor entities.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 2f8b95ceab2..6c4c64939ea 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -69,19 +69,6 @@ def platforms() -> list[str]: return [Platform.SWITCH] -async def test_switches( - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test switch entities.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED - - @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( appliance: HomeAppliance, diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index 34781c29eb8..c8275eb3d06 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -45,18 +45,6 @@ def platforms() -> list[str]: return [Platform.TIME] -async def test_time( - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, - client: MagicMock, -) -> None: - """Test time entity.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) async def test_paired_depaired_devices_flow( From 83b9b8b0328421c5ac79c986d811538bdab04c5f Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Thu, 1 May 2025 10:42:27 +0200 Subject: [PATCH 0017/1175] Fix state of fan entity for Miele hobs with extractor when turned off (#144025) --- homeassistant/components/miele/fan.py | 6 +- .../miele/fixtures/fan_devices.json | 124 ++++++++++++++++++ .../components/miele/snapshots/test_fan.ambr | 49 +++++++ 3 files changed, 177 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index 4781d27901f..fcd74a93bfb 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -99,8 +99,10 @@ class MieleFan(MieleEntity, FanEntity): @property def is_on(self) -> bool: """Return current on/off state.""" - assert self.device.state_ventilation_step is not None - return self.device.state_ventilation_step > 0 + return ( + self.device.state_ventilation_step is not None + and self.device.state_ventilation_step > 0 + ) @property def speed_count(self) -> int: diff --git a/tests/components/miele/fixtures/fan_devices.json b/tests/components/miele/fixtures/fan_devices.json index d3403c0f7bc..9904f6f5faa 100644 --- a/tests/components/miele/fixtures/fan_devices.json +++ b/tests/components/miele/fixtures/fan_devices.json @@ -210,5 +210,129 @@ "ecoFeedback": null, "batteryLevel": null } + }, + "DummyAppliance_74_off": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 74, + "value_localized": "Hob with vapour extraction" + }, + "deviceName": "", + "protocolVersion": 2, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "00", + "techType": "KMDA7473", + "matNumber": "", + "swids": ["000"] + }, + "xkmIdentLabel": { + "techType": "EK039W", + "releaseVersion": "02.80" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": false, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } } } diff --git a/tests/components/miele/snapshots/test_fan.ambr b/tests/components/miele/snapshots/test_fan.ambr index ffd6c90a388..595d4463462 100644 --- a/tests/components/miele/snapshots/test_fan.ambr +++ b/tests/components/miele/snapshots/test_fan.ambr @@ -48,6 +48,55 @@ 'state': 'on', }) # --- +# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.hob_with_extraction_fan_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': 'Fan', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fan', + 'unique_id': 'DummyAppliance_74_off-fan_readonly', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hob with extraction Fan', + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.hob_with_extraction_fan_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_fan_states[fan_devices.json-platforms0][fan.hood_fan-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From a084b9fddef5e67ecc6fd53b1d14fb62d91a5cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 11:24:05 +0200 Subject: [PATCH 0018/1175] Set `autouse` to `setup_credentials` Home Connect fixture (#144028) --- tests/components/home_connect/conftest.py | 2 +- .../components/home_connect/test_binary_sensor.py | 5 ----- tests/components/home_connect/test_button.py | 6 ------ tests/components/home_connect/test_config_flow.py | 2 -- tests/components/home_connect/test_coordinator.py | 11 ----------- tests/components/home_connect/test_diagnostics.py | 2 -- tests/components/home_connect/test_entity.py | 4 ---- tests/components/home_connect/test_init.py | 6 ------ tests/components/home_connect/test_light.py | 6 ------ tests/components/home_connect/test_number.py | 7 ------- tests/components/home_connect/test_select.py | 13 ------------- tests/components/home_connect/test_sensor.py | 11 ----------- tests/components/home_connect/test_services.py | 7 ------- tests/components/home_connect/test_switch.py | 14 -------------- tests/components/home_connect/test_time.py | 7 ------- 15 files changed, 1 insertion(+), 102 deletions(-) diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index 516701f2360..c3e5e859870 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -119,7 +119,7 @@ def mock_config_entry_v1_2(token_entry: dict[str, Any]) -> MockConfigEntry: ) -@pytest.fixture +@pytest.fixture(autouse=True) async def setup_credentials(hass: HomeAssistant) -> None: """Fixture to setup credentials.""" assert await async_setup_component(hass, "application_credentials", {}) diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 46da0fc0d8f..5dee63cdc74 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -46,7 +46,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -110,7 +109,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -174,7 +172,6 @@ async def test_binary_sensors_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -279,7 +276,6 @@ async def test_binary_sensors_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Tests for Home Connect Fridge appliance door states.""" @@ -315,7 +311,6 @@ async def test_connected_sensor_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index cb3a16fa76c..50997d83b94 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -38,7 +38,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -102,7 +101,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -177,7 +175,6 @@ async def test_button_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -242,7 +239,6 @@ async def test_button_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, entity_id: str, method_call: str, @@ -271,7 +267,6 @@ async def test_command_button_exception( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test if button entities availability are based on the appliance connection state.""" @@ -308,7 +303,6 @@ async def test_stop_program_button_exception( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test if button entities availability are based on the appliance connection state.""" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 19182a12194..39ce4eefacb 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -145,7 +145,6 @@ async def test_reauth_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, @@ -200,7 +199,6 @@ async def test_reauth_flow_with_different_account( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 36ca5cf0879..99185e7d373 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -79,7 +79,6 @@ def platforms() -> list[str]: return [Platform.SENSOR, Platform.SWITCH] -@pytest.mark.usefixtures("setup_credentials") @pytest.mark.parametrize("platforms", [("binary_sensor",)]) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_coordinator_failure_refresh_and_stream( @@ -213,7 +212,6 @@ async def test_coordinator_failure_refresh_and_stream( async def test_coordinator_not_fetching_on_disconnected_appliance( config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -236,7 +234,6 @@ async def test_coordinator_update_failing( mock_method: str, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that although is not possible to get settings and status, the config entry is loaded. @@ -284,7 +281,6 @@ async def test_event_listener( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, entity_registry: er.EntityRegistry, @@ -350,7 +346,6 @@ async def tests_receive_setting_and_status_for_first_time_at_events( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -407,7 +402,6 @@ async def test_event_listener_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test that the configuration entry is reloaded when the event stream raises an API error.""" @@ -427,7 +421,6 @@ async def test_event_listener_error( assert not config_entry._background_tasks -@pytest.mark.usefixtures("setup_credentials") @pytest.mark.parametrize("platforms", [("sensor",)]) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) @pytest.mark.parametrize( @@ -525,7 +518,6 @@ async def test_devices_updated_on_refresh( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, ) -> None: @@ -567,7 +559,6 @@ async def test_paired_disconnected_devices_not_fetching( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -598,7 +589,6 @@ async def test_coordinator_disabling_updates_for_appliance( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, hass_client: ClientSessionGenerator, @@ -692,7 +682,6 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, hass_client: ClientSessionGenerator, diff --git a/tests/components/home_connect/test_diagnostics.py b/tests/components/home_connect/test_diagnostics.py index ab6823411dc..23355bce582 100644 --- a/tests/components/home_connect/test_diagnostics.py +++ b/tests/components/home_connect/test_diagnostics.py @@ -21,7 +21,6 @@ async def test_async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, snapshot: SnapshotAssertion, ) -> None: @@ -37,7 +36,6 @@ async def test_async_get_device_diagnostics( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, diff --git a/tests/components/home_connect/test_entity.py b/tests/components/home_connect/test_entity.py index e91a01a907a..fb78de89735 100644 --- a/tests/components/home_connect/test_entity.py +++ b/tests/components/home_connect/test_entity.py @@ -106,7 +106,6 @@ async def test_program_options_retrieval( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the options are correctly retrieved at the start and updated on program updates.""" @@ -257,7 +256,6 @@ async def test_no_options_retrieval_on_unknown_program( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that no options are retrieved when the program is unknown.""" @@ -335,7 +333,6 @@ async def test_program_options_retrieval_after_appliance_connection( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the options are correctly retrieved at the start and updated on program updates.""" @@ -455,7 +452,6 @@ async def test_option_entity_functionality_exception( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the option entity handles exceptions correctly.""" diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 93a3caa6361..1671c69fdf6 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -43,7 +43,6 @@ async def test_entry_setup( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test setup and unload.""" @@ -64,7 +63,6 @@ async def test_token_refresh_success( integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, - setup_credentials: None, client: MagicMock, ) -> None: """Test where token is expired and the refresh attempt succeeds.""" @@ -147,7 +145,6 @@ async def test_token_refresh_error( integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, - setup_credentials: None, client: MagicMock, ) -> None: """Test where token is expired and the refresh attempt fails.""" @@ -181,7 +178,6 @@ async def test_client_error( expected_state: ConfigEntryState, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test client errors during setup integration.""" @@ -208,7 +204,6 @@ async def test_client_rate_limit_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test client errors during setup integration.""" @@ -241,7 +236,6 @@ async def test_required_program_or_at_least_an_option( device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index 4894f223d4e..c10f3a4eaa2 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -59,7 +59,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -123,7 +122,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -181,7 +179,6 @@ async def test_light_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -353,7 +350,6 @@ async def test_light_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test light functionality.""" @@ -406,7 +402,6 @@ async def test_light_color_different_than_custom( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that light color attributes are not set if color is different than custom.""" @@ -574,7 +569,6 @@ async def test_light_exception_handling( hass: HomeAssistant, integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test light exception handling.""" diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index 4de7d662cc3..a35e018f040 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -64,7 +64,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -141,7 +140,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -199,7 +197,6 @@ async def test_number_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -300,7 +297,6 @@ async def test_number_entity_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test number entity functionality.""" @@ -388,7 +384,6 @@ async def test_fetch_constraints_after_rate_limit_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that, if a API rate limit error is raised, the constraints are fetched later.""" @@ -459,7 +454,6 @@ async def test_number_entity_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test number entity error.""" @@ -548,7 +542,6 @@ async def test_options_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test options functionality.""" diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 4791b93332f..5702bd13ce5 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -68,7 +68,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -147,7 +146,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -215,7 +213,6 @@ async def test_select_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -266,7 +263,6 @@ async def test_select_entity_availability( async def test_filter_programs( config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, entity_registry: er.EntityRegistry, ) -> None: @@ -366,7 +362,6 @@ async def test_select_program_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test select functionality.""" @@ -440,7 +435,6 @@ async def test_select_exception_handling( hass: HomeAssistant, integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test exception handling.""" @@ -480,7 +474,6 @@ async def test_programs_updated_on_connect( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that devices reconnected. @@ -570,7 +563,6 @@ async def test_select_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test select functionality.""" @@ -633,7 +625,6 @@ async def test_fetch_allowed_values( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test fetch allowed values.""" @@ -690,7 +681,6 @@ async def test_fetch_allowed_values_after_rate_limit_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test fetch allowed values.""" @@ -765,7 +755,6 @@ async def test_default_values_after_fetch_allowed_values_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test fetch allowed values.""" @@ -818,7 +807,6 @@ async def test_select_entity_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test select entity error.""" @@ -936,7 +924,6 @@ async def test_options_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test options functionality.""" diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index a810de049ed..b25b2649f6f 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -95,7 +95,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -182,7 +181,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -240,7 +238,6 @@ async def test_sensor_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -366,7 +363,6 @@ async def test_program_sensors( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, ) -> None: """Test sequence for sensors that expose information about a program.""" entity_ids = ENTITY_ID_STATES.keys() @@ -440,7 +436,6 @@ async def test_program_sensor_edge_case( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test edge case for the program related entities.""" @@ -513,7 +508,6 @@ async def test_remaining_prog_time_edge_cases( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Run program sequence to test edge cases for the remaining_prog_time entity.""" @@ -594,7 +588,6 @@ async def test_sensors_states( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Tests for appliance sensors.""" @@ -654,7 +647,6 @@ async def test_event_sensors_states( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, entity_registry: er.EntityRegistry, caplog: pytest.LogCaptureFixture, @@ -743,7 +735,6 @@ async def test_sensor_unit_fetching( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" @@ -808,7 +799,6 @@ async def test_sensor_unit_fetching_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" @@ -861,7 +851,6 @@ async def test_sensor_unit_fetching_after_rate_limit_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index 2915cbe4f69..2618cf951b7 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -181,7 +181,6 @@ async def test_key_value_services( device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -231,7 +230,6 @@ async def test_programs_and_options_actions_deprecation( device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, issue_registry: ir.IssueRegistry, @@ -302,7 +300,6 @@ async def test_set_program_and_options( device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, snapshot: SnapshotAssertion, @@ -346,7 +343,6 @@ async def test_set_program_and_options_exceptions( device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, appliance: HomeAppliance, ) -> None: @@ -375,7 +371,6 @@ async def test_services_exception_device_id( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, appliance: HomeAppliance, device_registry: dr.DeviceRegistry, @@ -400,7 +395,6 @@ async def test_services_appliance_not_found( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, ) -> None: @@ -449,7 +443,6 @@ async def test_services_exception( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, appliance: HomeAppliance, device_registry: dr.DeviceRegistry, diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 6c4c64939ea..e0475c1778d 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -75,7 +75,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -154,7 +153,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -223,7 +221,6 @@ async def test_switch_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -311,7 +308,6 @@ async def test_switch_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, appliance: HomeAppliance, client: MagicMock, ) -> None: @@ -355,7 +351,6 @@ async def test_program_switch_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, appliance: HomeAppliance, client: MagicMock, ) -> None: @@ -462,7 +457,6 @@ async def test_switch_exception_handling( hass: HomeAssistant, integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test exception handling.""" @@ -537,7 +531,6 @@ async def test_ent_desc_switch_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, appliance: HomeAppliance, client: MagicMock, ) -> None: @@ -590,7 +583,6 @@ async def test_ent_desc_switch_exception_handling( hass: HomeAssistant, integration_setup: Callable[[MagicMock], Awaitable[bool]], config_entry: MockConfigEntry, - setup_credentials: None, appliance: HomeAppliance, client_with_exception: MagicMock, ) -> None: @@ -674,7 +666,6 @@ async def test_power_switch( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, appliance: HomeAppliance, client: MagicMock, ) -> None: @@ -719,7 +710,6 @@ async def test_power_switch_fetch_off_state_from_current_value( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test power switch functionality to fetch the off state from the current value.""" @@ -771,7 +761,6 @@ async def test_power_switch_service_validation_errors( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, exception_match: str, client: MagicMock, ) -> None: @@ -822,7 +811,6 @@ async def test_create_program_switch_deprecation_issue( service: str, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: @@ -904,7 +892,6 @@ async def test_program_switch_deprecation_issue_fix( service: str, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, hass_client: ClientSessionGenerator, @@ -1023,7 +1010,6 @@ async def test_options_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test options functionality.""" diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index c8275eb3d06..56cdefe7d57 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -52,7 +52,6 @@ async def test_paired_depaired_devices_flow( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -117,7 +116,6 @@ async def test_connected_devices( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -176,7 +174,6 @@ async def test_time_entity_availability( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, appliance: HomeAppliance, ) -> None: @@ -242,7 +239,6 @@ async def test_time_entity_functionality( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, ) -> None: """Test time entity functionality.""" @@ -287,7 +283,6 @@ async def test_time_entity_error( hass: HomeAssistant, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client_with_exception: MagicMock, ) -> None: """Test time entity error.""" @@ -330,7 +325,6 @@ async def test_create_alarm_clock_deprecation_issue( appliance: HomeAppliance, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, ) -> None: @@ -411,7 +405,6 @@ async def test_alarm_clock_deprecation_issue_fix( appliance: HomeAppliance, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - setup_credentials: None, client: MagicMock, issue_registry: ir.IssueRegistry, hass_client: ClientSessionGenerator, From c0f0a4a1aca5a30a4d15c7353c5d01d8181451fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 11:24:29 +0200 Subject: [PATCH 0019/1175] Listen for an event just once at Home Connect test (#144031) --- tests/components/home_connect/test_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 99185e7d373..aad17dcc058 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -328,7 +328,7 @@ async def test_event_listener( def event_filter(_: EventStateReportedData) -> bool: return True - hass.bus.async_listen(EVENT_STATE_REPORTED, listener_callback, event_filter) + hass.bus.async_listen_once(EVENT_STATE_REPORTED, listener_callback, event_filter) entity_registry.async_update_entity(entity_id, new_entity_id=new_entity_id) await hass.async_block_till_done() From 92944fa509c1c521600a4887ba704518e6ded001 Mon Sep 17 00:00:00 2001 From: OzGav Date: Thu, 1 May 2025 19:40:04 +1000 Subject: [PATCH 0020/1175] Media Player strings adjust grammar (#144030) --- homeassistant/components/media_player/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 459b54b8af2..617cb258af7 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -291,7 +291,7 @@ "description": "The term to search for." }, "media_filter_classes": { - "name": "Media filter classes", + "name": "Media class filter", "description": "List of media classes to filter the search results by." } } From 79aa7aacecb924342bb335f68fc15e0142fb8d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 12:04:25 +0200 Subject: [PATCH 0021/1175] Sort Home Connect test params (#144035) --- .../home_connect/test_binary_sensor.py | 30 ++--- tests/components/home_connect/test_button.py | 26 ++-- .../home_connect/test_config_flow.py | 15 +-- .../home_connect/test_coordinator.py | 55 ++++---- .../home_connect/test_diagnostics.py | 6 +- tests/components/home_connect/test_entity.py | 30 ++--- tests/components/home_connect/test_init.py | 32 +++-- tests/components/home_connect/test_light.py | 44 +++--- tests/components/home_connect/test_number.py | 52 ++++---- tests/components/home_connect/test_select.py | 92 ++++++------- tests/components/home_connect/test_sensor.py | 86 ++++++------ .../components/home_connect/test_services.py | 42 +++--- tests/components/home_connect/test_switch.py | 125 ++++++++---------- tests/components/home_connect/test_time.py | 48 ++++--- 14 files changed, 333 insertions(+), 350 deletions(-) diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 5dee63cdc74..0022d6987d7 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -42,13 +42,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -104,14 +104,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -170,9 +170,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_binary_sensors_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if binary sensor entities availability are based on the appliance connection state.""" @@ -268,15 +268,15 @@ async def test_binary_sensors_entity_availability( indirect=["appliance"], ) async def test_binary_sensors_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, event_key: EventKey, event_value_update: str, appliance: HomeAppliance, expected: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Tests for Home Connect Fridge appliance door states.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -309,9 +309,9 @@ async def test_binary_sensors_functionality( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_connected_sensor_functionality( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if the connected binary sensor reports the right values.""" diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index 50997d83b94..9971597c8e3 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -34,13 +34,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -96,14 +96,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -173,9 +173,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_button_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if button entities availability are based on the appliance connection state.""" @@ -237,9 +237,9 @@ async def test_button_entity_availability( ) async def test_button_functionality( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, entity_id: str, method_call: str, expected_kwargs: dict[str, Any], @@ -265,9 +265,9 @@ async def test_button_functionality( async def test_command_button_exception( hass: HomeAssistant, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test if button entities availability are based on the appliance connection state.""" entity_id = "button.washer_pause_program" @@ -301,9 +301,9 @@ async def test_command_button_exception( async def test_stop_program_button_exception( hass: HomeAssistant, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test if button entities availability are based on the appliance connection state.""" entity_id = "button.washer_stop_program" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 39ce4eefacb..d5a01d03258 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,8 +1,7 @@ """Test the Home Connect config flow.""" -from collections.abc import Awaitable, Callable from http import HTTPStatus -from unittest.mock import MagicMock, patch +from unittest.mock import patch from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN import pytest @@ -143,13 +142,13 @@ async def test_prevent_reconfiguring_same_account( @pytest.mark.usefixtures("current_request_with_host") async def test_reauth_flow( hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, ) -> None: """Test reauth flow.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -197,13 +196,13 @@ async def test_reauth_flow( @pytest.mark.usefixtures("current_request_with_host") async def test_reauth_flow_with_different_account( hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, ) -> None: """Test reauth flow.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index aad17dcc058..8602ecbe03a 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -83,10 +83,10 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_coordinator_failure_refresh_and_stream( hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - freezer: FrozenDateTimeFactory, appliance: HomeAppliance, ) -> None: """Test entity available state via coordinator refresh and event stream.""" @@ -210,9 +210,9 @@ async def test_coordinator_failure_refresh_and_stream( indirect=True, ) async def test_coordinator_not_fetching_on_disconnected_appliance( + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test that the coordinator does not fetch anything on disconnected appliance.""" @@ -231,10 +231,10 @@ async def test_coordinator_not_fetching_on_disconnected_appliance( INITIAL_FETCH_CLIENT_METHODS, ) async def test_coordinator_update_failing( - mock_method: str, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + mock_method: str, ) -> None: """Test that although is not possible to get settings and status, the config entry is loaded. @@ -274,16 +274,16 @@ async def test_coordinator_update_failing( ], ) async def test_event_listener( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, event_type: EventType, event_key: EventKey, event_value: str, entity_id: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - appliance: HomeAppliance, - entity_registry: er.EntityRegistry, ) -> None: """Test that the event listener works.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -344,9 +344,9 @@ async def test_event_listener( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def tests_receive_setting_and_status_for_first_time_at_events( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test that the event listener is capable of receiving settings and status for the first time.""" @@ -400,9 +400,9 @@ async def tests_receive_setting_and_status_for_first_time_at_events( async def test_event_listener_error( hass: HomeAssistant, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test that the configuration entry is reloaded when the event stream raises an API error.""" client_with_exception.stream_all_events = MagicMock( @@ -446,17 +446,17 @@ async def test_event_listener_error( ], ) async def test_event_listener_resilience( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + exception: HomeConnectError, entity_id: str, initial_state: str, event_key: EventKey, event_value: Any, after_event_expected_state: str, - exception: HomeConnectError, - hass: HomeAssistant, - appliance: HomeAppliance, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], ) -> None: """Test that the event listener is resilient to interruptions.""" future = hass.loop.create_future() @@ -516,10 +516,10 @@ async def test_event_listener_resilience( async def test_devices_updated_on_refresh( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - device_registry: dr.DeviceRegistry, ) -> None: """Test handling of devices added or deleted while event stream is down.""" appliances: list[HomeAppliance] = ( @@ -557,9 +557,9 @@ async def test_devices_updated_on_refresh( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_disconnected_devices_not_fetching( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test that Home Connect API is not fetched after pairing a disconnected device.""" @@ -587,11 +587,11 @@ async def test_paired_disconnected_devices_not_fetching( async def test_coordinator_disabling_updates_for_appliance( hass: HomeAssistant, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, ) -> None: """Test coordinator disables appliance updates on frequent connect/paired events. @@ -680,11 +680,10 @@ async def test_coordinator_disabling_updates_for_appliance( async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_reload( hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, ) -> None: """Test that updates are enabled again after unloading the entry. diff --git a/tests/components/home_connect/test_diagnostics.py b/tests/components/home_connect/test_diagnostics.py index 23355bce582..bf452ac7a92 100644 --- a/tests/components/home_connect/test_diagnostics.py +++ b/tests/components/home_connect/test_diagnostics.py @@ -19,9 +19,9 @@ from tests.common import MockConfigEntry async def test_async_get_config_entry_diagnostics( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" @@ -34,10 +34,10 @@ async def test_async_get_config_entry_diagnostics( async def test_async_get_device_diagnostics( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: """Test device config entry diagnostics.""" diff --git a/tests/components/home_connect/test_entity.py b/tests/components/home_connect/test_entity.py index fb78de89735..1dc2c71e18c 100644 --- a/tests/components/home_connect/test_entity.py +++ b/tests/components/home_connect/test_entity.py @@ -95,6 +95,10 @@ def platforms() -> list[str]: indirect=["appliance"], ) async def test_program_options_retrieval( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], array_of_programs_program_arg: str, event_key: EventKey, appliance: HomeAppliance, @@ -103,10 +107,6 @@ async def test_program_options_retrieval( options_availability_stage_2: list[bool], option_without_default: tuple[OptionKey, str], option_without_constraints: tuple[OptionKey, str], - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that the options are correctly retrieved at the start and updated on program updates.""" original_get_all_programs_mock = client.get_all_programs.side_effect @@ -250,13 +250,13 @@ async def test_program_options_retrieval( ], ) async def test_no_options_retrieval_on_unknown_program( - array_of_programs_program_arg: str, - event_key: EventKey, - appliance: HomeAppliance, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + appliance: HomeAppliance, + array_of_programs_program_arg: str, + event_key: EventKey, ) -> None: """Test that no options are retrieved when the program is unknown.""" @@ -326,14 +326,14 @@ async def test_no_options_retrieval_on_unknown_program( indirect=["appliance"], ) async def test_program_options_retrieval_after_appliance_connection( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], event_key: EventKey, appliance: HomeAppliance, option_key: OptionKey, option_entity_id: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that the options are correctly retrieved at the start and updated on program updates.""" array_of_home_appliances = client.get_home_appliances.return_value @@ -447,12 +447,12 @@ async def test_program_options_retrieval_after_appliance_connection( ], ) async def test_option_entity_functionality_exception( - set_active_program_option_side_effect: HomeConnectError | None, - set_selected_program_option_side_effect: HomeConnectError | None, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + set_active_program_option_side_effect: HomeConnectError | None, + set_selected_program_option_side_effect: HomeConnectError | None, ) -> None: """Test that the option entity handles exceptions correctly.""" entity_id = "switch.washer_i_dos_1_active" diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 1671c69fdf6..12989c7a847 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -41,9 +41,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_entry_setup( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test setup and unload.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -59,11 +59,11 @@ async def test_entry_setup( @pytest.mark.parametrize("token_expiration_time", [12345]) async def test_token_refresh_success( hass: HomeAssistant, - platforms: list[Platform], - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, client: MagicMock, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + config_entry: MockConfigEntry, + platforms: list[Platform], ) -> None: """Test where token is expired and the refresh attempt succeeds.""" @@ -138,14 +138,13 @@ async def test_token_refresh_success( ], ) async def test_token_refresh_error( - aioclient_mock_args: dict[str, Any], - expected_config_entry_state: ConfigEntryState, hass: HomeAssistant, - platforms: list[Platform], - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + aioclient_mock_args: dict[str, Any], + expected_config_entry_state: ConfigEntryState, ) -> None: """Test where token is expired and the refresh attempt fails.""" @@ -174,11 +173,11 @@ async def test_token_refresh_error( ], ) async def test_client_error( - exception: HomeConnectError, - expected_state: ConfigEntryState, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, + exception: HomeConnectError, + expected_state: ConfigEntryState, ) -> None: """Test client errors during setup integration.""" client_with_exception.get_home_appliances.return_value = None @@ -200,11 +199,10 @@ async def test_client_error( ], ) async def test_client_rate_limit_error( - raising_exception_method: str, - hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + raising_exception_method: str, ) -> None: """Test client errors during setup integration.""" retry_after = 42 @@ -234,9 +232,9 @@ async def test_client_rate_limit_error( async def test_required_program_or_at_least_an_option( hass: HomeAssistant, device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: "Test that the set_program_and_options does raise an exception if no program nor options are set." @@ -270,8 +268,8 @@ async def test_entity_migration( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, config_entry_v1_1: MockConfigEntry, - appliance: HomeAppliance, platforms: list[Platform], + appliance: HomeAppliance, ) -> None: """Test entity migration.""" diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index c10f3a4eaa2..ca5c9c4968c 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -55,13 +55,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Hood"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -117,14 +117,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -177,9 +177,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Hood"], indirect=True) async def test_light_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if light entities availability are based on the appliance connection state.""" @@ -341,16 +341,16 @@ async def test_light_availability( indirect=["appliance"], ) async def test_light_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, set_settings_args: dict[SettingKey, Any], service: str, exprected_attributes: dict[str, Any], state: str, appliance: HomeAppliance, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test light functionality.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -396,13 +396,13 @@ async def test_light_functionality( indirect=["appliance"], ) async def test_light_color_different_than_custom( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, events: dict[EventKey, Any], appliance: HomeAppliance, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that light color attributes are not set if color is different than custom.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -560,16 +560,16 @@ async def test_light_color_different_than_custom( ], ) async def test_light_exception_handling( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, setting: dict[SettingKey, dict[str, Any]], service: str, service_data: dict, attr_side_effect: list[type[HomeConnectError] | None], exception_match: str, - hass: HomeAssistant, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, - client_with_exception: MagicMock, ) -> None: """Test light exception handling.""" client_with_exception.get_settings.side_effect = None diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index a35e018f040..cc2ca8046ed 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -60,13 +60,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" client.get_available_program = AsyncMock( @@ -135,14 +135,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -195,9 +195,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True) async def test_number_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if number entities availability are based on the appliance connection state.""" @@ -285,6 +285,10 @@ async def test_number_entity_availability( ], ) async def test_number_entity_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, setting_key: SettingKey, @@ -294,10 +298,6 @@ async def test_number_entity_functionality( max_value: int, step_size: float, unit_of_measurement: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test number entity functionality.""" client.get_setting.side_effect = None @@ -372,6 +372,10 @@ async def test_number_entity_functionality( ) @patch("homeassistant.components.home_connect.entity.API_DEFAULT_RETRY_AFTER", new=0) async def test_fetch_constraints_after_rate_limit_error( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], retry_after: int | None, appliance: HomeAppliance, entity_id: str, @@ -381,10 +385,6 @@ async def test_fetch_constraints_after_rate_limit_error( max_value: int, step_size: int, unit_of_measurement: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that, if a API rate limit error is raised, the constraints are fetched later.""" @@ -448,13 +448,13 @@ async def test_fetch_constraints_after_rate_limit_error( ], ) async def test_number_entity_error( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, setting_key: SettingKey, mock_attr: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test number entity error.""" client_with_exception.get_settings.side_effect = None @@ -529,6 +529,10 @@ async def test_number_entity_error( indirect=["appliance"], ) async def test_options_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, option_key: OptionKey, appliance: HomeAppliance, @@ -539,10 +543,6 @@ async def test_options_functionality( set_active_program_options_side_effect: ActiveProgramNotSetError | None, set_selected_program_options_side_effect: SelectedProgramNotSetError | None, called_mock_method: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test options functionality.""" diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 5702bd13ce5..46730e8c595 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -64,13 +64,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" client.get_available_program = AsyncMock( @@ -141,14 +141,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -211,9 +211,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_select_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if select entities availability are based on the appliance connection state.""" @@ -261,10 +261,10 @@ async def test_select_entity_availability( async def test_filter_programs( + entity_registry: er.EntityRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - entity_registry: er.EntityRegistry, ) -> None: """Test select that only right programs are shown.""" client.get_all_programs.side_effect = None @@ -352,6 +352,10 @@ async def test_filter_programs( indirect=["appliance"], ) async def test_select_program_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, expected_initial_state: str, @@ -359,10 +363,6 @@ async def test_select_program_functionality( program_key: ProgramKey, program_to_set: str, event_key: EventKey, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test select functionality.""" assert config_entry.state is ConfigEntryState.NOT_LOADED @@ -428,14 +428,14 @@ async def test_select_program_functionality( ], ) async def test_select_exception_handling( + hass: HomeAssistant, + client_with_exception: MagicMock, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + config_entry: MockConfigEntry, entity_id: str, program_to_set: str, mock_attr: str, exception_match: str, - hass: HomeAssistant, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, - client_with_exception: MagicMock, ) -> None: """Test exception handling.""" client_with_exception.get_all_programs.side_effect = None @@ -470,11 +470,11 @@ async def test_select_exception_handling( @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_programs_updated_on_connect( - appliance: HomeAppliance, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + appliance: HomeAppliance, ) -> None: """Test that devices reconnected. @@ -554,16 +554,16 @@ async def test_programs_updated_on_connect( ], ) async def test_select_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, setting_key: SettingKey, expected_options: set[str], value_to_set: str, expected_value_call_arg: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test select functionality.""" assert config_entry.state is ConfigEntryState.NOT_LOADED @@ -617,15 +617,15 @@ async def test_select_functionality( ], ) async def test_fetch_allowed_values( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, test_setting_key: SettingKey, allowed_values: list[str | None], expected_options: set[str], - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test fetch allowed values.""" original_get_setting_side_effect = client.get_setting @@ -673,15 +673,15 @@ async def test_fetch_allowed_values( ], ) async def test_fetch_allowed_values_after_rate_limit_error( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, setting_key: SettingKey, allowed_values: list[str | None], expected_options: set[str], - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test fetch allowed values.""" @@ -747,15 +747,15 @@ async def test_fetch_allowed_values_after_rate_limit_error( ], ) async def test_default_values_after_fetch_allowed_values_error( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, setting_key: SettingKey, exception: Exception, expected_options: set[str], - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test fetch allowed values.""" @@ -799,15 +799,15 @@ async def test_default_values_after_fetch_allowed_values_error( ], ) async def test_select_entity_error( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, setting_key: SettingKey, allowed_value: str, value_to_set: str, mock_attr: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test select entity error.""" client_with_exception.get_settings.side_effect = None @@ -913,6 +913,10 @@ async def test_select_entity_error( ], ) async def test_options_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, option_key: OptionKey, allowed_values: list[str | None] | None, @@ -921,10 +925,6 @@ async def test_options_functionality( set_active_program_options_side_effect: ActiveProgramNotSetError | None, set_selected_program_options_side_effect: SelectedProgramNotSetError | None, called_mock_method: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test options functionality.""" if set_active_program_options_side_effect: diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index b25b2649f6f..9fbefc9944d 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -91,13 +91,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -176,14 +176,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -236,9 +236,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) async def test_sensor_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if sensor entities availability are based on the appliance connection state.""" @@ -355,14 +355,14 @@ ENTITY_ID_STATES = { ), ) async def test_program_sensors( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, states: tuple, event_run: dict[EventType, dict[EventKey, str | int]], - freezer: FrozenDateTimeFactory, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], ) -> None: """Test sequence for sensors that expose information about a program.""" entity_ids = ENTITY_ID_STATES.keys() @@ -428,15 +428,15 @@ async def test_program_sensors( ], ) async def test_program_sensor_edge_case( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], initial_operation_state: str, initial_state: str, event_order: tuple[EventType, EventType], entity_states: tuple[str, str], appliance: HomeAppliance, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test edge case for the program related entities.""" entity_id = "sensor.dishwasher_program_progress" @@ -503,12 +503,12 @@ ENTITY_ID_EDGE_CASE_STATES = [ @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) async def test_remaining_prog_time_edge_cases( - appliance: HomeAppliance, - freezer: FrozenDateTimeFactory, hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + appliance: HomeAppliance, ) -> None: """Run program sequence to test edge cases for the remaining_prog_time entity.""" entity_id = "sensor.dishwasher_program_finish_time" @@ -581,14 +581,14 @@ async def test_remaining_prog_time_edge_cases( indirect=["appliance"], ) async def test_sensors_states( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, event_key: EventKey, value_expected_state: list[tuple[str, str]], appliance: HomeAppliance, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Tests for appliance sensors.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -641,15 +641,15 @@ async def test_sensors_states( indirect=["appliance"], ) async def test_event_sensors_states( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, event_key: EventKey, appliance: HomeAppliance, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - entity_registry: er.EntityRegistry, - caplog: pytest.LogCaptureFixture, ) -> None: """Tests for appliance event sensors.""" caplog.set_level(logging.ERROR) @@ -726,16 +726,16 @@ async def test_event_sensors_states( indirect=["appliance"], ) async def test_sensor_unit_fetching( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, status_key: StatusKey, unit_get_status: str | None, unit_get_status_value: str | None, get_status_value_call_count: int, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" @@ -793,13 +793,13 @@ async def test_sensor_unit_fetching( indirect=["appliance"], ) async def test_sensor_unit_fetching_error( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, status_key: StatusKey, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" @@ -844,14 +844,14 @@ async def test_sensor_unit_fetching_error( indirect=["appliance"], ) async def test_sensor_unit_fetching_after_rate_limit_error( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, status_key: StatusKey, unit: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test that the sensor entities are capable of fetching units.""" diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index 2618cf951b7..b2056c41311 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -176,13 +176,13 @@ SERVICES_SET_PROGRAM_AND_OPTIONS = [ SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, ) async def test_key_value_services( - service_call: dict[str, Any], hass: HomeAssistant, device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, + service_call: dict[str, Any], ) -> None: """Create and test services.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -224,16 +224,16 @@ async def test_key_value_services( ], ) async def test_programs_and_options_actions_deprecation( - service_call: dict[str, Any], - issue_id: str, hass: HomeAssistant, + hass_client: ClientSessionGenerator, device_registry: dr.DeviceRegistry, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, + service_call: dict[str, Any], + issue_id: str, ) -> None: """Test deprecated service keys.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -294,14 +294,14 @@ async def test_programs_and_options_actions_deprecation( ), ) async def test_set_program_and_options( - service_call: dict[str, Any], - called_method: str, hass: HomeAssistant, device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, + service_call: dict[str, Any], + called_method: str, snapshot: SnapshotAssertion, ) -> None: """Test recognized options.""" @@ -337,14 +337,14 @@ async def test_set_program_and_options( ), ) async def test_set_program_and_options_exceptions( - service_call: dict[str, Any], - error_regex: str, hass: HomeAssistant, device_registry: dr.DeviceRegistry, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, appliance: HomeAppliance, + service_call: dict[str, Any], + error_regex: str, ) -> None: """Test recognized options.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -367,13 +367,13 @@ async def test_set_program_and_options_exceptions( SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, ) async def test_services_exception_device_id( - service_call: dict[str, Any], hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, appliance: HomeAppliance, - device_registry: dr.DeviceRegistry, + service_call: dict[str, Any], ) -> None: """Raise a HomeAssistantError when there is an API error.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -393,10 +393,10 @@ async def test_services_exception_device_id( async def test_services_appliance_not_found( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - device_registry: dr.DeviceRegistry, ) -> None: """Raise a ServiceValidationError when device id does not match.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -439,13 +439,13 @@ async def test_services_appliance_not_found( SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, ) async def test_services_exception( - service_call: dict[str, Any], hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client_with_exception: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, appliance: HomeAppliance, - device_registry: dr.DeviceRegistry, + service_call: dict[str, Any], ) -> None: """Raise a ValueError when device id does not match.""" assert config_entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index e0475c1778d..ca9688fd427 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -71,13 +71,13 @@ def platforms() -> list[str]: @pytest.mark.parametrize("appliance", ["Washer"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" client.get_available_program = AsyncMock( @@ -148,14 +148,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -219,9 +219,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True) async def test_switch_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if switch entities availability are based on the appliance connection state.""" @@ -300,16 +300,16 @@ async def test_switch_entity_availability( indirect=["appliance"], ) async def test_switch_functionality( - entity_id: str, - settings_key_arg: SettingKey, - setting_value_arg: Any, - service: str, - state: str, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], + entity_id: str, + service: str, + settings_key_arg: SettingKey, + setting_value_arg: Any, + state: str, appliance: HomeAppliance, - client: MagicMock, ) -> None: """Test switch functionality.""" @@ -345,14 +345,14 @@ async def test_switch_functionality( indirect=["appliance"], ) async def test_program_switch_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, program_key: ProgramKey, initial_state: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, - client: MagicMock, ) -> None: """Test switch functionality.""" @@ -450,14 +450,14 @@ async def test_program_switch_functionality( ], ) async def test_switch_exception_handling( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, service: str, mock_attr: str, exception_match: str, - hass: HomeAssistant, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, - client_with_exception: MagicMock, ) -> None: """Test exception handling.""" client_with_exception.get_all_programs.side_effect = None @@ -504,18 +504,16 @@ async def test_switch_exception_handling( @pytest.mark.parametrize( - ("entity_id", "status", "service", "state", "appliance"), + ("entity_id", "service", "state", "appliance"), [ ( "switch.fridgefreezer_freezer_super_mode", - {SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: True}, SERVICE_TURN_ON, STATE_ON, "FridgeFreezer", ), ( "switch.fridgefreezer_freezer_super_mode", - {SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: False}, SERVICE_TURN_OFF, STATE_OFF, "FridgeFreezer", @@ -524,15 +522,13 @@ async def test_switch_exception_handling( indirect=["appliance"], ) async def test_ent_desc_switch_functionality( - entity_id: str, - status: dict, - service: str, - state: str, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - appliance: HomeAppliance, - client: MagicMock, + entity_id: str, + service: str, + state: str, ) -> None: """Test switch functionality - entity description setup.""" @@ -550,7 +546,6 @@ async def test_ent_desc_switch_functionality( "entity_id", "status", "service", - "mock_attr", "appliance", "exception_match", ), @@ -559,7 +554,6 @@ async def test_ent_desc_switch_functionality( "switch.fridgefreezer_freezer_super_mode", {SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: ""}, SERVICE_TURN_ON, - "set_setting", "FridgeFreezer", r"Error.*turn.*on.*", ), @@ -567,7 +561,6 @@ async def test_ent_desc_switch_functionality( "switch.fridgefreezer_freezer_super_mode", {SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: ""}, SERVICE_TURN_OFF, - "set_setting", "FridgeFreezer", r"Error.*turn.*off.*", ), @@ -575,16 +568,14 @@ async def test_ent_desc_switch_functionality( indirect=["appliance"], ) async def test_ent_desc_switch_exception_handling( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, status: dict[SettingKey, str], service: str, - mock_attr: str, exception_match: str, - hass: HomeAssistant, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - config_entry: MockConfigEntry, - appliance: HomeAppliance, - client_with_exception: MagicMock, ) -> None: """Test switch exception handling - entity description setup.""" client_with_exception.get_settings.side_effect = None @@ -658,16 +649,16 @@ async def test_ent_desc_switch_exception_handling( indirect=["appliance"], ) async def test_power_switch( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, allowed_values: list[str | None] | None, service: str, setting_value_arg: str, power_state: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, - client: MagicMock, ) -> None: """Test power switch functionality.""" client.get_settings.side_effect = None @@ -706,11 +697,11 @@ async def test_power_switch( ], ) async def test_power_switch_fetch_off_state_from_current_value( - initial_value: str, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + initial_value: str, ) -> None: """Test power switch functionality to fetch the off state from the current value.""" client.get_settings.side_effect = None @@ -755,14 +746,14 @@ async def test_power_switch_fetch_off_state_from_current_value( ], ) async def test_power_switch_service_validation_errors( - entity_id: str, - allowed_values: list[str | None] | None | HomeConnectError, - service: str, hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], exception_match: str, - client: MagicMock, + entity_id: str, + allowed_values: list[str | None] | None | HomeConnectError, + service: str, ) -> None: """Test power switch functionality validation errors.""" client.get_settings.side_effect = None @@ -807,12 +798,11 @@ async def test_power_switch_service_validation_errors( ) async def test_create_program_switch_deprecation_issue( hass: HomeAssistant, - appliance: HomeAppliance, - service: str, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, + service: str, ) -> None: """Test that we create an issue when an automation or script is using a program switch entity or the entity is used by the user.""" entity_id = "switch.washer_program_mix" @@ -888,13 +878,12 @@ async def test_create_program_switch_deprecation_issue( ) async def test_program_switch_deprecation_issue_fix( hass: HomeAssistant, - appliance: HomeAppliance, - service: str, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, + service: str, ) -> None: """Test we can fix the issues created when a program switch entity is in an automation or in a script or when is used.""" entity_id = "switch.washer_program_mix" @@ -1001,16 +990,16 @@ async def test_program_switch_deprecation_issue_fix( indirect=["appliance"], ) async def test_options_functionality( - entity_id: str, - option_key: OptionKey, - appliance: HomeAppliance, + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], set_active_program_options_side_effect: ActiveProgramNotSetError | None, set_selected_program_options_side_effect: SelectedProgramNotSetError | None, called_mock_method: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, + entity_id: str, + option_key: OptionKey, + appliance: HomeAppliance, ) -> None: """Test options functionality.""" if set_active_program_options_side_effect: diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index 56cdefe7d57..f1edbfd2bd7 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -48,13 +48,13 @@ def platforms() -> list[str]: @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) async def test_paired_depaired_devices_flow( - appliance: HomeAppliance, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert config_entry.state == ConfigEntryState.NOT_LOADED @@ -111,14 +111,14 @@ async def test_paired_depaired_devices_flow( indirect=["appliance"], ) async def test_connected_devices( - appliance: HomeAppliance, - keys_to_check: tuple, hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + appliance: HomeAppliance, + keys_to_check: tuple, ) -> None: """Test that devices reconnected. @@ -172,9 +172,9 @@ async def test_connected_devices( @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) async def test_time_entity_availability( hass: HomeAssistant, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, appliance: HomeAppliance, ) -> None: """Test if time entities availability are based on the appliance connection state.""" @@ -233,13 +233,13 @@ async def test_time_entity_availability( ], ) async def test_time_entity_functionality( + hass: HomeAssistant, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], appliance: HomeAppliance, entity_id: str, setting_key: SettingKey, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, ) -> None: """Test time entity functionality.""" assert config_entry.state is ConfigEntryState.NOT_LOADED @@ -277,13 +277,13 @@ async def test_time_entity_functionality( ], ) async def test_time_entity_error( + hass: HomeAssistant, + client_with_exception: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, setting_key: SettingKey, mock_attr: str, - hass: HomeAssistant, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - client_with_exception: MagicMock, ) -> None: """Test time entity error.""" client_with_exception.get_settings.side_effect = None @@ -322,11 +322,10 @@ async def test_time_entity_error( @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) async def test_create_alarm_clock_deprecation_issue( hass: HomeAssistant, - appliance: HomeAppliance, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, ) -> None: """Test that we create an issue when an automation or script is using a alarm clock time entity or the entity is used by the user.""" entity_id = f"{TIME_DOMAIN}.oven_alarm_clock" @@ -402,12 +401,11 @@ async def test_create_alarm_clock_deprecation_issue( @pytest.mark.parametrize("appliance", ["Oven"], indirect=True) async def test_alarm_clock_deprecation_issue_fix( hass: HomeAssistant, - appliance: HomeAppliance, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + client: MagicMock, config_entry: MockConfigEntry, integration_setup: Callable[[MagicMock], Awaitable[bool]], - client: MagicMock, - issue_registry: ir.IssueRegistry, - hass_client: ClientSessionGenerator, ) -> None: """Test we can fix the issues created when a alarm clock time entity is in an automation or in a script or when is used.""" entity_id = f"{TIME_DOMAIN}.oven_alarm_clock" From 24252edf38921d4811606f300d30c30bd957fc34 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 14:02:39 +0200 Subject: [PATCH 0022/1175] Handle TimeoutError for lamarzocco (#144042) --- homeassistant/components/lamarzocco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 1d77dbc2f1a..ad9fec28fb4 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_failed" ) from ex - except RequestNotSuccessful as ex: + except (RequestNotSuccessful, TimeoutError) as ex: _LOGGER.debug(ex, exc_info=True) raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="api_error" From 60b6ff40645f90ce75eec80fde6cf0e20960b283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Thu, 1 May 2025 14:52:32 +0200 Subject: [PATCH 0023/1175] Matter Laundry Dryer fixture (#144043) * Create laundry_dryer.json * Add snapshots * Format fixture * Set CurrentPhase attribute * Set OperationalState attribute * Update snapshot --- tests/components/matter/conftest.py | 1 + .../matter/fixtures/nodes/laundry_dryer.json | 307 ++++++++++++++++++ .../matter/snapshots/test_button.ambr | 188 +++++++++++ .../matter/snapshots/test_select.ambr | 58 ++++ .../matter/snapshots/test_sensor.ambr | 120 +++++++ .../matter/snapshots/test_switch.ambr | 48 +++ 6 files changed, 722 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/laundry_dryer.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 04aeba4546f..8488b0e1af9 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -92,6 +92,7 @@ async def integration_fixture( "generic_switch", "generic_switch_multi", "humidity_sensor", + "laundry_dryer", "leak_sensor", "light_sensor", "microwave_oven", diff --git a/tests/components/matter/fixtures/nodes/laundry_dryer.json b/tests/components/matter/fixtures/nodes/laundry_dryer.json new file mode 100644 index 00000000000..a74bca934a0 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/laundry_dryer.json @@ -0,0 +1,307 @@ +{ + "node_id": 8, + "date_commissioned": "2025-05-01T11:45:46.203438", + "last_interview": "2025-05-01T11:45:46.203452", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 43, 45, 48, 49, 51, 54, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Mock Laundrydryer", + "0/40/4": 32768, + "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/17": true, + "0/40/18": "8A7EFAF22659A7C6", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 5, + "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, 17, 18, 19, 21, + 22, 24, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/45/0": 0, + "0/45/65532": 1, + "0/45/65533": 2, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [0, 65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkIH8Iu2", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZD1j4HmibD6Yw==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 11, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65532, 65533, 65528, 65529, 65531 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": 3, + "0/54/3": null, + "0/54/4": null, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65532, 65533, 65528, 65529, + 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRCBgkBwEkCAEwCUEEuBSQYARV1MtZ/zTYCZDFAchE6gYPl8EQsnZ/zBOFY/+CRpZdiSIJdKySB6kixHqnFG5AlLLuN0kV2p3RgtFNhDcKNQEoARgkAgE2AwQCBAEYMAQUHBnbZ0B6X2b4Hrmm7ND49lbGb4MwBRRqGquZZYwbDAaOinVVrS9sWTozoBgwC0AYHLmEMzw4m5K4nFJO6x8PB5xwkHJ0QtPgowB2/HYdTyR+MIPJRQfiPZB2WSzaDQpkMj+niAV9X59mKSwTntitGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyvr+z4yBxEDoiyCFg+i408LqC3j0UMvTszBv1051g2EMrAzBkj+0RZFsSl3eQ3D2c7mTcH6GERtlk4BqGvC1qDcKNQEpARgkAmAwBBRqGquZZYwbDAaOinVVrS9sWTozoDAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQCIuoikQZU9LkDKw7dcTVVXBDlTyBol3w070PIIw8BbaQD5qCeIv/3cI5/X5sAYTmemRq0ZPMjAw1dsN+wodzm8Y", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BIPshBqc9a7nNK00eRrviEzHfe/cfATY9VngqKv17+uAUpy3XujhZBjkAQyhYAaSKxVzSfVttY4FVQkpXIHZFlA=", + "2": 4939, + "3": 2, + "4": 8, + "5": "ha-freebox", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEg+yEGpz1ruc0rTR5Gu+ITMd979x8BNj1WeCoq/Xv64BSnLde6OFkGOQBDKFgBpIrFXNJ9W21jgVVCSlcgdkWUDcKNQEpARgkAmAwBBRPkvAMbwLEubfgETM7L7icezGlHzAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQIKyooBXllxj1uo4Zn4CBbZqECNdO3wwzlhl7ZEygrWa04gBa5rVqgg+JahrvXD6HPHu4XldWIULtqTCPPIm4OsY" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 6, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "1/6/0": false, + "1/6/65532": 2, + "1/6/65533": 6, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 124, + "1": 1 + } + ], + "1/29/1": [3, 6, 29, 86, 96], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "1/86/4": 0, + "1/86/5": ["Low", "Medium", "High"], + "1/86/65532": 2, + "1/86/65533": 1, + "1/86/65528": [], + "1/86/65529": [0], + "1/86/65531": [4, 5, 65532, 65533, 65528, 65529, 65531], + "1/96/0": ["pre-soak", "rinse", "spin"], + "1/96/1": 0, + "1/96/2": null, + "1/96/3": [ + { + "0": 0 + }, + { + "0": 1 + }, + { + "0": 2 + }, + { + "0": 3 + } + ], + "1/96/4": 1, + "1/96/5": { + "0": 0 + }, + "1/96/65532": 0, + "1/96/65533": 3, + "1/96/65528": [4], + "1/96/65529": [0, 1, 2, 3], + "1/96/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 448136eeed2..0563d1138b1 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -573,6 +573,194 @@ 'state': 'unknown', }) # --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_pause-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_laundrydryer_pause', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pause', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pause', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_pause-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Laundrydryer Pause', + }), + 'context': , + 'entity_id': 'button.mock_laundrydryer_pause', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_resume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_laundrydryer_resume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Resume', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'resume', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateResumeButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_resume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Laundrydryer Resume', + }), + 'context': , + 'entity_id': 'button.mock_laundrydryer_resume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_laundrydryer_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateStartButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Laundrydryer Start', + }), + 'context': , + 'entity_id': 'button.mock_laundrydryer_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.mock_laundrydryer_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateStopButton-96-65529', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[laundry_dryer][button.mock_laundrydryer_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Laundrydryer Stop', + }), + 'context': , + 'entity_id': 'button.mock_laundrydryer_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[microwave_oven][button.microwave_oven_pause-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index f4b86271a56..2665e59bf33 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -787,6 +787,64 @@ 'state': 'previous', }) # --- +# name: test_selects[laundry_dryer][select.mock_laundrydryer_temperature_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_laundrydryer_temperature_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_level', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-TemperatureControlSelectedTemperatureLevel-86-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[laundry_dryer][select.mock_laundrydryer_temperature_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Laundrydryer Temperature level', + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'context': , + 'entity_id': 'select.mock_laundrydryer_temperature_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Low', + }) +# --- # name: test_selects[multi_endpoint_light][select.inovelli_dimming_edge-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 550c9edd160..b7fd91ba45e 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2406,6 +2406,126 @@ 'state': '0.0', }) # --- +# name: test_sensors[laundry_dryer][sensor.mock_laundrydryer_current_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_laundrydryer_current_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateCurrentPhase-96-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[laundry_dryer][sensor.mock_laundrydryer_current_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Laundrydryer Current phase', + 'options': list([ + 'pre-soak', + 'rinse', + 'spin', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_laundrydryer_current_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pre-soak', + }) +# --- +# name: test_sensors[laundry_dryer][sensor.mock_laundrydryer_operational_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_laundrydryer_operational_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operational state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operational_state', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalState-96-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[laundry_dryer][sensor.mock_laundrydryer_operational_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Laundrydryer Operational state', + 'options': list([ + 'stopped', + 'running', + 'paused', + 'error', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_laundrydryer_operational_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'running', + }) +# --- # name: test_sensors[light_sensor][sensor.mock_light_sensor_illuminance-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index f7d0b66c5f1..9204a2b8e3a 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -334,6 +334,54 @@ 'state': 'off', }) # --- +# name: test_switches[laundry_dryer][switch.mock_laundrydryer_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_laundrydryer_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[laundry_dryer][switch.mock_laundrydryer_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Laundrydryer Power', + }), + 'context': , + 'entity_id': 'switch.mock_laundrydryer_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switches[on_off_plugin_unit][switch.mock_onoffpluginunit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 7fcad580cb95be1a5cd9ff8c83a601fa4212f936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 15:14:11 +0200 Subject: [PATCH 0024/1175] Update miele program codes and strings (#144049) --- homeassistant/components/miele/const.py | 28 ++++++++++++++++++--- homeassistant/components/miele/strings.json | 12 +++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 85934afae09..e77afe02e00 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -246,6 +246,7 @@ STATE_PROGRAM_PHASE_OVEN = { } STATE_PROGRAM_PHASE_WARMING_DRAWER = { 0: "not_running", + 3073: "heating_up", 3075: "door_open", 3094: "keeping_warm", 3088: "cooling_down", @@ -404,14 +405,21 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = { TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. 0: "no_program", # Extrapolated from other device types + 2: "cottons", + 3: "minimum_iron", + 4: "woollens_handcare", + 5: "delicates", + 6: "warm_air", + 8: "express", 10: "automatic_plus", 20: "cottons", 23: "cottons_hygiene", 30: "minimum_iron", - 31: "gentle_minimum_iron", + 31: "bed_linen", 40: "woollens_handcare", 50: "delicates", 60: "warm_air", + 66: "eco", 70: "cool_air", 80: "express", 90: "cottons", @@ -449,17 +457,29 @@ OVEN_PROGRAM_ID: dict[int, str] = { 31: "bottom_heat", 35: "moisture_plus_auto_roast", 40: "moisture_plus_fan_plus", + 48: "moisture_plus_auto_roast", + 49: "moisture_plus_fan_plus", + 50: "moisture_plus_intensive_bake", + 51: "moisture_plus_conventional_heat", 74: "moisture_plus_intensive_bake", 76: "moisture_plus_conventional_heat", - 49: "moisture_plus_fan_plus", + 323: "pyrolytic", + 326: "descale", + 335: "shabbat_program", + 336: "yom_tov", 356: "defrost", 357: "drying", 358: "heat_crockery", + 360: "low_temperature_cooking", 361: "steam_cooking", 362: "keeping_warm", 512: "1_tray", 513: "2_trays", 529: "baking_tray", + 554: "baiser_one_large", + 555: "baiser_several_small", + 556: "lemon_meringue_pie", + 557: "viennese_apple_strudel", 621: "prove_15_min", 622: "prove_30_min", 623: "prove_45_min", @@ -673,7 +693,7 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { 2019: "defrosting_with_steam", 2020: "blanching", 2021: "bottling", - 2022: "heat_crockery", + 2022: "sterilize_crockery", 2023: "prove_dough", 2027: "soak", 2029: "reheating_with_microwave", @@ -745,7 +765,7 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { 2129: "potatoes_floury_diced", 2130: "german_turnip_sliced", 2131: "german_turnip_cut_into_batons", - 2132: "german_turnip_sliced", + 2132: "german_turnip_diced", 2133: "pumpkin_diced", 2134: "corn_on_the_cob", 2135: "mangel_cut", diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 65a38612afd..7400c2f215c 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -316,6 +316,8 @@ "automatic_plus": "Automatic plus", "baking_tray": "Baking tray", "barista_assistant": "BaristaAssistant", + "baser_one_large": "Baiser one large", + "baser_severall_small": "Baiser several small", "basket_program": "Basket program", "basmati_rice_rapid_steam_cooking": "Basmati rice (rapid steam cooking)", "basmati_rice_steam_cooking": "Basmati rice (steam cooking)", @@ -471,7 +473,7 @@ "gentle_minimum_iron": "Gentle minimum iron", "gentle_smoothing": "Gentle smoothing", "german_turnip_cut_into_batons": "German turnip (cut into batons)", - "german_turnip_sliced": "German turnip (sliced)", + "german_turnip_diced": "German turnip (diced)", "gilt_head_bream_fillet": "Gilt-head bream (fillet)", "gilt_head_bream_whole": "Gilt-head bream (whole)", "glasses_warm": "Glasses warm", @@ -492,7 +494,6 @@ "greenage_plums": "Greenage plums", "halibut_fillet_2_cm": "Halibut (fillet, 2 cm)", "halibut_fillet_3_cm": "Halibut (fillet, 3 cm)", - "heat_crockery": "Heat crockery", "heating_damp_flannels": "Heating damp flannels", "hens_eggs_size_l_hard": "Hen’s eggs (size „L“, hard)", "hens_eggs_size_l_medium": "Hen’s eggs (size „L“, medium)", @@ -532,9 +533,11 @@ "latte_macchiato": "Latte macchiato", "leek_pieces": "Leek (pieces)", "leek_rings": "Leek (rings)", + "lemon_meringue_pie": "Lemon meringue pie", "long_coffee": "Long coffee", "long_grain_rice_general_rapid_steam_cooking": "Long grain rice (general, rapid steam cooking)", "long_grain_rice_general_steam_cooking": "Long grain rice (general, steam cooking)", + "low_temperature_cooking": "Low temperature cooking", "maintenance": "Maintenance program", "make_yoghurt": "Make yoghurt", "mangel_cut": "Mangel (cut)", @@ -673,6 +676,7 @@ "prove_dough": "Prove dough", "pumpkin_diced": "Pumpkin (diced)", "pumpkin_soup": "Pumpkin soup", + "pyrolytic": "Pyrolytic", "quick_mw": "Quick MW", "quick_power_wash": "QuickPowerWash", "quinces_diced": "Quinces (diced)", @@ -725,6 +729,7 @@ "sea_devil_fillet_3_cm": "Sea devil (fillet, 3 cm)", "sea_devil_fillet_4_cm": "Sea devil (fillet, 4 cm)", "separate_rinse_starch": "Separate rinse/starch", + "shabbat_program": "Shabbat program", "sheyang_rapid_steam_cooking": "Sheyang (rapid steam cooking)", "sheyang_steam_cooking": "Sheyang (steam cooking)", "shirts": "Shirts", @@ -755,6 +760,7 @@ "steam_care": "Steam care", "steam_cooking": "Steam cooking", "steam_smoothing": "Steam smoothing", + "sterilize_crockery": "Sterilize crockery", "stuffed_cabbage": "Stuffed cabbage", "sweat_onions": "Sweat onions", "swede_cut_into_batons": "Swede (cut into batons)", @@ -793,6 +799,7 @@ "veal_sausages": "Veal sausages", "venus_clams": "Venus clams", "very_hot_water": "Very hot water", + "viennese_apple_strudel": "Viennese apple strudel", "viennese_silverside": "Viennese silverside", "warm_air": "Warm air", "wheat_cracked": "Wheat (cracked)", @@ -817,6 +824,7 @@ "yellow_beans_cut": "Yellow beans (cut)", "yellow_beans_whole": "Yellow beans (whole)", "yellow_split_peas": "Yellow split peas", + "yom_tov": "Yom tov", "zander_fillet": "Zander (fillet)" } }, From 80d714b8651811b7765f93e1a5de0dd3a3f28cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 16:06:49 +0200 Subject: [PATCH 0025/1175] Use action property defined in MieleEntity (#144052) --- homeassistant/components/miele/button.py | 3 +-- homeassistant/components/miele/climate.py | 8 ++------ homeassistant/components/miele/entity.py | 2 +- homeassistant/components/miele/switch.py | 13 ++++--------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index e4aacc5124c..70d4489e9be 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -131,8 +131,7 @@ class MieleButton(MieleEntity, ButtonEntity): return ( super().available - and self.entity_description.press_data - in self.coordinator.data.actions[self._device_id].process_actions + and self.entity_description.press_data in self.action.process_actions ) async def async_press(self) -> None: diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 3b591965d2f..054ab227ca6 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -201,9 +201,7 @@ class MieleClimate(MieleEntity, ClimateEntity): """Return the maximum target temperature.""" return cast( float, - self.coordinator.data.actions[self._device_id] - .target_temperature[self.entity_description.zone - 1] - .max, + self.action.target_temperature[self.entity_description.zone - 1].max, ) @property @@ -211,9 +209,7 @@ class MieleClimate(MieleEntity, ClimateEntity): """Return the minimum target temperature.""" return cast( float, - self.coordinator.data.actions[self._device_id] - .target_temperature[self.entity_description.zone - 1] - .min, + self.action.target_temperature[self.entity_description.zone - 1].min, ) async def async_set_temperature(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/miele/entity.py b/homeassistant/components/miele/entity.py index a84c1f1108b..f9ed4f0bf48 100644 --- a/homeassistant/components/miele/entity.py +++ b/homeassistant/components/miele/entity.py @@ -47,7 +47,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]): return self.coordinator.data.devices[self._device_id] @property - def actions(self) -> MieleAction: + def action(self) -> MieleAction: """Return the actions object.""" return self.coordinator.data.actions[self._device_id] diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index 74a9f0c4785..427d90968b7 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -169,15 +169,14 @@ class MielePowerSwitch(MieleSwitch): @property def is_on(self) -> bool | None: """Return the state of the switch.""" - return self.coordinator.data.actions[self._device_id].power_off_enabled + return self.action.power_off_enabled @property def available(self) -> bool: """Return the availability of the entity.""" return ( - self.coordinator.data.actions[self._device_id].power_off_enabled - or self.coordinator.data.actions[self._device_id].power_on_enabled + self.action.power_off_enabled or self.action.power_on_enabled ) and super().available async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None: @@ -192,12 +191,8 @@ class MielePowerSwitch(MieleSwitch): "entity": self.entity_id, }, ) from err - self.coordinator.data.actions[self._device_id].power_on_enabled = cast( - bool, mode - ) - self.coordinator.data.actions[self._device_id].power_off_enabled = not cast( - bool, mode - ) + self.action.power_on_enabled = cast(bool, mode) + self.action.power_off_enabled = not cast(bool, mode) self.async_write_ha_state() From 4013b418dd2289133a0da8227d2d2951901c8f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 16:33:30 +0200 Subject: [PATCH 0026/1175] Use device class transation for door in miele (#144053) --- homeassistant/components/miele/strings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 7400c2f215c..7962f887e4f 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -115,9 +115,6 @@ }, "entity": { "binary_sensor": { - "door": { - "name": "Door" - }, "failure": { "name": "Failure" }, From b8881ed85bac8a333df2a896b4f68063a7275e26 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 1 May 2025 16:36:05 +0200 Subject: [PATCH 0027/1175] Fix test in Husqvarna Automower (#144055) --- .../husqvarna_automower/test_lawn_mower.py | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 044989e5cf0..a8c34a3fc79 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -21,37 +21,42 @@ from .const import TEST_MOWER_ID from tests.common import MockConfigEntry, async_fire_time_changed -async def test_lawn_mower_states( - hass: HomeAssistant, - mock_automower_client: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - values: dict[str, MowerAttributes], -) -> None: - """Test lawn_mower state.""" - await setup_integration(hass, mock_config_entry) - state = hass.states.get("lawn_mower.test_mower_1") - assert state is not None - assert state.state == LawnMowerActivity.DOCKED - - for activity, state, expected_state in ( +@pytest.mark.parametrize( + ("activity", "mower_state", "expected_state"), + [ (MowerActivities.UNKNOWN, MowerStates.PAUSED, LawnMowerActivity.PAUSED), - (MowerActivities.MOWING, MowerStates.NOT_APPLICABLE, LawnMowerActivity.MOWING), + (MowerActivities.MOWING, MowerStates.IN_OPERATION, LawnMowerActivity.MOWING), (MowerActivities.NOT_APPLICABLE, MowerStates.ERROR, LawnMowerActivity.ERROR), ( MowerActivities.GOING_HOME, MowerStates.IN_OPERATION, LawnMowerActivity.RETURNING, ), - ): - values[TEST_MOWER_ID].mower.activity = activity - values[TEST_MOWER_ID].mower.state = state - 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("lawn_mower.test_mower_1") - assert state.state == expected_state + ], +) +async def test_lawn_mower_states( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + values: dict[str, MowerAttributes], + activity: MowerActivities, + mower_state: MowerStates, + expected_state: LawnMowerActivity, +) -> None: + """Test lawn_mower state.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("lawn_mower.test_mower_1") + assert state is not None + assert state.state == LawnMowerActivity.DOCKED + values[TEST_MOWER_ID].mower.activity = activity + values[TEST_MOWER_ID].mower.state = mower_state + 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("lawn_mower.test_mower_1") + assert state.state == expected_state @pytest.mark.parametrize( From bab699eb0cdbddabc8e274273a1b079d5c346b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Thu, 1 May 2025 17:06:08 +0200 Subject: [PATCH 0028/1175] Matter Solar power fixture (#144058) --- tests/components/matter/conftest.py | 1 + .../matter/fixtures/nodes/solar_power.json | 334 ++++++++++++++++++ .../matter/snapshots/test_sensor.ambr | 174 +++++++++ 3 files changed, 509 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/solar_power.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 8488b0e1af9..f61cb54cd0b 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -110,6 +110,7 @@ async def integration_fixture( "silabs_laundrywasher", "silabs_water_heater", "smoke_detector", + "solar_power", "switch_unit", "temperature_sensor", "thermostat", diff --git a/tests/components/matter/fixtures/nodes/solar_power.json b/tests/components/matter/fixtures/nodes/solar_power.json new file mode 100644 index 00000000000..4b7c4af5b43 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/solar_power.json @@ -0,0 +1,334 @@ +{ + "node_id": 1, + "date_commissioned": "2025-04-26T13:59:01.038380", + "last_interview": "2025-04-26T13:59:01.038432", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 43, 48, 49, 50, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "SolarPower", + "0/40/4": 32768, + "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": "693B7500B6407671", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 5, + "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, 21, 22, + 24, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkLqrfVa", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZDZEOyJQB4D1w==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 37, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRARgkBwEkCAEwCUEEr/7Cv/8E0M1xlXrJsFennQiNL1eZk89SD0aQBqwBRM75xTNqokuHgKtObf8DW464ZlD9Pq++SURJv0WmvN2xPTcKNQEoARgkAgE2AwQCBAEYMAQUlHJKPttZOtq8Ane2vBQeAtYL97YwBRRqGquZZYwbDAaOinVVrS9sWTozoBgwC0AlmKJvIDcTdn2P6Bbc8PSdI08AqnQJRxpiogLNN1M05l0HJgGpE8G8h2W9yWuSvbeVulclJ+TLvzjafmQLWFPVGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyvr+z4yBxEDoiyCFg+i408LqC3j0UMvTszBv1051g2EMrAzBkj+0RZFsSl3eQ3D2c7mTcH6GERtlk4BqGvC1qDcKNQEpARgkAmAwBBRqGquZZYwbDAaOinVVrS9sWTozoDAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQCIuoikQZU9LkDKw7dcTVVXBDlTyBol3w070PIIw8BbaQD5qCeIv/3cI5/X5sAYTmemRq0ZPMjAw1dsN+wodzm8Y", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BIPshBqc9a7nNK00eRrviEzHfe/cfATY9VngqKv17+uAUpy3XujhZBjkAQyhYAaSKxVzSfVttY4FVQkpXIHZFlA=", + "2": 4939, + "3": 2, + "4": 1, + "5": "Maison", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEg+yEGpz1ruc0rTR5Gu+ITMd979x8BNj1WeCoq/Xv64BSnLde6OFkGOQBDKFgBpIrFXNJ9W21jgVVCSlcgdkWUDcKNQEpARgkAmAwBBRPkvAMbwLEubfgETM7L7icezGlHzAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQIKyooBXllxj1uo4Zn4CBbZqECNdO3wwzlhl7ZEygrWa04gBa5rVqgg+JahrvXD6HPHu4XldWIULtqTCPPIm4OsY" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 1296, + "1": 1 + }, + { + "0": 17, + "1": 1 + }, + { + "0": 23, + "1": 1 + } + ], + "1/29/1": [3, 29, 47, 144, 145, 156], + "1/29/2": [], + "1/29/3": [], + "1/29/4": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "1/47/0": 0, + "1/47/1": 0, + "1/47/2": "", + "1/47/31": [], + "1/47/65532": 1, + "1/47/65533": 1, + "1/47/65528": [], + "1/47/65529": [], + "1/47/65531": [0, 1, 2, 31, 65532, 65533, 65528, 65529, 65531], + "1/144/0": 1, + "1/144/1": 3, + "1/144/2": [ + { + "0": 5, + "1": true, + "2": 0, + "3": 5000000, + "4": [ + { + "0": 0, + "1": 5000000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 2, + "1": true, + "2": 0, + "3": 24000, + "4": [ + { + "0": 0, + "1": 24000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + }, + { + "0": 1, + "1": true, + "2": 0, + "3": 300000, + "4": [ + { + "0": 0, + "1": 300000, + "2": 5000, + "3": 2000, + "4": 3000 + } + ] + } + ], + "1/144/4": 234899, + "1/144/5": -3620, + "1/144/8": -850000, + "1/144/65532": 1, + "1/144/65533": 1, + "1/144/65528": [], + "1/144/65529": [], + "1/144/65531": [0, 1, 2, 4, 5, 8, 65532, 65533, 65528, 65529, 65531], + "1/145/0": null, + "1/145/2": { + "0": 42279000 + }, + "1/145/65532": 3, + "1/145/65533": 1, + "1/145/65528": [], + "1/145/65529": [], + "1/145/65531": [0, 65532, 65533, 65528, 65529, 65531], + "1/156/65532": 1, + "1/156/65533": 1, + "1/156/65528": [], + "1/156/65529": [], + "1/156/65531": [65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index b7fd91ba45e..4a3ac8d75f9 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -4196,6 +4196,180 @@ 'state': '0.000', }) # --- +# name: test_sensors[solar_power][sensor.solarpower_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solarpower_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SolarPower Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarpower_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-3.62', + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solarpower_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementWatt-144-8', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarPower Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarpower_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-850.0', + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solarpower_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementVoltage-144-4', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SolarPower Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarpower_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '234.899', + }) +# --- # name: test_sensors[temperature_sensor][sensor.mock_temperature_sensor_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 361d93eb96cf781c0ddbce65d8e971720c9d7d76 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 1 May 2025 18:35:48 +0200 Subject: [PATCH 0029/1175] Remove deprecated binary sensor in Husqvarna Automower (#144064) * Remove deprecated binary sensor in Husqvarna Automower * snapshot --- .../husqvarna_automower/binary_sensor.py | 60 ------------ .../husqvarna_automower/quality_scale.yaml | 4 +- .../husqvarna_automower/strings.json | 9 -- .../snapshots/test_binary_sensor.ambr | 94 ------------------- .../husqvarna_automower/test_binary_sensor.py | 40 +------- .../husqvarna_automower/test_init.py | 8 +- 6 files changed, 10 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/binary_sensor.py b/homeassistant/components/husqvarna_automower/binary_sensor.py index 1e5b9fac990..82c78123bde 100644 --- a/homeassistant/components/husqvarna_automower/binary_sensor.py +++ b/homeassistant/components/husqvarna_automower/binary_sensor.py @@ -3,29 +3,18 @@ from collections.abc import Callable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING from aioautomower.model import MowerActivities, MowerAttributes -from homeassistant.components.automation import automations_with_entity from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.components.script import scripts_with_entity from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) from . import AutomowerConfigEntry -from .const import DOMAIN from .coordinator import AutomowerDataUpdateCoordinator from .entity import AutomowerBaseEntity @@ -34,13 +23,6 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: - """Get list of related automations and scripts.""" - used_in = automations_with_entity(hass, entity_id) - used_in += scripts_with_entity(hass, entity_id) - return used_in - - @dataclass(frozen=True, kw_only=True) class AutomowerBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Automower binary sensor entity.""" @@ -59,12 +41,6 @@ MOWER_BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] = translation_key="leaving_dock", value_fn=lambda data: data.mower.activity == MowerActivities.LEAVING, ), - AutomowerBinarySensorEntityDescription( - key="returning_to_dock", - translation_key="returning_to_dock", - value_fn=lambda data: data.mower.activity == MowerActivities.GOING_HOME, - entity_registry_enabled_default=False, - ), ) @@ -107,39 +83,3 @@ class AutomowerBinarySensorEntity(AutomowerBaseEntity, BinarySensorEntity): def is_on(self) -> bool: """Return the state of the binary sensor.""" return self.entity_description.value_fn(self.mower_attributes) - - async def async_added_to_hass(self) -> None: - """Raise issue when entity is registered and was not disabled.""" - if TYPE_CHECKING: - assert self.unique_id - if not ( - entity_id := er.async_get(self.hass).async_get_entity_id( - BINARY_SENSOR_DOMAIN, DOMAIN, self.unique_id - ) - ): - return - if ( - self.enabled - and self.entity_description.key == "returning_to_dock" - and entity_used_in(self.hass, entity_id) - ): - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_entity_{self.entity_description.key}", - breaks_in_ha_version="2025.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_entity", - translation_placeholders={ - "entity_name": str(self.name), - "entity": entity_id, - }, - ) - else: - async_delete_issue( - self.hass, - DOMAIN, - f"deprecated_task_entity_{self.entity_description.key}", - ) - await super().async_added_to_hass() diff --git a/homeassistant/components/husqvarna_automower/quality_scale.yaml b/homeassistant/components/husqvarna_automower/quality_scale.yaml index 2fa41c02a4c..d0435c51eee 100644 --- a/homeassistant/components/husqvarna_automower/quality_scale.yaml +++ b/homeassistant/components/husqvarna_automower/quality_scale.yaml @@ -67,7 +67,9 @@ rules: reconfiguration-flow: status: exempt comment: no configuration possible - repair-issues: done + repair-issues: + status: exempt + comment: no issues available stale-devices: done # Platinum diff --git a/homeassistant/components/husqvarna_automower/strings.json b/homeassistant/components/husqvarna_automower/strings.json index 015d322c481..5b815e79263 100644 --- a/homeassistant/components/husqvarna_automower/strings.json +++ b/homeassistant/components/husqvarna_automower/strings.json @@ -39,9 +39,6 @@ "binary_sensor": { "leaving_dock": { "name": "Leaving dock" - }, - "returning_to_dock": { - "name": "Returning to dock" } }, "button": { @@ -323,12 +320,6 @@ } } }, - "issues": { - "deprecated_entity": { - "title": "The Husqvarna Automower {entity_name} sensor is deprecated", - "description": "The Husqvarna Automower entity `{entity}` is deprecated and will be removed in a future release.\nYou can use the new returning state of the lawn mower entity instead.\nPlease update your automations and scripts to replace the sensor entity with the newly added lawn mower entity.\nWhen you are done migrating you can disable `{entity}`." - } - }, "services": { "override_schedule": { "name": "Override schedule", diff --git a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr index a077eb134d4..bac9f187001 100644 --- a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr @@ -94,53 +94,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensor_snapshot[binary_sensor.test_mower_1_returning_to_dock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Returning to dock', - 'platform': 'husqvarna_automower', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'returning_to_dock', - 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_returning_to_dock', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_snapshot[binary_sensor.test_mower_1_returning_to_dock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Mower 1 Returning to dock', - }), - 'context': , - 'entity_id': 'binary_sensor.test_mower_1_returning_to_dock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensor_snapshot[binary_sensor.test_mower_2_charging-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -236,50 +189,3 @@ 'state': 'off', }) # --- -# name: test_binary_sensor_snapshot[binary_sensor.test_mower_2_returning_to_dock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.test_mower_2_returning_to_dock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Returning to dock', - 'platform': 'husqvarna_automower', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'returning_to_dock', - 'unique_id': '1234_returning_to_dock', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor_snapshot[binary_sensor.test_mower_2_returning_to_dock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test Mower 2 Returning to dock', - }), - 'context': , - 'entity_id': 'binary_sensor.test_mower_2_returning_to_dock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 30c9cc1bdd3..7812a684196 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -2,54 +2,16 @@ from unittest.mock import AsyncMock, patch -from aioautomower.model import MowerActivities, MowerAttributes -from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion -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 from . import setup_integration -from .const import TEST_MOWER_ID -from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_binary_sensor_states( - hass: HomeAssistant, - mock_automower_client: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - values: dict[str, MowerAttributes], -) -> None: - """Test binary sensor states.""" - await setup_integration(hass, mock_config_entry) - state = hass.states.get("binary_sensor.test_mower_1_charging") - assert state is not None - assert state.state == "off" - state = hass.states.get("binary_sensor.test_mower_1_leaving_dock") - assert state is not None - assert state.state == "off" - state = hass.states.get("binary_sensor.test_mower_1_returning_to_dock") - assert state is not None - assert state.state == "off" - - for activity, entity in ( - (MowerActivities.CHARGING, "test_mower_1_charging"), - (MowerActivities.LEAVING, "test_mower_1_leaving_dock"), - (MowerActivities.GOING_HOME, "test_mower_1_returning_to_dock"), - ): - values[TEST_MOWER_ID].mower.activity = activity - mock_automower_client.get_status.return_value = values - freezer.tick(SCAN_INTERVAL) - async_fire_time_changed(hass) - await hass.async_block_till_done() - state = hass.states.get(f"binary_sensor.{entity}") - assert state.state == "on" +from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/husqvarna_automower/test_init.py b/tests/components/husqvarna_automower/test_init.py index ec1fb7391b4..ecb92bb39cf 100644 --- a/tests/components/husqvarna_automower/test_init.py +++ b/tests/components/husqvarna_automower/test_init.py @@ -33,6 +33,7 @@ from tests.test_util.aiohttp import AiohttpClientMocker ADDITIONAL_NUMBER_ENTITIES = 1 ADDITIONAL_SENSOR_ENTITIES = 2 ADDITIONAL_SWITCH_ENTITIES = 1 +NUMBER_OF_ENTITIES_MOWER_2 = 11 async def test_load_unload_entry( @@ -250,7 +251,7 @@ async def test_coordinator_automatic_registry_cleanup( assert ( len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) - == current_entites - 12 + == current_entites - NUMBER_OF_ENTITIES_MOWER_2 ) assert ( len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) @@ -278,7 +279,10 @@ async def test_coordinator_automatic_registry_cleanup( async_fire_time_changed(hass) await hass.async_block_till_done() - assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 12 + assert ( + len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) + == NUMBER_OF_ENTITIES_MOWER_2 + ) assert ( len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == current_devices - 1 From 82b335a2c1382eb14ef6ffefc02783c5280efdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 1 May 2025 19:10:24 +0200 Subject: [PATCH 0030/1175] Flag strict typing for miele (#144060) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index 9752ae30fff..6aaa5d32a58 100644 --- a/.strict-typing +++ b/.strict-typing @@ -332,6 +332,7 @@ homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.met_eireann.* homeassistant.components.metoffice.* +homeassistant.components.miele.* homeassistant.components.mikrotik.* homeassistant.components.min_max.* homeassistant.components.minecraft_server.* diff --git a/mypy.ini b/mypy.ini index 94c47d7ce22..fbc8715f9b2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3076,6 +3076,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.miele.*] +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.mikrotik.*] check_untyped_defs = true disallow_incomplete_defs = true From 79f8bea48d7aff1847abd941dfdc90e221aa5ccc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 13:51:38 -0500 Subject: [PATCH 0031/1175] Avoid validation of ESPHome MAC when discovered entry is ignored or unchanged (#144071) fixes #144033 fixes #143991 --- .../components/esphome/config_flow.py | 10 +++ tests/components/esphome/test_config_flow.py | 66 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index d94ce99c6bf..75408246e78 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -22,6 +22,7 @@ import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ( + SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE, ConfigEntry, @@ -31,6 +32,7 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo @@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) ): return + if entry.source == SOURCE_IGNORE: + # Don't call _fetch_device_info() for ignored entries + raise AbortFlow("already_configured") + configured_host: str | None = entry.data.get(CONF_HOST) configured_port: int | None = entry.data.get(CONF_PORT) + if configured_host == host and configured_port == port: + # Don't probe to verify the mac is correct since + # the host and port matches. + raise AbortFlow("already_configured") configured_psk: str | None = entry.data.get(CONF_NOISE_PSK) await self._fetch_device_info(host, port or configured_port, configured_psk) updates: dict[str, Any] = {} diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 53abf6fb3ab..ead9167d258 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -27,7 +27,7 @@ from homeassistant.components.esphome.const import ( DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, DOMAIN, ) -from homeassistant.config_entries import ConfigFlowResult +from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -747,6 +747,35 @@ async def test_discovery_already_configured(hass: HomeAssistant) -> None: } +@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") +async def test_discovery_ignored(hass: HomeAssistant) -> None: + """Test discovery does not probe and ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + source=SOURCE_IGNORE, + ) + + entry.add_to_hass(hass) + + service_info = ZeroconfServiceInfo( + ip_address=ip_address("192.168.43.183"), + ip_addresses=[ip_address("192.168.43.183")], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={"mac": "1122334455aa"}, + type="mock_type", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: """Test discovery aborts if same mDNS packet arrives.""" @@ -786,8 +815,8 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: entry.add_to_hass(hass) service_info = ZeroconfServiceInfo( - ip_address=ip_address("192.168.43.183"), - ip_addresses=[ip_address("192.168.43.183")], + ip_address=ip_address("192.168.43.184"), + ip_addresses=[ip_address("192.168.43.184")], hostname="test8266.local.", name="mock_name", port=6053, @@ -806,9 +835,40 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: "mac": "11:22:33:44:55:aa", } + assert entry.data[CONF_HOST] == "192.168.43.184" assert entry.unique_id == "11:22:33:44:55:aa" +@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf") +async def test_discovery_abort_without_update_same_host_port( + hass: HomeAssistant, +) -> None: + """Test discovery aborts without update when hsot and port are the same.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + unique_id="11:22:33:44:55:aa", + ) + + entry.add_to_hass(hass) + + service_info = ZeroconfServiceInfo( + ip_address=ip_address("192.168.43.183"), + ip_addresses=[ip_address("192.168.43.183")], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={"address": "test8266.local", "mac": "1122334455aa"}, + type="mock_type", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") async def test_user_requires_psk(hass: HomeAssistant, mock_client: APIClient) -> None: """Test user step with requiring encryption key.""" From 71599b8e75ece570eb58fe273959074a6c2df2f3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 1 May 2025 22:20:50 +0300 Subject: [PATCH 0032/1175] Set Shelly PARALLEL_UPDATES (#144070) --- homeassistant/components/shelly/binary_sensor.py | 2 ++ homeassistant/components/shelly/button.py | 2 ++ homeassistant/components/shelly/climate.py | 2 ++ homeassistant/components/shelly/cover.py | 2 ++ homeassistant/components/shelly/event.py | 2 ++ homeassistant/components/shelly/light.py | 2 ++ homeassistant/components/shelly/number.py | 2 ++ homeassistant/components/shelly/quality_scale.yaml | 2 +- homeassistant/components/shelly/select.py | 2 ++ homeassistant/components/shelly/sensor.py | 2 ++ homeassistant/components/shelly/switch.py | 2 ++ homeassistant/components/shelly/text.py | 2 ++ homeassistant/components/shelly/update.py | 2 ++ homeassistant/components/shelly/valve.py | 2 ++ 14 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index b74578f1fb3..ed5a00fffb3 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -42,6 +42,8 @@ from .utils import ( is_rpc_momentary_input, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class BlockBinarySensorDescription( diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 06dffba5ead..77b4021b03b 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -32,6 +32,8 @@ from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .utils import get_device_entry_gen, get_rpc_key_ids +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class ShellyButtonDescription[ diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index f8cdb13ba9f..e1c55591da0 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -51,6 +51,8 @@ from .utils import ( is_rpc_thermostat_internal_actuator, ) +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index e9eb5acf161..d603636644b 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -21,6 +21,8 @@ from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoo from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import get_device_entry_gen, get_rpc_key_ids +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index ec5810581b1..c858e7b591f 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -38,6 +38,8 @@ from .utils import ( is_rpc_momentary_input, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class ShellyBlockEventDescription(EventEntityDescription): diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index ce31533b557..f5cffe37d5a 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -49,6 +49,8 @@ from .utils import ( percentage_to_brightness, ) +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index ab09ad1976a..49726f436d0 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -42,6 +42,8 @@ from .utils import ( get_virtual_component_ids, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription): diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 8fec824bcc1..83c3739a208 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -33,7 +33,7 @@ rules: entity-unavailable: done integration-owner: done log-when-unavailable: done - parallel-updates: todo + parallel-updates: done reauthentication-flow: done test-coverage: done diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index 98d374b496d..aec368f356b 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -28,6 +28,8 @@ from .utils import ( get_virtual_component_ids, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription): diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 79e4c97aead..986127b5836 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -63,6 +63,8 @@ from .utils import ( is_rpc_wifi_stations_disabled, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription): diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index ce9e4f065fb..507f701795e 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -39,6 +39,8 @@ from .utils import ( is_rpc_exclude_from_relay, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class BlockSwitchDescription(BlockEntityDescription, SwitchEntityDescription): diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index 811467f9e43..a780c464947 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -28,6 +28,8 @@ from .utils import ( get_virtual_component_ids, ) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class RpcTextDescription(RpcEntityDescription, TextEntityDescription): diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 12ce6dc70cd..2ff2462bd79 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -47,6 +47,8 @@ from .utils import get_device_entry_gen, get_release_url LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + @dataclass(frozen=True, kw_only=True) class RpcUpdateDescription(RpcEntityDescription, UpdateEntityDescription): diff --git a/homeassistant/components/shelly/valve.py b/homeassistant/components/shelly/valve.py index 1829f663b22..b748172ba3d 100644 --- a/homeassistant/components/shelly/valve.py +++ b/homeassistant/components/shelly/valve.py @@ -25,6 +25,8 @@ from .entity import ( ) from .utils import async_remove_shelly_entity, get_device_entry_gen +PARALLEL_UPDATES = 0 + @dataclass(kw_only=True, frozen=True) class BlockValveDescription(BlockEntityDescription, ValveEntityDescription): From 06bb692522a8b6c7dcf2069d1016b69f83ace08c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 15:23:56 -0500 Subject: [PATCH 0033/1175] Bump inkbird-ble to 0.16.1 (#144074) I made a mistake in one of the data lengths as I forgot to add the length of the id which is 2 bytes. I really wish vendors would stop putting raw data in this field. changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.16.0...v0.16.1 --- homeassistant/components/inkbird/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 79474f0cc28..38d406da62e 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -34,6 +34,10 @@ "local_name": "ITH-21-B", "connectable": false }, + { + "local_name": "IBS-P02B", + "connectable": false + }, { "local_name": "Ink@IAM-T1", "connectable": true @@ -49,5 +53,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.15.0"] + "requirements": ["inkbird-ble==0.16.1"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 9f3c53731c9..e796625f81c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -376,6 +376,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "inkbird", "local_name": "ITH-21-B", }, + { + "connectable": False, + "domain": "inkbird", + "local_name": "IBS-P02B", + }, { "connectable": True, "domain": "inkbird", diff --git a/requirements_all.txt b/requirements_all.txt index 235605bad07..593ba8bdacd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1239,7 +1239,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.15.0 +inkbird-ble==0.16.1 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75c80f5180f..5b3bb6d0a40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1054,7 +1054,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.15.0 +inkbird-ble==0.16.1 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From e2679004a19fe775a08926f76dc91cb6670fefc2 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 22:26:50 +0200 Subject: [PATCH 0034/1175] Add bluetooth connection availability to diagnostics for lamarzocco (#144012) * Add bluetooth connection availability to diagnostics for lamarzocco * make even more detailed --- .../components/lamarzocco/diagnostics.py | 12 +- .../snapshots/test_diagnostics.ambr | 1057 +++++++++-------- 2 files changed, 543 insertions(+), 526 deletions(-) diff --git a/homeassistant/components/lamarzocco/diagnostics.py b/homeassistant/components/lamarzocco/diagnostics.py index 6837dd6a9ee..7743523e01d 100644 --- a/homeassistant/components/lamarzocco/diagnostics.py +++ b/homeassistant/components/lamarzocco/diagnostics.py @@ -5,8 +5,10 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_MAC, CONF_TOKEN from homeassistant.core import HomeAssistant +from .const import CONF_USE_BLUETOOTH from .coordinator import LaMarzoccoConfigEntry TO_REDACT = { @@ -21,4 +23,12 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator = entry.runtime_data.config_coordinator device = coordinator.device - return async_redact_data(device.to_dict(), TO_REDACT) + data = { + "device": device.to_dict(), + "bluetooth_available": { + "options_enabled": entry.options.get(CONF_USE_BLUETOOTH, True), + CONF_MAC: CONF_MAC in entry.data, + CONF_TOKEN: CONF_TOKEN in entry.data, + }, + } + return async_redact_data(data, TO_REDACT) diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index 31292862824..33b4b4092f7 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -1,309 +1,22 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'dashboard': dict({ - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'config': dict({ - 'CMBackFlush': dict({ - 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', - 'status': 'Off', - }), - 'CMCoffeeBoiler': dict({ - 'enabled': True, - 'enabled_supported': False, - 'ready_start_time': None, - 'status': 'Ready', - 'target_temperature': 95.0, - 'target_temperature_max': 110, - 'target_temperature_min': 80, - 'target_temperature_step': 0.1, - }), - 'CMGroupDoses': dict({ - 'available_modes': list([ - 'PulsesType', - ]), - 'brewing_pressure': None, - 'brewing_pressure_supported': False, - 'continuous_dose': None, - 'continuous_dose_supported': False, - 'doses': dict({ - 'pulses_type': list([ - dict({ - 'dose': 126.0, - 'dose_index': 'DoseA', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 126.0, - 'dose_index': 'DoseB', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 160.0, - 'dose_index': 'DoseC', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - dict({ - 'dose': 77.0, - 'dose_index': 'DoseD', - 'dose_max': 9999.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), + 'bluetooth_available': dict({ + 'mac': False, + 'options_enabled': True, + 'token': True, + }), + 'device': dict({ + 'dashboard': dict({ + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'config': dict({ + 'CMBackFlush': dict({ + 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'status': 'Off', }), - 'mirror_with_group_1': None, - 'mirror_with_group_1_not_effective': False, - 'mirror_with_group_1_supported': False, - 'mode': 'PulsesType', - 'profile': None, - }), - 'CMHotWaterDose': dict({ - 'doses': list([ - dict({ - 'dose': 8.0, - 'dose_index': 'DoseA', - 'dose_max': 90.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), - 'enabled': True, - 'enabled_supported': False, - }), - 'CMMachineStatus': dict({ - 'available_modes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'brewing_start_time': None, - 'mode': 'BrewingMode', - 'next_status': dict({ - 'start_time': '2025-03-24T22:59:55.332000+00:00', - 'status': 'StandBy', - }), - 'status': 'PoweredOn', - }), - 'CMPreBrewing': dict({ - 'available_modes': list([ - 'PreBrewing', - 'PreInfusion', - 'Disabled', - ]), - 'dose_index_supported': True, - 'mode': 'PreInfusion', - 'times': dict({ - 'pre_brewing': list([ - dict({ - 'dose_index': 'DoseA', - 'seconds': dict({ - 'In': 0.5, - 'Out': 1.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseB', - 'seconds': dict({ - 'In': 0.5, - 'Out': 1.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseC', - 'seconds': dict({ - 'In': 3.3, - 'Out': 3.3, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseD', - 'seconds': dict({ - 'In': 2.0, - 'Out': 2.0, - }), - 'seconds_max': dict({ - 'In': 10.0, - 'Out': 10.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - ]), - 'pre_infusion': list([ - dict({ - 'dose_index': 'DoseA', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseB', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseC', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - dict({ - 'dose_index': 'DoseD', - 'seconds': dict({ - 'In': 0.0, - 'Out': 4.0, - }), - 'seconds_max': dict({ - 'In': 25.0, - 'Out': 25.0, - }), - 'seconds_min': dict({ - 'In': 0.0, - 'Out': 0.0, - }), - 'seconds_step': dict({ - 'In': 0.1, - 'Out': 0.1, - }), - }), - ]), - }), - }), - 'CMSteamBoilerTemperature': dict({ - 'enabled': True, - 'enabled_supported': True, - 'ready_start_time': None, - 'status': 'Off', - 'target_temperature': 123.9, - 'target_temperature_max': 140, - 'target_temperature_min': 95, - 'target_temperature_step': 0.1, - 'target_temperature_supported': True, - }), - }), - 'connected': True, - 'connection_date': '2025-03-20T16:44:47.479000+00:00', - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png', - 'location': 'HOME', - 'model_code': 'GS3AV', - 'model_name': 'GS3 AV', - 'name': 'GS012345', - 'offline_mode': False, - 'require_firmware_update': False, - 'serial_number': '**REDACTED**', - 'type': 'CoffeeMachine', - 'widgets': list([ - dict({ - 'code': 'CMMachineStatus', - 'index': 1, - 'output': dict({ - 'available_modes': list([ - 'BrewingMode', - 'StandBy', - ]), - 'brewing_start_time': None, - 'mode': 'BrewingMode', - 'next_status': dict({ - 'start_time': '2025-03-24T22:59:55.332000+00:00', - 'status': 'StandBy', - }), - 'status': 'PoweredOn', - }), - }), - dict({ - 'code': 'CMCoffeeBoiler', - 'index': 1, - 'output': dict({ + 'CMCoffeeBoiler': dict({ 'enabled': True, 'enabled_supported': False, 'ready_start_time': None, @@ -313,26 +26,7 @@ 'target_temperature_min': 80, 'target_temperature_step': 0.1, }), - }), - dict({ - 'code': 'CMSteamBoilerTemperature', - 'index': 1, - 'output': dict({ - 'enabled': True, - 'enabled_supported': True, - 'ready_start_time': None, - 'status': 'Off', - 'target_temperature': 123.9, - 'target_temperature_max': 140, - 'target_temperature_min': 95, - 'target_temperature_step': 0.1, - 'target_temperature_supported': True, - }), - }), - dict({ - 'code': 'CMGroupDoses', - 'index': 1, - 'output': dict({ + 'CMGroupDoses': dict({ 'available_modes': list([ 'PulsesType', ]), @@ -378,11 +72,33 @@ 'mode': 'PulsesType', 'profile': None, }), - }), - dict({ - 'code': 'CMPreBrewing', - 'index': 1, - 'output': dict({ + 'CMHotWaterDose': dict({ + 'doses': list([ + dict({ + 'dose': 8.0, + 'dose_index': 'DoseA', + 'dose_max': 90.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + 'enabled': True, + 'enabled_supported': False, + }), + 'CMMachineStatus': dict({ + 'available_modes': list([ + 'BrewingMode', + 'StandBy', + ]), + 'brewing_start_time': None, + 'mode': 'BrewingMode', + 'next_status': dict({ + 'start_time': '2025-03-24T22:59:55.332000+00:00', + 'status': 'StandBy', + }), + 'status': 'PoweredOn', + }), + 'CMPreBrewing': dict({ 'available_modes': list([ 'PreBrewing', 'PreInfusion', @@ -549,218 +265,509 @@ ]), }), }), - }), - dict({ - 'code': 'CMHotWaterDose', - 'index': 1, - 'output': dict({ - 'doses': list([ - dict({ - 'dose': 8.0, - 'dose_index': 'DoseA', - 'dose_max': 90.0, - 'dose_min': 0.0, - 'dose_step': 1, - }), - ]), + 'CMSteamBoilerTemperature': dict({ 'enabled': True, - 'enabled_supported': False, - }), - }), - dict({ - 'code': 'CMBackFlush', - 'index': 1, - 'output': dict({ - 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'enabled_supported': True, + 'ready_start_time': None, 'status': 'Off', + 'target_temperature': 123.9, + 'target_temperature_max': 140, + 'target_temperature_min': 95, + 'target_temperature_step': 0.1, + 'target_temperature_supported': True, }), }), - ]), - }), - 'schedule': dict({ - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'connected': True, - 'connection_date': '2025-03-21T03:00:19.892000+00:00', - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', - 'location': None, - 'model_code': 'LINEAMICRA', - 'model_name': 'Linea Micra', - 'name': 'MR123456', - 'offline_mode': False, - 'require_firmware_update': False, - 'serial_number': '**REDACTED**', - 'smart_wake_up_sleep': dict({ - 'schedules': list([ + 'connected': True, + 'connection_date': '2025-03-20T16:44:47.479000+00:00', + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/gs3av/gs3av-1.png', + 'location': 'HOME', + 'model_code': 'GS3AV', + 'model_name': 'GS3 AV', + 'name': 'GS012345', + 'offline_mode': False, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'type': 'CoffeeMachine', + 'widgets': list([ dict({ - 'days': list([ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday', - ]), - 'enabled': True, - 'id': 'Os2OswX', - 'offTimeMinutes': 1440, - 'onTimeMinutes': 1320, - 'steamBoiler': True, + 'code': 'CMMachineStatus', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'BrewingMode', + 'StandBy', + ]), + 'brewing_start_time': None, + 'mode': 'BrewingMode', + 'next_status': dict({ + 'start_time': '2025-03-24T22:59:55.332000+00:00', + 'status': 'StandBy', + }), + 'status': 'PoweredOn', + }), }), dict({ - 'days': list([ - 'Sunday', - ]), - 'enabled': True, - 'id': 'aXFz5bJ', - 'offTimeMinutes': 450, - 'onTimeMinutes': 420, - 'steamBoiler': False, + 'code': 'CMCoffeeBoiler', + 'index': 1, + 'output': dict({ + 'enabled': True, + 'enabled_supported': False, + 'ready_start_time': None, + 'status': 'Ready', + 'target_temperature': 95.0, + 'target_temperature_max': 110, + 'target_temperature_min': 80, + 'target_temperature_step': 0.1, + }), + }), + dict({ + 'code': 'CMSteamBoilerTemperature', + 'index': 1, + 'output': dict({ + 'enabled': True, + 'enabled_supported': True, + 'ready_start_time': None, + 'status': 'Off', + 'target_temperature': 123.9, + 'target_temperature_max': 140, + 'target_temperature_min': 95, + 'target_temperature_step': 0.1, + 'target_temperature_supported': True, + }), + }), + dict({ + 'code': 'CMGroupDoses', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'PulsesType', + ]), + 'brewing_pressure': None, + 'brewing_pressure_supported': False, + 'continuous_dose': None, + 'continuous_dose_supported': False, + 'doses': dict({ + 'pulses_type': list([ + dict({ + 'dose': 126.0, + 'dose_index': 'DoseA', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 126.0, + 'dose_index': 'DoseB', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 160.0, + 'dose_index': 'DoseC', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + dict({ + 'dose': 77.0, + 'dose_index': 'DoseD', + 'dose_max': 9999.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + }), + 'mirror_with_group_1': None, + 'mirror_with_group_1_not_effective': False, + 'mirror_with_group_1_supported': False, + 'mode': 'PulsesType', + 'profile': None, + }), + }), + dict({ + 'code': 'CMPreBrewing', + 'index': 1, + 'output': dict({ + 'available_modes': list([ + 'PreBrewing', + 'PreInfusion', + 'Disabled', + ]), + 'dose_index_supported': True, + 'mode': 'PreInfusion', + 'times': dict({ + 'pre_brewing': list([ + dict({ + 'dose_index': 'DoseA', + 'seconds': dict({ + 'In': 0.5, + 'Out': 1.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseB', + 'seconds': dict({ + 'In': 0.5, + 'Out': 1.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseC', + 'seconds': dict({ + 'In': 3.3, + 'Out': 3.3, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseD', + 'seconds': dict({ + 'In': 2.0, + 'Out': 2.0, + }), + 'seconds_max': dict({ + 'In': 10.0, + 'Out': 10.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + ]), + 'pre_infusion': list([ + dict({ + 'dose_index': 'DoseA', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseB', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseC', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + dict({ + 'dose_index': 'DoseD', + 'seconds': dict({ + 'In': 0.0, + 'Out': 4.0, + }), + 'seconds_max': dict({ + 'In': 25.0, + 'Out': 25.0, + }), + 'seconds_min': dict({ + 'In': 0.0, + 'Out': 0.0, + }), + 'seconds_step': dict({ + 'In': 0.1, + 'Out': 0.1, + }), + }), + ]), + }), + }), + }), + dict({ + 'code': 'CMHotWaterDose', + 'index': 1, + 'output': dict({ + 'doses': list([ + dict({ + 'dose': 8.0, + 'dose_index': 'DoseA', + 'dose_max': 90.0, + 'dose_min': 0.0, + 'dose_step': 1, + }), + ]), + 'enabled': True, + 'enabled_supported': False, + }), + }), + dict({ + 'code': 'CMBackFlush', + 'index': 1, + 'output': dict({ + 'last_cleaning_start_time': '2025-03-29T08:25:47.166000+00:00', + 'status': 'Off', + }), }), ]), - 'schedules_dict': dict({ - 'Os2OswX': dict({ - 'days': list([ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', - 'Sunday', - ]), - 'enabled': True, - 'id': 'Os2OswX', - 'offTimeMinutes': 1440, - 'onTimeMinutes': 1320, - 'steamBoiler': True, - }), - 'aXFz5bJ': dict({ - 'days': list([ - 'Sunday', - ]), - 'enabled': True, - 'id': 'aXFz5bJ', - 'offTimeMinutes': 450, - 'onTimeMinutes': 420, - 'steamBoiler': False, - }), - }), - 'smart_stand_by_after': 'PowerOn', - 'smart_stand_by_enabled': True, - 'smart_stand_by_minutes': 10, - 'smart_stand_by_minutes_max': 30, - 'smart_stand_by_minutes_min': 1, - 'smart_stand_by_minutes_step': 1, }), - 'smart_wake_up_sleep_supported': True, - 'type': 'CoffeeMachine', - }), - 'serial_number': '**REDACTED**', - 'settings': dict({ - 'actual_firmwares': list([ - dict({ - 'available_update': dict({ - 'build_version': 'v5.0.10', - 'change_log': ''' - What’s new in this version: - - * fixed an issue that could cause the machine powers up outside scheduled time - * minor improvements - ''', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', + 'schedule': dict({ + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'connected': True, + 'connection_date': '2025-03-21T03:00:19.892000+00:00', + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', + 'location': None, + 'model_code': 'LINEAMICRA', + 'model_name': 'Linea Micra', + 'name': 'MR123456', + 'offline_mode': False, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'smart_wake_up_sleep': dict({ + 'schedules': list([ + dict({ + 'days': list([ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]), + 'enabled': True, + 'id': 'Os2OswX', + 'offTimeMinutes': 1440, + 'onTimeMinutes': 1320, + 'steamBoiler': True, + }), + dict({ + 'days': list([ + 'Sunday', + ]), + 'enabled': True, + 'id': 'aXFz5bJ', + 'offTimeMinutes': 450, + 'onTimeMinutes': 420, + 'steamBoiler': False, + }), + ]), + 'schedules_dict': dict({ + 'Os2OswX': dict({ + 'days': list([ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]), + 'enabled': True, + 'id': 'Os2OswX', + 'offTimeMinutes': 1440, + 'onTimeMinutes': 1320, + 'steamBoiler': True, + }), + 'aXFz5bJ': dict({ + 'days': list([ + 'Sunday', + ]), + 'enabled': True, + 'id': 'aXFz5bJ', + 'offTimeMinutes': 450, + 'onTimeMinutes': 420, + 'steamBoiler': False, + }), }), - 'build_version': 'v5.0.9', - 'change_log': ''' - What’s new in this version: - - * New La Marzocco compatibility - * Improved connectivity - * Improved pairing process - * Improved statistics - * Boilers heating time - * Last backflush date (GS3 MP excluded) - * Automatic gateway updates option - ''', - 'status': 'ToUpdate', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - dict({ - 'available_update': None, - 'build_version': 'v1.17', - 'change_log': 'None', - 'status': 'Updated', - 'thing_model_code': 'LineaMicra', - 'type': 'Machine', - }), - ]), - 'auto_update': False, - 'auto_update_supported': True, - 'available_firmware_update': False, - 'ble_auth_token': None, - 'coffee_station': None, - 'connected': True, - 'connection_date': '2025-03-21T03:00:19.892000+00:00', - 'cropster_active': False, - 'cropster_supported': False, - 'factory_reset_supported': True, - 'firmwares': dict({ - 'Gateway': dict({ - 'available_update': dict({ - 'build_version': 'v5.0.10', - 'change_log': ''' - What’s new in this version: - - * fixed an issue that could cause the machine powers up outside scheduled time - * minor improvements - ''', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - 'build_version': 'v5.0.9', - 'change_log': ''' - What’s new in this version: - - * New La Marzocco compatibility - * Improved connectivity - * Improved pairing process - * Improved statistics - * Boilers heating time - * Last backflush date (GS3 MP excluded) - * Automatic gateway updates option - ''', - 'status': 'ToUpdate', - 'thing_model_code': 'LineaMicra', - 'type': 'Gateway', - }), - 'Machine': dict({ - 'available_update': None, - 'build_version': 'v1.17', - 'change_log': 'None', - 'status': 'Updated', - 'thing_model_code': 'LineaMicra', - 'type': 'Machine', + 'smart_stand_by_after': 'PowerOn', + 'smart_stand_by_enabled': True, + 'smart_stand_by_minutes': 10, + 'smart_stand_by_minutes_max': 30, + 'smart_stand_by_minutes_min': 1, + 'smart_stand_by_minutes_step': 1, }), + 'smart_wake_up_sleep_supported': True, + 'type': 'CoffeeMachine', }), - 'hemro_active': False, - 'hemro_supported': False, - 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', - 'is_plumbed_in': True, - 'location': None, - 'model_code': 'LINEAMICRA', - 'model_name': 'Linea Micra', - 'name': 'MR123456', - 'offline_mode': False, - 'plumb_in_supported': True, - 'require_firmware_update': False, 'serial_number': '**REDACTED**', - 'type': 'CoffeeMachine', - 'wifi_rssi': -51, - 'wifi_ssid': 'MyWifi', + 'settings': dict({ + 'actual_firmwares': list([ + dict({ + 'available_update': dict({ + 'build_version': 'v5.0.10', + 'change_log': ''' + What’s new in this version: + + * fixed an issue that could cause the machine powers up outside scheduled time + * minor improvements + ''', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'build_version': 'v5.0.9', + 'change_log': ''' + What’s new in this version: + + * New La Marzocco compatibility + * Improved connectivity + * Improved pairing process + * Improved statistics + * Boilers heating time + * Last backflush date (GS3 MP excluded) + * Automatic gateway updates option + ''', + 'status': 'ToUpdate', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + dict({ + 'available_update': None, + 'build_version': 'v1.17', + 'change_log': 'None', + 'status': 'Updated', + 'thing_model_code': 'LineaMicra', + 'type': 'Machine', + }), + ]), + 'auto_update': False, + 'auto_update_supported': True, + 'available_firmware_update': False, + 'ble_auth_token': None, + 'coffee_station': None, + 'connected': True, + 'connection_date': '2025-03-21T03:00:19.892000+00:00', + 'cropster_active': False, + 'cropster_supported': False, + 'factory_reset_supported': True, + 'firmwares': dict({ + 'Gateway': dict({ + 'available_update': dict({ + 'build_version': 'v5.0.10', + 'change_log': ''' + What’s new in this version: + + * fixed an issue that could cause the machine powers up outside scheduled time + * minor improvements + ''', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'build_version': 'v5.0.9', + 'change_log': ''' + What’s new in this version: + + * New La Marzocco compatibility + * Improved connectivity + * Improved pairing process + * Improved statistics + * Boilers heating time + * Last backflush date (GS3 MP excluded) + * Automatic gateway updates option + ''', + 'status': 'ToUpdate', + 'thing_model_code': 'LineaMicra', + 'type': 'Gateway', + }), + 'Machine': dict({ + 'available_update': None, + 'build_version': 'v1.17', + 'change_log': 'None', + 'status': 'Updated', + 'thing_model_code': 'LineaMicra', + 'type': 'Machine', + }), + }), + 'hemro_active': False, + 'hemro_supported': False, + 'image_url': 'https://lion.lamarzocco.io/img/thing-model/detail/lineamicra/lineamicra-1-c-bianco.png', + 'is_plumbed_in': True, + 'location': None, + 'model_code': 'LINEAMICRA', + 'model_name': 'Linea Micra', + 'name': 'MR123456', + 'offline_mode': False, + 'plumb_in_supported': True, + 'require_firmware_update': False, + 'serial_number': '**REDACTED**', + 'type': 'CoffeeMachine', + 'wifi_rssi': -51, + 'wifi_ssid': 'MyWifi', + }), }), }) # --- From 255beafe0895a92d780c34536968f9cdf0d91e9a Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 1 May 2025 22:29:44 +0200 Subject: [PATCH 0035/1175] Add connect/disconnect callbacks to lamarzocco (#144011) --- homeassistant/components/lamarzocco/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index cfe570efb53..751ef550516 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -97,14 +97,15 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): self.config_entry.async_create_background_task( hass=self.hass, target=self.device.connect_dashboard_websocket( - update_callback=lambda _: self.async_set_updated_data(None) + update_callback=lambda _: self.async_set_updated_data(None), + connect_callback=self.async_update_listeners, + disconnect_callback=self.async_update_listeners, ), name="lm_websocket_task", ) async def websocket_close(_: Any | None = None) -> None: - if self.device.websocket.connected: - await self.device.websocket.disconnect() + await self.device.websocket.disconnect() self.config_entry.async_on_unload( self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close) From a906a1754e26303fb2b5ee056d95b60970a71126 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 1 May 2025 15:45:44 -0500 Subject: [PATCH 0036/1175] Avoid DomainData lookup in ESPHome update platform (#144072) We can get this from entry.runtime_data --- homeassistant/components/esphome/update.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index a92204a80d2..01ac638bdb1 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum from .const import DOMAIN from .coordinator import ESPHomeDashboardCoordinator from .dashboard import async_get_dashboard -from .domain_data import DomainData from .entity import ( EsphomeEntity, convert_api_error_ha_error, @@ -62,7 +61,7 @@ async def async_setup_entry( if (dashboard := async_get_dashboard(hass)) is None: return - entry_data = DomainData.get(hass).get_entry_data(entry) + entry_data = entry.runtime_data assert entry_data.device_info is not None device_name = entry_data.device_info.name unsubs: list[CALLBACK_TYPE] = [] From abd17d9af9cc435286c849f7abdd1cde8641ae5f Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 1 May 2025 13:47:48 -0700 Subject: [PATCH 0037/1175] Pass empty set instead of empty dict to get_last_statistics (#144022) --- homeassistant/components/opower/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index d0e95b27ec3..adb32d914ee 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -349,7 +349,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): 1, target_id, True, - {}, + set(), ) if not last_target_stat: need_migration_source_ids.add(source_id) From 883ab44437710fd775175d6566313425ee4ab8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 1 May 2025 23:04:03 +0200 Subject: [PATCH 0038/1175] Move Home Connect entry state assertion at tests (#144027) --- tests/components/home_connect/conftest.py | 2 ++ .../components/home_connect/test_binary_sensor.py | 5 ----- tests/components/home_connect/test_button.py | 6 ------ tests/components/home_connect/test_coordinator.py | 10 ---------- tests/components/home_connect/test_diagnostics.py | 2 -- tests/components/home_connect/test_entity.py | 4 ---- tests/components/home_connect/test_init.py | 3 --- tests/components/home_connect/test_light.py | 6 ------ tests/components/home_connect/test_number.py | 7 ------- tests/components/home_connect/test_select.py | 13 ------------- tests/components/home_connect/test_sensor.py | 10 ---------- tests/components/home_connect/test_services.py | 7 ------- tests/components/home_connect/test_switch.py | 14 -------------- tests/components/home_connect/test_time.py | 7 ------- 14 files changed, 2 insertions(+), 94 deletions(-) diff --git a/tests/components/home_connect/conftest.py b/tests/components/home_connect/conftest.py index c3e5e859870..4442f9622de 100644 --- a/tests/components/home_connect/conftest.py +++ b/tests/components/home_connect/conftest.py @@ -36,6 +36,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.home_connect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -147,6 +148,7 @@ async def mock_integration_setup( config_entry.add_to_hass(hass) async def run(client: MagicMock) -> bool: + assert config_entry.state is ConfigEntryState.NOT_LOADED with ( patch("homeassistant.components.home_connect.PLATFORMS", platforms), patch( diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 0022d6987d7..934b6103982 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -51,7 +51,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -128,7 +127,6 @@ async def test_connected_devices( return get_status_original_mock.return_value client.get_status = AsyncMock(side_effect=get_status_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_status = get_status_original_mock @@ -179,7 +177,6 @@ async def test_binary_sensors_entity_availability( entity_ids = [ "binary_sensor.washer_remote_control", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -279,7 +276,6 @@ async def test_binary_sensors_functionality( expected: str, ) -> None: """Tests for Home Connect Fridge appliance door states.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED await client.add_events( @@ -316,7 +312,6 @@ async def test_connected_sensor_functionality( ) -> None: """Test if the connected binary sensor reports the right values.""" entity_id = "binary_sensor.washer_connectivity" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index 9971597c8e3..1aca781def6 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -43,7 +43,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -131,7 +130,6 @@ async def test_connected_devices( side_effect=get_available_commands_side_effect ) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_available_commands = get_available_commands_original_mock @@ -183,7 +181,6 @@ async def test_button_entity_availability( "button.washer_pause_program", "button.washer_stop_program", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -246,7 +243,6 @@ async def test_button_functionality( appliance: HomeAppliance, ) -> None: """Test if button entities availability are based on the appliance connection state.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -282,7 +278,6 @@ async def test_command_button_exception( ] ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED @@ -308,7 +303,6 @@ async def test_stop_program_button_exception( """Test if button entities availability are based on the appliance connection state.""" entity_id = "button.washer_stop_program" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 8602ecbe03a..1a51e5980cd 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -218,7 +218,6 @@ async def test_coordinator_not_fetching_on_disconnected_appliance( """Test that the coordinator does not fetch anything on disconnected appliance.""" appliance.connected = False - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -242,7 +241,6 @@ async def test_coordinator_update_failing( """ setattr(client, mock_method, AsyncMock(side_effect=HomeConnectError())) - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -286,7 +284,6 @@ async def test_event_listener( entity_id: str, ) -> None: """Test that the event listener works.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -353,7 +350,6 @@ async def tests_receive_setting_and_status_for_first_time_at_events( client.get_setting = AsyncMock(return_value=ArrayOfSettings([])) client.get_status = AsyncMock(return_value=ArrayOfStatus([])) - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -468,7 +464,6 @@ async def test_event_listener_resilience( side_effect=[stream_exception(), client.stream_all_events()] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) await hass.async_block_till_done() @@ -531,7 +526,6 @@ async def test_devices_updated_on_refresh( ) await async_setup_component(hass, HA_DOMAIN, {}) - assert config_entry.state == ConfigEntryState.NOT_LOADED await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -564,7 +558,6 @@ async def test_paired_disconnected_devices_not_fetching( ) -> None: """Test that Home Connect API is not fetched after pairing a disconnected device.""" client.get_home_appliances = AsyncMock(return_value=ArrayOfHomeAppliances([])) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -601,7 +594,6 @@ async def test_coordinator_disabling_updates_for_appliance( appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1" issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -692,7 +684,6 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r appliance_ha_id = "SIEMENS-HCS02DWH1-6BE58C26DCC1" issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -718,7 +709,6 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_diagnostics.py b/tests/components/home_connect/test_diagnostics.py index bf452ac7a92..9aef9e0d157 100644 --- a/tests/components/home_connect/test_diagnostics.py +++ b/tests/components/home_connect/test_diagnostics.py @@ -25,7 +25,6 @@ async def test_async_get_config_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -41,7 +40,6 @@ async def test_async_get_device_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test device config entry diagnostics.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_entity.py b/tests/components/home_connect/test_entity.py index 1dc2c71e18c..84d8178d4b7 100644 --- a/tests/components/home_connect/test_entity.py +++ b/tests/components/home_connect/test_entity.py @@ -157,7 +157,6 @@ async def test_program_options_retrieval( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -276,7 +275,6 @@ async def test_no_options_retrieval_on_unknown_program( client.get_all_programs = AsyncMock(side_effect=get_all_programs_with_options_mock) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -357,7 +355,6 @@ async def test_program_options_retrieval_after_appliance_connection( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -469,7 +466,6 @@ async def test_option_entity_functionality_exception( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 12989c7a847..9bd4eaeca0e 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -46,7 +46,6 @@ async def test_entry_setup( integration_setup: Callable[[MagicMock], Awaitable[bool]], ) -> None: """Test setup and unload.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -182,7 +181,6 @@ async def test_client_error( """Test client errors during setup integration.""" client_with_exception.get_home_appliances.return_value = None client_with_exception.get_home_appliances.side_effect = exception - assert config_entry.state == ConfigEntryState.NOT_LOADED assert not await integration_setup(client_with_exception) assert config_entry.state == expected_state assert client_with_exception.get_home_appliances.call_count == 1 @@ -239,7 +237,6 @@ async def test_required_program_or_at_least_an_option( ) -> None: "Test that the set_program_and_options does raise an exception if no program nor options are set." - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index ca5c9c4968c..abffa491ce4 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -64,7 +64,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -141,7 +140,6 @@ async def test_connected_devices( return await get_settings_original_mock.side_effect(ha_id) client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_settings = get_settings_original_mock @@ -186,7 +184,6 @@ async def test_light_availability( entity_ids = [ "light.hood_functional_light", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -353,7 +350,6 @@ async def test_light_functionality( appliance: HomeAppliance, ) -> None: """Test light functionality.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -405,7 +401,6 @@ async def test_light_color_different_than_custom( appliance: HomeAppliance, ) -> None: """Test that light color attributes are not set if color is different than custom.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED await hass.services.async_call( @@ -586,7 +581,6 @@ async def test_light_exception_handling( client_with_exception.set_setting.side_effect = [ exception() if exception else None for exception in attr_side_effect ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index cc2ca8046ed..1f2a9b8d73f 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -80,7 +80,6 @@ async def test_paired_depaired_devices_flow( ], ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -159,7 +158,6 @@ async def test_connected_devices( return get_settings_original_mock.return_value client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_settings = get_settings_original_mock @@ -209,7 +207,6 @@ async def test_number_entity_availability( # Setting constrains are not needed for this test # so we rise an error to easily test the availability client.get_setting = AsyncMock(side_effect=HomeConnectError()) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -316,7 +313,6 @@ async def test_number_entity_functionality( ) ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) @@ -420,7 +416,6 @@ async def test_fetch_constraints_after_rate_limit_error( ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -472,7 +467,6 @@ async def test_number_entity_error( ) ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state is ConfigEntryState.LOADED @@ -599,7 +593,6 @@ async def test_options_functionality( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 46730e8c595..11f6b3ce94b 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -84,7 +84,6 @@ async def test_paired_depaired_devices_flow( ], ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -174,7 +173,6 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_settings = get_settings_original_mock @@ -220,7 +218,6 @@ async def test_select_entity_availability( entity_ids = [ "select.washer_active_program", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -298,7 +295,6 @@ async def test_filter_programs( ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -365,7 +361,6 @@ async def test_select_program_functionality( event_key: EventKey, ) -> None: """Test select functionality.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -448,7 +443,6 @@ async def test_select_exception_handling( ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state is ConfigEntryState.LOADED @@ -494,7 +488,6 @@ async def test_programs_updated_on_connect( return await get_all_programs_mock.side_effect(ha_id) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_all_programs = get_all_programs_mock @@ -566,7 +559,6 @@ async def test_select_functionality( expected_value_call_arg: str, ) -> None: """Test select functionality.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -646,7 +638,6 @@ async def test_fetch_allowed_values( client.get_setting = AsyncMock(side_effect=get_setting_side_effect) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -713,7 +704,6 @@ async def test_fetch_allowed_values_after_rate_limit_error( ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -775,7 +765,6 @@ async def test_default_values_after_fetch_allowed_values_error( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) client.get_setting = AsyncMock(side_effect=exception) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -821,7 +810,6 @@ async def test_select_entity_error( ) ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state is ConfigEntryState.LOADED @@ -952,7 +940,6 @@ async def test_options_functionality( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index 9fbefc9944d..33cb8d2c804 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -100,7 +100,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -200,7 +199,6 @@ async def test_connected_devices( return get_status_original_mock.return_value client.get_status = AsyncMock(side_effect=get_status_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_status = get_status_original_mock @@ -246,7 +244,6 @@ async def test_sensor_entity_availability( "sensor.dishwasher_operation_state", "sensor.dishwasher_salt_nearly_empty", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -452,7 +449,6 @@ async def test_program_sensor_edge_case( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -515,7 +511,6 @@ async def test_remaining_prog_time_edge_cases( 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(client) assert config_entry.state == ConfigEntryState.LOADED @@ -591,7 +586,6 @@ async def test_sensors_states( appliance: HomeAppliance, ) -> None: """Tests for appliance sensors.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -653,7 +647,6 @@ async def test_event_sensors_states( ) -> None: """Tests for appliance event sensors.""" caplog.set_level(logging.ERROR) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -763,7 +756,6 @@ async def test_sensor_unit_fetching( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -819,7 +811,6 @@ async def test_sensor_unit_fetching_error( client.get_status = AsyncMock(side_effect=get_status_mock) client.get_status_value = AsyncMock(side_effect=HomeConnectError()) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -881,7 +872,6 @@ async def test_sensor_unit_fetching_after_rate_limit_error( ] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) async_fire_time_changed(hass) await hass.async_block_till_done() diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index b2056c41311..97c60a72237 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -185,7 +185,6 @@ async def test_key_value_services( service_call: dict[str, Any], ) -> None: """Create and test services.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -236,7 +235,6 @@ async def test_programs_and_options_actions_deprecation( issue_id: str, ) -> None: """Test deprecated service keys.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -305,7 +303,6 @@ async def test_set_program_and_options( snapshot: SnapshotAssertion, ) -> None: """Test recognized options.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -347,7 +344,6 @@ async def test_set_program_and_options_exceptions( error_regex: str, ) -> None: """Test recognized options.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED @@ -376,7 +372,6 @@ async def test_services_exception_device_id( service_call: dict[str, Any], ) -> None: """Raise a HomeAssistantError when there is an API error.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED @@ -399,7 +394,6 @@ async def test_services_appliance_not_found( integration_setup: Callable[[MagicMock], Awaitable[bool]], ) -> None: """Raise a ServiceValidationError when device id does not match.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -448,7 +442,6 @@ async def test_services_exception( service_call: dict[str, Any], ) -> None: """Raise a ValueError when device id does not match.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index ca9688fd427..404e3a5bcea 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -91,7 +91,6 @@ async def test_paired_depaired_devices_flow( ], ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -181,7 +180,6 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_settings = get_settings_original_mock @@ -230,7 +228,6 @@ async def test_switch_entity_availability( "switch.dishwasher_child_lock", "switch.dishwasher_program_eco50", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -313,7 +310,6 @@ async def test_switch_functionality( ) -> None: """Test switch functionality.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -380,7 +376,6 @@ async def test_program_switch_functionality( ) client.stop_program = AsyncMock(side_effect=mock_stop_program) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED assert hass.states.is_state(entity_id, initial_state) @@ -488,7 +483,6 @@ async def test_switch_exception_handling( ] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED @@ -532,7 +526,6 @@ async def test_ent_desc_switch_functionality( ) -> None: """Test switch functionality - entity description setup.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -589,7 +582,6 @@ async def test_ent_desc_switch_exception_handling( for key, value in status.items() ] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state == ConfigEntryState.LOADED @@ -675,7 +667,6 @@ async def test_power_switch( ] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -715,7 +706,6 @@ async def test_power_switch_fetch_off_state_from_current_value( ] ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -781,7 +771,6 @@ async def test_power_switch_service_validation_errors( client.get_settings.return_value = ArrayOfSettings([setting]) client.get_setting = AsyncMock(return_value=setting) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -842,7 +831,6 @@ async def test_create_program_switch_deprecation_issue( }, ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -923,7 +911,6 @@ async def test_program_switch_deprecation_issue_fix( }, ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -1018,7 +1005,6 @@ async def test_options_functionality( ) ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED assert hass.states.get(entity_id) diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index f1edbfd2bd7..c94f3affc41 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -57,7 +57,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -135,7 +134,6 @@ async def test_connected_devices( return await get_settings_original_mock.side_effect(ha_id) client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED client.get_settings = get_settings_original_mock @@ -181,7 +179,6 @@ async def test_time_entity_availability( entity_ids = [ "time.oven_alarm_clock", ] - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -242,7 +239,6 @@ async def test_time_entity_functionality( setting_key: SettingKey, ) -> None: """Test time entity functionality.""" - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -296,7 +292,6 @@ async def test_time_entity_error( ) ] ) - assert config_entry.state is ConfigEntryState.NOT_LOADED assert await integration_setup(client_with_exception) assert config_entry.state is ConfigEntryState.LOADED @@ -367,7 +362,6 @@ async def test_create_alarm_clock_deprecation_issue( }, ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED @@ -447,7 +441,6 @@ async def test_alarm_clock_deprecation_issue_fix( }, ) - assert config_entry.state == ConfigEntryState.NOT_LOADED assert await integration_setup(client) assert config_entry.state == ConfigEntryState.LOADED From 4e8d68a2efca4e98019453ab5c5491a36f3ca222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6lsch?= <20746434+andreaskoelsch@users.noreply.github.com> Date: Fri, 2 May 2025 00:07:52 +0200 Subject: [PATCH 0039/1175] Fix brightness calculation when using brightness_step_pct (#143786) --- homeassistant/components/light/__init__.py | 5 +- tests/components/light/test_init.py | 59 ++++++++++++++++------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7b548533058..d2869670ba4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: brightness += params.pop(ATTR_BRIGHTNESS_STEP) else: - brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255) + brightness_pct = round(brightness / 255 * 100) + brightness = round( + (brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255 + ) params[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 29604ce7595..014e3ec8c35 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -958,21 +958,6 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: _, data = entity1.last_call("turn_on") assert data["brightness"] == 40 # 50 - 10 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": [entity0.entity_id, entity1.entity_id], - "brightness_step_pct": 10, - }, - blocking=True, - ) - - _, data = entity0.last_call("turn_on") - assert data["brightness"] == 116 # 90 + (255 * 0.10) - _, data = entity1.last_call("turn_on") - assert data["brightness"] == 66 # 40 + (255 * 0.10) - await hass.services.async_call( "light", "turn_on", @@ -983,7 +968,49 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None: blocking=True, ) - assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off + assert entity0.state == "off" # 40 - 126; brightness is 0, light should turn off + + +async def test_light_brightness_step_pct(hass: HomeAssistant) -> None: + """Test that percentage based brightness steps work as expected.""" + entity = MockLight("Test_0", STATE_ON) + + setup_test_component_platform(hass, light.DOMAIN, [entity]) + + entity.supported_features = light.SUPPORT_BRIGHTNESS + # Set color modes to none to trigger backwards compatibility in LightEntity + entity.supported_color_modes = None + entity.color_mode = None + entity.brightness = 255 + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + assert state.attributes["brightness"] == 255 # 100% + + def reduce_brightness_by_ten_percent(): + return hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [entity.entity_id], + "brightness_step_pct": -10, + }, + blocking=True, + ) + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 90 # 100% - 10% = 90% + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 80 # 90% - 10% = 80% + + await reduce_brightness_by_ten_percent() + _, data = entity.last_call("turn_on") + assert round(data["brightness"] / 2.55) == 70 # 80% - 10% = 70% @pytest.mark.usefixtures("enable_custom_integrations") From fca62f1ae8241c14588861e1f7f73e1217058740 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 2 May 2025 08:32:44 +0200 Subject: [PATCH 0040/1175] Move SamsungTV test constants to fixture files (#144086) --- tests/components/samsungtv/conftest.py | 8 +- tests/components/samsungtv/const.py | 101 ------------- .../fixtures/device_info_UE43LS003.json | 34 +++++ .../fixtures/device_info_UE48JU6400.json | 28 ++++ .../fixtures/ws_installed_app_event.json | 29 ++++ .../samsungtv/snapshots/test_diagnostics.ambr | 137 ++++++++++++++++++ .../components/samsungtv/test_config_flow.py | 7 +- .../components/samsungtv/test_diagnostics.py | 131 ++++------------- tests/components/samsungtv/test_init.py | 7 +- .../components/samsungtv/test_media_player.py | 20 ++- 10 files changed, 283 insertions(+), 219 deletions(-) create mode 100644 tests/components/samsungtv/fixtures/device_info_UE43LS003.json create mode 100644 tests/components/samsungtv/fixtures/device_info_UE48JU6400.json create mode 100644 tests/components/samsungtv/fixtures/ws_installed_app_event.json create mode 100644 tests/components/samsungtv/snapshots/test_diagnostics.ambr diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 4b3ad59defd..c33fd89ec56 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -19,9 +19,11 @@ from samsungtvws.event import ED_INSTALLED_APP_EVENT from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand -from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT +from homeassistant.components.samsungtv.const import DOMAIN, WEBSOCKET_SSL_PORT -from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI +from .const import SAMPLE_DEVICE_INFO_WIFI + +from tests.common import load_json_object_fixture @pytest.fixture @@ -186,7 +188,7 @@ def rest_api_fixture_non_ssl_only() -> Generator[None]: """Mock rest_device_info to fail for ssl and work for non-ssl.""" if self.port == WEBSOCKET_SSL_PORT: raise ResponseError - return SAMPLE_DEVICE_INFO_UE48JU6400 + return load_json_object_fixture("device_info_UE48JU6400.json", DOMAIN) with patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index c1a9da4e284..5d09087dadd 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -1,7 +1,5 @@ """Constants for the samsungtv tests.""" -from samsungtvws.event import ED_INSTALLED_APP_EVENT - from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, METHOD_LEGACY, @@ -94,102 +92,3 @@ SAMPLE_DEVICE_INFO_WIFI = { "networkType": "wireless", }, } - -SAMPLE_DEVICE_INFO_FRAME = { - "device": { - "FrameTVSupport": "true", - "GamePadSupport": "true", - "ImeSyncedSupport": "true", - "OS": "Tizen", - "TokenAuthSupport": "true", - "VoiceSupport": "true", - "countryCode": "FR", - "description": "Samsung DTV RCR", - "developerIP": "0.0.0.0", - "developerMode": "0", - "duid": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "firmwareVersion": "Unknown", - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "ip": "1.2.3.4", - "model": "17_KANTM_UHD", - "modelName": "UE43LS003", - "name": "[TV] Samsung Frame (43)", - "networkType": "wired", - "resolution": "3840x2160", - "smartHubAgreement": "true", - "type": "Samsung SmartTV", - "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "wifiMac": "aa:ee:tt:hh:ee:rr", - }, - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "isSupport": ( - '{"DMP_DRM_PLAYREADY":"false","DMP_DRM_WIDEVINE":"false","DMP_available":"true",' - '"EDEN_available":"true","FrameTVSupport":"true","ImeSyncedSupport":"true",' - '"TokenAuthSupport":"true","remote_available":"true","remote_fourDirections":"true",' - '"remote_touchPad":"true","remote_voiceControl":"true"}\n' - ), - "name": "[TV] Samsung Frame (43)", - "remote": "1.0", - "type": "Samsung SmartTV", - "uri": "https://1.2.3.4:8002/api/v2/", - "version": "2.0.25", -} - -SAMPLE_DEVICE_INFO_UE48JU6400 = { - "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", - "name": "[TV] TV-UE48JU6470", - "version": "2.0.25", - "device": { - "type": "Samsung SmartTV", - "duid": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", - "model": "15_HAWKM_UHD_2D", - "modelName": "UE48JU6400", - "description": "Samsung DTV RCR", - "networkType": "wired", - "ssid": "", - "ip": "1.2.3.4", - "firmwareVersion": "Unknown", - "name": "[TV] TV-UE48JU6470", - "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", - "udn": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", - "resolution": "1920x1080", - "countryCode": "AT", - "msfVersion": "2.0.25", - "smartHubAgreement": "true", - "wifiMac": "aa:bb:aa:aa:aa:aa", - "developerMode": "0", - "developerIP": "", - }, - "type": "Samsung SmartTV", - "uri": "https://1.2.3.4:8002/api/v2/", -} - -SAMPLE_EVENT_ED_INSTALLED_APP = { - "event": ED_INSTALLED_APP_EVENT, - "from": "host", - "data": { - "data": [ - { - "appId": "111299001912", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/111299001912/250x250.png", - "is_lock": 0, - "name": "YouTube", - }, - { - "appId": "3201608010191", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201608010191/250x250.png", - "is_lock": 0, - "name": "Deezer", - }, - { - "appId": "3201606009684", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201606009684/250x250.png", - "is_lock": 0, - "name": "Spotify - Music and Podcasts", - }, - ] - }, -} diff --git a/tests/components/samsungtv/fixtures/device_info_UE43LS003.json b/tests/components/samsungtv/fixtures/device_info_UE43LS003.json new file mode 100644 index 00000000000..ac961fafd6b --- /dev/null +++ b/tests/components/samsungtv/fixtures/device_info_UE43LS003.json @@ -0,0 +1,34 @@ +{ + "device": { + "FrameTVSupport": "true", + "GamePadSupport": "true", + "ImeSyncedSupport": "true", + "OS": "Tizen", + "TokenAuthSupport": "true", + "VoiceSupport": "true", + "countryCode": "FR", + "description": "Samsung DTV RCR", + "developerIP": "0.0.0.0", + "developerMode": "0", + "duid": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "firmwareVersion": "Unknown", + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "ip": "1.2.3.4", + "model": "17_KANTM_UHD", + "modelName": "UE43LS003", + "name": "[TV] Samsung Frame (43)", + "networkType": "wired", + "resolution": "3840x2160", + "smartHubAgreement": "true", + "type": "Samsung SmartTV", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "wifiMac": "aa:ee:tt:hh:ee:rr" + }, + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "isSupport": "{\"DMP_DRM_PLAYREADY\":\"false\",\"DMP_DRM_WIDEVINE\":\"false\",\"DMP_available\":\"true\",\"EDEN_available\":\"true\",\"FrameTVSupport\":\"true\",\"ImeSyncedSupport\":\"true\",\"TokenAuthSupport\":\"true\",\"remote_available\":\"true\",\"remote_fourDirections\":\"true\",\"remote_touchPad\":\"true\",\"remote_voiceControl\":\"true\"}\n", + "name": "[TV] Samsung Frame (43)", + "remote": "1.0", + "type": "Samsung SmartTV", + "uri": "https://1.2.3.4:8002/api/v2/", + "version": "2.0.25" +} diff --git a/tests/components/samsungtv/fixtures/device_info_UE48JU6400.json b/tests/components/samsungtv/fixtures/device_info_UE48JU6400.json new file mode 100644 index 00000000000..65cecf095a2 --- /dev/null +++ b/tests/components/samsungtv/fixtures/device_info_UE48JU6400.json @@ -0,0 +1,28 @@ +{ + "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "name": "[TV] TV-UE48JU6470", + "version": "2.0.25", + "device": { + "type": "Samsung SmartTV", + "duid": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "model": "15_HAWKM_UHD_2D", + "modelName": "UE48JU6400", + "description": "Samsung DTV RCR", + "networkType": "wired", + "ssid": "", + "ip": "1.2.3.4", + "firmwareVersion": "Unknown", + "name": "[TV] TV-UE48JU6470", + "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "udn": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "resolution": "1920x1080", + "countryCode": "AT", + "msfVersion": "2.0.25", + "smartHubAgreement": "true", + "wifiMac": "aa:bb:aa:aa:aa:aa", + "developerMode": "0", + "developerIP": "" + }, + "type": "Samsung SmartTV", + "uri": "https://1.2.3.4:8002/api/v2/" +} diff --git a/tests/components/samsungtv/fixtures/ws_installed_app_event.json b/tests/components/samsungtv/fixtures/ws_installed_app_event.json new file mode 100644 index 00000000000..81c64f60958 --- /dev/null +++ b/tests/components/samsungtv/fixtures/ws_installed_app_event.json @@ -0,0 +1,29 @@ +{ + "event": "ed.installedApp.get", + "from": "host", + "data": { + "data": [ + { + "appId": "111299001912", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/111299001912/250x250.png", + "is_lock": 0, + "name": "YouTube" + }, + { + "appId": "3201608010191", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201608010191/250x250.png", + "is_lock": 0, + "name": "Deezer" + }, + { + "appId": "3201606009684", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201606009684/250x250.png", + "is_lock": 0, + "name": "Spotify - Music and Podcasts" + } + ] + } +} diff --git a/tests/components/samsungtv/snapshots/test_diagnostics.ambr b/tests/components/samsungtv/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..dd1b3654186 --- /dev/null +++ b/tests/components/samsungtv/snapshots/test_diagnostics.ambr @@ -0,0 +1,137 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'device_info': dict({ + 'device': dict({ + 'modelName': '82GXARRS', + 'name': '[TV] Living Room', + 'networkType': 'wireless', + 'type': 'Samsung SmartTV', + 'wifiMac': 'aa:bb:aa:aa:aa:aa', + }), + 'id': 'uuid:be9554b9-c9fb-41f4-8920-22da015376a4', + }), + 'entry': dict({ + 'data': dict({ + 'host': 'fake_host', + 'ip_address': 'test', + 'mac': 'aa:bb:cc:dd:ee:ff', + 'method': 'websocket', + 'model': '82GXARRS', + 'name': 'fake', + 'port': 8002, + 'token': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'samsungtv', + 'entry_id': '123456', + 'minor_version': 2, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Mock Title', + 'unique_id': 'be9554b9-c9fb-41f4-8920-22da015376a4', + 'version': 2, + }), + }) +# --- +# name: test_entry_diagnostics_encrypte_offline + dict({ + 'device_info': None, + 'entry': dict({ + 'data': dict({ + 'host': 'fake_host', + 'ip_address': 'test', + 'mac': 'aa:bb:cc:dd:ee:ff', + 'method': 'encrypted', + 'name': 'fake', + 'port': 8000, + 'session_id': '**REDACTED**', + 'token': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'samsungtv', + 'entry_id': '123456', + 'minor_version': 2, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Mock Title', + 'unique_id': 'be9554b9-c9fb-41f4-8920-22da015376a4', + 'version': 2, + }), + }) +# --- +# name: test_entry_diagnostics_encrypted + dict({ + 'device_info': dict({ + 'device': dict({ + 'countryCode': 'AT', + 'description': 'Samsung DTV RCR', + 'developerIP': '', + 'developerMode': '0', + 'duid': 'uuid:223da676-497a-4e06-9507-5e27ec4f0fb3', + 'firmwareVersion': 'Unknown', + 'id': 'uuid:223da676-497a-4e06-9507-5e27ec4f0fb3', + 'ip': '1.2.3.4', + 'model': '15_HAWKM_UHD_2D', + 'modelName': 'UE48JU6400', + 'msfVersion': '2.0.25', + 'name': '[TV] TV-UE48JU6470', + 'networkType': 'wired', + 'resolution': '1920x1080', + 'smartHubAgreement': 'true', + 'ssid': '', + 'type': 'Samsung SmartTV', + 'udn': 'uuid:223da676-497a-4e06-9507-5e27ec4f0fb3', + 'wifiMac': 'aa:bb:aa:aa:aa:aa', + }), + 'id': 'uuid:223da676-497a-4e06-9507-5e27ec4f0fb3', + 'name': '[TV] TV-UE48JU6470', + 'type': 'Samsung SmartTV', + 'uri': 'https://1.2.3.4:8002/api/v2/', + 'version': '2.0.25', + }), + 'entry': dict({ + 'data': dict({ + 'host': 'fake_host', + 'ip_address': 'test', + 'mac': 'aa:bb:cc:dd:ee:ff', + 'method': 'encrypted', + 'model': 'UE48JU6400', + 'name': 'fake', + 'port': 8000, + 'session_id': '**REDACTED**', + 'token': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'samsungtv', + 'entry_id': '123456', + 'minor_version': 2, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Mock Title', + 'unique_id': 'be9554b9-c9fb-41f4-8920-22da015376a4', + 'version': 2, + }), + }) +# --- diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5ff259c2120..12c222033e0 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -63,10 +63,9 @@ from .const import ( MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, - SAMPLE_DEVICE_INFO_FRAME, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture RESULT_ALREADY_CONFIGURED = "already_configured" RESULT_ALREADY_IN_PROGRESS = "already_in_progress" @@ -956,7 +955,9 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from dhcp.""" # Even though it is named "wifiMac", it matches the mac of the wired connection - rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_FRAME + rest_api.rest_device_info.return_value = load_json_object_fixture( + "device_info_UE43LS003.json", DOMAIN + ) # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 53d52456de5..3f40c51d5d0 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -4,138 +4,63 @@ from unittest.mock import Mock import pytest from samsungtvws.exceptions import HttpApiError +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props -from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.core import HomeAssistant from . import setup_samsungtv_entry -from .const import ( - MOCK_ENTRY_WS_WITH_MAC, - MOCK_ENTRYDATA_ENCRYPTED_WS, - SAMPLE_DEVICE_INFO_UE48JU6400, - SAMPLE_DEVICE_INFO_WIFI, -) +from .const import MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS -from tests.common import ANY +from tests.common import load_json_object_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @pytest.mark.usefixtures("remotews", "rest_api") async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSessionGenerator + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS_WITH_MAC) - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "created_at": ANY, - "data": { - "host": "fake_host", - "ip_address": "test", - "mac": "aa:bb:cc:dd:ee:ff", - "method": "websocket", - "model": "82GXARRS", - "name": "fake", - "port": 8002, - "token": REDACTED, - }, - "disabled_by": None, - "discovery_keys": {}, - "domain": "samsungtv", - "entry_id": "123456", - "minor_version": 2, - "modified_at": ANY, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "subentries": [], - "title": "Mock Title", - "unique_id": "be9554b9-c9fb-41f4-8920-22da015376a4", - "version": 2, - }, - "device_info": SAMPLE_DEVICE_INFO_WIFI, - } + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) @pytest.mark.usefixtures("remoteencws") async def test_entry_diagnostics_encrypted( - hass: HomeAssistant, rest_api: Mock, hass_client: ClientSessionGenerator + hass: HomeAssistant, + rest_api: Mock, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_UE48JU6400 + rest_api.rest_device_info.return_value = load_json_object_fixture( + "device_info_UE48JU6400.json", DOMAIN + ) config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "created_at": ANY, - "data": { - "host": "fake_host", - "ip_address": "test", - "mac": "aa:bb:cc:dd:ee:ff", - "method": "encrypted", - "model": "UE48JU6400", - "name": "fake", - "port": 8000, - "token": REDACTED, - "session_id": REDACTED, - }, - "disabled_by": None, - "discovery_keys": {}, - "domain": "samsungtv", - "entry_id": "123456", - "minor_version": 2, - "modified_at": ANY, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "subentries": [], - "title": "Mock Title", - "unique_id": "be9554b9-c9fb-41f4-8920-22da015376a4", - "version": 2, - }, - "device_info": SAMPLE_DEVICE_INFO_UE48JU6400, - } + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) @pytest.mark.usefixtures("remoteencws") async def test_entry_diagnostics_encrypte_offline( - hass: HomeAssistant, rest_api: Mock, hass_client: ClientSessionGenerator + hass: HomeAssistant, + rest_api: Mock, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" rest_api.rest_device_info.side_effect = HttpApiError config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { - "entry": { - "created_at": ANY, - "data": { - "host": "fake_host", - "ip_address": "test", - "mac": "aa:bb:cc:dd:ee:ff", - "method": "encrypted", - "name": "fake", - "port": 8000, - "token": REDACTED, - "session_id": REDACTED, - }, - "disabled_by": None, - "discovery_keys": {}, - "domain": "samsungtv", - "entry_id": "123456", - "minor_version": 2, - "modified_at": ANY, - "options": {}, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "source": "user", - "subentries": [], - "title": "Mock Title", - "unique_id": "be9554b9-c9fb-41f4-8920-22da015376a4", - "version": 2, - }, - "device_info": None, - } + assert await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) == snapshot(exclude=props("created_at", "modified_at")) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 9f1efc0f013..59dbfad0552 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -45,10 +45,9 @@ from .const import ( MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, - SAMPLE_DEVICE_INFO_UE48JU6400, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture ENTITY_ID = f"{MP_DOMAIN}.fake_name" MOCK_CONFIG = { @@ -117,7 +116,9 @@ async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test Samsung TV integration is setup.""" - rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_UE48JU6400 + rest_api.rest_device_info.return_value = load_json_object_fixture( + "device_info_UE48JU6400.json", DOMAIN + ) await setup_samsungtv_entry(hass, MOCK_CONFIG) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 10e5249aac3..0a4587827d1 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -85,12 +85,14 @@ from .const import ( MOCK_CONFIG, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS, - SAMPLE_DEVICE_INFO_FRAME, SAMPLE_DEVICE_INFO_WIFI, - SAMPLE_EVENT_ED_INSTALLED_APP, ) -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) ENTITY_ID = f"{MP_DOMAIN}.fake" MOCK_CONFIGWS = { @@ -689,7 +691,9 @@ async def test_turn_off_websocket( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off.""" - remotews.app_list_data = SAMPLE_EVENT_ED_INSTALLED_APP + remotews.app_list_data = load_json_object_fixture( + "ws_installed_app_event.json", DOMAIN + ) with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[OSError("Boom"), DEFAULT_MOCK], @@ -728,7 +732,9 @@ async def test_turn_off_websocket_frame( hass: HomeAssistant, remotews: Mock, rest_api: Mock ) -> None: """Test for turn_off.""" - rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_FRAME + rest_api.rest_device_info.return_value = load_json_object_fixture( + "device_info_UE43LS003.json", DOMAIN + ) with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[OSError("Boom"), DEFAULT_MOCK], @@ -1136,7 +1142,9 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: @pytest.mark.usefixtures("rest_api") async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for select_source.""" - remotews.app_list_data = SAMPLE_EVENT_ED_INSTALLED_APP + remotews.app_list_data = load_json_object_fixture( + "ws_installed_app_event.json", DOMAIN + ) await setup_samsungtv_entry(hass, MOCK_CONFIGWS) remotews.send_commands.reset_mock() From 3af0d6e4841f5bfa438aba799dd36a70f852e4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 2 May 2025 10:08:46 +0200 Subject: [PATCH 0041/1175] Use `is` instead of `==` on check against enum value at Home Connect (#144083) * Use `is` instead of `==` on check against enum value at Home Connect * Revert HTTP status checks --- homeassistant/components/home_connect/time.py | 4 +-- .../home_connect/test_binary_sensor.py | 10 +++---- tests/components/home_connect/test_button.py | 12 ++++---- .../home_connect/test_config_flow.py | 4 +-- .../home_connect/test_coordinator.py | 24 ++++++++-------- .../home_connect/test_diagnostics.py | 4 +-- tests/components/home_connect/test_entity.py | 8 +++--- tests/components/home_connect/test_init.py | 16 +++++------ tests/components/home_connect/test_light.py | 12 ++++---- tests/components/home_connect/test_number.py | 8 +++--- tests/components/home_connect/test_select.py | 10 +++---- tests/components/home_connect/test_sensor.py | 24 ++++++++-------- .../components/home_connect/test_services.py | 14 +++++----- tests/components/home_connect/test_switch.py | 28 +++++++++---------- tests/components/home_connect/test_time.py | 10 +++---- 15 files changed, 94 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/home_connect/time.py b/homeassistant/components/home_connect/time.py index adf26d2d973..6a6e57c4dd3 100644 --- a/homeassistant/components/home_connect/time.py +++ b/homeassistant/components/home_connect/time.py @@ -79,7 +79,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity): async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" await super().async_added_to_hass() - if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK: + if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK: automations = automations_with_entity(self.hass, self.entity_id) scripts = scripts_with_entity(self.hass, self.entity_id) items = automations + scripts @@ -123,7 +123,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity): async def async_will_remove_from_hass(self) -> None: """Call when entity will be removed from hass.""" - if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK: + if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK: async_delete_issue( self.hass, DOMAIN, diff --git a/tests/components/home_connect/test_binary_sensor.py b/tests/components/home_connect/test_binary_sensor.py index 934b6103982..a88c8954c64 100644 --- a/tests/components/home_connect/test_binary_sensor.py +++ b/tests/components/home_connect/test_binary_sensor.py @@ -52,7 +52,7 @@ async def test_paired_depaired_devices_flow( ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -128,7 +128,7 @@ async def test_connected_devices( client.get_status = AsyncMock(side_effect=get_status_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_status = get_status_original_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) @@ -178,7 +178,7 @@ async def test_binary_sensors_entity_availability( "binary_sensor.washer_remote_control", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -277,7 +277,7 @@ async def test_binary_sensors_functionality( ) -> None: """Tests for Home Connect Fridge appliance door states.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await client.add_events( [ EventMessage( @@ -313,7 +313,7 @@ async def test_connected_sensor_functionality( """Test if the connected binary sensor reports the right values.""" entity_id = "binary_sensor.washer_connectivity" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state(entity_id, STATE_ON) diff --git a/tests/components/home_connect/test_button.py b/tests/components/home_connect/test_button.py index 1aca781def6..ee4d5f1d729 100644 --- a/tests/components/home_connect/test_button.py +++ b/tests/components/home_connect/test_button.py @@ -44,7 +44,7 @@ async def test_paired_depaired_devices_flow( ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -131,7 +131,7 @@ async def test_connected_devices( ) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_available_commands = get_available_commands_original_mock client.get_all_programs = get_all_programs_mock @@ -182,7 +182,7 @@ async def test_button_entity_availability( "button.washer_stop_program", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -244,7 +244,7 @@ async def test_button_functionality( ) -> None: """Test if button entities availability are based on the appliance connection state.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity = hass.states.get(entity_id) assert entity @@ -279,7 +279,7 @@ async def test_command_button_exception( ) ) assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity = hass.states.get(entity_id) assert entity @@ -304,7 +304,7 @@ async def test_stop_program_button_exception( entity_id = "button.washer_stop_program" assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity = hass.states.get(entity_id) assert entity diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index d5a01d03258..a8929120acb 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -189,7 +189,7 @@ async def test_reauth_flow( assert entry.state is ConfigEntryState.LOADED assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -239,5 +239,5 @@ async def test_reauth_flow_with_different_account( 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"] == "wrong_account" diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 1a51e5980cd..40af64f9042 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -99,7 +99,7 @@ async def test_coordinator_failure_refresh_and_stream( entity_id_2 = "binary_sensor.washer_remote_start" await async_setup_component(hass, HA_DOMAIN, {}) await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id_1) assert state assert state.state != STATE_UNAVAILABLE @@ -219,7 +219,7 @@ async def test_coordinator_not_fetching_on_disconnected_appliance( appliance.connected = False await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for method in INITIAL_FETCH_CLIENT_METHODS: assert getattr(client, method).call_count == 0 @@ -242,7 +242,7 @@ async def test_coordinator_update_failing( setattr(client, mock_method, AsyncMock(side_effect=HomeConnectError())) await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED getattr(client, mock_method).assert_called() @@ -285,7 +285,7 @@ async def test_event_listener( ) -> None: """Test that the event listener works.""" await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) @@ -351,7 +351,7 @@ async def tests_receive_setting_and_status_for_first_time_at_events( client.get_status = AsyncMock(return_value=ArrayOfStatus([])) await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await client.add_events( [ @@ -391,7 +391,7 @@ async def tests_receive_setting_and_status_for_first_time_at_events( ) await hass.async_block_till_done() assert len(config_entry._background_tasks) == 1 - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED async def test_event_listener_error( @@ -467,7 +467,7 @@ async def test_event_listener_resilience( await integration_setup(client) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert len(config_entry._background_tasks) == 1 state = hass.states.get(entity_id) @@ -527,7 +527,7 @@ async def test_devices_updated_on_refresh( await async_setup_component(hass, HA_DOMAIN, {}) await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for appliance in appliances[:2]: assert device_registry.async_get_device({(DOMAIN, appliance.ha_id)}) @@ -559,7 +559,7 @@ async def test_paired_disconnected_devices_not_fetching( """Test that Home Connect API is not fetched after pairing a disconnected device.""" client.get_home_appliances = AsyncMock(return_value=ArrayOfHomeAppliances([])) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED appliance.connected = False await client.add_events( @@ -595,7 +595,7 @@ async def test_coordinator_disabling_updates_for_appliance( issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state("switch.dishwasher_power", STATE_ON) @@ -685,7 +685,7 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r issue_id = f"home_connect_too_many_connected_paired_events_{appliance_ha_id}" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state("switch.dishwasher_power", STATE_ON) @@ -710,7 +710,7 @@ async def test_coordinator_disabling_updates_for_appliance_is_gone_after_entry_r assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED get_settings_original_side_effect = client.get_settings.side_effect diff --git a/tests/components/home_connect/test_diagnostics.py b/tests/components/home_connect/test_diagnostics.py index 9aef9e0d157..858f331a33d 100644 --- a/tests/components/home_connect/test_diagnostics.py +++ b/tests/components/home_connect/test_diagnostics.py @@ -26,7 +26,7 @@ async def test_async_get_config_entry_diagnostics( ) -> None: """Test config entry diagnostics.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert await async_get_config_entry_diagnostics(hass, config_entry) == snapshot @@ -41,7 +41,7 @@ async def test_async_get_device_diagnostics( ) -> None: """Test device config entry diagnostics.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/home_connect/test_entity.py b/tests/components/home_connect/test_entity.py index 84d8178d4b7..61a0c4005fb 100644 --- a/tests/components/home_connect/test_entity.py +++ b/tests/components/home_connect/test_entity.py @@ -158,7 +158,7 @@ async def test_program_options_retrieval( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id, (state, _) in zip( option_entity_id.values(), options_state_stage_1, strict=True @@ -276,7 +276,7 @@ async def test_no_options_retrieval_on_unknown_program( client.get_all_programs = AsyncMock(side_effect=get_all_programs_with_options_mock) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert client.get_available_program.call_count == 0 @@ -356,7 +356,7 @@ async def test_program_options_retrieval_after_appliance_connection( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert not hass.states.get(option_entity_id) @@ -467,7 +467,7 @@ async def test_option_entity_functionality_exception( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id) diff --git a/tests/components/home_connect/test_init.py b/tests/components/home_connect/test_init.py index 9bd4eaeca0e..2820eea3031 100644 --- a/tests/components/home_connect/test_init.py +++ b/tests/components/home_connect/test_init.py @@ -47,12 +47,12 @@ async def test_entry_setup( ) -> None: """Test setup and unload.""" assert await integration_setup(client) - 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("token_expiration_time", [12345]) @@ -85,7 +85,7 @@ async def test_token_refresh_success( client._auth = auth return client - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with ( patch("homeassistant.components.home_connect.PLATFORMS", platforms), patch("homeassistant.components.home_connect.HomeConnectClient") as client_mock, @@ -93,7 +93,7 @@ async def test_token_refresh_success( client_mock.side_effect = MagicMock(side_effect=init_side_effect) 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 # Verify token request assert aioclient_mock.call_count == 1 @@ -154,7 +154,7 @@ async def test_token_refresh_error( **aioclient_mock_args, ) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.home_connect.HomeConnectClient", return_value=client ): @@ -216,12 +216,12 @@ async def test_client_rate_limit_error( mock.side_effect = side_effect setattr(client, raising_exception_method, mock) - assert config_entry.state == ConfigEntryState.NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.home_connect.coordinator.asyncio_sleep", ) as asyncio_sleep_mock: assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert mock.call_count >= 2 asyncio_sleep_mock.assert_called_once_with(retry_after) @@ -238,7 +238,7 @@ async def test_required_program_or_at_least_an_option( "Test that the set_program_and_options does raise an exception if no program nor options are set." assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/home_connect/test_light.py b/tests/components/home_connect/test_light.py index abffa491ce4..b467dd2a7d2 100644 --- a/tests/components/home_connect/test_light.py +++ b/tests/components/home_connect/test_light.py @@ -65,7 +65,7 @@ async def test_paired_depaired_devices_flow( ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -141,7 +141,7 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) @@ -185,7 +185,7 @@ async def test_light_availability( "light.hood_functional_light", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -351,7 +351,7 @@ async def test_light_functionality( ) -> None: """Test light functionality.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED service_data = exprected_attributes.copy() service_data[ATTR_ENTITY_ID] = entity_id @@ -402,7 +402,7 @@ async def test_light_color_different_than_custom( ) -> None: """Test that light color attributes are not set if color is different than custom.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -582,7 +582,7 @@ async def test_light_exception_handling( exception() if exception else None for exception in attr_side_effect ] assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Assert that an exception is called. with pytest.raises(HomeConnectError): diff --git a/tests/components/home_connect/test_number.py b/tests/components/home_connect/test_number.py index 1f2a9b8d73f..58d6dae2900 100644 --- a/tests/components/home_connect/test_number.py +++ b/tests/components/home_connect/test_number.py @@ -81,7 +81,7 @@ async def test_paired_depaired_devices_flow( ) ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -159,7 +159,7 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) @@ -208,7 +208,7 @@ async def test_number_entity_availability( # so we rise an error to easily test the availability client.get_setting = AsyncMock(side_effect=HomeConnectError()) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -594,7 +594,7 @@ async def test_options_functionality( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) assert entity_state assert entity_state.attributes["unit_of_measurement"] == unit diff --git a/tests/components/home_connect/test_select.py b/tests/components/home_connect/test_select.py index 11f6b3ce94b..a4263808276 100644 --- a/tests/components/home_connect/test_select.py +++ b/tests/components/home_connect/test_select.py @@ -85,7 +85,7 @@ async def test_paired_depaired_devices_flow( ) ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -174,7 +174,7 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock client.get_all_programs = get_all_programs_mock @@ -219,7 +219,7 @@ async def test_select_entity_availability( "select.washer_active_program", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -489,7 +489,7 @@ async def test_programs_updated_on_connect( client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_all_programs = get_all_programs_mock state = hass.states.get("select.washer_active_program") @@ -941,7 +941,7 @@ async def test_options_functionality( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) assert entity_state assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index 33cb8d2c804..47badd8d06d 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -101,7 +101,7 @@ async def test_paired_depaired_devices_flow( ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -200,7 +200,7 @@ async def test_connected_devices( client.get_status = AsyncMock(side_effect=get_status_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_status = get_status_original_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) @@ -245,7 +245,7 @@ async def test_sensor_entity_availability( "sensor.dishwasher_salt_nearly_empty", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await client.add_events( [ @@ -367,7 +367,7 @@ async def test_program_sensors( 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 config_entry.state is ConfigEntryState.NOT_LOADED client.get_status.return_value.status.extend( Status( key=StatusKey(event_key.value), @@ -377,7 +377,7 @@ async def test_program_sensors( for event_key, value in EVENT_PROG_DELAYED_START[EventType.STATUS].items() ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await client.add_events( [ @@ -450,7 +450,7 @@ async def test_program_sensor_edge_case( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state(entity_id, initial_state) @@ -512,7 +512,7 @@ async def test_remaining_prog_time_edge_cases( freezer.move_to(time_to_freeze) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for ( event, @@ -587,7 +587,7 @@ async def test_sensors_states( ) -> None: """Tests for appliance sensors.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for value, expected_state in value_expected_state: await client.add_events( @@ -648,7 +648,7 @@ async def test_event_sensors_states( """Tests for appliance event sensors.""" caplog.set_level(logging.ERROR) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert not hass.states.get(entity_id) @@ -757,7 +757,7 @@ async def test_sensor_unit_fetching( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED entity_state = hass.states.get(entity_id) assert entity_state @@ -812,7 +812,7 @@ async def test_sensor_unit_fetching_error( client.get_status_value = AsyncMock(side_effect=HomeConnectError()) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id) @@ -875,7 +875,7 @@ async def test_sensor_unit_fetching_after_rate_limit_error( assert await integration_setup(client) async_fire_time_changed(hass) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert client.get_status_value.call_count == 2 diff --git a/tests/components/home_connect/test_services.py b/tests/components/home_connect/test_services.py index 97c60a72237..33a7f7aee71 100644 --- a/tests/components/home_connect/test_services.py +++ b/tests/components/home_connect/test_services.py @@ -186,7 +186,7 @@ async def test_key_value_services( ) -> None: """Create and test services.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -236,7 +236,7 @@ async def test_programs_and_options_actions_deprecation( ) -> None: """Test deprecated service keys.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -304,7 +304,7 @@ async def test_set_program_and_options( ) -> None: """Test recognized options.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -345,7 +345,7 @@ async def test_set_program_and_options_exceptions( ) -> None: """Test recognized options.""" assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -373,7 +373,7 @@ async def test_services_exception_device_id( ) -> None: """Raise a HomeAssistantError when there is an API error.""" assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -395,7 +395,7 @@ async def test_services_appliance_not_found( ) -> None: """Raise a ServiceValidationError when device id does not match.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED service_call = SERVICE_KV_CALL_PARAMS[0] @@ -443,7 +443,7 @@ async def test_services_exception( ) -> None: """Raise a ValueError when device id does not match.""" assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 404e3a5bcea..40d2468fb3e 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -92,7 +92,7 @@ async def test_paired_depaired_devices_flow( ) ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -181,7 +181,7 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock client.get_all_programs = get_all_programs_mock @@ -229,7 +229,7 @@ async def test_switch_entity_availability( "switch.dishwasher_program_eco50", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -311,7 +311,7 @@ async def test_switch_functionality( """Test switch functionality.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}) await hass.async_block_till_done() @@ -377,7 +377,7 @@ async def test_program_switch_functionality( client.stop_program = AsyncMock(side_effect=mock_stop_program) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state(entity_id, initial_state) await hass.services.async_call( @@ -484,7 +484,7 @@ async def test_switch_exception_handling( ) assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Assert that an exception is called. with pytest.raises(HomeConnectError): @@ -527,7 +527,7 @@ async def test_ent_desc_switch_functionality( """Test switch functionality - entity description setup.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}) await hass.async_block_till_done() @@ -583,7 +583,7 @@ async def test_ent_desc_switch_exception_handling( ] ) assert await integration_setup(client_with_exception) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED # Assert that an exception is called. with pytest.raises(HomeConnectError): @@ -668,7 +668,7 @@ async def test_power_switch( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}) await hass.async_block_till_done() @@ -707,7 +707,7 @@ async def test_power_switch_fetch_off_state_from_current_value( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.is_state("switch.dishwasher_power", STATE_OFF) @@ -772,7 +772,7 @@ async def test_power_switch_service_validation_errors( client.get_setting = AsyncMock(return_value=setting) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED with pytest.raises(HomeAssistantError, match=exception_match): await hass.services.async_call( @@ -832,7 +832,7 @@ async def test_create_program_switch_deprecation_issue( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -912,7 +912,7 @@ async def test_program_switch_deprecation_issue_fix( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call( SWITCH_DOMAIN, @@ -1006,7 +1006,7 @@ async def test_options_functionality( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get(entity_id) await hass.services.async_call( diff --git a/tests/components/home_connect/test_time.py b/tests/components/home_connect/test_time.py index c94f3affc41..9e114768b6f 100644 --- a/tests/components/home_connect/test_time.py +++ b/tests/components/home_connect/test_time.py @@ -58,7 +58,7 @@ async def test_paired_depaired_devices_flow( ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -135,7 +135,7 @@ async def test_connected_devices( client.get_settings = AsyncMock(side_effect=get_settings_side_effect) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) @@ -180,7 +180,7 @@ async def test_time_entity_availability( "time.oven_alarm_clock", ] assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED for entity_id in entity_ids: state = hass.states.get(entity_id) @@ -363,7 +363,7 @@ async def test_create_alarm_clock_deprecation_issue( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call( TIME_DOMAIN, @@ -442,7 +442,7 @@ async def test_alarm_clock_deprecation_issue_fix( ) assert await integration_setup(client) - assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.services.async_call( TIME_DOMAIN, From 86b845f04acb6f4368c46cd4b912fd2053f38bae Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 2 May 2025 12:32:41 +0300 Subject: [PATCH 0042/1175] Mark exception-translations done in Shelly (#144073) --- homeassistant/components/shelly/quality_scale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 83c3739a208..601170879d1 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -56,7 +56,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: todo - exception-translations: todo + exception-translations: done icon-translations: todo reconfiguration-flow: done repair-issues: done From b0f1c71129dbb04418c60498cdd9267af544355b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 2 May 2025 12:39:28 +0300 Subject: [PATCH 0043/1175] Handle missing action exceptions in SamsungTV (#143630) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/samsungtv/bridge.py | 10 +++++-- .../components/samsungtv/media_player.py | 10 +++++-- .../components/samsungtv/strings.json | 6 ++++ .../components/samsungtv/test_media_player.py | 30 +++++++++---------- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index e782b1dfcd9..11da83219c7 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -46,6 +46,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_component from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -53,6 +54,7 @@ from homeassistant.util import dt as dt_util from .const import ( CONF_SESSION_ID, + DOMAIN, ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, LOGGER, @@ -371,9 +373,13 @@ class SamsungTVLegacyBridge(SamsungTVBridge): except (ConnectionClosed, BrokenPipeError): # BrokenPipe can occur when the commands is sent to fast self._remote = None - except (UnhandledResponse, AccessDenied): + except (UnhandledResponse, AccessDenied) as err: # We got a response so it's on. - LOGGER.debug("Failed sending command %s", key, exc_info=True) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="error_sending_command", + translation_placeholders={"error": repr(err), "host": self.host}, + ) from err except OSError: # Different reasons, e.g. hostname not resolveable pass diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 4fb2e6bd1a2..cc3ca5f142e 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -29,13 +29,14 @@ from homeassistant.components.media_player import ( MediaType, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.async_ import create_eager_task from .bridge import SamsungTVWSBridge -from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, LOGGER +from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, LOGGER from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator from .entity import SamsungTVEntity @@ -308,7 +309,12 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity): try: await dmr_device.async_set_volume_level(volume) except UpnpActionResponseError as err: - LOGGER.warning("Unable to set volume level on %s: %r", self._host, err) + assert self._host + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="error_set_volume", + translation_placeholders={"error": repr(err), "host": self._host}, + ) from err async def async_volume_up(self) -> None: """Volume up the media player.""" diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index 84e5fded03f..fc3be3fcc19 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -68,6 +68,12 @@ "service_unsupported": { "message": "Entity {entity} does not support this action." }, + "error_set_volume": { + "message": "Unable to set volume level on {host}: {error}" + }, + "error_sending_command": { + "message": "Unable to send command to {host}: {error}" + }, "encrypted_mode_auth_failed": { "message": "Token and session ID are required in encrypted mode." }, diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 0a4587827d1..7dc5c6489d8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -77,7 +77,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceNotSupported +from homeassistant.exceptions import HomeAssistantError, ServiceNotSupported from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -563,9 +563,11 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> """Testing unhandled response exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) - await hass.services.async_call( - MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert err.value.translation_key == "error_sending_command" state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -1219,9 +1221,7 @@ async def test_websocket_unsupported_remote_control( @pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server") -async def test_volume_control_upnp( - hass: HomeAssistant, dmr_device: Mock, caplog: pytest.LogCaptureFixture -) -> None: +async def test_volume_control_upnp(hass: HomeAssistant, dmr_device: Mock) -> None: """Test for Upnp volume control.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) @@ -1237,21 +1237,21 @@ async def test_volume_control_upnp( True, ) dmr_device.async_set_volume_level.assert_called_once_with(0.5) - assert "Unable to set volume level on" not in caplog.text # Upnp action failed dmr_device.async_set_volume_level.reset_mock() dmr_device.async_set_volume_level.side_effect = UpnpActionResponseError( status=500, error_code=501, error_desc="Action Failed" ) - await hass.services.async_call( - MP_DOMAIN, - SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, - True, - ) + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, + True, + ) + assert err.value.translation_key == "error_set_volume" dmr_device.async_set_volume_level.assert_called_once_with(0.6) - assert "Unable to set volume level on" in caplog.text @pytest.mark.usefixtures("remotews", "rest_api") From 9861bd88b9b773ec314b97228f2bf08b410df170 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 04:44:38 -0500 Subject: [PATCH 0044/1175] Avoid working out suggested id in entity_platform when already registered (#144079) If the entity is already registered, avoid trying to work out the suggested_entity_id and suggested_object_id as async_get_or_create will discard them anyways. --- homeassistant/helpers/entity_platform.py | 41 ++++++++++++++---------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d4fa567e929..f543891d3f3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -843,24 +843,31 @@ class EntityPlatform: else: device = None - # An entity may suggest the entity_id by setting entity_id itself - suggested_entity_id: str | None = entity.entity_id - if suggested_entity_id is not None: - suggested_object_id = split_entity_id(entity.entity_id)[1] - else: - if device and entity.has_entity_name: - device_name = device.name_by_user or device.name - if entity.use_device_name: - suggested_object_id = device_name - else: - suggested_object_id = ( - f"{device_name} {entity.suggested_object_id}" - ) - if not suggested_object_id: - suggested_object_id = entity.suggested_object_id + if not registered_entity_id: + # Do not bother working out a suggested_object_id + # if the entity is already registered as it will + # be ignored. + # + # An entity may suggest the entity_id by setting entity_id itself + suggested_entity_id: str | None = entity.entity_id + if suggested_entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + if device and entity.has_entity_name: + device_name = device.name_by_user or device.name + if entity.use_device_name: + suggested_object_id = device_name + else: + suggested_object_id = ( + f"{device_name} {entity.suggested_object_id}" + ) + if not suggested_object_id: + suggested_object_id = entity.suggested_object_id - if self.entity_namespace is not None: - suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" + if self.entity_namespace is not None: + suggested_object_id = ( + f"{self.entity_namespace} {suggested_object_id}" + ) disabled_by: RegistryEntryDisabler | None = None if not entity.entity_registry_enabled_default: From 81444c8f4a500d085d93d6142958c7a84049285d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Bed=C5=99ich?= Date: Fri, 2 May 2025 13:49:33 +0200 Subject: [PATCH 0045/1175] Disable S3 checksums (#144092) Disable S3 checksums (#143995) --- homeassistant/components/s3/__init__.py | 7 +++++++ tests/components/s3/test_init.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/s3/__init__.py index 95e5e7d738c..ea6b8e244b1 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/s3/__init__.py @@ -7,6 +7,7 @@ from typing import cast from aiobotocore.client import AioBaseClient as S3Client from aiobotocore.session import AioSession +from botocore.config import Config from botocore.exceptions import ClientError, ConnectionError, ParamValidationError from homeassistant.config_entries import ConfigEntry @@ -32,6 +33,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: """Set up S3 from a config entry.""" data = cast(dict, entry.data) + # due to https://github.com/home-assistant/core/issues/143995 + config = Config( + request_checksum_calculation="when_required", + response_checksum_validation="when_required", + ) try: session = AioSession() # pylint: disable-next=unnecessary-dunder-call @@ -40,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: endpoint_url=data.get(CONF_ENDPOINT_URL), aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY], aws_access_key_id=data[CONF_ACCESS_KEY_ID], + config=config, ).__aenter__() await client.head_bucket(Bucket=data[CONF_BUCKET]) except ClientError as err: diff --git a/tests/components/s3/test_init.py b/tests/components/s3/test_init.py index afa11f5cf72..8255bbd0c66 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/s3/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from botocore.config import Config from botocore.exceptions import ( ClientError, EndpointConnectionError, @@ -73,3 +74,19 @@ async def test_setup_entry_head_bucket_error( ) await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_checksum_settings_present( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that checksum validation is set to be compatible with third-party S3 providers.""" + # due to https://github.com/home-assistant/core/issues/143995 + with patch( + "homeassistant.components.s3.AioSession.create_client" + ) as mock_create_client: + await setup_integration(hass, mock_config_entry) + + config_arg = mock_create_client.call_args[1]["config"] + assert isinstance(config_arg, Config) + assert config_arg.request_checksum_calculation == "when_required" + assert config_arg.response_checksum_validation == "when_required" From cbf4676ae405da3b4f9689a23ad30f43336ea0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 2 May 2025 17:31:11 +0200 Subject: [PATCH 0046/1175] Improve handling of missing miele program codes (#144093) * Use device class transation * Improve handling of unknown program codes * Address review comment --- homeassistant/components/miele/const.py | 20 ++++++++++------ homeassistant/components/miele/sensor.py | 24 +++---------------- .../miele/snapshots/test_sensor.ambr | 2 ++ 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index e77afe02e00..1802c6c9cd0 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -2,6 +2,8 @@ from enum import IntEnum +from pymiele import MieleEnum + DOMAIN = "miele" MANUFACTURER = "Miele" @@ -325,13 +327,17 @@ STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = { MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER, } -STATE_PROGRAM_TYPE = { - 0: "normal_operation_mode", - 1: "own_program", - 2: "automatic_program", - 3: "cleaning_care_program", - 4: "maintenance_program", -} + +class StateProgramType(MieleEnum): + """Defines program types.""" + + normal_operation_mode = 0 + own_program = 1 + automatic_program = 2 + cleaning_care_program = 3 + maintenance_program = 4 + unknown = -9999 + WASHING_MACHINE_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index b2ddd695042..867de3d814b 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -30,9 +30,9 @@ from homeassistant.helpers.typing import StateType from .const import ( STATE_PROGRAM_ID, STATE_PROGRAM_PHASE, - STATE_PROGRAM_TYPE, STATE_STATUS_TAGS, MieleAppliance, + StateProgramType, StateStatus, ) from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator @@ -181,10 +181,10 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( description=MieleSensorDescription( key="state_program_type", translation_key="program_type", - value_fn=lambda value: value.state_program_type, + value_fn=lambda value: StateProgramType(value.state_program_type).name, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, - options=sorted(set(STATE_PROGRAM_TYPE.values())), + options=sorted(set(StateProgramType.keys())), ), ), MieleSensorDefinition( @@ -440,8 +440,6 @@ async def async_setup_entry( entity_class = MieleProgramIdSensor case "state_program_phase": entity_class = MielePhaseSensor - case "state_program_type": - entity_class = MieleTypeSensor case _: entity_class = MieleSensor if ( @@ -553,22 +551,6 @@ class MielePhaseSensor(MieleSensor): ) -class MieleTypeSensor(MieleSensor): - """Representation of the program type sensor.""" - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - ret_val = STATE_PROGRAM_TYPE.get(int(self.device.state_program_type)) - if ret_val is None: - _LOGGER.debug( - "Unknown program type: %s on device type: %s", - self.device.state_program_type, - self.device.device_type, - ) - return ret_val - - class MieleProgramIdSensor(MieleSensor): """Representation of the program id sensor.""" diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 9cc2aa83b01..bd9c305fe18 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -788,6 +788,7 @@ 'maintenance_program', 'normal_operation_mode', 'own_program', + 'unknown', ]), }), 'config_entry_id': , @@ -829,6 +830,7 @@ 'maintenance_program', 'normal_operation_mode', 'own_program', + 'unknown', ]), }), 'context': , From 5e463d6af49374a97acd6da6f2adbeed8584e344 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Fri, 2 May 2025 11:34:58 -0400 Subject: [PATCH 0047/1175] bump aiokem to 0.5.9 (#144098) fix: bump aiokem to 0.5.9 --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 93e284167f5..0c9f0c20e6f 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.6"] + "requirements": ["aiokem==0.5.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 593ba8bdacd..2c4eb978684 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.6 +aiokem==0.5.9 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b3bb6d0a40..ca4a2107cdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimaplib==2.0.1 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.6 +aiokem==0.5.9 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 4967c287f87ee5ef9c11bce981f619684d59423b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 2 May 2025 18:34:09 +0200 Subject: [PATCH 0048/1175] Add DHCP discovery to Knocki (#144048) * Add DHCP discovery to Knocki * Update homeassistant/components/knocki/quality_scale.yaml Co-authored-by: Josef Zweck --------- Co-authored-by: Josef Zweck --- .../components/knocki/config_flow.py | 18 +++++ homeassistant/components/knocki/manifest.json | 5 ++ .../components/knocki/quality_scale.yaml | 6 +- homeassistant/generated/dhcp.py | 4 + tests/components/knocki/__init__.py | 1 + tests/components/knocki/test_config_flow.py | 75 ++++++++++++++++++- 6 files changed, 104 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knocki/config_flow.py b/homeassistant/components/knocki/config_flow.py index 654dd4a4d1f..7818c752a87 100644 --- a/homeassistant/components/knocki/config_flow.py +++ b/homeassistant/components/knocki/config_flow.py @@ -10,7 +10,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import DOMAIN, LOGGER @@ -62,3 +64,19 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, data_schema=DATA_SCHEMA, ) + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle a DHCP discovery.""" + device_registry = dr.async_get(self.hass) + if device_entry := device_registry.async_get_device( + identifiers={(DOMAIN, discovery_info.hostname)} + ): + device_registry.async_update_device( + device_entry.id, + new_connections={ + (dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress) + }, + ) + return await super().async_step_dhcp(discovery_info) diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index a91119ca831..18f25f0ab0e 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -3,6 +3,11 @@ "name": "Knocki", "codeowners": ["@joostlek", "@jgatto1", "@JakeBosh"], "config_flow": true, + "dhcp": [ + { + "hostname": "knc*" + } + ], "documentation": "https://www.home-assistant.io/integrations/knocki", "integration_type": "hub", "iot_class": "cloud_push", diff --git a/homeassistant/components/knocki/quality_scale.yaml b/homeassistant/components/knocki/quality_scale.yaml index 45b3764d786..d1c5994b277 100644 --- a/homeassistant/components/knocki/quality_scale.yaml +++ b/homeassistant/components/knocki/quality_scale.yaml @@ -50,10 +50,8 @@ rules: # Gold devices: done diagnostics: todo - discovery-update-info: - status: exempt - comment: This is a cloud service and does not benefit from device updates. - discovery: todo + discovery-update-info: done + discovery: done docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 53506ed1748..88fb8e06d02 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -311,6 +311,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "polisy*", "macaddress": "000DB9*", }, + { + "domain": "knocki", + "hostname": "knc*", + }, { "domain": "lamarzocco", "registered_devices": True, diff --git a/tests/components/knocki/__init__.py b/tests/components/knocki/__init__.py index 4ebf6b0dd01..3de1e80d9e4 100644 --- a/tests/components/knocki/__init__.py +++ b/tests/components/knocki/__init__.py @@ -10,3 +10,4 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/knocki/test_config_flow.py b/tests/components/knocki/test_config_flow.py index 188175035da..4affbd2a197 100644 --- a/tests/components/knocki/test_config_flow.py +++ b/tests/components/knocki/test_config_flow.py @@ -6,13 +6,23 @@ from knocki import KnockiConnectionError, KnockiInvalidAuthError import pytest from homeassistant.components.knocki.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo + +from . import setup_integration from tests.common import MockConfigEntry +DHCP_DISCOVERY = DhcpServiceInfo( + ip="1.1.1.1", + hostname="KNC1-W-00000214", + macaddress="aa:bb:cc:dd:ee:ff", +) + async def test_full_flow( hass: HomeAssistant, @@ -111,3 +121,66 @@ async def test_exceptions( }, ) assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_dhcp( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test DHCP discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] is FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "test-id" + + +async def test_dhcp_mac( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test updating the mac address in the DHCP discovery.""" + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device(identifiers={(DOMAIN, "KNC1-W-00000214")}) + assert device + assert device.connections == set() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + device = device_registry.async_get_device(identifiers={(DOMAIN, "KNC1-W-00000214")}) + assert device + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} + + +async def test_dhcp_already_setup( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery with already setup device.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From 762d284102ce6faa4f02e1e5e96baa6bd8db11fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 2 May 2025 19:31:56 +0200 Subject: [PATCH 0049/1175] Improve naming of miele freezers and fridges (#144062) * Use device class transation * Improve naming of miele freezers and fridges * Address review * Address review comment * Simplify --- homeassistant/components/miele/climate.py | 8 +++++-- .../miele/snapshots/test_climate.ambr | 24 +++++++++---------- tests/components/miele/test_climate.py | 2 +- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 054ab227ca6..22257448e3a 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity): t_key = ZONE1_DEVICES.get( cast(MieleAppliance, self.device.device_type), "zone_1" ) + if self.device.device_type in ( + MieleAppliance.FRIDGE, + MieleAppliance.FREEZER, + ): + self._attr_name = None if description.zone == 2: if self.device.device_type in ( @@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the target temperature.""" - if self.entity_description.target_fn(self.device) is None: - return None + return cast(float | None, self.entity_description.target_fn(self.device)) @property diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 15490047d36..85f7bf212f5 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-entry] +# name: test_climate_states[platforms0-freezer][climate.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19,7 +19,7 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.freezer_freezer', + 'entity_id': 'climate.freezer', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,7 +31,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Freezer', + 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, 'supported_features': , @@ -40,11 +40,11 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-state] +# name: test_climate_states[platforms0-freezer][climate.freezer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': -18, - 'friendly_name': 'Freezer Freezer', + 'friendly_name': 'Freezer', 'hvac_modes': list([ , ]), @@ -55,14 +55,14 @@ 'temperature': -18, }), 'context': , - 'entity_id': 'climate.freezer_freezer', + 'entity_id': 'climate.freezer', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'cool', }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-entry] +# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -82,7 +82,7 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.refrigerator_refrigerator', + 'entity_id': 'climate.refrigerator', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -94,7 +94,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Refrigerator', + 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, 'supported_features': , @@ -103,11 +103,11 @@ 'unit_of_measurement': None, }) # --- -# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-state] +# name: test_climate_states[platforms0-freezer][climate.refrigerator-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': 4, - 'friendly_name': 'Refrigerator Refrigerator', + 'friendly_name': 'Refrigerator', 'hvac_modes': list([ , ]), @@ -118,7 +118,7 @@ 'temperature': 4, }), 'context': , - 'entity_id': 'climate.refrigerator_refrigerator', + 'entity_id': 'climate.refrigerator', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index 73e530eb87c..f03edada841 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -26,7 +26,7 @@ pytestmark = [ ), ] -ENTITY_ID = "climate.freezer_freezer" +ENTITY_ID = "climate.freezer" SERVICE_SET_TEMPERATURE = "set_temperature" From 97be2c4ac9c7a344812ab19a06167a3d0e700e12 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 2 May 2025 11:17:58 -0700 Subject: [PATCH 0050/1175] Bump py-nextbusnext to 2.1.2 (#144081)r Bump py-nextbusnext version Fixes #144059 --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 6300dc1cdc9..a4f6d54f58c 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.0.5"] + "requirements": ["py-nextbusnext==2.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c4eb978684..7a9addb66a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1759,7 +1759,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.5 +py-nextbusnext==2.1.2 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca4a2107cdb..a01ecdd406c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1461,7 +1461,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.0.5 +py-nextbusnext==2.1.2 # homeassistant.components.nightscout py-nightscout==1.2.2 From 2890fc7dd2d47e486e369a6ea3574bb9daadbfc4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 13:43:06 -0500 Subject: [PATCH 0051/1175] Only create a single resolver object if there are multiple aiohttp sessions (#144090) --- homeassistant/helpers/aiohttp_client.py | 36 ++++++++++++++++++++++--- tests/conftest.py | 4 ++- tests/helpers/test_aiohttp_client.py | 12 +++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 3d8dc247857..a9976cf7e32 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -28,6 +28,7 @@ from homeassistant.util.json import json_loads from .frame import warn_use from .json import json_dumps +from .singleton import singleton if TYPE_CHECKING: from aiohttp.typedefs import JSONDecoder @@ -39,6 +40,7 @@ DATA_CONNECTOR: HassKey[dict[tuple[bool, int, str], aiohttp.BaseConnector]] = Ha DATA_CLIENTSESSION: HassKey[dict[tuple[bool, int, str], aiohttp.ClientSession]] = ( HassKey("aiohttp_clientsession") ) +DATA_RESOLVER: HassKey[HassAsyncDNSResolver] = HassKey("aiohttp_resolver") SERVER_SOFTWARE = ( f"{APPLICATION_NAME}/{__version__} " @@ -70,6 +72,21 @@ MAXIMUM_CONNECTIONS = 4096 MAXIMUM_CONNECTIONS_PER_HOST = 100 +class HassAsyncDNSResolver(AsyncDualMDNSResolver): + """Home Assistant AsyncDNSResolver. + + This is a wrapper around the AsyncDualMDNSResolver to only + close the resolver when the Home Assistant instance is closed. + """ + + async def real_close(self) -> None: + """Close the resolver.""" + await super().close() + + async def close(self) -> None: + """Close the resolver.""" + + class HassClientResponse(aiohttp.ClientResponse): """aiohttp.ClientResponse with a json method that uses json_loads by default.""" @@ -363,7 +380,7 @@ def _async_get_connector( ssl=ssl_context, limit=MAXIMUM_CONNECTIONS, limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST, - resolver=_async_make_resolver(hass), + resolver=_async_get_or_create_resolver(hass), ) connectors[connector_key] = connector @@ -376,6 +393,19 @@ def _async_get_connector( return connector +@singleton(DATA_RESOLVER) @callback -def _async_make_resolver(hass: HomeAssistant) -> AsyncDualMDNSResolver: - return AsyncDualMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) +def _async_get_or_create_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver: + """Return the HassAsyncDNSResolver.""" + resolver = _async_make_resolver(hass) + + async def _async_close_resolver(event: Event) -> None: + await resolver.real_close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_resolver) + return resolver + + +@callback +def _async_make_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver: + return HassAsyncDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass)) diff --git a/tests/conftest.py b/tests/conftest.py index ff4a09096e0..9b861d5bde5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1319,9 +1319,11 @@ def disable_translations_once( @pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session") async def mock_zeroconf_resolver() -> AsyncGenerator[_patch]: """Mock out the zeroconf resolver.""" + resolver = AsyncResolver() + resolver.real_close = resolver.close patcher = patch( "homeassistant.helpers.aiohttp_client._async_make_resolver", - return_value=AsyncResolver(), + return_value=resolver, ) patcher.start() try: diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 6d2a7e7a8bb..e44111634d1 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -401,3 +401,15 @@ async def test_async_mdnsresolver( resp = await session.post("http://localhost/xyz", json={"x": 1}) assert resp.status == 200 assert await resp.json() == {"x": 1} + + +async def test_resolver_is_singleton(hass: HomeAssistant) -> None: + """Test that the resolver is a singleton.""" + session = client.async_get_clientsession(hass) + session2 = client.async_get_clientsession(hass) + session3 = client.async_create_clientsession(hass) + assert isinstance(session._connector, aiohttp.TCPConnector) + assert isinstance(session2._connector, aiohttp.TCPConnector) + assert isinstance(session3._connector, aiohttp.TCPConnector) + assert session._connector._resolver is session2._connector._resolver + assert session._connector._resolver is session3._connector._resolver From 4c2e9fc7590bb242e602b45db84582daa72d89ad Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 3 May 2025 05:13:12 +1000 Subject: [PATCH 0052/1175] Bump teslemetry-stream to 0.7.7 (#144085) --- homeassistant/components/teslemetry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 8194fb3d6db..5b7454b87b6 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.5"] + "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7a9addb66a5..2beb7ab9632 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2897,7 +2897,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.5 +teslemetry-stream==0.7.7 # homeassistant.components.tessie tessie-api==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a01ecdd406c..c6b808f4818 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2341,7 +2341,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.5 +teslemetry-stream==0.7.7 # homeassistant.components.tessie tessie-api==0.1.1 From df4297be62408714d49c6d36bd04b015cdf4fc88 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 2 May 2025 22:29:54 +0200 Subject: [PATCH 0053/1175] Fix intermittent unavailability for lamarzocco brew active sensor (#144120) * Fix brew active intermittent unavailability for lamarzocco * Whitespaces --- .../components/lamarzocco/binary_sensor.py | 2 +- .../components/lamarzocco/coordinator.py | 26 ++++++++++++++----- homeassistant/components/lamarzocco/entity.py | 5 ++-- homeassistant/components/lamarzocco/number.py | 14 +++++----- .../lamarzocco/test_binary_sensor.py | 11 ++++++++ 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 98cf7cf222e..9bf04129095 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( ).status is MachineState.BREWING ), - available_fn=lambda device: device.websocket.connected, + available_fn=lambda coordinator: not coordinator.websocket_terminated, entity_category=EntityCategory.DIAGNOSTIC, ), LaMarzoccoBinarySensorEntityDescription( diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index 751ef550516..f0f64e02c28 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]): _default_update_interval = SCAN_INTERVAL config_entry: LaMarzoccoConfigEntry + websocket_terminated = True def __init__( self, @@ -92,15 +93,9 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): await self.device.get_dashboard() _LOGGER.debug("Current status: %s", self.device.dashboard.to_dict()) - _LOGGER.debug("Init WebSocket in background task") - self.config_entry.async_create_background_task( hass=self.hass, - target=self.device.connect_dashboard_websocket( - update_callback=lambda _: self.async_set_updated_data(None), - connect_callback=self.async_update_listeners, - disconnect_callback=self.async_update_listeners, - ), + target=self.connect_websocket(), name="lm_websocket_task", ) @@ -112,6 +107,23 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): ) self.config_entry.async_on_unload(websocket_close) + async def connect_websocket(self) -> None: + """Connect to the websocket.""" + + _LOGGER.debug("Init WebSocket in background task") + + self.websocket_terminated = False + self.async_update_listeners() + + await self.device.connect_dashboard_websocket( + update_callback=lambda _: self.async_set_updated_data(None), + connect_callback=self.async_update_listeners, + disconnect_callback=self.async_update_listeners, + ) + + self.websocket_terminated = True + self.async_update_listeners() + class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator): """Coordinator for La Marzocco settings.""" diff --git a/homeassistant/components/lamarzocco/entity.py b/homeassistant/components/lamarzocco/entity.py index 2e3a7f2ce83..6dc024645ce 100644 --- a/homeassistant/components/lamarzocco/entity.py +++ b/homeassistant/components/lamarzocco/entity.py @@ -3,7 +3,6 @@ from collections.abc import Callable from dataclasses import dataclass -from pylamarzocco import LaMarzoccoMachine from pylamarzocco.const import FirmwareType from homeassistant.const import CONF_ADDRESS, CONF_MAC @@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator class LaMarzoccoEntityDescription(EntityDescription): """Description for all LM entities.""" - available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True + available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True @@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity): def available(self) -> bool: """Return True if entity is available.""" if super().available: - return self.entity_description.available_fn(self.coordinator.device) + return self.entity_description.available_fn(self.coordinator) return False def __init__( diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 81a03b4d6ee..7c4fe33a041 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .seconds.seconds_out ), available_fn=( - lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + lambda coordinator: cast( + PreBrewing, + coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING], ).mode is PreExtractionMode.PREINFUSION ), @@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .times.pre_brewing[0] .seconds.seconds_in ), - available_fn=lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + available_fn=lambda coordinator: cast( + PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING] ).mode is PreExtractionMode.PREBREWING, supported_fn=( @@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( .seconds.seconds_out ), available_fn=( - lambda machine: cast( - PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING] + lambda coordinator: cast( + PreBrewing, + coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING], ).mode is PreExtractionMode.PREBREWING ), diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 8e92c9bbba9..570b5aef8ec 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests for La Marzocco binary sensors.""" +from collections.abc import Generator from datetime import timedelta from unittest.mock import MagicMock, patch @@ -33,6 +34,16 @@ async def test_binary_sensors( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.fixture(autouse=True) +def mock_websocket_terminated() -> Generator[bool]: + """Mock websocket terminated.""" + with patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated", + new=False, + ) as mock_websocket_terminated: + yield mock_websocket_terminated + + async def test_brew_active_unavailable( hass: HomeAssistant, mock_lamarzocco: MagicMock, From 32b7edb6085cab955c3893293906d7da7efa40fc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 May 2025 23:33:39 +0300 Subject: [PATCH 0054/1175] Update frontend to 20250502.0 (#144114) --- 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 28b01aff616..2cfa9572ff3 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==20250430.2"] + "requirements": ["home-assistant-frontend==20250502.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c484a526374..6bcd21f4d99 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.45.0 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2beb7ab9632..8f452b7d29f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6b808f4818..8b317d0369e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250430.2 +home-assistant-frontend==20250502.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From 247d2e7efdb5dd95737031f7571203c9cc834e51 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Fri, 2 May 2025 22:35:34 +0200 Subject: [PATCH 0055/1175] Bump aioautomower to 2025.5.1 (#144118) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 8e4be4c71f3..705975bb966 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["aioautomower"], "quality_scale": "silver", - "requirements": ["aioautomower==2025.4.4"] + "requirements": ["aioautomower==2025.5.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f452b7d29f..6b6da010f8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -201,7 +201,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2025.4.4 +aioautomower==2025.5.1 # homeassistant.components.azure_devops aioazuredevops==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b317d0369e..77df15a6272 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aioaseko==1.0.0 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2025.4.4 +aioautomower==2025.5.1 # homeassistant.components.azure_devops aioazuredevops==2.2.1 From e74f9183825d896339c338cc545727ed1985f231 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 15:53:19 -0500 Subject: [PATCH 0056/1175] Bump aiodns to 3.3.0 (#144115) --- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dnsip/test_config_flow.py | 24 ++++++++++++-------- 7 files changed, 20 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index d25459b95b7..35802adb7f3 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.2.0"] + "requirements": ["aiodns==3.3.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bcd21f4d99..de493201acd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 -aiodns==3.2.0 +aiodns==3.3.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index fcfe8e3448d..c71cf0dbaf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.13.2" dependencies = [ - "aiodns==3.2.0", + "aiodns==3.3.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index 1e91dca8391..5bbf33025c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiodns==3.2.0 +aiodns==3.3.0 aiohasupervisor==0.3.1 aiohttp==3.11.18 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6b6da010f8a..3b93f9f40d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.2.0 +aiodns==3.3.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77df15a6272..40997e5e24b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -211,7 +211,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.2.0 +aiodns==3.3.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 9d92cb3554c..1a565345275 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -224,16 +224,20 @@ async def test_options_flow(hass: HomeAssistant) -> None: 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_RESOLVER: "8.8.8.8", - CONF_RESOLVER_IPV6: "2001:4860:4860::8888", - CONF_PORT: 53, - CONF_PORT_IPV6: 53, - }, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RESOLVER: "8.8.8.8", + CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + CONF_PORT: 53, + CONF_PORT_IPV6: 53, + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { From 3183bb78ff8aad19c147e73c43a5813aa3305abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Sat, 3 May 2025 00:16:49 +0200 Subject: [PATCH 0057/1175] Update pywmspro to 0.2.2 to make error handling more robust (#144124) --- homeassistant/components/wmspro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json index dd65be3e7e7..d4eda3a90a6 100644 --- a/homeassistant/components/wmspro/manifest.json +++ b/homeassistant/components/wmspro/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/wmspro", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["pywmspro==0.2.1"] + "requirements": ["pywmspro==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b93f9f40d7..8c223ab9bc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ pywilight==0.0.74 pywizlight==0.6.2 # homeassistant.components.wmspro -pywmspro==0.2.1 +pywmspro==0.2.2 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40997e5e24b..3bf20a45ada 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2099,7 +2099,7 @@ pywilight==0.0.74 pywizlight==0.6.2 # homeassistant.components.wmspro -pywmspro==0.2.1 +pywmspro==0.2.2 # homeassistant.components.ws66i pyws66i==1.1 From 4450f919c3f2ad7c69a71ca153fd92f74c581d95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 2 May 2025 17:46:59 -0500 Subject: [PATCH 0058/1175] Bump PyISY to 3.4.1 (#144127) --- homeassistant/components/isy994/helpers.py | 3 +-- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 3686a182fe9..587c0544d6c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: for dtype, _, node_id in folder.children: if dtype != TAG_FOLDER: continue - entity_folder = folder[node_id] - + entity_folder: Programs = folder[node_id] actions = None status = entity_folder.get_by_name(KEY_STATUS) if not status or status.protocol != PROTO_PROGRAM: diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 5cd3bb73a89..bbfc7deb80d 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -24,7 +24,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyisy"], - "requirements": ["pyisy==3.4.0"], + "requirements": ["pyisy==3.4.1"], "ssdp": [ { "manufacturer": "Universal Devices Inc.", diff --git a/requirements_all.txt b/requirements_all.txt index 8c223ab9bc2..40509329dd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2054,7 +2054,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.4.0 +pyisy==3.4.1 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf20a45ada..4c561092925 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ pyiskra==0.1.15 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.4.0 +pyisy==3.4.1 # homeassistant.components.ituran pyituran==0.1.4 From 5e39fb6da1c2353935f5172f1470e8bf5279cfca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 03:08:56 -0500 Subject: [PATCH 0059/1175] Bump bleak-esphome to 2.15.1 (#144129) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index b07e78316d8..1f619b2017c 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index e2e3cb34721..beaf68decd9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==30.1.0", "esphome-dashboard-api==1.3.0", - "bleak-esphome==2.14.0" + "bleak-esphome==2.15.1" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 40509329dd2..d9a8d90ade0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.14.0 +bleak-esphome==2.15.1 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c561092925..4ea60a459d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -538,7 +538,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.14.0 +bleak-esphome==2.15.1 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 From 9780db1c2212776db98d6c2c877c7504134a5385 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 03:09:28 -0500 Subject: [PATCH 0060/1175] Bump Bluetooth deps to improve auto recovery process (#144133) --- .../components/bluetooth/manifest.json | 4 +- homeassistant/package_constraints.txt | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/bluetooth/test_wrappers.py | 59 ------------------- 5 files changed, 8 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 1ffee18d8fb..5e74f7b5561 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,9 +18,9 @@ "bleak==0.22.3", "bleak-retry-connector==3.9.0", "bluetooth-adapters==0.21.4", - "bluetooth-auto-recovery==1.4.5", + "bluetooth-auto-recovery==1.5.1", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.45.0" + "habluetooth==3.47.1" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de493201acd..8b53ae13687 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bcrypt==4.2.0 bleak-retry-connector==3.9.0 bleak==0.22.3 bluetooth-adapters==0.21.4 -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 bluetooth-data-tools==1.28.1 cached-ipaddress==0.10.0 certifi>=2021.5.30 @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 -habluetooth==3.45.0 +habluetooth==3.47.1 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index d9a8d90ade0..d52a573d7bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ bluemaestro-ble==0.4.0 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.45.0 +habluetooth==3.47.1 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ea60a459d1..20e4f5af2d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ bluemaestro-ble==0.4.0 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.5 +bluetooth-auto-recovery==1.5.1 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble @@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.45.0 +habluetooth==3.47.1 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index c5908776882..bfe7445f614 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -316,65 +316,6 @@ async def test_release_slot_on_connect_exception( cancel_hci1() -@pytest.mark.usefixtures("enable_bluetooth", "two_adapters") -async def test_we_switch_adapters_on_failure( - hass: HomeAssistant, - install_bleak_catcher, -) -> None: - """Ensure we try the next best adapter after a failure.""" - hci0_device_advs, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices( - hass - ) - ble_device = hci0_device_advs["00:00:00:00:00:01"][0] - client = bleak.BleakClient(ble_device) - - class FakeBleakClientFailsHCI0Only(BaseFakeBleakClient): - """Fake bleak client that fails to connect.""" - - async def connect(self, *args, **kwargs): - """Connect.""" - if "/hci0/" in self._device.details["path"]: - return False - return True - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - - # After two tries we should switch to hci1 - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is True - - # ..and we remember that hci1 works as long as the client doesn't change - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is True - - # If we replace the client, we should try hci0 again - client = bleak.BleakClient(ble_device) - - with patch( - "habluetooth.wrappers.get_platform_client_backend_type", - return_value=FakeBleakClientFailsHCI0Only, - ): - assert await client.connect() is False - cancel_hci0() - cancel_hci1() - - @pytest.mark.usefixtures("enable_bluetooth", "two_adapters") async def test_passing_subclassed_str_as_address( hass: HomeAssistant, From 558b0ec3b1d6278cc30f7f47ca2d57d7ed9626f4 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 3 May 2025 11:51:26 +0200 Subject: [PATCH 0061/1175] Fix small issues with mqtt translations and improve readability (#144091) --- homeassistant/components/mqtt/strings.json | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index d2234121803..7339f3869a1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -256,8 +256,8 @@ "green_template": "Green template", "last_reset_value_template": "Last reset value template", "optimistic": "Optimistic", - "payload_off": "Payload off", - "payload_on": "Payload on", + "payload_off": "Payload \"off\"", + "payload_on": "Payload \"on\"", "qos": "QoS", "red_template": "Red template", "retain": "Retain", @@ -278,7 +278,7 @@ "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", - "on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.", + "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "payload_off": "The payload that represents the off state.", "payload_on": "The payload that represents the on state.", @@ -287,7 +287,7 @@ "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", - "supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", + "supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)" }, "sections": { @@ -325,7 +325,7 @@ "data_description": { "brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.", "brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.", - "brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)", + "brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)", "brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.", "brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)", "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." @@ -385,7 +385,7 @@ "hs_value_template": "HS value template" }, "data_description": { - "hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.", + "hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.", "hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)", "hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)", "hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value." @@ -574,15 +574,15 @@ "discovery": "Option to enable MQTT automatic discovery.", "discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.", "birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.", - "birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.", - "birth_payload": "The `birth` message that is published when MQTT is ready and connected.", - "birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected", - "birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.", - "will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.", - "will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.", - "will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.", - "will_qos": "The quality of service of the `will` message that is published by your MQTT broker.", - "will_retain": "When set, your MQTT broker will retain the `will` message." + "birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.", + "birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.", + "birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected", + "birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.", + "will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.", + "will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.", + "will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.", + "will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.", + "will_retain": "When set, your MQTT broker will retain the \"will\" message." } } }, From 1d500fda67dd251196340600b9474df817f69ab4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 3 May 2025 14:35:04 +0200 Subject: [PATCH 0062/1175] Fix fritz coordinator typing (#144146) --- homeassistant/components/fritz/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 9199692f564..d253e9b5b12 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -521,7 +521,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): return {} def manage_device_info( - self, dev_info: Device, dev_mac: str, consider_home: bool + self, dev_info: Device, dev_mac: str, consider_home: float ) -> bool: """Update device lists and return if device is new.""" _LOGGER.debug("Client dev_info: %s", dev_info) From db2435dc3617f5277c7bec2095d391c331cbdcd3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 3 May 2025 14:35:17 +0200 Subject: [PATCH 0063/1175] Fix litterrobot entity typing (#144147) --- homeassistant/components/litterrobot/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 9e9cc8f0740..4117069aa0e 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -17,7 +17,7 @@ from .coordinator import LitterRobotDataUpdateCoordinator _WhiskerEntityT = TypeVar("_WhiskerEntityT", bound=Robot | Pet) -def get_device_info(whisker_entity: _WhiskerEntityT) -> DeviceInfo: +def get_device_info(whisker_entity: Robot | Pet) -> DeviceInfo: """Get device info for a robot or pet.""" if isinstance(whisker_entity, Robot): return DeviceInfo( From 64b7f2c285d4ef8dd9de7fa2ca8def5cb879552f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sat, 3 May 2025 14:39:46 +0200 Subject: [PATCH 0064/1175] Improve select platform in Husqvarna Automower (#144117) --- .../components/husqvarna_automower/select.py | 14 ++++++-------- .../components/husqvarna_automower/test_select.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/select.py b/homeassistant/components/husqvarna_automower/select.py index 9124a0705e1..1dde9e16295 100644 --- a/homeassistant/components/husqvarna_automower/select.py +++ b/homeassistant/components/husqvarna_automower/select.py @@ -19,10 +19,10 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 HEADLIGHT_MODES: list = [ - HeadlightModes.ALWAYS_OFF.lower(), - HeadlightModes.ALWAYS_ON.lower(), - HeadlightModes.EVENING_AND_NIGHT.lower(), - HeadlightModes.EVENING_ONLY.lower(), + HeadlightModes.ALWAYS_OFF, + HeadlightModes.ALWAYS_ON, + HeadlightModes.EVENING_AND_NIGHT, + HeadlightModes.EVENING_ONLY, ] @@ -65,13 +65,11 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity): @property def current_option(self) -> str: """Return the current option for the entity.""" - return cast( - HeadlightModes, self.mower_attributes.settings.headlight.mode - ).lower() + return cast(HeadlightModes, self.mower_attributes.settings.headlight.mode) @handle_sending_exception() async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self.coordinator.api.commands.set_headlight_mode( - self.mower_id, cast(HeadlightModes, option.upper()) + self.mower_id, HeadlightModes(option) ) diff --git a/tests/components/husqvarna_automower/test_select.py b/tests/components/husqvarna_automower/test_select.py index 01e7607735b..f1b855a90a3 100644 --- a/tests/components/husqvarna_automower/test_select.py +++ b/tests/components/husqvarna_automower/test_select.py @@ -74,7 +74,7 @@ async def test_select_commands( blocking=True, ) mocked_method = mock_automower_client.commands.set_headlight_mode - mocked_method.assert_called_once_with(TEST_MOWER_ID, service.upper()) + mocked_method.assert_called_once_with(TEST_MOWER_ID, service) assert len(mocked_method.mock_calls) == 1 mocked_method.side_effect = ApiError("Test error") From a2bc3e390815ca7b9dac461f7dcdcd9c9baedbdf Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 3 May 2025 14:44:18 +0200 Subject: [PATCH 0065/1175] Switch to common clientsession for lamarzocco (#144137) --- homeassistant/components/lamarzocco/__init__.py | 4 ++-- homeassistant/components/lamarzocco/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index ad9fec28fb4..ff977438f38 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( 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_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import ( @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - assert entry.unique_id serial = entry.unique_id - client = async_create_clientsession(hass) + client = async_get_clientsession(hass) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index e352e337d0b..8cb2e4dfc61 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, } - self._client = async_create_clientsession(self.hass) + self._client = async_get_clientsession(self.hass) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], From ee555a3700859201d03eb5ef5a5721f27da1ad8b Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 3 May 2025 17:08:34 +0300 Subject: [PATCH 0066/1175] Mark Shelly icon-translations as done (#144148) --- homeassistant/components/shelly/quality_scale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 601170879d1..6277681347d 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -57,7 +57,7 @@ rules: entity-disabled-by-default: done entity-translations: todo exception-translations: done - icon-translations: todo + icon-translations: done reconfiguration-flow: done repair-issues: done stale-devices: From 0ca9ad1cc0bf9e687ebdc1742ec8edc6e08f2467 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 3 May 2025 17:17:37 +0300 Subject: [PATCH 0067/1175] Mark Shelly docs-data-update as done (#144151) --- homeassistant/components/shelly/quality_scale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 6277681347d..39a032a57f6 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -42,7 +42,7 @@ rules: diagnostics: done discovery-update-info: done discovery: done - docs-data-update: todo + docs-data-update: done docs-examples: done docs-known-limitations: done docs-supported-devices: done From b48a2cf2b5c0729a083f482a51154d0e317ba3d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 10:12:37 -0500 Subject: [PATCH 0068/1175] Add tests to ensure ESPHome entity_ids are preserved on upgrade (#144116) --- tests/components/esphome/test_entity.py | 152 ++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 71a9c16cee3..ee6e6b6785f 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -10,8 +10,11 @@ from aioesphomeapi import ( BinarySensorState, SensorInfo, SensorState, + build_unique_id, ) +import pytest +from homeassistant.components.esphome import DOMAIN from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_RESTORED, @@ -19,6 +22,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + Platform, ) from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -513,3 +517,151 @@ async def test_entity_without_name_device_with_friendly_name( # Make sure we have set the name to `None` as otherwise # the friendly_name will be "The Best Mixer " assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer" + + +@pytest.mark.usefixtures("hass_storage") +async def test_entity_id_preserved_on_upgrade( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + assert ( + build_unique_id("11:22:33:44:55:AA", entity_info[0]) + == "11:22:33:44:55:AA-binary_sensor-my" + ) + + entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + suggested_object_id="should_not_change", + ) + assert entry.entity_id == "binary_sensor.should_not_change" + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.should_not_change") + assert state is not None + + +@pytest.mark.usefixtures("hass_storage") +async def test_entity_id_preserved_on_upgrade_old_format_entity_id( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade from old format.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + assert ( + build_unique_id("11:22:33:44:55:AA", entity_info[0]) + == "11:22:33:44:55:AA-binary_sensor-my" + ) + + entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + suggested_object_id="my", + ) + assert entry.entity_id == "binary_sensor.my" + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"name": "mixer"}, + ) + state = hass.states.get("binary_sensor.my") + assert state is not None + + +async def test_entity_id_preserved_on_upgrade_when_in_storage( + hass: HomeAssistant, + mock_client: APIClient, + hass_storage: dict[str, Any], + mock_esphome_device: MockESPHomeDeviceType, + entity_registry: er.EntityRegistry, +) -> None: + """Test entity_id is preserved on upgrade with user defined entity_id.""" + entity_info = [ + BinarySensorInfo( + object_id="my", + key=1, + name="my", + unique_id="binary_sensor_my", + ), + ] + states = [ + BinarySensorState(key=1, state=True, missing_state=False), + ] + user_service = [] + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.mixer_my") + assert state is not None + # now rename the entity + ent_reg_entry = entity_registry.async_get_or_create( + Platform.BINARY_SENSOR, + DOMAIN, + "11:22:33:44:55:AA-binary_sensor-my", + ) + entity_registry.async_update_entity( + ent_reg_entry.entity_id, + new_entity_id="binary_sensor.user_named", + ) + await hass.config_entries.async_unload(device.entry.entry_id) + await hass.async_block_till_done() + entry = device.entry + entry_id = entry.entry_id + storage_key = f"esphome.{entry_id}" + assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1 + binary_sensor_data: dict[str, Any] = hass_storage[storage_key]["data"][ + "binary_sensor" + ][0] + assert binary_sensor_data["name"] == "my" + assert binary_sensor_data["object_id"] == "my" + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + entry=entry, + device_info={"friendly_name": "The Best Mixer", "name": "mixer"}, + ) + state = hass.states.get("binary_sensor.user_named") + assert state is not None From 4122f94fb63aefaa6c51061da4b18650196aa3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Sat, 3 May 2025 17:16:02 +0200 Subject: [PATCH 0069/1175] Add DHCP discovery to Home Connect (#144095) * Add DHCP discovery to Home Connect * Added tests * Use enums * Use more enums --- .../components/home_connect/manifest.json | 14 ++ homeassistant/generated/dhcp.py | 15 ++ .../home_connect/test_config_flow.py | 188 ++++++++++++++++-- 3 files changed, 204 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 8a608a900be..e550d22e0ca 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,6 +4,20 @@ "codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"], "config_flow": true, "dependencies": ["application_credentials", "repairs"], + "dhcp": [ + { + "hostname": "balay-*", + "macaddress": "C8D778*" + }, + { + "hostname": "(bosch|siemens)-*", + "macaddress": "68A40E*" + }, + { + "hostname": "siemens-*", + "macaddress": "38B4D3*" + } + ], "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 88fb8e06d02..26302b0ac8b 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -258,6 +258,21 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "guardian*", "macaddress": "30AEA4*", }, + { + "domain": "home_connect", + "hostname": "balay-*", + "macaddress": "C8D778*", + }, + { + "domain": "home_connect", + "hostname": "(bosch|siemens)-*", + "macaddress": "68A40E*", + }, + { + "domain": "home_connect", + "hostname": "siemens-*", + "macaddress": "38B4D3*", + }, { "domain": "homewizard", "registered_devices": True, diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index a8929120acb..73aed382780 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -7,15 +7,12 @@ from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN import pytest from homeassistant import config_entries, setup -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.components.home_connect.const import 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 +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN @@ -26,6 +23,39 @@ from tests.typing import ClientSessionGenerator CLIENT_ID = "1234" CLIENT_SECRET = "5678" +DHCP_DISCOVERY = ( + DhcpServiceInfo( + ip="1.1.1.1", + hostname="balay-dishwasher-000000000000000000", + macaddress="C8:D7:78:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="BOSCH-ABCDE1234-68A40E000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="SIEMENS-ABCDE1234-68A40E000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="SIEMENS-ABCDE1234-38B4D3000000", + macaddress="38:B4:D3:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="siemens-dishwasher-000000000000000000", + macaddress="68:A4:0E:00:00:00", + ), + DhcpServiceInfo( + ip="1.1.1.1", + hostname="siemens-dishwasher-000000000000000000", + macaddress="38:B4:D3:00:00:00", + ), +) + @pytest.mark.usefixtures("current_request_with_host") async def test_full_flow( @@ -36,10 +66,6 @@ async def test_full_flow( """Check full flow.""" assert await setup.async_setup_component(hass, "home_connect", {}) - await async_import_client_credential( - hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) - ) - result = await hass.config_entries.flow.async_init( "home_connect", context={"source": config_entries.SOURCE_USER} ) @@ -95,10 +121,6 @@ async def test_prevent_reconfiguring_same_account( assert await setup.async_setup_component(hass, "home_connect", {}) - await async_import_client_credential( - hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET) - ) - result = await hass.config_entries.flow.async_init( "home_connect", context={"source": config_entries.SOURCE_USER} ) @@ -135,7 +157,7 @@ async def test_prevent_reconfiguring_same_account( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -241,3 +263,143 @@ async def test_reauth_flow_with_different_account( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_account" + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_zeroconf_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test zeroconf flow.""" + assert await setup.async_setup_component(hass, "home_connect", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + 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" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.home_connect.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 hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, "1234567890") + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_zeroconf_flow_already_setup( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, +) -> None: + """Test zeroconf discovery with already setup device.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=DHCP_DISCOVERY[0], + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.parametrize("dchp_discovery", DHCP_DISCOVERY) +async def test_dhcp_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + dchp_discovery: DhcpServiceInfo, +) -> None: + """Test DHCP discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dchp_discovery + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + 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" + f"&state={state}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": FAKE_REFRESH_TOKEN, + "access_token": FAKE_ACCESS_TOKEN, + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.home_connect.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 hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, "1234567890") + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("current_request_with_host") +async def test_dhcp_flow_already_setup( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery with already setup device.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY[0] + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From debec3bfbc29acb1060e2da9729a3cd1d0f96811 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 3 May 2025 18:13:43 +0200 Subject: [PATCH 0070/1175] Improve supported color modes description (#144144) --- homeassistant/components/mqtt/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 7339f3869a1..23a2a888989 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -244,7 +244,6 @@ "title": "Configure MQTT device \"{mqtt_device}\"", "description": "Please configure MQTT specific details for {platform} entity \"{entity}\":", "data": { - "on_command_type": "ON command type", "blue_template": "Blue template", "brightness_template": "Brightness template", "command_template": "Command template", @@ -255,6 +254,7 @@ "force_update": "Force update", "green_template": "Green template", "last_reset_value_template": "Last reset value template", + "on_command_type": "ON command type", "optimistic": "Optimistic", "payload_off": "Payload \"off\"", "payload_on": "Payload \"on\"", @@ -275,19 +275,19 @@ "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.", "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", "color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.", + "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", - "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", - "payload_off": "The payload that represents the off state.", - "payload_on": "The payload that represents the on state.", + "payload_off": "The payload that represents the \"off\" state.", + "payload_on": "The payload that represents the \"on\" state.", "qos": "The QoS value a {platform} entity should use.", "red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", - "supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", + "supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)" }, "sections": { From aea5760424fa1deeae896d5871912ed9ddbdbb6b Mon Sep 17 00:00:00 2001 From: Florian Sabonchi <54689374+florian-sabonchi@users.noreply.github.com> Date: Sat, 3 May 2025 20:25:27 +0200 Subject: [PATCH 0071/1175] Fix check for locked device in AVM Fritz!SmartHome (#141697) * feat: raise execption on hvac mode while device is locked * fix: test for setting hvac mode while device is locked. * feat: update translation * feat: add separate translations for HVAC and temperature * fix: test cases * fix: test cases for test_set_preset_mode_boost * rev: code review * rev: exception string * feat: updated error message and added helper function * Update homeassistant/components/fritzbox/strings.json Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * fix: translation key * remove check_active_or_lock_mode from async_set_preset_mode --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/fritzbox/climate.py | 27 +++-- .../components/fritzbox/strings.json | 8 +- tests/components/fritzbox/test_climate.py | 113 +++++++++++++++++- 3 files changed, 130 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 194bc5621b3..573877fa71b 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" + self.check_active_or_lock_mode() if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF: await self.async_set_hkr_state("off") elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None: @@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" - if self.data.holiday_active or self.data.summer_active: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="change_hvac_while_active_mode", - ) + self.check_active_or_lock_mode() if self.hvac_mode is hvac_mode: LOGGER.debug( "%s is already in requested hvac mode %s", self.name, hvac_mode @@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" - if self.data.holiday_active or self.data.summer_active: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="change_preset_while_active_mode", - ) + self.check_active_or_lock_mode() await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode]) @property @@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open return attrs + + def check_active_or_lock_mode(self) -> None: + """Check if in summer/vacation mode or lock enabled.""" + if self.data.holiday_active or self.data.summer_active: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_settings_while_active_mode", + ) + + if self.data.lock: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="change_settings_while_lock_enabled", + ) diff --git a/homeassistant/components/fritzbox/strings.json b/homeassistant/components/fritzbox/strings.json index bb7d2f0fdf1..38bc6dc9c39 100644 --- a/homeassistant/components/fritzbox/strings.json +++ b/homeassistant/components/fritzbox/strings.json @@ -88,11 +88,11 @@ "manual_switching_disabled": { "message": "Can't toggle switch while manual switching is disabled for the device." }, - "change_preset_while_active_mode": { - "message": "Can't change preset while holiday or summer mode is active on the device." + "change_settings_while_lock_enabled": { + "message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device" }, - "change_hvac_while_active_mode": { - "message": "Can't change HVAC mode while holiday or summer mode is active on the device." + "change_settings_while_active_mode": { + "message": "Can't change settings while holiday or summer mode is active on the device." } } } diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 5bf81ef0238..bdf9dba8b42 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -211,6 +211,8 @@ async def test_set_temperature( ) -> None: """Test setting temperature.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -288,6 +290,8 @@ async def test_set_hvac_mode( ) -> None: """Test setting hvac mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.target_temperature = target_temperature if current_preset is PRESET_COMFORT: @@ -335,6 +339,8 @@ async def test_set_preset_mode_comfort( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.comfort_temperature = comfort_temperature await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz @@ -366,6 +372,8 @@ async def test_set_preset_mode_eco( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + + device.lock = False device.eco_temperature = eco_temperature await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz @@ -387,6 +395,8 @@ async def test_set_preset_mode_boost( ) -> None: """Test setting preset mode.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -471,11 +481,106 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: assert state +@pytest.mark.parametrize( + "service_data", + [ + {ATTR_TEMPERATURE: 23}, + { + ATTR_HVAC_MODE: HVACMode.HEAT, + ATTR_TEMPERATURE: 25, + }, + ], +) +async def test_set_temperature_lock( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, +) -> None: + """Test setting temperature while device is locked.""" + device = FritzDeviceClimateMock() + + device.lock = True + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, + True, + ) + + +@pytest.mark.parametrize( + ("service_data", "target_temperature", "current_preset", "expected_call_args"), + [ + # mode off always sets target temperature to 0 + ({ATTR_HVAC_MODE: HVACMode.OFF}, 22, PRESET_COMFORT, [call(0, True)]), + ({ATTR_HVAC_MODE: HVACMode.OFF}, 16, PRESET_ECO, [call(0, True)]), + ({ATTR_HVAC_MODE: HVACMode.OFF}, 16, None, [call(0, True)]), + # mode heat sets target temperature based on current scheduled preset, + # when not already in mode heat + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_COMFORT, [call(22, True)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_ECO, [call(16, True)]), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, None, [call(22, True)]), + # mode heat does not set target temperature, when already in mode heat + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_COMFORT, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_ECO, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, None, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_COMFORT, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_ECO, []), + ({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, None, []), + ], +) +async def test_set_hvac_mode_lock( + hass: HomeAssistant, + fritz: Mock, + service_data: dict, + target_temperature: float, + current_preset: str, + expected_call_args: list[_Call], +) -> None: + """Test setting hvac mode while device is locked.""" + device = FritzDeviceClimateMock() + + device.lock = True + device.target_temperature = target_temperature + + if current_preset is PRESET_COMFORT: + device.nextchange_temperature = device.eco_temperature + elif current_preset is PRESET_ECO: + device.nextchange_temperature = device.comfort_temperature + else: + device.nextchange_endperiod = 0 + + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + with pytest.raises( + HomeAssistantError, + match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device", + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, **service_data}, + True, + ) + + async def test_holidy_summer_mode( hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock ) -> None: """Test holiday and summer mode.""" device = FritzDeviceClimateMock() + device.lock = False + await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) @@ -510,7 +615,7 @@ async def test_holidy_summer_mode( with pytest.raises( HomeAssistantError, - match="Can't change HVAC mode while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -520,7 +625,7 @@ async def test_holidy_summer_mode( ) with pytest.raises( HomeAssistantError, - match="Can't change preset while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -546,7 +651,7 @@ async def test_holidy_summer_mode( with pytest.raises( HomeAssistantError, - match="Can't change HVAC mode while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", @@ -556,7 +661,7 @@ async def test_holidy_summer_mode( ) with pytest.raises( HomeAssistantError, - match="Can't change preset while holiday or summer mode is active on the device", + match="Can't change settings while holiday or summer mode is active on the device", ): await hass.services.async_call( "climate", From fb94f8ea189cb18e2b11c5166687e2b9ebb4bd60 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 3 May 2025 21:04:59 +0200 Subject: [PATCH 0072/1175] Make the network device tracking feature optional in AVM Fritz!Tools (#144149) * make the network device tracking feature optional * fix doc strings * Apply suggestions from code review Co-authored-by: Jan Bouwhuis --------- Co-authored-by: Jan Bouwhuis --- homeassistant/components/fritz/__init__.py | 7 +++++ homeassistant/components/fritz/config_flow.py | 24 +++++++++++++-- homeassistant/components/fritz/const.py | 3 ++ homeassistant/components/fritz/coordinator.py | 30 ++++++++++++++----- homeassistant/components/fritz/strings.json | 22 +++++++++----- tests/components/fritz/test_config_flow.py | 2 ++ 6 files changed, 71 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 05a2a07707f..9610fe4b34d 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -15,6 +15,8 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from .const import ( + CONF_FEATURE_DEVICE_TRACKING, + DEFAULT_CONF_FEATURE_DEVICE_TRACKING, DEFAULT_SSL, DOMAIN, FRITZ_AUTH_EXCEPTIONS, @@ -38,6 +40,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool: """Set up fritzboxtools from config entry.""" _LOGGER.debug("Setting up FRITZ!Box Tools component") + avm_wrapper = AvmWrapper( hass=hass, config_entry=entry, @@ -46,6 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL), + device_discovery_enabled=entry.options.get( + CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_FEATURE_DEVICE_TRACKING + ), ) try: @@ -62,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo raise ConfigEntryAuthFailed("Missing UPnP configuration") await avm_wrapper.async_config_entry_first_refresh() + await avm_wrapper.async_trigger_cleanup() entry.runtime_data = avm_wrapper diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index fb17f872cb6..2c22a35c4dd 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -35,7 +35,9 @@ from homeassistant.helpers.service_info.ssdp import ( from homeassistant.helpers.typing import VolDictType from .const import ( + CONF_FEATURE_DEVICE_TRACKING, CONF_OLD_DISCOVERY, + DEFAULT_CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_HOST, DEFAULT_HTTP_PORT, @@ -72,7 +74,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize FRITZ!Box Tools flow.""" self._name: str = "" self._password: str = "" - self._use_tls: bool = False + self._use_tls: bool = DEFAULT_SSL + self._feature_device_discovery: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING self._port: int | None = None self._username: str = "" self._model: str = "" @@ -141,6 +144,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): options={ CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(), CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY, + CONF_FEATURE_DEVICE_TRACKING: self._feature_device_discovery, }, ) @@ -204,6 +208,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._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING] self._port = self._determine_port(user_input) error = await self.async_fritz_tools_init() @@ -234,6 +239,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + vol.Required( + CONF_FEATURE_DEVICE_TRACKING, + default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING, + ): bool, } ), errors=errors or {}, @@ -250,6 +259,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + vol.Required( + CONF_FEATURE_DEVICE_TRACKING, + default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING, + ): bool, } ), description_placeholders={"name": self._name}, @@ -405,7 +418,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow): """Handle options flow.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) options = self.config_entry.options data_schema = vol.Schema( @@ -420,6 +433,13 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow): CONF_OLD_DISCOVERY, default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY), ): bool, + vol.Optional( + CONF_FEATURE_DEVICE_TRACKING, + default=options.get( + CONF_FEATURE_DEVICE_TRACKING, + DEFAULT_CONF_FEATURE_DEVICE_TRACKING, + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index 2237823bc3b..32f52e68458 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -40,6 +40,9 @@ PLATFORMS = [ CONF_OLD_DISCOVERY = "old_discovery" DEFAULT_CONF_OLD_DISCOVERY = False +CONF_FEATURE_DEVICE_TRACKING = "feature_device_tracking" +DEFAULT_CONF_FEATURE_DEVICE_TRACKING = True + DSL_CONNECTION: Literal["dsl"] = "dsl" DEFAULT_DEVICE_NAME = "Unknown device" diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index d253e9b5b12..e22a66d254f 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -39,6 +39,7 @@ from homeassistant.util.hass_dict import HassKey from .const import ( CONF_OLD_DISCOVERY, + DEFAULT_CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_OLD_DISCOVERY, DEFAULT_HOST, DEFAULT_SSL, @@ -175,6 +176,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): username: str = DEFAULT_USERNAME, host: str = DEFAULT_HOST, use_tls: bool = DEFAULT_SSL, + device_discovery_enabled: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING, ) -> None: """Initialize FritzboxTools class.""" super().__init__( @@ -202,6 +204,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): self.port = port self.username = username self.use_tls = use_tls + self.device_discovery_enabled = device_discovery_enabled self.has_call_deflections: bool = False self._model: str | None = None self._current_firmware: str | None = None @@ -332,10 +335,15 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): "entity_states": {}, } try: - await self.async_scan_devices() + await self.async_update_device_info() + + if self.device_discovery_enabled: + await self.async_scan_devices() + entity_data["entity_states"] = await self.hass.async_add_executor_job( self._entity_states_update ) + if self.has_call_deflections: entity_data[ "call_deflections" @@ -551,12 +559,8 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): if new_device: async_dispatcher_send(self.hass, self.signal_device_new) - async def async_scan_devices(self, now: datetime | None = None) -> None: - """Scan for new devices and return a list of found device ids.""" - - if self.hass.is_stopping: - _ha_is_stopping("scan devices") - return + async def async_update_device_info(self, now: datetime | None = None) -> None: + """Update own device information.""" _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) ( @@ -565,6 +569,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): self._release_url, ) = await self._async_update_device_info() + async def async_scan_devices(self, now: datetime | None = None) -> None: + """Scan for new network devices.""" + + if self.hass.is_stopping: + _ha_is_stopping("scan devices") + return + _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() if self._options: @@ -683,7 +694,10 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]): async def async_trigger_cleanup(self) -> None: """Trigger device trackers cleanup.""" - device_hosts = await self._async_update_hosts_info() + _LOGGER.debug("Device tracker cleanup triggered") + device_hosts = {self.mac: Device(True, "", "", "", "", None)} + if self.device_discovery_enabled: + device_hosts = await self._async_update_hosts_info() entity_reg: er.EntityRegistry = er.async_get(self.hass) config_entry = self.config_entry diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 6191fc524dd..ee23a8cfbef 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -4,7 +4,9 @@ "data_description_port": "Leave empty to use the default port.", "data_description_username": "Username for the FRITZ!Box.", "data_description_password": "Password for the FRITZ!Box.", - "data_description_ssl": "Use SSL to connect to the FRITZ!Box." + "data_description_ssl": "Use SSL to connect to the FRITZ!Box.", + "data_description_feature_device_tracking": "Enable or disable the network device tracking feature.", + "data_feature_device_tracking": "Enable network device tracking" }, "config": { "flow_title": "{name}", @@ -15,12 +17,14 @@ "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "ssl": "[%key:common::config_flow::data::ssl%]" + "ssl": "[%key:common::config_flow::data::ssl%]", + "feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]" }, "data_description": { "username": "[%key:component::fritz::common::data_description_username%]", "password": "[%key:component::fritz::common::data_description_password%]", - "ssl": "[%key:component::fritz::common::data_description_ssl%]" + "ssl": "[%key:component::fritz::common::data_description_ssl%]", + "feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]" } }, "reauth_confirm": { @@ -57,14 +61,16 @@ "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "ssl": "[%key:common::config_flow::data::ssl%]" + "ssl": "[%key:common::config_flow::data::ssl%]", + "feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]" }, "data_description": { "host": "[%key:component::fritz::common::data_description_host%]", "port": "[%key:component::fritz::common::data_description_port%]", "username": "[%key:component::fritz::common::data_description_username%]", "password": "[%key:component::fritz::common::data_description_password%]", - "ssl": "[%key:component::fritz::common::data_description_ssl%]" + "ssl": "[%key:component::fritz::common::data_description_ssl%]", + "feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]" } } }, @@ -89,11 +95,13 @@ "init": { "data": { "consider_home": "Seconds to consider a device at 'home'", - "old_discovery": "Enable old discovery method" + "old_discovery": "Enable old discovery method", + "feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]" }, "data_description": { "consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.", - "old_discovery": "Enable old discovery method. This is needed for some scenarios." + "old_discovery": "Enable old discovery method. This is needed for some scenarios.", + "feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]" } } } diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index ee3ae881b2c..f790489c341 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.device_tracker import ( DEFAULT_CONSIDER_HOME, ) from homeassistant.components.fritz.const import ( + CONF_FEATURE_DEVICE_TRACKING, CONF_OLD_DISCOVERY, DOMAIN, ERROR_AUTH_INVALID, @@ -744,6 +745,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["data"] == { CONF_OLD_DISCOVERY: False, CONF_CONSIDER_HOME: 37, + CONF_FEATURE_DEVICE_TRACKING: True, } From 30e4264aa9d987d09e65dcb4360f99a26ca9f500 Mon Sep 17 00:00:00 2001 From: Charlie Rusbridger Date: Sat, 3 May 2025 20:10:33 +0100 Subject: [PATCH 0073/1175] Use kodi posters, fall back to thumbnails if unavailable. (#144066) --- homeassistant/components/kodi/browse_media.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 60e99d98cb1..3873f385881 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None): _LOGGER.debug("Unknown media type received: %s", media_content_type) raise UnknownMediaType from err - thumbnail = item.get("thumbnail") + if "art" in item: + thumbnail = item["art"].get("poster", item.get("thumbnail")) + else: + thumbnail = item.get("thumbnail") if thumbnail is not None and get_thumbnail_url is not None: thumbnail = await get_thumbnail_url( media_content_type, media_content_id, thumbnail_url=thumbnail @@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type): title = None media = None - properties = ["thumbnail"] + properties = ["thumbnail", "art"] if search_type == MediaType.ALBUM: if search_id: album = await media_library.get_album_details( album_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - album["albumdetails"].get("thumbnail") + album["albumdetails"]["art"].get( + "poster", album["albumdetails"].get("thumbnail") + ) ) title = album["albumdetails"]["label"] media = await media_library.get_songs( @@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type): "album", "thumbnail", "track", + "art", ], ) media = media.get("songs") @@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type): artist_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - artist["artistdetails"].get("thumbnail") + artist["artistdetails"]["art"].get( + "poster", artist["artistdetails"].get("thumbnail") + ) ) title = artist["artistdetails"]["label"] else: @@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type): movie_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - movie["moviedetails"].get("thumbnail") + movie["moviedetails"]["art"].get( + "poster", movie["moviedetails"].get("thumbnail") + ) ) - title = movie["moviedetails"]["label"] else: media = await media_library.get_movies(properties) media = media.get("movies") @@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type): if search_id: media = await media_library.get_seasons( tv_show_id=int(search_id), - properties=["thumbnail", "season", "tvshowid"], + properties=["thumbnail", "season", "tvshowid", "art"], ) media = media.get("seasons") tvshow = await media_library.get_tv_show_details( tv_show_id=int(search_id), properties=properties ) thumbnail = media_library.thumbnail_url( - tvshow["tvshowdetails"].get("thumbnail") + tvshow["tvshowdetails"]["art"].get( + "poster", tvshow["tvshowdetails"].get("thumbnail") + ) ) title = tvshow["tvshowdetails"]["label"] else: @@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type): media = await media_library.get_episodes( tv_show_id=int(tv_show_id), season_id=int(season_id), - properties=["thumbnail", "tvshowid", "seasonid"], + properties=["thumbnail", "tvshowid", "seasonid", "art"], ) media = media.get("episodes") if media: @@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type): season_id=int(media[0]["seasonid"]), properties=properties ) thumbnail = media_library.thumbnail_url( - season["seasondetails"].get("thumbnail") + season["seasondetails"]["art"].get( + "poster", season["seasondetails"].get("thumbnail") + ) ) title = season["seasondetails"]["label"] @@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type): properties=["thumbnail", "channeltype", "channel", "broadcastnow"], ) media = media.get("channels") + title = "Channels" return thumbnail, title, media From 716b559e5d50a00f47b96f581f13bc969df88c8c Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 3 May 2025 12:12:01 -0700 Subject: [PATCH 0074/1175] Skip the update right after the migration in Opower (#144088) * Wait for the migration to finish in Opower * Don't call async_block_till_done since this can timeout and seems to meant for tests * Don't call async_block_till_done since this can timeout and seems to meant for tests --- .../components/opower/coordinator.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index adb32d914ee..dd0b2c87bb5 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -190,7 +190,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): return_sum = 0.0 last_stats_time = None else: - await self._async_maybe_migrate_statistics( + migrated = await self._async_maybe_migrate_statistics( account.utility_account_id, { cost_statistic_id: compensation_statistic_id, @@ -203,6 +203,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): return_statistic_id: return_metadata, }, ) + if migrated: + # Skip update to avoid working on old data since the migration is done + # asynchronously. Update the statistics in the next refresh in 12h. + _LOGGER.debug( + "Statistics migration completed. Skipping update for now" + ) + continue cost_reads = await self._async_get_cost_reads( account, self.api.utility.timezone(), @@ -326,7 +333,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): utility_account_id: str, migration_map: dict[str, str], metadata_map: dict[str, StatisticMetaData], - ) -> None: + ) -> bool: """Perform one-time statistics migration based on the provided map. Splits negative values from source IDs into target IDs. @@ -339,7 +346,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): """ if not migration_map: - return + return False need_migration_source_ids = set() for source_id, target_id in migration_map.items(): @@ -354,7 +361,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): if not last_target_stat: need_migration_source_ids.add(source_id) if not need_migration_source_ids: - return + return False _LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids) @@ -416,7 +423,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): if not need_migration_source_ids: _LOGGER.debug("No migration needed") - return + return False for stat_id, stats in processed_stats.items(): _LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id) @@ -442,6 +449,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): }, ) + return True + async def _async_get_cost_reads( self, account: Account, time_zone_str: str, start_time: float | None = None ) -> list[CostRead]: From 1264c2cbfa9ebefef3bc4be65a53115e35b8dea5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 14:21:03 -0500 Subject: [PATCH 0075/1175] Bump zeroconf to 0.147.0 (#144158) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e2637d792e2..fe190e78956 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.146.5"] + "requirements": ["zeroconf==0.147.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8b53ae13687..6845f3fab9b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -75,7 +75,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 yarl==1.20.0 -zeroconf==0.146.5 +zeroconf==0.147.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/pyproject.toml b/pyproject.toml index c71cf0dbaf2..8623d54b963 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ dependencies = [ "voluptuous-openapi==0.0.7", "yarl==1.20.0", "webrtc-models==0.3.0", - "zeroconf==0.146.5", + "zeroconf==0.147.0", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 5bbf33025c3..e8b9e12bfe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.7 yarl==1.20.0 webrtc-models==0.3.0 -zeroconf==0.146.5 +zeroconf==0.147.0 diff --git a/requirements_all.txt b/requirements_all.txt index d52a573d7bd..42140c39fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3156,7 +3156,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.146.5 +zeroconf==0.147.0 # homeassistant.components.zeversolar zeversolar==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20e4f5af2d1..44d3f1d9143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2555,7 +2555,7 @@ yt-dlp[default]==2025.03.31 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.146.5 +zeroconf==0.147.0 # homeassistant.components.zeversolar zeversolar==0.3.2 From b9aadb252f10a711675fff8928820f978f1e9582 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 3 May 2025 17:21:22 -0400 Subject: [PATCH 0076/1175] Point thumbnail TTS media source to right logo (#144162) --- homeassistant/components/tts/media_source.py | 2 +- tests/components/tts/test_media_source.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index d3c0998bb77..f096e082364 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -180,7 +180,7 @@ class TTSMediaSource(MediaSource): raise BrowseError("Unknown provider") if isinstance(engine_instance, TextToSpeechEntity): - engine_domain = engine_instance.platform.domain + engine_domain = engine_instance.platform.platform_name else: engine_domain = engine diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index c9d70c7f43e..eb4b09cab5b 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -78,6 +78,7 @@ async def test_browsing(hass: HomeAssistant, setup: str) -> None: assert item_child.children is None assert item_child.can_play is False assert item_child.can_expand is True + assert item_child.thumbnail == "https://brands.home-assistant.io/_/test/logo.png" item_child = await media_source.async_browse_media( hass, item.children[0].media_content_id + "?message=bla" From a6131b3ebf13d2eec262fc37dadeb3dc364a9a93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 18:22:48 -0500 Subject: [PATCH 0077/1175] Bump habluetooth to 3.48.2 (#144157) --- 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 5e74f7b5561..f9377443296 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.1", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.47.1" + "habluetooth==3.48.2" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6845f3fab9b..73415df8abd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 -habluetooth==3.47.1 +habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 42140c39fcc..be4709419ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.47.1 +habluetooth==3.48.2 # homeassistant.components.cloud hass-nabucasa==0.96.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44d3f1d9143..686aa81a4a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.3.7 # homeassistant.components.bluetooth -habluetooth==3.47.1 +habluetooth==3.48.2 # homeassistant.components.cloud hass-nabucasa==0.96.0 From a15a3c12d57303a31531cceb2636c6aa6be11ad3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 19:40:28 -0500 Subject: [PATCH 0078/1175] Pass requestor_uuid to bond API calls (#144128) --- homeassistant/components/bond/__init__.py | 3 ++- homeassistant/components/bond/config_flow.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index eb28bebdb06..00b8c8a0e13 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientError, ClientResponseError, ClientTimeout -from bond_async import Bond, BPUPSubscriptions, start_bpup +from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool token=token, timeout=ClientTimeout(total=_API_TIMEOUT), session=async_get_clientsession(hass), + requestor_uuid=RequestorUUID.HOME_ASSISTANT, ) hub = BondHub(bond, host) try: diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index ffa0098840c..9fcfbd342d8 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -8,7 +8,7 @@ import logging from typing import Any from aiohttp import ClientConnectionError, ClientResponseError -from bond_async import Bond +from bond_async import Bond, RequestorUUID import voluptuous as vol from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult @@ -34,7 +34,12 @@ TOKEN_SCHEMA = vol.Schema({}) async def async_get_token(hass: HomeAssistant, host: str) -> str | None: """Try to fetch the token from the bond device.""" - bond = Bond(host, "", session=async_get_clientsession(hass)) + bond = Bond( + host, + "", + session=async_get_clientsession(hass), + requestor_uuid=RequestorUUID.HOME_ASSISTANT, + ) response: dict[str, str] = {} with contextlib.suppress(ClientConnectionError): response = await bond.token() @@ -45,7 +50,10 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st """Validate the user input allows us to connect.""" bond = Bond( - data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass) + data[CONF_HOST], + data[CONF_ACCESS_TOKEN], + session=async_get_clientsession(hass), + requestor_uuid=RequestorUUID.HOME_ASSISTANT, ) try: hub = BondHub(bond, data[CONF_HOST]) From 2a5c0d9b88c1a89597048ebe02e0c7c2999e8f1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 20:50:17 -0500 Subject: [PATCH 0079/1175] Add support for updating ESPHome deep sleep devices (#144161) Co-authored-by: Keith Burzinski --- homeassistant/components/esphome/strings.json | 5 +- homeassistant/components/esphome/update.py | 87 ++++-- tests/components/esphome/test_update.py | 294 ++++++++++++++++++ 3 files changed, 363 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index bc198d514ab..eab88e8df95 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -195,7 +195,10 @@ "message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information." }, "error_uploading": { - "message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information." + "message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information." + }, + "ota_in_progress": { + "message": "An OTA (Over-The-Air) update is already in progress for {configuration}." } } } diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 01ac638bdb1..d24d8919461 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -125,21 +125,17 @@ class ESPHomeDashboardUpdateEntity( (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) } ) + self._install_lock = asyncio.Lock() + self._available_future: asyncio.Future[None] | None = None self._update_attrs() @callback def _update_attrs(self) -> None: """Update the supported features.""" - # If the device has deep sleep, we can't assume we can install updates - # as the ESP will not be connectable (by design). coordinator = self.coordinator device_info = self._device_info # Install support can change at run time - if ( - coordinator.last_update_success - and coordinator.supports_update - and not device_info.has_deep_sleep - ): + if coordinator.last_update_success and coordinator.supports_update: self._attr_supported_features = UpdateEntityFeature.INSTALL else: self._attr_supported_features = NO_FEATURES @@ -178,6 +174,13 @@ class ESPHomeDashboardUpdateEntity( self, static_info: list[EntityInfo] | None = None ) -> None: """Handle updated data from the device.""" + if ( + self._entry_data.available + and self._available_future + and not self._available_future.done() + ): + self._available_future.set_result(None) + self._available_future = None self._update_attrs() self.async_write_ha_state() @@ -192,17 +195,46 @@ class ESPHomeDashboardUpdateEntity( entry_data.async_subscribe_device_updated(self._handle_device_update) ) + async def async_will_remove_from_hass(self) -> None: + """Handle entity about to be removed from Home Assistant.""" + if self._available_future and not self._available_future.done(): + self._available_future.cancel() + self._available_future = None + + async def _async_wait_available(self) -> None: + """Wait until the device is available.""" + # If the device has deep sleep, we need to wait for it to wake up + # and connect to the network to be able to install the update. + if self._entry_data.available: + return + self._available_future = self.hass.loop.create_future() + try: + await self._available_future + finally: + self._available_future = None + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()): - coordinator = self.coordinator - api = coordinator.api - device = coordinator.data.get(self._device_info.name) - assert device is not None - configuration = device["configuration"] - try: + if self._install_lock.locked(): + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="ota_in_progress", + translation_placeholders={ + "configuration": self._device_info.name, + }, + ) + + # Ensure only one OTA per device at a time + async with self._install_lock: + # Ensure only one compile at a time for ALL devices + async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()): + coordinator = self.coordinator + api = coordinator.api + device = coordinator.data.get(self._device_info.name) + assert device is not None + configuration = device["configuration"] if not await api.compile(configuration): raise HomeAssistantError( translation_domain=DOMAIN, @@ -211,14 +243,25 @@ class ESPHomeDashboardUpdateEntity( "configuration": configuration, }, ) - if not await api.upload(configuration, "OTA"): - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="error_uploading", - translation_placeholders={ - "configuration": configuration, - }, - ) + + # If the device uses deep sleep, there's a small chance it goes + # to sleep right after the dashboard connects but before the OTA + # starts. In that case, the update won't go through, so we try + # again to catch it on its next wakeup. + attempts = 2 if self._device_info.has_deep_sleep else 1 + try: + for attempt in range(1, attempts + 1): + await self._async_wait_available() + if await api.upload(configuration, "OTA"): + break + if attempt == attempts: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="error_uploading", + translation_placeholders={ + "configuration": configuration, + }, + ) finally: await self.coordinator.async_request_refresh() diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index c9b88d9fb57..63294a6ad69 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -1,5 +1,6 @@ """Test ESPHome update entities.""" +import asyncio from typing import Any from unittest.mock import patch @@ -546,3 +547,296 @@ async def test_generic_device_update_entity_has_update( ) mock_client.update_command.assert_called_with(key=1, command=UpdateCommand.CHECK) + + +async def test_attempt_to_update_twice( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + mock_dashboard: dict[str, Any], +) -> None: + """Test attempting to update twice.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + await async_get_dashboard(hass).async_refresh() + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + + async def delayed_compile(*args: Any, **kwargs: Any) -> None: + """Delay the update.""" + await asyncio.sleep(0) + return True + + # Compile success, upload fails + with ( + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + delayed_compile, + ), + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=False, + ), + ): + update_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + ) + + with pytest.raises(HomeAssistantError, match="update is already in progress"): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + + with pytest.raises(HomeAssistantError, match="OTA"): + await update_task + + +async def test_update_deep_sleep_already_online( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + mock_dashboard: dict[str, Any], +) -> None: + """Test attempting to update twice.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + await async_get_dashboard(hass).async_refresh() + await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={"has_deep_sleep": True}, + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + + # Compile success, upload success + with ( + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, + ), + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=True, + ), + ): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + + +async def test_update_deep_sleep_offline( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + mock_dashboard: dict[str, Any], +) -> None: + """Test device comes online while updating.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + await async_get_dashboard(hass).async_refresh() + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={"has_deep_sleep": True}, + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + await device.mock_disconnect(True) + + # Compile success, upload success + with ( + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, + ), + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=True, + ), + ): + update_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + ) + await asyncio.sleep(0) + assert not update_task.done() + await device.mock_connect() + await hass.async_block_till_done() + + +async def test_update_deep_sleep_offline_sleep_during_ota( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + mock_dashboard: dict[str, Any], +) -> None: + """Test device goes to sleep right as we start the OTA.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + await async_get_dashboard(hass).async_refresh() + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={"has_deep_sleep": True}, + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + await device.mock_disconnect(True) + + upload_attempt = 0 + upload_attempt_2_future = hass.loop.create_future() + disconnect_future = hass.loop.create_future() + + async def upload_takes_a_while(*args: Any, **kwargs: Any) -> None: + """Delay the update.""" + nonlocal upload_attempt + upload_attempt += 1 + if upload_attempt == 1: + # We are simulating the device going back to sleep + # before the upload can be started + # Wait for the device to go unavailable + # before returning false + await disconnect_future + return False + upload_attempt_2_future.set_result(None) + return True + + # Compile success, upload fails first time, success second time + with ( + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, + ), + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + upload_takes_a_while, + ), + ): + update_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + ) + await asyncio.sleep(0) + assert not update_task.done() + await device.mock_connect() + # Mock device being at the end of its sleep cycle + # and going to sleep right as the upload starts + # This can happen because there is non zero time + # between when we tell the dashboard to upload and + # when the upload actually starts + await device.mock_disconnect(True) + disconnect_future.set_result(None) + assert not upload_attempt_2_future.done() + # Now the device wakes up and the upload is attempted + await device.mock_connect() + await upload_attempt_2_future + await hass.async_block_till_done() + + +async def test_update_deep_sleep_offline_cancelled_unload( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + mock_dashboard: dict[str, Any], +) -> None: + """Test deep sleep update attempt is cancelled on unload.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ] + await async_get_dashboard(hass).async_refresh() + device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={"has_deep_sleep": True}, + ) + await hass.async_block_till_done() + state = hass.states.get("update.test_firmware") + assert state is not None + await device.mock_disconnect(True) + + # Compile success, upload success, but we cancel the update + with ( + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, + ), + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=True, + ), + ): + update_task = hass.async_create_task( + hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, + blocking=True, + ) + ) + await asyncio.sleep(0) + assert not update_task.done() + await hass.config_entries.async_unload(device.entry.entry_id) + await hass.async_block_till_done() + assert update_task.cancelled() From 516a3c0504ddc97974267137534989a55de69718 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 4 May 2025 10:02:11 +0200 Subject: [PATCH 0080/1175] Fix licenses check for setuptools (#144181) --- script/licenses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index aed3bec9998..f801603738a 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -208,7 +208,6 @@ EXCEPTIONS = { # https://github.com/jaraco/skeleton/pull/170 # https://github.com/jaraco/skeleton/pull/171 "jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21 - "setuptools", # MIT } TODO = { From d1615f9a6ed77668f5f7cff9874aa8d2370edffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 4 May 2025 11:30:37 +0200 Subject: [PATCH 0081/1175] Bump pymiele to 0.4.3 (#144176) * Use device class transation * Bump pymiele to 0.4.3 --------- Co-authored-by: Shay Levy --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index dc9b420e07e..c0795922875 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.4.1"], + "requirements": ["pymiele==0.4.3"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index be4709419ab..80467891b8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2135,7 +2135,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.1 +pymiele==0.4.3 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 686aa81a4a5..6650853d379 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1747,7 +1747,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.1 +pymiele==0.4.3 # homeassistant.components.mochad pymochad==0.2.0 From b5d499dda88634a3498fd42522a78da6eb58a36e Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 12:06:46 +0200 Subject: [PATCH 0082/1175] Fix spelling of "comma-separated (list)" in `fritzbox_callmonitor` (#144191) Also fix one missing sentence-casing in corresponding "title" string. --- homeassistant/components/fritzbox_callmonitor/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox_callmonitor/strings.json b/homeassistant/components/fritzbox_callmonitor/strings.json index 437b218a8e2..35af748ebe7 100644 --- a/homeassistant/components/fritzbox_callmonitor/strings.json +++ b/homeassistant/components/fritzbox_callmonitor/strings.json @@ -39,9 +39,9 @@ "options": { "step": { "init": { - "title": "Configure Prefixes", + "title": "Configure prefixes", "data": { - "prefixes": "Prefixes (comma separated list)" + "prefixes": "Prefixes (comma-separated list)" } } }, From 1e0d1c46ab40b8f74520b96dcebaa70eb05ff319 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Sun, 4 May 2025 12:07:18 +0200 Subject: [PATCH 0083/1175] Bump homematicip to 2.0.1.1 (#144182) Co-authored-by: Shay Levy --- homeassistant/components/homematicip_cloud/hap.py | 2 +- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homematicip_cloud/test_hap.py | 2 +- tests/components/homematicip_cloud/test_init.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index d55b98b8c18..6f98836a1ff 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -9,10 +9,10 @@ from typing import Any from homematicip.async_home import AsyncHome from homematicip.auth import Auth -from homematicip.base.base_connection import HmipConnectionError from homematicip.base.enums import EventType from homematicip.connection.connection_context import ConnectionContextBuilder from homematicip.connection.rest_connection import RestConnection +from homematicip.exceptions.connection_exceptions import HmipConnectionError import homeassistant from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index afd5863891d..15bc24c110f 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", "loggers": ["homematicip"], - "requirements": ["homematicip==2.0.1"] + "requirements": ["homematicip==2.0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 80467891b8d..9d116efa284 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 # homeassistant.components.homematicip_cloud -homematicip==2.0.1 +homematicip==2.0.1.1 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6650853d379..b14067bfd17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -997,7 +997,7 @@ home-assistant-frontend==20250502.0 home-assistant-intents==2025.4.30 # homeassistant.components.homematicip_cloud -homematicip==2.0.1 +homematicip==2.0.1.1 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 1732459149c..e34424d3439 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -3,8 +3,8 @@ from unittest.mock import Mock, patch from homematicip.auth import Auth -from homematicip.base.base_connection import HmipConnectionError from homematicip.connection.connection_context import ConnectionContext +from homematicip.exceptions.connection_exceptions import HmipConnectionError import pytest from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index a3578baa9aa..f28b3870705 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import AsyncMock, Mock, patch -from homematicip.base.base_connection import HmipConnectionError from homematicip.connection.connection_context import ConnectionContext +from homematicip.exceptions.connection_exceptions import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( CONF_ACCESSPOINT, From b9e11b0f458eca575d2d9b87cee36271e1ae0a1c Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 12:07:30 +0200 Subject: [PATCH 0084/1175] Fix spelling of "comma-separated" and "IP address" in `cast` (#144188) --- homeassistant/components/cast/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json index 8c7c7c0cff0..aa52d21e05f 100644 --- a/homeassistant/components/cast/strings.json +++ b/homeassistant/components/cast/strings.json @@ -10,12 +10,12 @@ "known_hosts": "Add known host" }, "data_description": { - "known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working" + "known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working" } } }, "error": { - "invalid_known_hosts": "Known hosts must be a comma separated list of hosts." + "invalid_known_hosts": "Known hosts must be a comma-separated list of hosts." } }, "options": { From 04982f5e122374462f4010cbfd90a0bbf43b3a3b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 4 May 2025 12:08:07 +0200 Subject: [PATCH 0085/1175] Add missing pollen category to AccuWeather (#144185) * Add extreme level to pollen map * Sort * Sort --- homeassistant/components/accuweather/const.py | 1 + homeassistant/components/accuweather/strings.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 7216f5a0b9b..e1dc4a9abcb 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = { 2: "moderate", 3: "high", 4: "very_high", + 5: "extreme", } UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40) UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6) diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index e81ef782d98..19e52be1ce3 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -72,6 +72,7 @@ "level": { "name": "Level", "state": { + "extreme": "Extreme", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "Moderate", @@ -89,6 +90,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -123,6 +125,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -167,6 +170,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -181,6 +185,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", @@ -195,6 +200,7 @@ "level": { "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "state": { + "extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]", "high": "[%key:common::state::high%]", "low": "[%key:common::state::low%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", From 2aa82da615beeffbf24fb623d6a928c9fe5df91e Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 12:09:09 +0200 Subject: [PATCH 0086/1175] Fix spelling of "comma-separated (list)" in `huawei_lte` (#144189) --- homeassistant/components/huawei_lte/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 50879c9e166..2845338b9cf 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -61,7 +61,7 @@ }, "data_description": { "name": "Used to distinguish between notification services in case there are multiple Huawei LTE devices configured. Changes to this option value take effect after Home Assistant restart.", - "recipient": "Comma separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.", + "recipient": "Comma-separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.", "track_wired_clients": "Whether the device tracker entities track also clients attached to the router's wired Ethernet network, in addition to wireless clients.", "unauthenticated_mode": "Whether to run in unauthenticated mode. Unauthenticated mode provides a limited set of features, but may help in case there are problems accessing the router's web interface from a browser while the integration is active. Changes to this option value take effect after integration reload." } From cb37d4d36a351dfb7644554f2e8e74d7cd3186f1 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 12:09:31 +0200 Subject: [PATCH 0087/1175] Fix spelling of "comma-separated (list / event name)" in `doorbird` (#144190) --- homeassistant/components/doorbird/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index ad43e8c1c1c..285b544e465 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -3,10 +3,10 @@ "step": { "init": { "data": { - "events": "Comma separated list of events." + "events": "Comma-separated list of events." }, "data_description": { - "events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" + "events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" } } } From de496c693e5d7c547fd4b14c571ba65e5ceae98d Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 May 2025 21:36:13 +1000 Subject: [PATCH 0088/1175] Add hazard lights binary sensor to Teslemetry (#144166) --- .../components/teslemetry/binary_sensor.py | 6 ++ .../components/teslemetry/icons.json | 6 ++ .../components/teslemetry/strings.json | 3 + .../snapshots/test_binary_sensor.ambr | 60 +++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index a62dbe1e00f..7dca1667b29 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -391,6 +391,12 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( entity_registry_enabled_default=False, streaming_firmware="2024.44.25", ), + TeslemetryBinarySensorEntityDescription( + key="lights_hazards_active", + streaming_listener=lambda x, y: x.listen_LightsHazardsActive(y), + entity_registry_enabled_default=False, + streaming_firmware="2025.2.6", + ), TeslemetryBinarySensorEntityDescription( key="lights_high_beams", streaming_listener=lambda x, y: x.listen_LightsHighBeams(y), diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 06ac1595a80..5bc3f52b9b7 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -73,6 +73,12 @@ "on": "mdi:snowflake-melt" } }, + "lights_hazards_active": { + "state": { + "off": "mdi:car-light-dimmed", + "on": "mdi:hazard-lights" + } + }, "lights_high_beams": { "state": { "off": "mdi:car-light-dimmed", diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 54568c971c4..456850fde3e 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -203,6 +203,9 @@ "defrost_for_preconditioning": { "name": "Defrost for preconditioning" }, + "lights_hazards_active": { + "name": "Hazard lights" + }, "lights_high_beams": { "name": "High beams" }, diff --git a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr index d957bdedcf4..0af85a6846d 100644 --- a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr @@ -1514,6 +1514,53 @@ 'state': 'unknown', }) # --- +# name: test_binary_sensor[binary_sensor.test_hazard_lights-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_hazard_lights', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Hazard lights', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lights_hazards_active', + 'unique_id': 'LRW3F7EK4NC700000-lights_hazards_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_hazard_lights-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Hazard lights', + }), + 'context': , + 'entity_id': 'binary_sensor.test_hazard_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor[binary_sensor.test_high_beams-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3504,6 +3551,19 @@ 'state': 'unknown', }) # --- +# name: test_binary_sensor_refresh[binary_sensor.test_hazard_lights-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Hazard lights', + }), + 'context': , + 'entity_id': 'binary_sensor.test_hazard_lights', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_binary_sensor_refresh[binary_sensor.test_high_beams-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ From 8c6edd8b818c7cec2ded4a607df5f0141a70bc94 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 May 2025 21:41:45 +1000 Subject: [PATCH 0089/1175] Add better typing to Teslemetry switch platform (#144168) --- homeassistant/components/teslemetry/switch.py | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index 645a8398820..9d30c73220d 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -2,12 +2,13 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable from dataclasses import dataclass from itertools import chain from typing import Any from tesla_fleet_api.const import AutoSeat, Scope +from tesla_fleet_api.teslemetry.vehicles import TeslemetryVehicle from teslemetry_stream import TeslemetryStreamVehicle from homeassistant.components.switch import ( @@ -37,15 +38,14 @@ PARALLEL_UPDATES = 0 class TeslemetrySwitchEntityDescription(SwitchEntityDescription): """Describes Teslemetry Switch entity.""" - on_func: Callable - off_func: Callable + on_func: Callable[[TeslemetryVehicle], Awaitable[dict[str, Any]]] + off_func: Callable[[TeslemetryVehicle], Awaitable[dict[str, Any]]] scopes: list[Scope] value_func: Callable[[StateType], bool] = bool streaming_listener: Callable[ - [TeslemetryStreamVehicle, Callable[[StateType], None]], + [TeslemetryStreamVehicle, Callable[[bool | None], None]], Callable[[], None], ] - streaming_value_fn: Callable[[StateType], bool] = bool streaming_firmware: str = "2024.26" unique_id: str | None = None @@ -53,15 +53,18 @@ class TeslemetrySwitchEntityDescription(SwitchEntityDescription): VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( TeslemetrySwitchEntityDescription( key="vehicle_state_sentry_mode", - streaming_listener=lambda x, y: x.listen_SentryMode(y), - streaming_value_fn=lambda x: x != "Off", + streaming_listener=lambda vehicle, callback: vehicle.listen_SentryMode( + lambda value: callback(None if value is None else value != "Off") + ), on_func=lambda api: api.set_sentry_mode(on=True), off_func=lambda api: api.set_sentry_mode(on=False), scopes=[Scope.VEHICLE_CMDS], ), TeslemetrySwitchEntityDescription( key="climate_state_auto_seat_climate_left", - streaming_listener=lambda x, y: x.listen_AutoSeatClimateLeft(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_AutoSeatClimateLeft( + callback + ), on_func=lambda api: api.remote_auto_seat_climate_request( AutoSeat.FRONT_LEFT, True ), @@ -72,7 +75,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( ), TeslemetrySwitchEntityDescription( key="climate_state_auto_seat_climate_right", - streaming_listener=lambda x, y: x.listen_AutoSeatClimateRight(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_AutoSeatClimateRight(callback), on_func=lambda api: api.remote_auto_seat_climate_request( AutoSeat.FRONT_RIGHT, True ), @@ -83,7 +87,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( ), TeslemetrySwitchEntityDescription( key="climate_state_auto_steering_wheel_heat", - streaming_listener=lambda x, y: x.listen_HvacSteeringWheelHeatAuto(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_HvacSteeringWheelHeatAuto(callback), on_func=lambda api: api.remote_auto_steering_wheel_heat_climate_request( on=True ), @@ -94,8 +99,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( ), TeslemetrySwitchEntityDescription( key="climate_state_defrost_mode", - streaming_listener=lambda x, y: x.listen_DefrostMode(y), - streaming_value_fn=lambda x: x != "Off", + streaming_listener=lambda vehicle, callback: vehicle.listen_DefrostMode( + lambda value: callback(None if value is None else value != "Off") + ), on_func=lambda api: api.set_preconditioning_max(on=True, manual_override=False), off_func=lambda api: api.set_preconditioning_max( on=False, manual_override=False @@ -106,8 +112,11 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( key="charge_state_charging_state", unique_id="charge_state_user_charge_enable_request", value_func=lambda state: state in {"Starting", "Charging"}, - streaming_listener=lambda x, y: x.listen_DetailedChargeState(y), - streaming_value_fn=lambda x: x in {"Starting", "Charging"}, + streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState( + lambda value: callback( + None if value is None else value in {"Starting", "Charging"} + ) + ), on_func=lambda api: api.charge_start(), off_func=lambda api: api.charge_stop(), scopes=[Scope.VEHICLE_CMDS, Scope.VEHICLE_CHARGING_CMDS], @@ -239,11 +248,9 @@ class TeslemetryStreamingVehicleSwitchEntity( ) ) - def _value_callback(self, value: StateType) -> None: + def _value_callback(self, value: bool | None) -> None: """Update the value of the entity.""" - self._attr_is_on = ( - None if value is None else self.entity_description.streaming_value_fn(value) - ) + self._attr_is_on = value self.async_write_ha_state() From 5a475ec7ea2f5204ab1406eeb18df9e988c9b490 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 May 2025 21:42:25 +1000 Subject: [PATCH 0090/1175] Improve typing of binary sensors in Teslemetry (#144169) --- .../components/teslemetry/binary_sensor.py | 183 ++++++++++++------ 1 file changed, 126 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index 7dca1667b29..da5072e2535 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -58,26 +58,28 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="state", polling=True, - polling_value_fn=lambda x: x == TeslemetryState.ONLINE, - streaming_listener=lambda x, y: x.listen_State(y), + polling_value_fn=lambda value: value == TeslemetryState.ONLINE, + streaming_listener=lambda vehicle, callback: vehicle.listen_State(callback), device_class=BinarySensorDeviceClass.CONNECTIVITY, ), TeslemetryBinarySensorEntityDescription( key="cellular", - streaming_listener=lambda x, y: x.listen_Cellular(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_Cellular(callback), device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="wifi", - streaming_listener=lambda x, y: x.listen_Wifi(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_Wifi(callback), device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="charge_state_battery_heater_on", polling=True, - streaming_listener=lambda x, y: x.listen_BatteryHeaterOn(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_BatteryHeaterOn( + callback + ), device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -85,8 +87,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="charge_state_charger_phases", polling=True, - streaming_listener=lambda x, y: x.listen_ChargerPhases( - lambda z: y(None if z is None else z > 1) + streaming_listener=lambda vehicle, callback: vehicle.listen_ChargerPhases( + lambda value: callback(None if value is None else value > 1) ), polling_value_fn=lambda x: cast(int, x) > 1, entity_registry_enabled_default=False, @@ -94,7 +96,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="charge_state_preconditioning_enabled", polling=True, - streaming_listener=lambda x, y: x.listen_PreconditioningEnabled(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_PreconditioningEnabled(callback), entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -107,7 +110,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="charge_state_scheduled_charging_pending", polling=True, - streaming_listener=lambda x, y: x.listen_ScheduledChargingPending(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_ScheduledChargingPending(callback), entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), @@ -175,8 +179,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="vehicle_state_fd_window", polling=True, - streaming_listener=lambda x, y: x.listen_FrontDriverWindow( - lambda z: y(WINDOW_STATES.get(z)) + streaming_listener=lambda vehicle, callback: vehicle.listen_FrontDriverWindow( + lambda value: callback(None if value is None else WINDOW_STATES.get(value)) ), device_class=BinarySensorDeviceClass.WINDOW, entity_category=EntityCategory.DIAGNOSTIC, @@ -184,8 +188,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="vehicle_state_fp_window", polling=True, - streaming_listener=lambda x, y: x.listen_FrontPassengerWindow( - lambda z: y(WINDOW_STATES.get(z)) + streaming_listener=lambda vehicle, + callback: vehicle.listen_FrontPassengerWindow( + lambda value: callback(None if value is None else WINDOW_STATES.get(value)) ), device_class=BinarySensorDeviceClass.WINDOW, entity_category=EntityCategory.DIAGNOSTIC, @@ -193,8 +198,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="vehicle_state_rd_window", polling=True, - streaming_listener=lambda x, y: x.listen_RearDriverWindow( - lambda z: y(WINDOW_STATES.get(z)) + streaming_listener=lambda vehicle, callback: vehicle.listen_RearDriverWindow( + lambda value: callback(None if value is None else WINDOW_STATES.get(value)) ), device_class=BinarySensorDeviceClass.WINDOW, entity_category=EntityCategory.DIAGNOSTIC, @@ -202,8 +207,8 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="vehicle_state_rp_window", polling=True, - streaming_listener=lambda x, y: x.listen_RearPassengerWindow( - lambda z: y(WINDOW_STATES.get(z)) + streaming_listener=lambda vehicle, callback: vehicle.listen_RearPassengerWindow( + lambda value: callback(None if value is None else WINDOW_STATES.get(value)) ), device_class=BinarySensorDeviceClass.WINDOW, entity_category=EntityCategory.DIAGNOSTIC, @@ -212,182 +217,234 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( key="vehicle_state_df", polling=True, device_class=BinarySensorDeviceClass.DOOR, - streaming_listener=lambda x, y: x.listen_FrontDriverDoor(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_FrontDriverDoor( + callback + ), entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="vehicle_state_dr", polling=True, device_class=BinarySensorDeviceClass.DOOR, - streaming_listener=lambda x, y: x.listen_RearDriverDoor(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_RearDriverDoor( + callback + ), entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="vehicle_state_pf", polling=True, device_class=BinarySensorDeviceClass.DOOR, - streaming_listener=lambda x, y: x.listen_FrontPassengerDoor(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_FrontPassengerDoor( + callback + ), entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="vehicle_state_pr", polling=True, device_class=BinarySensorDeviceClass.DOOR, - streaming_listener=lambda x, y: x.listen_RearPassengerDoor(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_RearPassengerDoor( + callback + ), entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="automatic_blind_spot_camera", - streaming_listener=lambda x, y: x.listen_AutomaticBlindSpotCamera(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_AutomaticBlindSpotCamera(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="automatic_emergency_braking_off", - streaming_listener=lambda x, y: x.listen_AutomaticEmergencyBrakingOff(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_AutomaticEmergencyBrakingOff(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="blind_spot_collision_warning_chime", - streaming_listener=lambda x, y: x.listen_BlindSpotCollisionWarningChime(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_BlindSpotCollisionWarningChime(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="bms_full_charge_complete", - streaming_listener=lambda x, y: x.listen_BmsFullchargecomplete(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_BmsFullchargecomplete(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="brake_pedal", - streaming_listener=lambda x, y: x.listen_BrakePedal(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_BrakePedal( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="charge_port_cold_weather_mode", - streaming_listener=lambda x, y: x.listen_ChargePortColdWeatherMode(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_ChargePortColdWeatherMode(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="service_mode", - streaming_listener=lambda x, y: x.listen_ServiceMode(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_ServiceMode( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="pin_to_drive_enabled", - streaming_listener=lambda x, y: x.listen_PinToDriveEnabled(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_PinToDriveEnabled( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="drive_rail", - streaming_listener=lambda x, y: x.listen_DriveRail(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_DriveRail(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="driver_seat_belt", - streaming_listener=lambda x, y: x.listen_DriverSeatBelt(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_DriverSeatBelt( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="driver_seat_occupied", - streaming_listener=lambda x, y: x.listen_DriverSeatOccupied(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_DriverSeatOccupied( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="passenger_seat_belt", - streaming_listener=lambda x, y: x.listen_PassengerSeatBelt(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_PassengerSeatBelt( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="fast_charger_present", - streaming_listener=lambda x, y: x.listen_FastChargerPresent(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_FastChargerPresent( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="gps_state", - streaming_listener=lambda x, y: x.listen_GpsState(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_GpsState(callback), entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, ), TeslemetryBinarySensorEntityDescription( key="guest_mode_enabled", - streaming_listener=lambda x, y: x.listen_GuestModeEnabled(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_GuestModeEnabled( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="dc_dc_enable", - streaming_listener=lambda x, y: x.listen_DCDCEnable(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_DCDCEnable( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="emergency_lane_departure_avoidance", - streaming_listener=lambda x, y: x.listen_EmergencyLaneDepartureAvoidance(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_EmergencyLaneDepartureAvoidance(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="supercharger_session_trip_planner", - streaming_listener=lambda x, y: x.listen_SuperchargerSessionTripPlanner(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_SuperchargerSessionTripPlanner(callback), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="wiper_heat_enabled", - streaming_listener=lambda x, y: x.listen_WiperHeatEnabled(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_WiperHeatEnabled( + callback + ), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="rear_display_hvac_enabled", - streaming_listener=lambda x, y: x.listen_RearDisplayHvacEnabled(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_RearDisplayHvacEnabled(callback), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="offroad_lightbar_present", - streaming_listener=lambda x, y: x.listen_OffroadLightbarPresent(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_OffroadLightbarPresent(callback), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="homelink_nearby", - streaming_listener=lambda x, y: x.listen_HomelinkNearby(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_HomelinkNearby( + callback + ), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="europe_vehicle", - streaming_listener=lambda x, y: x.listen_EuropeVehicle(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_EuropeVehicle( + callback + ), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="right_hand_drive", - streaming_listener=lambda x, y: x.listen_RightHandDrive(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_RightHandDrive( + callback + ), streaming_firmware="2024.44.25", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="located_at_home", - streaming_listener=lambda x, y: x.listen_LocatedAtHome(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtHome( + callback + ), streaming_firmware="2024.44.32", ), TeslemetryBinarySensorEntityDescription( key="located_at_work", - streaming_listener=lambda x, y: x.listen_LocatedAtWork(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtWork( + callback + ), streaming_firmware="2024.44.32", ), TeslemetryBinarySensorEntityDescription( key="located_at_favorite", - streaming_listener=lambda x, y: x.listen_LocatedAtFavorite(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_LocatedAtFavorite( + callback + ), streaming_firmware="2024.44.32", entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="charge_enable_request", - streaming_listener=lambda x, y: x.listen_ChargeEnableRequest(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_ChargeEnableRequest( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="defrost_for_preconditioning", - streaming_listener=lambda x, y: x.listen_DefrostForPreconditioning(y), + streaming_listener=lambda vehicle, + callback: vehicle.listen_DefrostForPreconditioning(callback), entity_registry_enabled_default=False, streaming_firmware="2024.44.25", ), @@ -399,36 +456,48 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( ), TeslemetryBinarySensorEntityDescription( key="lights_high_beams", - streaming_listener=lambda x, y: x.listen_LightsHighBeams(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_LightsHighBeams( + callback + ), entity_registry_enabled_default=False, streaming_firmware="2025.2.6", ), TeslemetryBinarySensorEntityDescription( key="seat_vent_enabled", - streaming_listener=lambda x, y: x.listen_SeatVentEnabled(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_SeatVentEnabled( + callback + ), entity_registry_enabled_default=False, streaming_firmware="2025.2.6", ), TeslemetryBinarySensorEntityDescription( key="speed_limit_mode", - streaming_listener=lambda x, y: x.listen_SpeedLimitMode(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_SpeedLimitMode( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="remote_start_enabled", - streaming_listener=lambda x, y: x.listen_RemoteStartEnabled(y), + streaming_listener=lambda vehicle, callback: vehicle.listen_RemoteStartEnabled( + callback + ), entity_registry_enabled_default=False, ), TeslemetryBinarySensorEntityDescription( key="hvil", - streaming_listener=lambda x, y: x.listen_Hvil(lambda z: y(z == "Fault")), + streaming_listener=lambda vehicle, callback: vehicle.listen_Hvil( + lambda value: callback(None if value is None else value == "Fault") + ), device_class=BinarySensorDeviceClass.PROBLEM, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), TeslemetryBinarySensorEntityDescription( key="hvac_auto_mode", - streaming_listener=lambda x, y: x.listen_HvacAutoMode(lambda z: y(z == "On")), + streaming_listener=lambda vehicle, callback: vehicle.listen_HvacAutoMode( + lambda value: callback(None if value is None else value == "On") + ), entity_registry_enabled_default=False, ), ) @@ -437,7 +506,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( ENERGY_LIVE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( TeslemetryBinarySensorEntityDescription( key="grid_status", - polling_value_fn=lambda x: x == "Active", + polling_value_fn=lambda value: value == "Active", device_class=BinarySensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), From 80466841794927654dd7b62bf1f19b68b6959b4e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 May 2025 21:44:56 +1000 Subject: [PATCH 0091/1175] Update models const in Teslemetry (#144175) --- homeassistant/components/teslemetry/__init__.py | 4 ++-- homeassistant/components/teslemetry/const.py | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 9efa55de54f..1eb1ea54091 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER, MODELS +from .const import DOMAIN, LOGGER from .coordinator import ( TeslemetryEnergyHistoryCoordinator, TeslemetryEnergySiteInfoCoordinator, @@ -119,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - manufacturer="Tesla", configuration_url="https://teslemetry.com/console", name=product["display_name"], - model=MODELS.get(vin[3]), + model=api.model, serial_number=vin, ) diff --git a/homeassistant/components/teslemetry/const.py b/homeassistant/components/teslemetry/const.py index 01c6c33f505..ebda486aedf 100644 --- a/homeassistant/components/teslemetry/const.py +++ b/homeassistant/components/teslemetry/const.py @@ -9,13 +9,6 @@ DOMAIN = "teslemetry" LOGGER = logging.getLogger(__package__) -MODELS = { - "S": "Model S", - "3": "Model 3", - "X": "Model X", - "Y": "Model Y", -} - ENERGY_HISTORY_FIELDS = [ "solar_energy_exported", "generator_energy_exported", From 87fab1fa149182616d604cf16b332ceef217312b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 4 May 2025 22:18:39 +1000 Subject: [PATCH 0092/1175] Rename classes in Teslemetry (#144179) --- .../components/teslemetry/binary_sensor.py | 4 +-- homeassistant/components/teslemetry/button.py | 4 +-- .../components/teslemetry/climate.py | 14 ++++---- homeassistant/components/teslemetry/cover.py | 34 +++++++++++-------- .../components/teslemetry/device_tracker.py | 13 ++++--- homeassistant/components/teslemetry/entity.py | 12 +++---- homeassistant/components/teslemetry/lock.py | 14 ++++---- .../components/teslemetry/media_player.py | 8 +++-- homeassistant/components/teslemetry/number.py | 8 ++--- homeassistant/components/teslemetry/select.py | 8 +++-- homeassistant/components/teslemetry/sensor.py | 6 ++-- homeassistant/components/teslemetry/switch.py | 8 ++--- homeassistant/components/teslemetry/update.py | 8 +++-- 13 files changed, 80 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index da5072e2535..99c21cbe03e 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -24,7 +24,7 @@ from .const import TeslemetryState from .entity import ( TeslemetryEnergyInfoEntity, TeslemetryEnergyLiveEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .models import TeslemetryEnergyData, TeslemetryVehicleData @@ -569,7 +569,7 @@ async def async_setup_entry( class TeslemetryVehiclePollingBinarySensorEntity( - TeslemetryVehicleEntity, BinarySensorEntity + TeslemetryVehiclePollingEntity, BinarySensorEntity ): """Base class for Teslemetry vehicle binary sensors.""" diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index 4ca2fd9b166..6cb9d996b95 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import TeslemetryConfigEntry -from .entity import TeslemetryVehicleEntity +from .entity import TeslemetryVehiclePollingEntity from .helpers import handle_command, handle_vehicle_command from .models import TeslemetryVehicleData @@ -73,7 +73,7 @@ async def async_setup_entry( ) -class TeslemetryButtonEntity(TeslemetryVehicleEntity, ButtonEntity): +class TeslemetryButtonEntity(TeslemetryVehiclePollingEntity, ButtonEntity): """Base class for Teslemetry buttons.""" entity_description: TeslemetryButtonEntityDescription diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index c1c8fcd2f73..0a1c23adcb0 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -30,7 +30,7 @@ from . import TeslemetryConfigEntry from .const import DOMAIN, TeslemetryClimateSide from .entity import ( TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_vehicle_command @@ -64,7 +64,7 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingClimateEntity( + TeslemetryVehiclePollingClimateEntity( vehicle, TeslemetryClimateSide.DRIVER, entry.runtime_data.scopes ) if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" @@ -74,7 +74,7 @@ async def async_setup_entry( for vehicle in entry.runtime_data.vehicles ), ( - TeslemetryPollingCabinOverheatProtectionEntity( + TeslemetryVehiclePollingCabinOverheatProtectionEntity( vehicle, entry.runtime_data.scopes ) if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" @@ -178,7 +178,9 @@ class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity): self.async_write_ha_state() -class TeslemetryPollingClimateEntity(TeslemetryClimateEntity, TeslemetryVehicleEntity): +class TeslemetryVehiclePollingClimateEntity( + TeslemetryClimateEntity, TeslemetryVehiclePollingEntity +): """Polling vehicle climate entity.""" _attr_supported_features = ( @@ -430,8 +432,8 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntit self.async_write_ha_state() -class TeslemetryPollingCabinOverheatProtectionEntity( - TeslemetryVehicleEntity, TeslemetryCabinOverheatProtectionEntity +class TeslemetryVehiclePollingCabinOverheatProtectionEntity( + TeslemetryVehiclePollingEntity, TeslemetryCabinOverheatProtectionEntity ): """Vehicle Cabin Overheat Protection.""" diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index cde1d3f7d4f..de036edc32a 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -21,7 +21,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from . import TeslemetryConfigEntry from .entity import ( TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_vehicle_command @@ -43,13 +43,15 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingWindowEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingWindowEntity(vehicle, entry.runtime_data.scopes) if vehicle.api.pre2021 or vehicle.firmware < "2024.26" else TeslemetryStreamingWindowEntity(vehicle, entry.runtime_data.scopes) for vehicle in entry.runtime_data.vehicles ), ( - TeslemetryPollingChargePortEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingChargePortEntity( + vehicle, entry.runtime_data.scopes + ) if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" else TeslemetryStreamingChargePortEntity( vehicle, entry.runtime_data.scopes @@ -57,7 +59,9 @@ async def async_setup_entry( for vehicle in entry.runtime_data.vehicles ), ( - TeslemetryPollingFrontTrunkEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingFrontTrunkEntity( + vehicle, entry.runtime_data.scopes + ) if vehicle.api.pre2021 or vehicle.firmware < "2024.26" else TeslemetryStreamingFrontTrunkEntity( vehicle, entry.runtime_data.scopes @@ -65,7 +69,9 @@ async def async_setup_entry( for vehicle in entry.runtime_data.vehicles ), ( - TeslemetryPollingRearTrunkEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingRearTrunkEntity( + vehicle, entry.runtime_data.scopes + ) if vehicle.api.pre2021 or vehicle.firmware < "2024.26" else TeslemetryStreamingRearTrunkEntity( vehicle, entry.runtime_data.scopes @@ -121,8 +127,8 @@ class TeslemetryWindowEntity(TeslemetryRootEntity, CoverEntity): self.async_write_ha_state() -class TeslemetryPollingWindowEntity( - TeslemetryVehicleEntity, TeslemetryWindowEntity, CoverEntity +class TeslemetryVehiclePollingWindowEntity( + TeslemetryVehiclePollingEntity, TeslemetryWindowEntity, CoverEntity ): """Polling cover entity for windows.""" @@ -238,8 +244,8 @@ class TeslemetryChargePortEntity( self.async_write_ha_state() -class TeslemetryPollingChargePortEntity( - TeslemetryVehicleEntity, TeslemetryChargePortEntity +class TeslemetryVehiclePollingChargePortEntity( + TeslemetryVehiclePollingEntity, TeslemetryChargePortEntity ): """Polling cover entity for the charge port.""" @@ -312,8 +318,8 @@ class TeslemetryFrontTrunkEntity(TeslemetryRootEntity, CoverEntity): # In the future this could be extended to add aftermarket close support through a option flow -class TeslemetryPollingFrontTrunkEntity( - TeslemetryVehicleEntity, TeslemetryFrontTrunkEntity +class TeslemetryVehiclePollingFrontTrunkEntity( + TeslemetryVehiclePollingEntity, TeslemetryFrontTrunkEntity ): """Polling cover entity for the front trunk.""" @@ -381,8 +387,8 @@ class TeslemetryRearTrunkEntity(TeslemetryRootEntity, CoverEntity): self.async_write_ha_state() -class TeslemetryPollingRearTrunkEntity( - TeslemetryVehicleEntity, TeslemetryRearTrunkEntity +class TeslemetryVehiclePollingRearTrunkEntity( + TeslemetryVehiclePollingEntity, TeslemetryRearTrunkEntity ): """Base class for the rear trunk cover entities.""" @@ -424,7 +430,7 @@ class TeslemetryStreamingRearTrunkEntity( self._attr_is_closed = None if value is None else not value -class TeslemetrySunroofEntity(TeslemetryVehicleEntity, CoverEntity): +class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity): """Cover entity for the sunroof.""" _attr_device_class = CoverDeviceClass.WINDOW diff --git a/homeassistant/components/teslemetry/device_tracker.py b/homeassistant/components/teslemetry/device_tracker.py index bb90a7b19bd..6a3df6ecb6a 100644 --- a/homeassistant/components/teslemetry/device_tracker.py +++ b/homeassistant/components/teslemetry/device_tracker.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from . import TeslemetryConfigEntry -from .entity import TeslemetryVehicleEntity, TeslemetryVehicleStreamEntity +from .entity import TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity from .models import TeslemetryVehicleData PARALLEL_UPDATES = 0 @@ -74,7 +74,8 @@ async def async_setup_entry( """Set up the Teslemetry device tracker platform from a config entry.""" entities: list[ - TeslemetryPollingDeviceTrackerEntity | TeslemetryStreamingDeviceTrackerEntity + TeslemetryVehiclePollingDeviceTrackerEntity + | TeslemetryStreamingDeviceTrackerEntity ] = [] # Only add vehicle location entities if the user has granted vehicle location scope. if Scope.VEHICLE_LOCATION not in entry.runtime_data.scopes: @@ -85,7 +86,9 @@ async def async_setup_entry( if vehicle.api.pre2021 or vehicle.firmware < description.streaming_firmware: if description.polling_prefix: entities.append( - TeslemetryPollingDeviceTrackerEntity(vehicle, description) + TeslemetryVehiclePollingDeviceTrackerEntity( + vehicle, description + ) ) else: entities.append( @@ -95,7 +98,9 @@ async def async_setup_entry( async_add_entities(entities) -class TeslemetryPollingDeviceTrackerEntity(TeslemetryVehicleEntity, TrackerEntity): +class TeslemetryVehiclePollingDeviceTrackerEntity( + TeslemetryVehiclePollingEntity, TrackerEntity +): """Base class for Teslemetry Tracker Entities.""" entity_description: TeslemetryDeviceTrackerEntityDescription diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 9ce812980db..4930129642f 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -38,7 +38,7 @@ class TeslemetryRootEntity(Entity): ) -class TeslemetryEntity( +class TeslemetryPollingEntity( TeslemetryRootEntity, CoordinatorEntity[ TeslemetryVehicleDataCoordinator @@ -98,7 +98,7 @@ class TeslemetryEntity( """Update the attributes of the entity.""" -class TeslemetryVehicleEntity(TeslemetryEntity): +class TeslemetryVehiclePollingEntity(TeslemetryPollingEntity): """Parent class for Teslemetry Vehicle entities.""" _last_update: int = 0 @@ -130,7 +130,7 @@ class TeslemetryVehicleEntity(TeslemetryEntity): return self.coordinator.data.get(self.key) -class TeslemetryEnergyLiveEntity(TeslemetryEntity): +class TeslemetryEnergyLiveEntity(TeslemetryPollingEntity): """Parent class for Teslemetry Energy Site Live entities.""" api: EnergySite @@ -151,7 +151,7 @@ class TeslemetryEnergyLiveEntity(TeslemetryEntity): super().__init__(data.live_coordinator, key) -class TeslemetryEnergyInfoEntity(TeslemetryEntity): +class TeslemetryEnergyInfoEntity(TeslemetryPollingEntity): """Parent class for Teslemetry Energy Site Info Entities.""" api: EnergySite @@ -170,7 +170,7 @@ class TeslemetryEnergyInfoEntity(TeslemetryEntity): super().__init__(data.info_coordinator, key) -class TeslemetryEnergyHistoryEntity(TeslemetryEntity): +class TeslemetryEnergyHistoryEntity(TeslemetryPollingEntity): """Parent class for Teslemetry Energy History Entities.""" def __init__( @@ -189,7 +189,7 @@ class TeslemetryEnergyHistoryEntity(TeslemetryEntity): super().__init__(data.history_coordinator, key) -class TeslemetryWallConnectorEntity(TeslemetryEntity): +class TeslemetryWallConnectorEntity(TeslemetryPollingEntity): """Parent class for Teslemetry Wall Connector Entities.""" _attr_has_entity_name = True diff --git a/homeassistant/components/teslemetry/lock.py b/homeassistant/components/teslemetry/lock.py index 68505a12a13..75cf72c9c88 100644 --- a/homeassistant/components/teslemetry/lock.py +++ b/homeassistant/components/teslemetry/lock.py @@ -17,7 +17,7 @@ from . import TeslemetryConfigEntry from .const import DOMAIN from .entity import ( TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_vehicle_command @@ -38,7 +38,7 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingVehicleLockEntity( + TeslemetryVehiclePollingVehicleLockEntity( vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes ) if vehicle.api.pre2021 or vehicle.firmware < "2024.26" @@ -48,7 +48,7 @@ async def async_setup_entry( for vehicle in entry.runtime_data.vehicles ), ( - TeslemetryPollingCableLockEntity( + TeslemetryVehiclePollingCableLockEntity( vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes ) if vehicle.api.pre2021 or vehicle.firmware < "2024.26" @@ -81,8 +81,8 @@ class TeslemetryVehicleLockEntity(TeslemetryRootEntity, LockEntity): self.async_write_ha_state() -class TeslemetryPollingVehicleLockEntity( - TeslemetryVehicleEntity, TeslemetryVehicleLockEntity +class TeslemetryVehiclePollingVehicleLockEntity( + TeslemetryVehiclePollingEntity, TeslemetryVehicleLockEntity ): """Polling vehicle lock entity for Teslemetry.""" @@ -152,8 +152,8 @@ class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity): self.async_write_ha_state() -class TeslemetryPollingCableLockEntity( - TeslemetryVehicleEntity, TeslemetryCableLockEntity +class TeslemetryVehiclePollingCableLockEntity( + TeslemetryVehiclePollingEntity, TeslemetryCableLockEntity ): """Polling cable lock entity for Teslemetry.""" diff --git a/homeassistant/components/teslemetry/media_player.py b/homeassistant/components/teslemetry/media_player.py index 50f15618e66..11615d94614 100644 --- a/homeassistant/components/teslemetry/media_player.py +++ b/homeassistant/components/teslemetry/media_player.py @@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from . import TeslemetryConfigEntry from .entity import ( TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_vehicle_command @@ -52,7 +52,7 @@ async def async_setup_entry( """Set up the Teslemetry Media platform from a config entry.""" async_add_entities( - TeslemetryPollingMediaEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingMediaEntity(vehicle, entry.runtime_data.scopes) if vehicle.api.pre2021 or vehicle.firmware < "2025.2.6" else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes) for vehicle in entry.runtime_data.vehicles @@ -107,7 +107,9 @@ class TeslemetryMediaEntity(TeslemetryRootEntity, MediaPlayerEntity): await handle_vehicle_command(self.api.media_prev_track()) -class TeslemetryPollingMediaEntity(TeslemetryVehicleEntity, TeslemetryMediaEntity): +class TeslemetryVehiclePollingMediaEntity( + TeslemetryVehiclePollingEntity, TeslemetryMediaEntity +): """Polling vehicle media player class.""" def __init__( diff --git a/homeassistant/components/teslemetry/number.py b/homeassistant/components/teslemetry/number.py index 117c0a8c233..466fc9f5ee6 100644 --- a/homeassistant/components/teslemetry/number.py +++ b/homeassistant/components/teslemetry/number.py @@ -33,7 +33,7 @@ from . import TeslemetryConfigEntry from .entity import ( TeslemetryEnergyInfoEntity, TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_command, handle_vehicle_command @@ -140,7 +140,7 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingNumberEntity( + TeslemetryVehiclePollingNumberEntity( vehicle, description, entry.runtime_data.scopes, @@ -183,8 +183,8 @@ class TeslemetryVehicleNumberEntity(TeslemetryRootEntity, NumberEntity): self.async_write_ha_state() -class TeslemetryPollingNumberEntity( - TeslemetryVehicleEntity, TeslemetryVehicleNumberEntity +class TeslemetryVehiclePollingNumberEntity( + TeslemetryVehiclePollingEntity, TeslemetryVehicleNumberEntity ): """Vehicle polling number entity.""" diff --git a/homeassistant/components/teslemetry/select.py b/homeassistant/components/teslemetry/select.py index 9e13d15edc4..be90636497e 100644 --- a/homeassistant/components/teslemetry/select.py +++ b/homeassistant/components/teslemetry/select.py @@ -20,7 +20,7 @@ from . import TeslemetryConfigEntry from .entity import ( TeslemetryEnergyInfoEntity, TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_command, handle_vehicle_command @@ -177,7 +177,7 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingSelectEntity( + TeslemetryVehiclePollingSelectEntity( vehicle, description, entry.runtime_data.scopes ) if vehicle.api.pre2021 @@ -223,7 +223,9 @@ class TeslemetrySelectEntity(TeslemetryRootEntity, SelectEntity): self.async_write_ha_state() -class TeslemetryPollingSelectEntity(TeslemetryVehicleEntity, TeslemetrySelectEntity): +class TeslemetryVehiclePollingSelectEntity( + TeslemetryVehiclePollingEntity, TeslemetrySelectEntity +): """Base polling vehicle select entity class.""" def __init__( diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index b87bd334e8c..20e2abfe9e6 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -41,7 +41,7 @@ from .entity import ( TeslemetryEnergyHistoryEntity, TeslemetryEnergyInfoEntity, TeslemetryEnergyLiveEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, TeslemetryWallConnectorEntity, ) @@ -1633,7 +1633,7 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor) self.async_write_ha_state() -class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): +class TeslemetryVehicleSensorEntity(TeslemetryVehiclePollingEntity, SensorEntity): """Base class for Teslemetry vehicle metric sensors.""" entity_description: TeslemetryVehicleSensorEntityDescription @@ -1696,7 +1696,7 @@ class TeslemetryStreamTimeSensorEntity(TeslemetryVehicleStreamEntity, SensorEnti self.async_write_ha_state() -class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): +class TeslemetryVehicleTimeSensorEntity(TeslemetryVehiclePollingEntity, SensorEntity): """Base class for Teslemetry vehicle time sensors.""" entity_description: TeslemetryTimeEntityDescription diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index 9d30c73220d..acd17ac4165 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -25,7 +25,7 @@ from . import TeslemetryConfigEntry from .entity import ( TeslemetryEnergyInfoEntity, TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_command, handle_vehicle_command @@ -134,7 +134,7 @@ async def async_setup_entry( async_add_entities( chain( ( - TeslemetryPollingVehicleSwitchEntity( + TeslemetryVehiclePollingVehicleSwitchEntity( vehicle, description, entry.runtime_data.scopes ) if vehicle.api.pre2021 @@ -184,8 +184,8 @@ class TeslemetryVehicleSwitchEntity(TeslemetryRootEntity, SwitchEntity): self.async_write_ha_state() -class TeslemetryPollingVehicleSwitchEntity( - TeslemetryVehicleEntity, TeslemetryVehicleSwitchEntity +class TeslemetryVehiclePollingVehicleSwitchEntity( + TeslemetryVehiclePollingEntity, TeslemetryVehicleSwitchEntity ): """Base class for Teslemetry polling vehicle switch entities.""" diff --git a/homeassistant/components/teslemetry/update.py b/homeassistant/components/teslemetry/update.py index b8d40877de4..144a97039fc 100644 --- a/homeassistant/components/teslemetry/update.py +++ b/homeassistant/components/teslemetry/update.py @@ -15,7 +15,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from . import TeslemetryConfigEntry from .entity import ( TeslemetryRootEntity, - TeslemetryVehicleEntity, + TeslemetryVehiclePollingEntity, TeslemetryVehicleStreamEntity, ) from .helpers import handle_vehicle_command @@ -38,7 +38,7 @@ async def async_setup_entry( """Set up the Teslemetry update platform from a config entry.""" async_add_entities( - TeslemetryPollingUpdateEntity(vehicle, entry.runtime_data.scopes) + TeslemetryVehiclePollingUpdateEntity(vehicle, entry.runtime_data.scopes) if vehicle.api.pre2021 or vehicle.firmware < "2024.44.25" else TeslemetryStreamingUpdateEntity(vehicle, entry.runtime_data.scopes) for vehicle in entry.runtime_data.vehicles @@ -62,7 +62,9 @@ class TeslemetryUpdateEntity(TeslemetryRootEntity, UpdateEntity): self.async_write_ha_state() -class TeslemetryPollingUpdateEntity(TeslemetryVehicleEntity, TeslemetryUpdateEntity): +class TeslemetryVehiclePollingUpdateEntity( + TeslemetryVehiclePollingEntity, TeslemetryUpdateEntity +): """Teslemetry Updates entity.""" def __init__( From 9e388f5b1335bda9daff25b188483a3b991339a1 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 14:21:06 +0200 Subject: [PATCH 0093/1175] Fix spelling of "comma-separated (network addresses)" in `nmap_tracker` (#144197) --- homeassistant/components/nmap_tracker/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index 3cbbea007b1..5605ce82ac3 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -23,9 +23,9 @@ "user": { "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP addresses (192.168.1.1), IP networks (192.168.0.0/24) or IP ranges (192.168.1.0-32).", "data": { - "hosts": "Network addresses (comma separated) to scan", + "hosts": "Network addresses (comma-separated) to scan", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "exclude": "Network addresses (comma separated) to exclude from scanning", + "exclude": "Network addresses (comma-separated) to exclude from scanning", "scan_options": "Raw configurable scan options for Nmap" } } From 095318114bc90f19ef00d195586dc90225c0c42d Mon Sep 17 00:00:00 2001 From: Michael Hannon <62535904+mhannon11@users.noreply.github.com> Date: Sun, 4 May 2025 23:58:32 +1000 Subject: [PATCH 0094/1175] Add Zimi Cloud Connect Integration (#129876) * Give entry unique id with MAC, strings.json tweaks * Update codeowners * Add config_flow tests * Update requirements * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Store controller reference in entry.runtime_data instead of hass.data * Add typing * Removed hass data pop on unload. (No longer needed when hass data moved for runtime_data) * Refactor config_flow based on feedback from @zweckj with inline validation, simpler defaults, better description data * Add Michael to codeowners * Remove manual debug override in entity * Populate via_device * remove empty keys from manifest.json * Refactor with DataUpdateCoordinator Device Entities use existing push update method * set via_device to match zcc identifier * Changed logger to use debug level * Define the zimi constants * Move extraaneous code out from try * Move __del__ to async_wil_remove_from_hass * Use zcc device for name * Print debug if mac mismatch Add final exception if api is not ready after connect * Re-work configuration flow: 1. Remove unused CONF_TIMEOUT, CONF_VERBOSITY and CONF_WATCHDOG 2. Move connect() logic out of ZimiCoordinator 3. Add fast connect check during ConfigFlow to check mac matches 4. Use zcc version 3.2.3 with default watchdog time value (and remove this from HA) * Add error detail to mac mismatch * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/const.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/coordinator.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/coordinator.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove coordinator and move setup to __init__ * Set name in _attr_name * Use _light directly for status etc; Remove _state and _brightness; SImplify update() * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * No need to delete device, fix return Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove non-failing items from try Abort duplicate configurations Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Move attr change to notify Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove superflous defalt * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Move aysnc_connect_to_controller to helpers.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Invert if api Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Added ZimiConfigEntry to type runtime_data correctly. Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Use _abort_if_unique_id_configured Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Invert error logic for cleaner flow Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Add ZimiDimmer class * Set colour_mode only in ZimiDimmer * Use device name instead of entity name Update deviceinfo for zcc Update deviceinfo for lights More ZimiDimmer and ZimiLight cleanup * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck * Add missing import for CONNECTION_NETWORK_MAC * @mhannon11 Fixed some minor style changes BUT these tests need re-working now that the config_flow has a second call to the zcc helper to check the API. The tests as written now fail with connect_fail * Remove some code from try * Moved static items from initialiser * Remove superflous assert when unloading entry * refactor - move title out of data * One call to async_add_entities Update ZimiDimmer to initialise color_modes after calling super() * Create ZimiEntity base class (as ToggleEntity) * Updated test of config_flow * Move api_mock parameters to test cases * Much improved tests * Test for input value mismatch and then recovery of flow * Import FlowResultType * Implement Entities event setup correctly * Initial quality_scale.yml * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/manifest.json Co-authored-by: Josef Zweck * Add link to zcc repo * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck * Removed unecessary f-strings * Filled in all of the quality scale * Updated in line with latest documentation improvements * FIx missing import for Entity * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck * Simplify logger and throw * Update homeassistant/components/zimi/helpers.py Co-authored-by: Josef Zweck * Re-factor config_flow with multi-stage steps * Add comments to notify * Don't set hw_version * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck * mark docs-troubleshooting done * Update with zcc-helper version supporting PEP 625 sdist rules on PyPi * Comment re characteristic ID * Pulls in latest zcc that closes UDP listening port correctly after discovery timeout * Re-factored config_flow 1. Try discovery and auto-populate 2. Try manual configuration (with optional values for port and mac) In most cases, auto-discovery does it all. Discovery will only fail if UDP broadcast is not possible to/from zcc. * Do not show error message if discovery fails * Refactor with self.data and async_show_step_finish() * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck * refactor import to use ConfigFlow * Change status for discovery * Add dynamic title to config flow * string * Revert title from form but add IP:port to static title * Automatically finish configuration if possible, if not show form * Use StrEnum instead of Exception class * Remove MAC from user forms * Disconnect api before form completion * Assign to self.mac instead of returning as detail * Updated test suite * Update test status * mark action exemptions todo * Remove mac related error cases from flow completely * Remove unused MAC error strings * Moved error details to logs Removed _error_tuple Removed error details * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * rename check_errors * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * Update zcc-helper and support HA devices via zcc manufacter_info fields * Partial implementation - Use updated zcc-helper to discover multiple controllers * Config_flow with support for auto-discovery of one or more zcc or fallback to manual configuration. * Don't re-connect to api if validate_connection already did * Make fast=False is used for creation * Pull in improved zcc_helper version to address data completeness after machine_info implementation * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck * Import and use ConfigFlowResult * Latest zcc to fix discovers() return value bug * Update config_flow.py * Update homeassistant/components/zimi/manifest.json Co-authored-by: Josef Zweck * Use latest release version of 3.3 (no changes to rc4) * Improved sentence casing * Update strings.json * Update homeassistant/components/zimi/entity.py Co-authored-by: Joost Lekkerkerker * Remove superflous logging Use Zimi network_name as ZCC name Cleanup device info inputs * Remove __del__ * Rename arguments * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Move PLATFORMS to init * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare * Remove debug at init * Update homeassistant/components/zimi/helpers.py Co-authored-by: Martin Hjelmare * Remove _attr_has_entity = False * More naming changes * Revised config_flow to use zcc-helper for validation using new zcc-helper version * Update homeassistant/components/zimi/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/__init__.py Co-authored-by: Martin Hjelmare * Removed commented enum * s/_entity/_device/g * Update homeassistant/components/zimi/entity.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/helpers.py Co-authored-by: Martin Hjelmare * Don't log error when raising exception * Updated tests for new config_flow * Refactor with new zcc that uses Exception classes to pass errors * Updated tests for config_flow to use Exceptions * Device name is based on model * Device name is None Maps better to ZCC concept where devices do not have a name but the individual entities have names. * Fix quality filename * Bump zcc-helper to 3.4 release version * Remove name override * Bump zcc-helper to 3.4.1 with new device_name attribute used to populate devinfo * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare * Add missing transalation picked up by CI * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare * Bump zcc-helper to only classify light and dimmer controlPointType as lights * Bump to non dev version of zcc-helper * Ruff fixes * Add missing data description for pytest * Remove confusing comment * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare * f-strings * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Assert result type, step and errors between each step * test for duplicate entry * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare * Remove duplicate test for discovery failure * Calculate brightness * Don't re-raise Exception in helper * Fix ruff and mypi errors * Add tests for missing connection exceptions * Added standard invalid_host and timeout strings * Explain limitations in discovery. * Update quality_scale.yaml * Update quality_scale.yaml * Removed duplicate strings with reference --------- Co-authored-by: markhannon Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> Co-authored-by: Josef Zweck Co-authored-by: Joost Lekkerkerker Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + homeassistant/components/zimi/__init__.py | 67 ++++ homeassistant/components/zimi/config_flow.py | 172 ++++++++ homeassistant/components/zimi/const.py | 3 + homeassistant/components/zimi/entity.py | 66 ++++ homeassistant/components/zimi/helpers.py | 38 ++ homeassistant/components/zimi/light.py | 103 +++++ homeassistant/components/zimi/manifest.json | 10 + .../components/zimi/quality_scale.yaml | 99 +++++ homeassistant/components/zimi/strings.json | 46 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/zimi/__init__.py | 1 + tests/components/zimi/test_config_flow.py | 371 ++++++++++++++++++ 16 files changed, 991 insertions(+) create mode 100644 homeassistant/components/zimi/__init__.py create mode 100644 homeassistant/components/zimi/config_flow.py create mode 100644 homeassistant/components/zimi/const.py create mode 100644 homeassistant/components/zimi/entity.py create mode 100644 homeassistant/components/zimi/helpers.py create mode 100644 homeassistant/components/zimi/light.py create mode 100644 homeassistant/components/zimi/manifest.json create mode 100644 homeassistant/components/zimi/quality_scale.yaml create mode 100644 homeassistant/components/zimi/strings.json create mode 100644 tests/components/zimi/__init__.py create mode 100644 tests/components/zimi/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 490f97879a4..752bbb31460 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1796,6 +1796,8 @@ build.json @home-assistant/supervisor /tests/components/zeversolar/ @kvanzuijlen /homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES /tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES +/homeassistant/components/zimi/ @markhannon +/tests/components/zimi/ @markhannon /homeassistant/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant /homeassistant/components/zone/ @home-assistant/core diff --git a/homeassistant/components/zimi/__init__.py b/homeassistant/components/zimi/__init__.py new file mode 100644 index 00000000000..db91f7816c4 --- /dev/null +++ b/homeassistant/components/zimi/__init__.py @@ -0,0 +1,67 @@ +"""The zcc integration.""" + +from __future__ import annotations + +import logging + +from zcc import ControlPoint, ControlPointError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC + +from .const import DOMAIN +from .helpers import async_connect_to_controller + +PLATFORMS = [Platform.LIGHT] + +_LOGGER = logging.getLogger(__name__) + + +type ZimiConfigEntry = ConfigEntry[ControlPoint] + + +async def async_setup_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool: + """Connect to Zimi Controller and register device.""" + + try: + api = await async_connect_to_controller( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + ) + + except ControlPointError as error: + raise ConfigEntryNotReady(f"Zimi setup failed: {error}") from error + + _LOGGER.debug("\n%s", api.describe()) + + entry.runtime_data = api + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, api.mac)}, + manufacturer=api.brand, + name=f"{api.network_name}", + model="Zimi Cloud Connect", + sw_version=api.firmware_version, + connections={(CONNECTION_NETWORK_MAC, api.mac)}, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + _LOGGER.debug("Zimi setup complete") + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool: + """Unload a config entry.""" + + api = entry.runtime_data + api.disconnect() + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/zimi/config_flow.py b/homeassistant/components/zimi/config_flow.py new file mode 100644 index 00000000000..1037a05a2ce --- /dev/null +++ b/homeassistant/components/zimi/config_flow.py @@ -0,0 +1,172 @@ +"""Config flow for zcc integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol +from zcc import ( + ControlPoint, + ControlPointCannotConnectError, + ControlPointConnectionRefusedError, + ControlPointDescription, + ControlPointDiscoveryService, + ControlPointError, + ControlPointInvalidHostError, + ControlPointTimeoutError, +) + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +DEFAULT_PORT = 5003 +STEP_MANUAL_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + +SELECTED_HOST_AND_PORT = "selected_host_and_port" + + +class ZimiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for zcc.""" + + api: ControlPoint = None + api_descriptions: list[ControlPointDescription] + data: dict[str, Any] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial auto-discovery step.""" + + self.data = {} + + try: + self.api_descriptions = await ControlPointDiscoveryService().discovers() + except ControlPointError: + # ControlPointError is expected if no zcc are found on LAN + return await self.async_step_manual() + + if len(self.api_descriptions) == 1: + self.data[CONF_HOST] = self.api_descriptions[0].host + self.data[CONF_PORT] = self.api_descriptions[0].port + await self.check_connection(self.data[CONF_HOST], self.data[CONF_PORT]) + return await self.create_entry() + + return await self.async_step_selection() + + async def async_step_selection( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle selection of zcc to configure if multiple are discovered.""" + + errors: dict[str, str] | None = {} + + if user_input is not None: + self.data[CONF_HOST] = user_input[SELECTED_HOST_AND_PORT].split(":")[0] + self.data[CONF_PORT] = int(user_input[SELECTED_HOST_AND_PORT].split(":")[1]) + errors = await self.check_connection( + self.data[CONF_HOST], self.data[CONF_PORT] + ) + if not errors: + return await self.create_entry() + + available_options = [ + SelectOptionDict( + label=f"{description.host}:{description.port}", + value=f"{description.host}:{description.port}", + ) + for description in self.api_descriptions + ] + + available_schema = vol.Schema( + { + vol.Required( + SELECTED_HOST_AND_PORT, default=available_options[0]["value"] + ): SelectSelector( + SelectSelectorConfig( + options=available_options, + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ) + + return self.async_show_form( + step_id="selection", data_schema=available_schema, errors=errors + ) + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle manual configuration step if needed.""" + + errors: dict[str, str] | None = {} + + if user_input is not None: + self.data = {**self.data, **user_input} + + errors = await self.check_connection( + self.data[CONF_HOST], self.data[CONF_PORT] + ) + + if not errors: + return await self.create_entry() + + return self.async_show_form( + step_id="manual", + data_schema=self.add_suggested_values_to_schema( + STEP_MANUAL_DATA_SCHEMA, self.data + ), + errors=errors, + ) + + async def check_connection(self, host: str, port: int) -> dict[str, str] | None: + """Check connection to zcc. + + Stores mac and returns None if successful, otherwise returns error message. + """ + + try: + result = await ControlPointDiscoveryService().validate_connection( + self.data[CONF_HOST], self.data[CONF_PORT] + ) + except ControlPointInvalidHostError: + return {"base": "invalid_host"} + except ControlPointConnectionRefusedError: + return {"base": "connection_refused"} + except ControlPointCannotConnectError: + return {"base": "cannot_connect"} + except ControlPointTimeoutError: + return {"base": "timeout"} + except Exception: + _LOGGER.exception("Unexpected error") + return {"base": "unknown"} + + self.data[CONF_MAC] = format_mac(result.mac) + + return None + + async def create_entry(self) -> ConfigFlowResult: + """Create entry for zcc.""" + + await self.async_set_unique_id(self.data[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"ZIMI Controller ({self.data[CONF_HOST]}:{self.data[CONF_PORT]})", + data=self.data, + ) diff --git a/homeassistant/components/zimi/const.py b/homeassistant/components/zimi/const.py new file mode 100644 index 00000000000..1a426875b75 --- /dev/null +++ b/homeassistant/components/zimi/const.py @@ -0,0 +1,3 @@ +"""Constants for the zcc integration.""" + +DOMAIN = "zimi" diff --git a/homeassistant/components/zimi/entity.py b/homeassistant/components/zimi/entity.py new file mode 100644 index 00000000000..64781454b2c --- /dev/null +++ b/homeassistant/components/zimi/entity.py @@ -0,0 +1,66 @@ +"""Base entity for zimi integrations.""" + +from __future__ import annotations + +import logging + +from zcc import ControlPoint +from zcc.device import ControlPointDevice + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZimiEntity(Entity): + """Representation of a Zimi API entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None: + """Initialize an HA Entity which is a ZimiDevice.""" + + self._device = device + self._attr_unique_id = self._device.identifier + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.manufacture_info.identifier)}, + manufacturer=self._device.manufacture_info.manufacturer, + model=self._device.manufacture_info.model, + name=self._device.manufacture_info.name, + hw_version=device.manufacture_info.hwVersion, + sw_version=device.manufacture_info.firmwareVersion, + suggested_area=device.room, + via_device=(DOMAIN, api.mac), + ) + self._attr_name = self._device.name.strip() + self._attr_suggested_area = self._device.room + + @property + def available(self) -> bool: + """Return True if Home Assistant is able to read the state and control the underlying device.""" + return self._device.is_connected + + async def async_added_to_hass(self) -> None: + """Subscribe to the events.""" + await super().async_added_to_hass() + self._device.subscribe(self) + + async def async_will_remove_from_hass(self) -> None: + """Cleanup ZimiLight with removal of notification prior to removal.""" + self._device.unsubscribe(self) + await super().async_will_remove_from_hass() + + def notify(self, _observable: object) -> None: + """Receive notification from device that state has changed. + + No data is fetched for the notification but schedule_update_ha_state is called. + """ + + _LOGGER.debug( + "Received notification() for %s in %s", self._device.name, self._device.room + ) + self.schedule_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/zimi/helpers.py b/homeassistant/components/zimi/helpers.py new file mode 100644 index 00000000000..81d9a986f46 --- /dev/null +++ b/homeassistant/components/zimi/helpers.py @@ -0,0 +1,38 @@ +"""The zcc integration helpers.""" + +from __future__ import annotations + +import logging + +from zcc import ControlPoint, ControlPointDescription + +from homeassistant.exceptions import ConfigEntryNotReady + +_LOGGER = logging.getLogger(__name__) + + +async def async_connect_to_controller( + host: str, port: int, fast: bool = False +) -> ControlPoint: + """Connect to Zimi Cloud Controller with defined parameters.""" + + _LOGGER.debug("Connecting to %s:%d", host, port) + + api = ControlPoint( + description=ControlPointDescription( + host=host, + port=port, + ) + ) + await api.connect(fast=fast) + + if api.ready: + _LOGGER.debug("Connected") + + if not fast: + api.start_watchdog() + _LOGGER.debug("Started watchdog") + + return api + + raise ConfigEntryNotReady("Connection failed: not ready") diff --git a/homeassistant/components/zimi/light.py b/homeassistant/components/zimi/light.py new file mode 100644 index 00000000000..a93bbb53b3d --- /dev/null +++ b/homeassistant/components/zimi/light.py @@ -0,0 +1,103 @@ +"""Light platform for zcc integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from zcc import ControlPoint +from zcc.device import ControlPointDevice + +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ZimiConfigEntry +from .entity import ZimiEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ZimiConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Zimi Light platform.""" + + api = config_entry.runtime_data + + lights: list[ZimiLight | ZimiDimmer] = [ + ZimiLight(device, api) for device in api.lights if device.type != "dimmer" + ] + + lights.extend( + [ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"] + ) + + async_add_entities(lights) + + +class ZimiLight(ZimiEntity, LightEntity): + """Representation of a Zimi Light.""" + + def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None: + """Initialize a ZimiLight.""" + + super().__init__(device, api) + + self._attr_color_mode = ColorMode.ONOFF + self._attr_supported_color_modes = {ColorMode.ONOFF} + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on (with optional brightness).""" + + _LOGGER.debug( + "Sending turn_on() for %s in %s", self._device.name, self._device.room + ) + + await self._device.turn_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + + _LOGGER.debug( + "Sending turn_off() for %s in %s", self._device.name, self._device.room + ) + + await self._device.turn_off() + + +class ZimiDimmer(ZimiLight): + """Zimi Light supporting dimming.""" + + def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None: + """Initialize a ZimiDimmer.""" + super().__init__(device, api) + self._attr_color_mode = ColorMode.BRIGHTNESS + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + if self._device.type != "dimmer": + raise ValueError("ZimiDimmer needs a dimmable light") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on (with optional brightness).""" + + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) * 100 / 255 + _LOGGER.debug( + "Sending turn_on(brightness=%d) for %s in %s", + brightness, + self._device.name, + self._device.room, + ) + + await self._device.set_brightness(brightness) + + @property + def brightness(self) -> int | None: + """Return the brightness of the light.""" + return round(self._device.brightness * 255 / 100) diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json new file mode 100644 index 00000000000..d0dd3e09e06 --- /dev/null +++ b/homeassistant/components/zimi/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "zimi", + "name": "zimi", + "codeowners": ["@markhannon"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zimi", + "iot_class": "local_push", + "quality_scale": "bronze", + "requirements": ["zcc-helper==3.5"] +} diff --git a/homeassistant/components/zimi/quality_scale.yaml b/homeassistant/components/zimi/quality_scale.yaml new file mode 100644 index 00000000000..98e6c5b627c --- /dev/null +++ b/homeassistant/components/zimi/quality_scale.yaml @@ -0,0 +1,99 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + There are no service actions. + appropriate-polling: + status: done + comment: | + There is no polling of the entities. + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: + status: done + comment: | + https://mark_hannon@bitbucket.org/mark_hannon/zcc.git + docs-actions: + status: exempt + comment: | + There are no service actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + config-entry-unloading: done + log-when-unavailable: todo + entity-unavailable: done + action-exceptions: todo + reauthentication-flow: + status: exempt + comment: | + There is no user authentication needed. + parallel-updates: + status: todo + comment: | + Test of parallel updates will be done before setting. + test-coverage: todo + integration-owner: done + docs-installation-parameters: done + docs-configuration-parameters: + status: exempt + comment: | + This integration has no options flow + + # Gold + entity-translations: todo + entity-device-class: + status: todo + comment: | + Will set device classes for subsequent entities - not relevant for light. + devices: done + entity-category: todo + entity-disabled-by-default: todo + discovery: + status: todo + comment: > + Discovery is supported for the case where the Zimi Cloud Controller(s) are + connected to a local LAN network. Discover is not supported if the Zimi + Cloud Controller(s) are not connected to the local LAN network. + stale-devices: todo + diagnostics: todo + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + dynamic-devices: + status: todo + comment: | + New devices will be automatically added - but only when the zcc connection is re-established. + discovery-update-info: + status: todo + comment: > + Discovery is not supported. + repair-issues: todo + docs-use-cases: todo + docs-supported-devices: done + docs-supported-functions: todo + docs-troubleshooting: done + docs-data-update: done + docs-known-limitations: done + docs-examples: todo + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + This integration does not use web sessions. + strict-typing: + status: todo diff --git a/homeassistant/components/zimi/strings.json b/homeassistant/components/zimi/strings.json new file mode 100644 index 00000000000..530eb86ef05 --- /dev/null +++ b/homeassistant/components/zimi/strings.json @@ -0,0 +1,46 @@ +{ + "config": { + "step": { + "user": { + "title": "Zimi - Discover device(s)", + "description": "Discover and auto-configure Zimi Cloud Connect device." + }, + "selection": { + "title": "Zimi - Select device", + "description": "Select Zimi Cloud Connect device to configure.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "selected_host_and_port": "Selected ZCC" + }, + "data_description": { + "host": "Mandatory - ZCC IP address.", + "port": "Mandatory - ZCC port number (default=5003).", + "selected_host_and_port": "Selected ZCC IP address and port number" + } + }, + "manual": { + "title": "Zimi - Configure device", + "description": "Enter details of your Zimi Cloud Connect device.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "data_description": { + "host": "[%key:component::zimi::config::step::selection::data_description::host%]", + "port": "[%key:component::zimi::config::step::selection::data_description::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "connection_refused": "Connection refused" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8174dfc60b1..680d0a7bb2c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -736,6 +736,7 @@ FLOWS = { "zerproc", "zeversolar", "zha", + "zimi", "zodiac", "zwave_js", "zwave_me", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5e97e4c6626..1b9e9216827 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -7630,6 +7630,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "zimi": { + "name": "zimi", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "zodiac": { "integration_type": "hub", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 9d116efa284..8f902317db7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3155,6 +3155,9 @@ zabbix-utils==2.0.2 # homeassistant.components.zamg zamg==0.3.6 +# homeassistant.components.zimi +zcc-helper==3.5 + # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14067bfd17..79c1bcc79f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2554,6 +2554,9 @@ yt-dlp[default]==2025.03.31 # homeassistant.components.zamg zamg==0.3.6 +# homeassistant.components.zimi +zcc-helper==3.5 + # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/tests/components/zimi/__init__.py b/tests/components/zimi/__init__.py new file mode 100644 index 00000000000..0e95ffc9c33 --- /dev/null +++ b/tests/components/zimi/__init__.py @@ -0,0 +1 @@ +"""Tests for the zimi component.""" diff --git a/tests/components/zimi/test_config_flow.py b/tests/components/zimi/test_config_flow.py new file mode 100644 index 00000000000..9ec0c624b6f --- /dev/null +++ b/tests/components/zimi/test_config_flow.py @@ -0,0 +1,371 @@ +"""Tests for the zimi config flow.""" + +from unittest.mock import MagicMock, patch + +import pytest +from zcc import ( + ControlPointCannotConnectError, + ControlPointConnectionRefusedError, + ControlPointDescription, + ControlPointError, + ControlPointInvalidHostError, + ControlPointTimeoutError, +) + +from homeassistant import config_entries +from homeassistant.components.zimi.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.device_registry import format_mac + +from tests.common import MockConfigEntry + +INPUT_MAC = "aa:bb:cc:dd:ee:ff" +INPUT_MAC_EXTRA = "aa:bb:cc:dd:ee:ee" +INPUT_HOST = "192.168.1.100" +INPUT_HOST_EXTRA = "192.168.1.101" +INPUT_PORT = 5003 +INPUT_PORT_EXTRA = 5004 + +INVALID_INPUT_MAC = "xyz" +MISMATCHED_INPUT_MAC = "aa:bb:cc:dd:ee:ee" +SELECTED_HOST_AND_PORT = "selected_host_and_port" + + +@pytest.fixture +def discovery_mock(): + """Mock the ControlPointDiscoveryService.""" + with patch( + "homeassistant.components.zimi.config_flow.ControlPointDiscoveryService", + autospec=True, + ) as mock: + mock.return_value = mock + yield mock + + +async def test_user_discovery_success( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test user form transitions to creation if zcc discovery succeeds.""" + + discovery_mock.discovers.return_value = [ + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT) + ] + + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "host": INPUT_HOST, + "port": INPUT_PORT, + "mac": format_mac(INPUT_MAC), + } + + +async def test_user_discovery_success_selection( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test user form transitions via selection to creation if zcc discovery succeeds has multiple hosts.""" + + discovery_mock.discovers.return_value = [ + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT), + ControlPointDescription(host=INPUT_HOST_EXTRA, port=INPUT_PORT_EXTRA), + ] + + 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"] == "selection" + assert result["errors"] == {} + + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription( + host=INPUT_HOST_EXTRA, port=INPUT_PORT_EXTRA, mac=INPUT_MAC_EXTRA + ) + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + SELECTED_HOST_AND_PORT: f"{INPUT_HOST_EXTRA}:{INPUT_PORT_EXTRA!s}", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + "host": INPUT_HOST_EXTRA, + "port": INPUT_PORT_EXTRA, + "mac": format_mac(INPUT_MAC_EXTRA), + } + + +async def test_user_discovery_duplicates( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test that flow is aborted if duplicates are added.""" + + MockConfigEntry( + domain=DOMAIN, + unique_id=INPUT_MAC, + data={ + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + "mac": format_mac(INPUT_MAC), + }, + ).add_to_hass(hass) + + discovery_mock.discovers.return_value = [ + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT) + ] + + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_finish_manual_success( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test manual form transitions to creation with valid data.""" + + discovery_mock.discovers.side_effect = ControlPointError("Discovery failed") + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + 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"] == "manual" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})" + assert result["data"] == { + "host": INPUT_HOST, + "port": INPUT_PORT, + "mac": format_mac(INPUT_MAC), + } + + +async def test_manual_cannot_connect( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test manual form transitions via cannot_connect to creation.""" + + discovery_mock.discovers.side_effect = ControlPointError("Discovery failed") + + 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"] == "manual" + assert result["errors"] == {} + + # First attempt fails with CANNOT_CONNECT when attempting to connect + discovery_mock.return_value.validate_connection.side_effect = ( + ControlPointCannotConnectError + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] == {"base": "cannot_connect"} + + # Second attempt succeeds + discovery_mock.return_value.validate_connection.side_effect = None + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})" + assert result["data"] == { + "host": INPUT_HOST, + "port": INPUT_PORT, + "mac": format_mac(INPUT_MAC), + } + + +async def test_manual_gethostbyname_error( + hass: HomeAssistant, + discovery_mock: MagicMock, +) -> None: + """Test manual form transitions via gethostbyname failure to creation.""" + + discovery_mock.discovers.side_effect = ControlPointError("Discovery failed") + + 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"] == "manual" + assert result["errors"] == {} + + # First attempt fails with name lookup failure when attempting to connect + discovery_mock.return_value.validate_connection.side_effect = ( + ControlPointInvalidHostError + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] + assert result["errors"] == {"base": "invalid_host"} + + # Second attempt succeeds + discovery_mock.return_value.validate_connection.side_effect = None + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})" + assert result["data"] == { + "host": INPUT_HOST, + "port": INPUT_PORT, + "mac": format_mac(INPUT_MAC), + } + + +@pytest.mark.parametrize( + ("side_effect", "error_expected"), + [ + ( + ControlPointInvalidHostError, + {"base": "invalid_host"}, + ), + ( + ControlPointConnectionRefusedError, + {"base": "connection_refused"}, + ), + ( + ControlPointCannotConnectError, + {"base": "cannot_connect"}, + ), + ( + ControlPointTimeoutError, + {"base": "timeout"}, + ), + ( + Exception, + {"base": "unknown"}, + ), + ], +) +async def test_manual_connection_errors( + hass: HomeAssistant, + discovery_mock: MagicMock, + side_effect: Exception, + error_expected: dict, +) -> None: + """Test manual form connection errors.""" + + discovery_mock.discovers.side_effect = ControlPointError("Discovery failed") + + 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"] == "manual" + assert result["errors"] == {} + + # First attempt fails with connection errors + discovery_mock.return_value.validate_connection.side_effect = side_effect + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] == error_expected + + # Second attempt succeeds + discovery_mock.return_value.validate_connection.side_effect = None + discovery_mock.return_value.validate_connection.return_value = ( + ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC) + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: INPUT_HOST, + CONF_PORT: INPUT_PORT, + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})" + assert result["data"] == { + "host": INPUT_HOST, + "port": INPUT_PORT, + "mac": format_mac(INPUT_MAC), + } From 2c368c79d124c061dda84b3dae39cf722f71396d Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sun, 4 May 2025 16:41:44 +0200 Subject: [PATCH 0095/1175] Update `denonavr` to `1.1.0` (#144199) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 328ab504bd1..3cf2e5b5bda 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.0.1"], + "requirements": ["denonavr==1.1.0"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index 8f902317db7..e212af09a19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -776,7 +776,7 @@ deluge-client==1.10.2 demetriek==1.2.0 # homeassistant.components.denonavr -denonavr==1.0.1 +denonavr==1.1.0 # homeassistant.components.devialet devialet==1.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79c1bcc79f2..e1d54e6f30b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ deluge-client==1.10.2 demetriek==1.2.0 # homeassistant.components.denonavr -denonavr==1.0.1 +denonavr==1.1.0 # homeassistant.components.devialet devialet==1.5.7 From 11993532041d277411425b65cb3ae93a3fcc9023 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 17:55:58 +0200 Subject: [PATCH 0096/1175] Fix sentence-casing of "Phone number" in `peco` (#144208) --- homeassistant/components/peco/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/peco/strings.json b/homeassistant/components/peco/strings.json index cdf5bb497db..c4683056dd7 100644 --- a/homeassistant/components/peco/strings.json +++ b/homeassistant/components/peco/strings.json @@ -4,7 +4,7 @@ "user": { "data": { "county": "County", - "phone_number": "Phone Number" + "phone_number": "Phone number" }, "data_description": { "county": "County used for outage number retrieval", From 490bb46a8224487fc55214774dba5038c33e1f05 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 17:56:25 +0200 Subject: [PATCH 0097/1175] Make spelling of "Auto-charge" switch consistent in TechnoVE (#144206) * Make spelling of "Auto-charge" switch consistent in TechnoVE Also fix sentence-casing in "Charging enabled" switch. * Update test_switch.ambr --- homeassistant/components/technove/strings.json | 4 ++-- tests/components/technove/snapshots/test_switch.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/technove/strings.json b/homeassistant/components/technove/strings.json index 05260845a03..29aba780f26 100644 --- a/homeassistant/components/technove/strings.json +++ b/homeassistant/components/technove/strings.json @@ -76,10 +76,10 @@ }, "switch": { "auto_charge": { - "name": "Auto charge" + "name": "Auto-charge" }, "session_active": { - "name": "Charging Enabled" + "name": "Charging enabled" } } }, diff --git a/tests/components/technove/snapshots/test_switch.ambr b/tests/components/technove/snapshots/test_switch.ambr index a5f8411747b..0e93143ffed 100644 --- a/tests/components/technove/snapshots/test_switch.ambr +++ b/tests/components/technove/snapshots/test_switch.ambr @@ -24,7 +24,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Auto charge', + 'original_name': 'Auto-charge', 'platform': 'technove', 'previous_unique_id': None, 'supported_features': 0, @@ -36,7 +36,7 @@ # name: test_switches[switch.technove_station_auto_charge-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TechnoVE Station Auto charge', + 'friendly_name': 'TechnoVE Station Auto-charge', }), 'context': , 'entity_id': 'switch.technove_station_auto_charge', @@ -71,7 +71,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Charging Enabled', + 'original_name': 'Charging enabled', 'platform': 'technove', 'previous_unique_id': None, 'supported_features': 0, @@ -83,7 +83,7 @@ # name: test_switches[switch.technove_station_charging_enabled-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TechnoVE Station Charging Enabled', + 'friendly_name': 'TechnoVE Station Charging enabled', }), 'context': , 'entity_id': 'switch.technove_station_charging_enabled', From 8048d2bfb846c10cac308aa438fc8bd906484a8d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 4 May 2025 12:00:40 -0400 Subject: [PATCH 0098/1175] Fix intent TurnOn creating stack trace for buttons (#144205) --- homeassistant/components/intent/__init__.py | 28 +++- tests/components/intent/test_init.py | 135 ++++++++++++++++---- 2 files changed, 140 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index dfbe8d0135c..72853276ab3 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -10,6 +10,11 @@ from aiohttp import web import voluptuous as vol from homeassistant.components import http, sensor +from homeassistant.components.button import ( + DOMAIN as BUTTON_DOMAIN, + SERVICE_PRESS as SERVICE_PRESS_BUTTON, + ButtonDeviceClass, +) from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import ( ATTR_POSITION, @@ -20,6 +25,7 @@ from homeassistant.components.cover import ( CoverDeviceClass, ) from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, @@ -80,6 +86,7 @@ __all__ = [ ] ONOFF_DEVICE_CLASSES = { + ButtonDeviceClass, CoverDeviceClass, ValveDeviceClass, SwitchDeviceClass, @@ -103,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: intent.INTENT_TURN_ON, HOMEASSISTANT_DOMAIN, SERVICE_TURN_ON, - description="Turns on/opens a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.", + description="Turns on/opens/presses a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.", device_classes=ONOFF_DEVICE_CLASSES, ), ) @@ -168,6 +175,25 @@ class OnOffIntentHandler(intent.ServiceIntentHandler): """Call service on entity with handling for special cases.""" hass = intent_obj.hass + if state.domain in (BUTTON_DOMAIN, INPUT_BUTTON_DOMAIN): + if service != SERVICE_TURN_ON: + raise intent.IntentHandleError( + f"Entity {state.entity_id} cannot be turned off" + ) + + await self._run_then_background( + hass.async_create_task( + hass.services.async_call( + state.domain, + SERVICE_PRESS_BUTTON, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + blocking=True, + ) + ) + ) + return + if state.domain == COVER_DOMAIN: # on = open # off = close diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 0db9682d0ad..3779930e360 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -2,8 +2,10 @@ import pytest -from homeassistant.components.cover import SERVICE_OPEN_COVER -from homeassistant.components.lock import SERVICE_LOCK +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER +from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, @@ -121,41 +123,130 @@ async def test_turn_on_intent(hass: HomeAssistant) -> None: assert call.data == {"entity_id": ["light.test_light"]} -async def test_translated_turn_on_intent( +@pytest.mark.parametrize("domain", ["button", "input_button"]) +async def test_turn_on_intent_button( + hass: HomeAssistant, entity_registry: er.EntityRegistry, domain +) -> None: + """Test HassTurnOn intent on button domains.""" + assert await async_setup_component(hass, "intent", {}) + + button = entity_registry.async_get_or_create(domain, "test", "button_uid") + + hass.states.async_set(button.entity_id, "unknown") + button_service_calls = async_mock_service(hass, domain, SERVICE_PRESS) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": button.entity_id}} + ) + + await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": button.entity_id}} + ) + + assert len(button_service_calls) == 1 + call = button_service_calls[0] + assert call.domain == domain + assert call.service == SERVICE_PRESS + assert call.data == {"entity_id": button.entity_id} + + +async def test_turn_on_off_intent_valve( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: - """Test HassTurnOn intent on domains which don't have the intent.""" - result = await async_setup_component(hass, "homeassistant", {}) - result = await async_setup_component(hass, "intent", {}) - await hass.async_block_till_done() - assert result + """Test HassTurnOn/Off intent on valve domains.""" + assert await async_setup_component(hass, "intent", {}) + + valve = entity_registry.async_get_or_create("valve", "test", "valve_uid") + + hass.states.async_set(valve.entity_id, "closed") + open_calls = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE) + close_calls = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE) + + await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": valve.entity_id}} + ) + + assert len(open_calls) == 1 + call = open_calls[0] + assert call.domain == "valve" + assert call.service == SERVICE_OPEN_VALVE + assert call.data == {"entity_id": valve.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": valve.entity_id}} + ) + + assert len(close_calls) == 1 + call = close_calls[0] + assert call.domain == "valve" + assert call.service == SERVICE_CLOSE_VALVE + assert call.data == {"entity_id": valve.entity_id} + + +async def test_turn_on_off_intent_cover( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test HassTurnOn/Off intent on cover domains.""" + assert await async_setup_component(hass, "intent", {}) cover = entity_registry.async_get_or_create("cover", "test", "cover_uid") - lock = entity_registry.async_get_or_create("lock", "test", "lock_uid") hass.states.async_set(cover.entity_id, "closed") - hass.states.async_set(lock.entity_id, "unlocked") - cover_service_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - lock_service_calls = async_mock_service(hass, "lock", SERVICE_LOCK) + open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": cover.entity_id}} ) + + assert len(open_calls) == 1 + call = open_calls[0] + assert call.domain == "cover" + assert call.service == SERVICE_OPEN_COVER + assert call.data == {"entity_id": cover.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": cover.entity_id}} + ) + + assert len(close_calls) == 1 + call = close_calls[0] + assert call.domain == "cover" + assert call.service == SERVICE_CLOSE_COVER + assert call.data == {"entity_id": cover.entity_id} + + +async def test_turn_on_off_intent_lock( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test HassTurnOn/Off intent on lock domains.""" + assert await async_setup_component(hass, "intent", {}) + + lock = entity_registry.async_get_or_create("lock", "test", "lock_uid") + + hass.states.async_set(lock.entity_id, "locked") + unlock_calls = async_mock_service(hass, "lock", SERVICE_UNLOCK) + lock_calls = async_mock_service(hass, "lock", SERVICE_LOCK) + await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": lock.entity_id}} ) - await hass.async_block_till_done() - assert len(cover_service_calls) == 1 - call = cover_service_calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": cover.entity_id} - - assert len(lock_service_calls) == 1 - call = lock_service_calls[0] + assert len(lock_calls) == 1 + call = lock_calls[0] assert call.domain == "lock" - assert call.service == "lock" + assert call.service == SERVICE_LOCK + assert call.data == {"entity_id": lock.entity_id} + + await intent.async_handle( + hass, "test", "HassTurnOff", {"name": {"value": lock.entity_id}} + ) + + assert len(unlock_calls) == 1 + call = unlock_calls[0] + assert call.domain == "lock" + assert call.service == SERVICE_UNLOCK assert call.data == {"entity_id": lock.entity_id} From 2960271b8155a08fba1990ec55cd2d143efa58db Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 4 May 2025 12:15:01 -0400 Subject: [PATCH 0099/1175] bump aiokem to 0.5.10 (#144203) --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 0c9f0c20e6f..6b2f6190883 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.9"] + "requirements": ["aiokem==0.5.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index e212af09a19..448c24d952c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.9 +aiokem==0.5.10 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1d54e6f30b..bcbbb58d94f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimaplib==2.0.1 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.9 +aiokem==0.5.10 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 9cd2080de2d9090a0a3bf3574a1197961df3c13e Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sun, 4 May 2025 12:41:39 -0400 Subject: [PATCH 0100/1175] Avoid delaying HA startup in Rehlko (#144202) --- homeassistant/components/rehlko/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index 19702527259..49ceb8ac870 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -40,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo ) rehlko.set_refresh_token_callback(async_refresh_token_update) - rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) try: await rehlko.authenticate( @@ -48,6 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo entry.data[CONF_PASSWORD], entry.data.get(CONF_REFRESH_TOKEN), ) + homes = await rehlko.get_homes() except AuthenticationError as ex: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, @@ -60,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo translation_key="cannot_connect", ) from ex coordinators: dict[int, RehlkoUpdateCoordinator] = {} - homes = await rehlko.get_homes() entry.runtime_data = RehlkoRuntimeData( coordinators=coordinators, @@ -86,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo await coordinator.async_config_entry_first_refresh() coordinators[device_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Retrys enabled after successful connection to prevent blocking startup + rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) return True From 429682cecd561e0ce4c95fd2779534edca517709 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 May 2025 11:42:07 -0500 Subject: [PATCH 0101/1175] Remove unnecessary intermediate functions in `entry_data` for ESPHome (#144173) --- .../components/esphome/entry_data.py | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 023c6f70da4..1e6375d8caf 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -8,6 +8,7 @@ from collections.abc import Callable, Iterable from dataclasses import dataclass, field from functools import partial import logging +from operator import delitem from typing import TYPE_CHECKING, Any, Final, TypedDict, cast from aioesphomeapi import ( @@ -183,18 +184,7 @@ class RuntimeEntryData: """Register to receive callbacks when static info changes for an EntityInfo type.""" callbacks = self.entity_info_callbacks.setdefault(entity_info_type, []) callbacks.append(callback_) - return partial( - self._async_unsubscribe_register_static_info, callbacks, callback_ - ) - - @callback - def _async_unsubscribe_register_static_info( - self, - callbacks: list[Callable[[list[EntityInfo]], None]], - callback_: Callable[[list[EntityInfo]], None], - ) -> None: - """Unsubscribe to when static info is registered.""" - callbacks.remove(callback_) + return partial(callbacks.remove, callback_) @callback def async_register_key_static_info_updated_callback( @@ -206,18 +196,7 @@ class RuntimeEntryData: callback_key = (type(static_info), static_info.key) callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, []) callbacks.append(callback_) - return partial( - self._async_unsubscribe_static_key_info_updated, callbacks, callback_ - ) - - @callback - def _async_unsubscribe_static_key_info_updated( - self, - callbacks: list[Callable[[EntityInfo], None]], - callback_: Callable[[EntityInfo], None], - ) -> None: - """Unsubscribe to when static info is updated .""" - callbacks.remove(callback_) + return partial(callbacks.remove, callback_) @callback def async_set_assist_pipeline_state(self, state: bool) -> None: @@ -232,14 +211,7 @@ class RuntimeEntryData: ) -> CALLBACK_TYPE: """Subscribe to assist pipeline updates.""" self.assist_pipeline_update_callbacks.append(update_callback) - return partial(self._async_unsubscribe_assist_pipeline_update, update_callback) - - @callback - def _async_unsubscribe_assist_pipeline_update( - self, update_callback: CALLBACK_TYPE - ) -> None: - """Unsubscribe to assist pipeline updates.""" - self.assist_pipeline_update_callbacks.remove(update_callback) + return partial(self.assist_pipeline_update_callbacks.remove, update_callback) @callback def async_remove_entities( @@ -337,12 +309,7 @@ class RuntimeEntryData: def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE: """Subscribe to state updates.""" self.device_update_subscriptions.add(callback_) - return partial(self._async_unsubscribe_device_update, callback_) - - @callback - def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None: - """Unsubscribe to device updates.""" - self.device_update_subscriptions.remove(callback_) + return partial(self.device_update_subscriptions.remove, callback_) @callback def async_subscribe_static_info_updated( @@ -350,14 +317,7 @@ class RuntimeEntryData: ) -> 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_) + return partial(self.static_info_update_subscriptions.remove, callback_) @callback def async_subscribe_state_update( @@ -369,14 +329,7 @@ class RuntimeEntryData: """Subscribe to state updates.""" subscription_key = (state_type, state_key) self.state_subscriptions[subscription_key] = entity_callback - return partial(self._async_unsubscribe_state_update, subscription_key) - - @callback - def _async_unsubscribe_state_update( - self, subscription_key: tuple[type[EntityState], int] - ) -> None: - """Unsubscribe to state updates.""" - self.state_subscriptions.pop(subscription_key) + return partial(delitem, self.state_subscriptions, subscription_key) @callback def async_update_state(self, state: EntityState) -> None: @@ -523,7 +476,7 @@ class RuntimeEntryData: ) -> CALLBACK_TYPE: """Register to receive callbacks when the Assist satellite's configuration is updated.""" self.assist_satellite_config_update_callbacks.append(callback_) - return lambda: self.assist_satellite_config_update_callbacks.remove(callback_) + return partial(self.assist_satellite_config_update_callbacks.remove, callback_) @callback def async_assist_satellite_config_updated( @@ -540,7 +493,7 @@ class RuntimeEntryData: ) -> CALLBACK_TYPE: """Register to receive callbacks when the Assist satellite's wake word is set.""" self.assist_satellite_set_wake_word_callbacks.append(callback_) - return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_) + return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_) @callback def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None: From 8e202bc202898efd316548d0c3b4169a891fb697 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 19:13:53 +0200 Subject: [PATCH 0102/1175] Improve the user-facing strings of `heos` (#144218) --- homeassistant/components/heos/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index c99d73a70d7..76b71f70e28 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -56,8 +56,8 @@ "options": { "step": { "init": { - "title": "HEOS Options", - "description": "You can sign-in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign-out of your account.", + "title": "HEOS options", + "description": "You can sign in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign out of your account.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" @@ -102,7 +102,7 @@ }, "move_queue_item": { "name": "Move queue item", - "description": "Move one or more items within the play queue.", + "description": "Moves one or more items within the play queue.", "fields": { "queue_ids": { "name": "Queue IDs", From eca811d0d476a74ea23fa0b6d900805d9a0bbf50 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 19:35:59 +0200 Subject: [PATCH 0103/1175] Fix sentence-casing in user-facing strings of `tami4` (#144212) Fix sentence-casing in user-facing string of `tami4` --- homeassistant/components/tami4/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tami4/strings.json b/homeassistant/components/tami4/strings.json index 040c18fc56d..b89ccbe8bd9 100644 --- a/homeassistant/components/tami4/strings.json +++ b/homeassistant/components/tami4/strings.json @@ -29,17 +29,17 @@ "config": { "step": { "user": { - "title": "SMS Verification", + "title": "SMS verification", "description": "Enter your phone number (same as what you used to register to the tami4 app)", "data": { - "phone": "Phone Number" + "phone": "Phone number" } }, "otp": { "title": "[%key:component::tami4::config::step::user::title%]", "description": "Enter the code you received via SMS", "data": { - "otp": "SMS Code" + "otp": "SMS code" } } }, From 2e7b60c3cae87e9a82597f5c838f6ce757e9fb64 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 19:36:24 +0200 Subject: [PATCH 0104/1175] Fix spelling of "sign in" and "setup" in `verisure` (#144214) - use "sign in" for the verb - use "setup" for the noun - fix sentence-casing of "Verification code" --- homeassistant/components/verisure/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 051f17262a0..6241225ed4e 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "description": "Sign-in with your Verisure My Pages account.", + "description": "Sign in with your Verisure My Pages account.", "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } @@ -11,7 +11,7 @@ "mfa": { "data": { "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", - "code": "Verification Code" + "code": "Verification code" } }, "installation": { @@ -37,7 +37,7 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "unknown_mfa": "Unknown error occurred during MFA set up" + "unknown_mfa": "Unknown error occurred during MFA setup" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", From c2a69bcb20298820cf040d4156f341b310efa197 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 19:36:57 +0200 Subject: [PATCH 0105/1175] Improve user-facing strings of `blink` (#144219) - treat "sign in" as verb for consistency, removing the hyphen - fix sentence-casing of two strings --- homeassistant/components/blink/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/strings.json b/homeassistant/components/blink/strings.json index 74f8ae1cb28..8f8df125aab 100644 --- a/homeassistant/components/blink/strings.json +++ b/homeassistant/components/blink/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Sign-in with Blink account", + "title": "Sign in with Blink account", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" @@ -30,7 +30,7 @@ "step": { "simple_options": { "data": { - "scan_interval": "Scan Interval (seconds)" + "scan_interval": "Scan interval (seconds)" }, "title": "Blink options", "description": "Configure Blink integration" @@ -93,7 +93,7 @@ }, "config_entry_id": { "name": "Integration ID", - "description": "The Blink Integration ID." + "description": "The Blink integration ID." } } } From 9b30f32cad144e10f023d5d962b44d5a29bad251 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 20:12:01 +0200 Subject: [PATCH 0106/1175] =?UTF-8?q?Replace=20"Sign-in=20=E2=80=A6"=20wit?= =?UTF-8?q?h=20"Sign=20in=20=E2=80=A6"=20in=20`ring`=20(#144222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config flow titles in Home Assistant use verbs by default ("Set up …" / "Configure …" / "Select …" etc. Therefore "Sign-in …" (noun) for the initial setup in `ring` is replaced with "Sign in …" (verb). --- homeassistant/components/ring/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index 2d7e0b17da1..d1a3deafa71 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Sign-in with Ring account", + "title": "Sign in with Ring account", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" From 8eaddbf2b237db9c330c2ff42d6ee7b5e92a0e47 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 4 May 2025 20:56:33 +0200 Subject: [PATCH 0107/1175] Replace "log-in" with "log in" in `zwave_me` (#144223) --- homeassistant/components/zwave_me/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/strings.json b/homeassistant/components/zwave_me/strings.json index 0c5a1d30976..9bc0d2b8ab7 100644 --- a/homeassistant/components/zwave_me/strings.json +++ b/homeassistant/components/zwave_me/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Input IP address with port and access token of Z-Way server. To get the token go to the Z-Way user interface Smart Home UI > Menu > Settings > Users > Administrator > API token.\n\nExample of connecting to Z-Way running as an add-on:\nURL: {add_on_url}\nToken: {local_token}\n\nExample of connecting to Z-Way in the local network:\nURL: {local_url}\nToken: {local_token}\n\nExample of connecting to Z-Way via remote access find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nExample of connecting to Z-Way with a static public IP address:\nURL: {remote_url}\nToken: {local_token}\n\nWhen connecting via find.z-wave.me you need to use a token with a global scope (log-in to Z-Way via find.z-wave.me for this).", + "description": "Input IP address with port and access token of Z-Way server. To get the token go to the Z-Way user interface Smart Home UI > Menu > Settings > Users > Administrator > API token.\n\nExample of connecting to Z-Way running as an add-on:\nURL: {add_on_url}\nToken: {local_token}\n\nExample of connecting to Z-Way in the local network:\nURL: {local_url}\nToken: {local_token}\n\nExample of connecting to Z-Way via remote access find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nExample of connecting to Z-Way with a static public IP address:\nURL: {remote_url}\nToken: {local_token}\n\nWhen connecting via find.z-wave.me you need to use a token with a global scope (log in to Z-Way via find.z-wave.me for this).", "data": { "url": "[%key:common::config_flow::data::url%]", "token": "[%key:common::config_flow::data::api_token%]" From cad2d72ed98f8ae145fc13ac056420c6cdca3cd4 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sun, 4 May 2025 19:59:49 -0400 Subject: [PATCH 0108/1175] Bump python-roborock to 2.18.2 (#144235) --- 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 531590d5d6e..784d2c6ad27 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -19,7 +19,7 @@ "loggers": ["roborock"], "quality_scale": "silver", "requirements": [ - "python-roborock==2.16.1", + "python-roborock==2.18.2", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 448c24d952c..74b9dc12407 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.16.1 +python-roborock==2.18.2 # homeassistant.components.smarttub python-smarttub==0.0.39 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcbbb58d94f..c21f56d0f68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2017,7 +2017,7 @@ python-picnic-api2==1.2.4 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.16.1 +python-roborock==2.18.2 # homeassistant.components.smarttub python-smarttub==0.0.39 From e0916fdd26ad97db0a0978627f81318814b9e230 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Sun, 4 May 2025 23:02:32 -0400 Subject: [PATCH 0109/1175] Change roborock to use home_data_v3 (#144238) --- homeassistant/components/roborock/__init__.py | 2 +- tests/components/roborock/conftest.py | 4 ++-- tests/components/roborock/test_init.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/roborock/__init__.py b/homeassistant/components/roborock/__init__.py index 81b412c6770..6697779adf6 100644 --- a/homeassistant/components/roborock/__init__.py +++ b/homeassistant/components/roborock/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) -> ) _LOGGER.debug("Getting home data") try: - home_data = await api_client.get_home_data_v2(user_data) + home_data = await api_client.get_home_data_v3(user_data) except RoborockInvalidCredentials as err: raise ConfigEntryAuthFailed( "Invalid credentials", diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d807e35710b..f95e4795d1d 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -72,7 +72,7 @@ def bypass_api_client_fixture() -> None: """Skip calls to the API client.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", return_value=HOME_DATA, ), patch( @@ -183,7 +183,7 @@ def bypass_api_fixture_v1_only(bypass_api_fixture) -> None: home_data_copy = deepcopy(HOME_DATA) home_data_copy.received_devices = [] with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", return_value=home_data_copy, ): yield diff --git a/tests/components/roborock/test_init.py b/tests/components/roborock/test_init.py index a1bcfc462e4..01a8aa26de7 100644 --- a/tests/components/roborock/test_init.py +++ b/tests/components/roborock/test_init.py @@ -54,7 +54,7 @@ async def test_config_entry_not_ready( """Test that when coordinator update fails, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", ), patch( "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", @@ -71,7 +71,7 @@ async def test_config_entry_not_ready_home_data( """Test that when we fail to get home data, entry retries.""" with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", side_effect=RoborockException(), ), patch( @@ -164,7 +164,7 @@ async def test_reauth_started( ) -> None: """Test reauth flow started.""" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", side_effect=RoborockInvalidCredentials(), ): await async_setup_component(hass, DOMAIN, {}) @@ -249,7 +249,7 @@ async def test_not_supported_protocol( home_data_copy = deepcopy(HOME_DATA) home_data_copy.received_devices[0].pv = "random" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", return_value=home_data_copy, ): await hass.config_entries.async_setup(mock_roborock_entry.entry_id) @@ -267,7 +267,7 @@ async def test_not_supported_a01_device( home_data_copy = deepcopy(HOME_DATA) home_data_copy.products[2].category = "random" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", return_value=home_data_copy, ): await async_setup_component(hass, DOMAIN, {}) @@ -282,7 +282,7 @@ async def test_invalid_user_agreement( ) -> None: """Test that we fail setting up if the user agreement is out of date.""" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", side_effect=RoborockInvalidUserAgreement(), ): await hass.config_entries.async_setup(mock_roborock_entry.entry_id) @@ -299,7 +299,7 @@ async def test_no_user_agreement( ) -> None: """Test that we fail setting up if the user has no agreement.""" with patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", side_effect=RoborockNoUserAgreement(), ): await hass.config_entries.async_setup(mock_roborock_entry.entry_id) @@ -330,7 +330,7 @@ async def test_stale_device( with ( patch( - "homeassistant.components.roborock.RoborockApiClient.get_home_data_v2", + "homeassistant.components.roborock.RoborockApiClient.get_home_data_v3", return_value=hd, ), patch( From c6b9a40234c4f621d8159da94eb2c799634b3065 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:05:33 -0700 Subject: [PATCH 0110/1175] Increase the local calendar update interval to avoid re-parsing the calendar state unnecessarily (#144234) --- homeassistant/components/local_calendar/calendar.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index df6f994a46c..252fe703d6c 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -36,6 +36,11 @@ _LOGGER = logging.getLogger(__name__) PRODID = "-//homeassistant.io//local_calendar 1.0//EN" +# The calendar on disk is only changed when this entity is updated, so there +# is no need to poll for changes. The calendar enttiy base class will handle +# refreshing the entity state based on the start or end time of the event. +SCAN_INTERVAL = timedelta(days=1) + async def async_setup_entry( hass: HomeAssistant, From 68d62ab58e3d63502da90acfba21f1553bbd90ab Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:06:27 -0700 Subject: [PATCH 0111/1175] Update local calendar to process calendar events in the executor (#144233) --- .../components/local_calendar/calendar.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index 252fe703d6c..8534cc1bfbf 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -94,20 +94,27 @@ class LocalCalendarEntity(CalendarEntity): self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - events = self._calendar.timeline_tz(start_date.tzinfo).overlapping( - start_date, - end_date, - ) - return [_get_calendar_event(event) for event in events] + + def events_in_range() -> list[CalendarEvent]: + events = self._calendar.timeline_tz(start_date.tzinfo).overlapping( + start_date, + end_date, + ) + return [_get_calendar_event(event) for event in events] + + return await self.hass.async_add_executor_job(events_in_range) async def async_update(self) -> None: """Update entity state with the next upcoming event.""" - now = dt_util.now() - events = self._calendar.timeline_tz(now.tzinfo).active_after(now) - if event := next(events, None): - self._event = _get_calendar_event(event) - else: - self._event = None + + def next_event() -> CalendarEvent | None: + now = dt_util.now() + events = self._calendar.timeline_tz(now.tzinfo).active_after(now) + if event := next(events, None): + return _get_calendar_event(event) + return None + + self._event = await self.hass.async_add_executor_job(next_event) async def _async_store(self) -> None: """Persist the calendar to disk.""" From fa6a2f08ab2c49775b3c0e12ff0c77d5db92bd19 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:07:02 -0700 Subject: [PATCH 0112/1175] Update remote calendar to do all event handling in an executor (#144232) --- .../components/remote_calendar/calendar.py | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/remote_calendar/calendar.py b/homeassistant/components/remote_calendar/calendar.py index bd83a5f18cc..2f60918f010 100644 --- a/homeassistant/components/remote_calendar/calendar.py +++ b/homeassistant/components/remote_calendar/calendar.py @@ -29,7 +29,7 @@ async def async_setup_entry( """Set up the remote calendar platform.""" coordinator = entry.runtime_data entity = RemoteCalendarEntity(coordinator, entry) - async_add_entities([entity]) + async_add_entities([entity], True) class RemoteCalendarEntity( @@ -48,25 +48,46 @@ class RemoteCalendarEntity( super().__init__(coordinator) self._attr_name = entry.data[CONF_CALENDAR_NAME] self._attr_unique_id = entry.entry_id + self._event: CalendarEvent | None = None @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - now = dt_util.now() - events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now) - if event := next(events, None): - return _get_calendar_event(event) - return None + return self._event 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 = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping( - start_date, - end_date, - ) - return [_get_calendar_event(event) for event in events] + + def events_in_range() -> list[CalendarEvent]: + """Return all events in the given time range.""" + events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping( + start_date, + end_date, + ) + return [_get_calendar_event(event) for event in events] + + return await self.hass.async_add_executor_job(events_in_range) + + async def async_update(self) -> None: + """Refresh the timeline. + + This is called when the coordinator updates. Creating the timeline may + require walking through the entire calendar and handling recurring + events, so it is done as a separate task without blocking the event loop. + """ + await super().async_update() + + def next_timeline_event() -> CalendarEvent | None: + """Return the next active event.""" + now = dt_util.now() + events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now) + if event := next(events, None): + return _get_calendar_event(event) + return None + + self._event = await self.hass.async_add_executor_job(next_timeline_event) def _get_calendar_event(event: Event) -> CalendarEvent: From e2a813714079d53b5eeff53227cb7933cb4f1bf1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 4 May 2025 20:40:49 -0700 Subject: [PATCH 0113/1175] Bump ical to 9.2.0 (#144240) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 2bedc7a3163..32af3e675b3 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 90cd5a6d2ac..eba26e88d5a 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index a630c18c669..fb48ca72337 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==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index da078395484..b31fa3389dc 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.1.0"] + "requirements": ["ical==9.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74b9dc12407..8ebee15acc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1200,7 +1200,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.1.0 +ical==9.2.0 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c21f56d0f68..cadd68bb101 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.1.0 +ical==9.2.0 # homeassistant.components.caldav icalendar==6.1.0 From e3b3c32751007a73ec4743e222f1a92a19a49756 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 5 May 2025 14:28:01 +1000 Subject: [PATCH 0114/1175] Add valet switch to Teslemetry (#144167) * Add valet switch * Add snapshot --- homeassistant/components/teslemetry/switch.py | 10 +++ .../teslemetry/snapshots/test_switch.ambr | 62 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index acd17ac4165..f1082122e5c 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -60,6 +60,16 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetrySwitchEntityDescription, ...] = ( off_func=lambda api: api.set_sentry_mode(on=False), scopes=[Scope.VEHICLE_CMDS], ), + TeslemetrySwitchEntityDescription( + key="vehicle_state_valet_mode", + streaming_listener=lambda vehicle, value: vehicle.listen_ValetModeEnabled( + value + ), + streaming_firmware="2024.44.25", + on_func=lambda api: api.set_valet_mode(on=True, password=""), + off_func=lambda api: api.set_valet_mode(on=False, password=""), + scopes=[Scope.VEHICLE_CMDS], + ), TeslemetrySwitchEntityDescription( key="climate_state_auto_seat_climate_left", streaming_listener=lambda vehicle, callback: vehicle.listen_AutoSeatClimateLeft( diff --git a/tests/components/teslemetry/snapshots/test_switch.ambr b/tests/components/teslemetry/snapshots/test_switch.ambr index 0586b454a91..ffbfc06026e 100644 --- a/tests/components/teslemetry/snapshots/test_switch.ambr +++ b/tests/components/teslemetry/snapshots/test_switch.ambr @@ -383,6 +383,54 @@ 'state': 'off', }) # --- +# name: test_switch[switch.test_valet_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.test_valet_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valet mode', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'vehicle_state_valet_mode', + 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_valet_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[switch.test_valet_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test Valet mode', + }), + 'context': , + 'entity_id': 'switch.test_valet_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switch_alt[switch.energy_site_allow_charging_from_grid-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ @@ -495,6 +543,20 @@ 'state': 'off', }) # --- +# name: test_switch_alt[switch.test_valet_mode-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test Valet mode', + }), + 'context': , + 'entity_id': 'switch.test_valet_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switch_streaming[switch.test_auto_seat_climate_left] 'on' # --- From 41ecb24135193935235510f09fa731fbe852d173 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 5 May 2025 14:54:00 +1000 Subject: [PATCH 0115/1175] Set api type more specifically in Teslemetry (#144178) * Set api type more specifically * remove extra spacing * Fix class after rebase --------- Co-authored-by: Allen Porter --- homeassistant/components/teslemetry/button.py | 2 ++ homeassistant/components/teslemetry/climate.py | 2 -- homeassistant/components/teslemetry/cover.py | 6 ++++++ homeassistant/components/teslemetry/entity.py | 3 ++- homeassistant/components/teslemetry/lock.py | 5 +++++ homeassistant/components/teslemetry/media_player.py | 1 - homeassistant/components/teslemetry/number.py | 1 + homeassistant/components/teslemetry/select.py | 1 + homeassistant/components/teslemetry/switch.py | 7 ++++--- 9 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index 6cb9d996b95..2de2868551b 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Any from tesla_fleet_api.const import Scope +from tesla_fleet_api.teslemetry import Vehicle from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant @@ -76,6 +77,7 @@ async def async_setup_entry( class TeslemetryButtonEntity(TeslemetryVehiclePollingEntity, ButtonEntity): """Base class for Teslemetry buttons.""" + api: Vehicle entity_description: TeslemetryButtonEntityDescription def __init__( diff --git a/homeassistant/components/teslemetry/climate.py b/homeassistant/components/teslemetry/climate.py index 0a1c23adcb0..1bc52b23026 100644 --- a/homeassistant/components/teslemetry/climate.py +++ b/homeassistant/components/teslemetry/climate.py @@ -91,7 +91,6 @@ class TeslemetryClimateEntity(TeslemetryRootEntity, ClimateEntity): """Vehicle Climate Control.""" api: Vehicle - _attr_precision = PRECISION_HALVES _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF] @@ -372,7 +371,6 @@ class TeslemetryCabinOverheatProtectionEntity(TeslemetryRootEntity, ClimateEntit """Vehicle Cabin Overheat Protection.""" api: Vehicle - _attr_precision = PRECISION_WHOLE _attr_target_temperature_step = 5 _attr_min_temp = 30 diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index de036edc32a..be85a877c86 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -6,6 +6,7 @@ from itertools import chain from typing import Any from tesla_fleet_api.const import Scope, SunRoofCommand, Trunk, WindowCommand +from tesla_fleet_api.teslemetry import Vehicle from teslemetry_stream import Signal from teslemetry_stream.const import WindowState @@ -103,6 +104,7 @@ class CoverRestoreEntity(RestoreEntity, CoverEntity): class TeslemetryWindowEntity(TeslemetryRootEntity, CoverEntity): """Base class for window cover entities.""" + api: Vehicle _attr_device_class = CoverDeviceClass.WINDOW _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -224,6 +226,7 @@ class TeslemetryChargePortEntity( ): """Base class for for charge port cover entities.""" + api: Vehicle _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -304,6 +307,7 @@ class TeslemetryStreamingChargePortEntity( class TeslemetryFrontTrunkEntity(TeslemetryRootEntity, CoverEntity): """Base class for the front trunk cover entities.""" + api: Vehicle _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN @@ -365,6 +369,7 @@ class TeslemetryStreamingFrontTrunkEntity( class TeslemetryRearTrunkEntity(TeslemetryRootEntity, CoverEntity): """Cover entity for the rear trunk.""" + api: Vehicle _attr_device_class = CoverDeviceClass.DOOR _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -433,6 +438,7 @@ class TeslemetryStreamingRearTrunkEntity( class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity): """Cover entity for the sunroof.""" + api: Vehicle _attr_device_class = CoverDeviceClass.WINDOW _attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 4930129642f..170d4e3a3ae 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -26,7 +26,6 @@ class TeslemetryRootEntity(Entity): _attr_has_entity_name = True scoped: bool - api: Vehicle | EnergySite def raise_for_scope(self, scope: Scope): """Raise an error if a scope is not available.""" @@ -248,6 +247,8 @@ class TeslemetryWallConnectorEntity(TeslemetryPollingEntity): class TeslemetryVehicleStreamEntity(TeslemetryRootEntity): """Parent class for Teslemetry Vehicle Stream entities.""" + api: Vehicle + def __init__(self, data: TeslemetryVehicleData, key: str) -> None: """Initialize common aspects of a Teslemetry entity.""" self.vehicle = data diff --git a/homeassistant/components/teslemetry/lock.py b/homeassistant/components/teslemetry/lock.py index 75cf72c9c88..fda52357f5c 100644 --- a/homeassistant/components/teslemetry/lock.py +++ b/homeassistant/components/teslemetry/lock.py @@ -6,6 +6,7 @@ from itertools import chain from typing import Any from tesla_fleet_api.const import Scope +from tesla_fleet_api.teslemetry import Vehicle from homeassistant.components.lock import LockEntity from homeassistant.core import HomeAssistant @@ -64,6 +65,8 @@ async def async_setup_entry( class TeslemetryVehicleLockEntity(TeslemetryRootEntity, LockEntity): """Base vehicle lock entity for Teslemetry.""" + api: Vehicle + async def async_lock(self, **kwargs: Any) -> None: """Lock the doors.""" self.raise_for_scope(Scope.VEHICLE_CMDS) @@ -135,6 +138,8 @@ class TeslemetryStreamingVehicleLockEntity( class TeslemetryCableLockEntity(TeslemetryRootEntity, LockEntity): """Base cable Lock entity for Teslemetry.""" + api: Vehicle + async def async_lock(self, **kwargs: Any) -> None: """Charge cable Lock cannot be manually locked.""" raise ServiceValidationError( diff --git a/homeassistant/components/teslemetry/media_player.py b/homeassistant/components/teslemetry/media_player.py index 11615d94614..bf1fffed583 100644 --- a/homeassistant/components/teslemetry/media_player.py +++ b/homeassistant/components/teslemetry/media_player.py @@ -63,7 +63,6 @@ class TeslemetryMediaEntity(TeslemetryRootEntity, MediaPlayerEntity): """Base vehicle media player class.""" api: Vehicle - _attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_volume_step = VOLUME_STEP diff --git a/homeassistant/components/teslemetry/number.py b/homeassistant/components/teslemetry/number.py index 466fc9f5ee6..bb9f5b588a0 100644 --- a/homeassistant/components/teslemetry/number.py +++ b/homeassistant/components/teslemetry/number.py @@ -172,6 +172,7 @@ async def async_setup_entry( class TeslemetryVehicleNumberEntity(TeslemetryRootEntity, NumberEntity): """Vehicle number entity base class.""" + api: Vehicle entity_description: TeslemetryNumberVehicleEntityDescription async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/teslemetry/select.py b/homeassistant/components/teslemetry/select.py index be90636497e..c24c47feb2e 100644 --- a/homeassistant/components/teslemetry/select.py +++ b/homeassistant/components/teslemetry/select.py @@ -208,6 +208,7 @@ async def async_setup_entry( class TeslemetrySelectEntity(TeslemetryRootEntity, SelectEntity): """Parent vehicle select entity class.""" + api: Vehicle entity_description: TeslemetrySelectEntityDescription _climate: bool = False diff --git a/homeassistant/components/teslemetry/switch.py b/homeassistant/components/teslemetry/switch.py index f1082122e5c..f607429be46 100644 --- a/homeassistant/components/teslemetry/switch.py +++ b/homeassistant/components/teslemetry/switch.py @@ -8,7 +8,7 @@ from itertools import chain from typing import Any from tesla_fleet_api.const import AutoSeat, Scope -from tesla_fleet_api.teslemetry.vehicles import TeslemetryVehicle +from tesla_fleet_api.teslemetry import Vehicle from teslemetry_stream import TeslemetryStreamVehicle from homeassistant.components.switch import ( @@ -38,8 +38,8 @@ PARALLEL_UPDATES = 0 class TeslemetrySwitchEntityDescription(SwitchEntityDescription): """Describes Teslemetry Switch entity.""" - on_func: Callable[[TeslemetryVehicle], Awaitable[dict[str, Any]]] - off_func: Callable[[TeslemetryVehicle], Awaitable[dict[str, Any]]] + on_func: Callable[[Vehicle], Awaitable[dict[str, Any]]] + off_func: Callable[[Vehicle], Awaitable[dict[str, Any]]] scopes: list[Scope] value_func: Callable[[StateType], bool] = bool streaming_listener: Callable[ @@ -176,6 +176,7 @@ async def async_setup_entry( class TeslemetryVehicleSwitchEntity(TeslemetryRootEntity, SwitchEntity): """Base class for all Teslemetry switch entities.""" + api: Vehicle _attr_device_class = SwitchDeviceClass.SWITCH entity_description: TeslemetrySwitchEntityDescription From 65da1e79b9d1dfa99cfd43886a063ed03ac3c6d0 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 5 May 2025 09:39:23 +0200 Subject: [PATCH 0116/1175] Change some strings to international English in `fronius` (#144244) Change to international English in `fronius` --- homeassistant/components/fronius/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index 7c42cca29de..ef55c51cb14 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -140,16 +140,16 @@ "ac_module_temperature_sensor_faulty_l3": "AC module temperature sensor faulty (L3)", "dc_module_temperature_sensor_faulty": "DC module temperature sensor faulty", "internal_processor_status": "Warning about the internal processor status. See status code for more information", - "eeprom_reinitialised": "EEPROM has been re-initialised", - "initialisation_error_usb_flash_drive_not_supported": "Initialisation error – USB flash drive is not supported", - "initialisation_error_usb_stick_over_current": "Initialisation error – Overcurrent on USB stick", + "eeprom_reinitialised": "EEPROM has been re-initialized", + "initialisation_error_usb_flash_drive_not_supported": "Initialization error – USB flash drive is not supported", + "initialisation_error_usb_stick_over_current": "Initialization error – Overcurrent on USB stick", "no_usb_flash_drive_connected": "No USB flash drive connected", - "update_file_not_recognised_or_missing": "Update file not recognised or not present", + "update_file_not_recognised_or_missing": "Update file not recognized or not present", "update_file_does_not_match_device": "Update file does not match the device, update file too old", "write_or_read_error_occurred": "Write or read error occurred", "file_could_not_be_opened": "File could not be opened", "log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)", - "initialisation_error_file_system_error_on_usb": "Initialisation error in file system on USB flash drive", + "initialisation_error_file_system_error_on_usb": "Initialization error in file system on USB flash drive", "error_during_logging_data_recording": "Error during recording of logging data", "error_during_update_process": "Error occurred during update process", "update_file_corrupt": "Update file corrupt", From aa062515b8c59ae09ff8fe2ff92fb43a42af7b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 5 May 2025 10:46:30 +0300 Subject: [PATCH 0117/1175] Remove unused huawei_lte YAML schemas, error out on YAML config (#144217) --- .../components/huawei_lte/__init__.py | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index be9d02e45fd..6126968eab6 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -23,7 +23,6 @@ from huawei_lte_api.exceptions import ( from requests.exceptions import Timeout import voluptuous as vol -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_HW_VERSION, @@ -90,36 +89,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) -NOTIFY_SCHEMA = vol.Any( - None, - vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_RECIPIENT): vol.Any( - None, vol.All(cv.ensure_list, [cv.string]) - ), - } - ), -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_URL): cv.url, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA, - } - ) - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url}) From 58906008b969312da91fc7494c54b68616424a2b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 5 May 2025 10:39:06 +0200 Subject: [PATCH 0118/1175] Add last attempted automatic backup sensor (#144194) add last_attempted_automatic_backup sensor --- .../components/backup/coordinator.py | 2 + homeassistant/components/backup/sensor.py | 6 +++ homeassistant/components/backup/strings.json | 3 ++ .../backup/snapshots/test_sensors.ambr | 48 +++++++++++++++++++ tests/components/backup/test_sensors.py | 4 ++ 5 files changed, 63 insertions(+) diff --git a/homeassistant/components/backup/coordinator.py b/homeassistant/components/backup/coordinator.py index 377f23567e0..dba05ba0225 100644 --- a/homeassistant/components/backup/coordinator.py +++ b/homeassistant/components/backup/coordinator.py @@ -30,6 +30,7 @@ class BackupCoordinatorData: """Class to hold backup data.""" backup_manager_state: BackupManagerState + last_attempted_automatic_backup: datetime | None last_successful_automatic_backup: datetime | None next_scheduled_automatic_backup: datetime | None @@ -70,6 +71,7 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]): """Update backup manager data.""" return BackupCoordinatorData( self.backup_manager.state, + self.backup_manager.config.data.last_attempted_automatic_backup, self.backup_manager.config.data.last_completed_automatic_backup, self.backup_manager.config.data.schedule.next_automatic_backup, ) diff --git a/homeassistant/components/backup/sensor.py b/homeassistant/components/backup/sensor.py index 59e98ae7c2d..08e7ec49e3d 100644 --- a/homeassistant/components/backup/sensor.py +++ b/homeassistant/components/backup/sensor.py @@ -46,6 +46,12 @@ BACKUP_MANAGER_DESCRIPTIONS = ( device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.last_successful_automatic_backup, ), + BackupSensorEntityDescription( + key="last_attempted_automatic_backup", + translation_key="last_attempted_automatic_backup", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.last_attempted_automatic_backup, + ), ) diff --git a/homeassistant/components/backup/strings.json b/homeassistant/components/backup/strings.json index 357bcdbb72f..37adf9e9faf 100644 --- a/homeassistant/components/backup/strings.json +++ b/homeassistant/components/backup/strings.json @@ -37,6 +37,9 @@ "next_scheduled_automatic_backup": { "name": "Next scheduled automatic backup" }, + "last_attempted_automatic_backup": { + "name": "Last attempted automatic backup" + }, "last_successful_automatic_backup": { "name": "Last successful automatic backup" } diff --git a/tests/components/backup/snapshots/test_sensors.ambr b/tests/components/backup/snapshots/test_sensors.ambr index be12afdbf1e..b68d706dfb3 100644 --- a/tests/components/backup/snapshots/test_sensors.ambr +++ b/tests/components/backup/snapshots/test_sensors.ambr @@ -62,6 +62,54 @@ 'state': 'idle', }) # --- +# name: test_sensors[sensor.backup_last_attempted_automatic_backup-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.backup_last_attempted_automatic_backup', + '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 attempted automatic backup', + 'platform': 'backup', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_attempted_automatic_backup', + 'unique_id': 'last_attempted_automatic_backup', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.backup_last_attempted_automatic_backup-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Backup Last attempted automatic backup', + }), + 'context': , + 'entity_id': 'sensor.backup_last_attempted_automatic_backup', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.backup_last_successful_automatic_backup-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/backup/test_sensors.py b/tests/components/backup/test_sensors.py index bee61887ea5..6ff1aca7c6d 100644 --- a/tests/components/backup/test_sensors.py +++ b/tests/components/backup/test_sensors.py @@ -104,6 +104,8 @@ async def test_sensor_updates( ) await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("sensor.backup_last_attempted_automatic_backup") + assert state.state == "2024-11-11T03:45:00+00:00" state = hass.states.get("sensor.backup_last_successful_automatic_backup") assert state.state == "2024-11-11T03:45:00+00:00" state = hass.states.get("sensor.backup_next_scheduled_automatic_backup") @@ -113,6 +115,8 @@ async def test_sensor_updates( async_fire_time_changed(hass) await hass.async_block_till_done() + state = hass.states.get("sensor.backup_last_attempted_automatic_backup") + assert state.state == "2024-11-13T11:00:00+00:00" state = hass.states.get("sensor.backup_last_successful_automatic_backup") assert state.state == "2024-11-13T11:00:00+00:00" state = hass.states.get("sensor.backup_next_scheduled_automatic_backup") From 66b2e06cd3c8209fb6ff0da792d6e71d15f8ad7b Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 5 May 2025 02:35:32 -0700 Subject: [PATCH 0119/1175] Fix Invalid statistic_id for Opower: National Grid (#144243) Co-authored-by: J. Nick Koston --- homeassistant/components/opower/coordinator.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index dd0b2c87bb5..a8d24b68fd2 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -113,14 +113,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): _LOGGER.error("Error getting accounts: %s", err) raise for account in accounts: - id_prefix = "_".join( + id_prefix = ( ( - self.api.utility.subdomain(), - account.meter_type.name.lower(), - # Some utilities like AEP have "-" in their account id. - # Replace it with "_" to avoid "Invalid statistic_id" - account.utility_account_id.replace("-", "_").lower(), + f"{self.api.utility.subdomain()}_{account.meter_type.name}_" + f"{account.utility_account_id}" ) + # Some utilities like AEP have "-" in their account id. + # Other utilities like ngny-gas have "-" in their subdomain. + # Replace it with "_" to avoid "Invalid statistic_id" + .replace("-", "_") + .lower() ) cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost" compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation" From d88cd72d133e199822af8990fe1ce629a94e2118 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 May 2025 11:58:24 +0200 Subject: [PATCH 0120/1175] Move more SamsungTV test constants to fixture files (#144249) * Add SSDP fixtures to SamsungTV * Adjust * Improve * Improve --- tests/components/samsungtv/const.py | 45 ++++------ .../fixtures/ssdp_device_main_tv_agent.json | 11 +++ .../ssdp_service_remote_control_receiver.json | 11 +++ .../ssdp_service_rendering_control.json | 11 +++ .../components/samsungtv/test_config_flow.py | 90 ++++++------------- 5 files changed, 77 insertions(+), 91 deletions(-) create mode 100644 tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json create mode 100644 tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json create mode 100644 tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 5d09087dadd..e4977a536b0 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -2,6 +2,7 @@ from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, + DOMAIN, METHOD_LEGACY, METHOD_WEBSOCKET, ) @@ -15,13 +16,9 @@ from homeassistant.const import ( CONF_PORT, CONF_TOKEN, ) -from homeassistant.helpers.service_info.ssdp import ( - ATTR_UPNP_FRIENDLY_NAME, - ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_UDN, - SsdpServiceInfo, -) +from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo + +from tests.common import load_json_object_fixture MOCK_CONFIG = { CONF_HOST: "fake_host", @@ -59,28 +56,6 @@ MOCK_ENTRY_WS_WITH_MAC = { CONF_TOKEN: "123456789", } -MOCK_SSDP_DATA_RENDERING_CONTROL_ST = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1", - ssdp_location="https://fake_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", - }, -) -MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="urn:samsung.com:service:MainTVAgent2:1", - ssdp_location="https://fake_host:12345/tv_agent", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", - }, -) SAMPLE_DEVICE_INFO_WIFI = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -92,3 +67,15 @@ SAMPLE_DEVICE_INFO_WIFI = { "networkType": "wireless", }, } + +MOCK_SSDP_DATA = SsdpServiceInfo( + **load_json_object_fixture("ssdp_service_remote_control_receiver.json", DOMAIN) +) + +MOCK_SSDP_DATA_RENDERING_CONTROL_ST = SsdpServiceInfo( + **load_json_object_fixture("ssdp_service_rendering_control.json", DOMAIN) +) + +MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = SsdpServiceInfo( + **load_json_object_fixture("ssdp_device_main_tv_agent.json", DOMAIN) +) diff --git a/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json b/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json new file mode 100644 index 00000000000..2970f14bf5f --- /dev/null +++ b/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json @@ -0,0 +1,11 @@ +{ + "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:samsung.com:service:MainTVAgent2:1", + "ssdp_st": "urn:samsung.com:service:MainTVAgent2:1", + "upnp": { + "friendlyName": "[TV] fake_name", + "manufacturer": "Samsung fake_manufacturer", + "modelName": "fake_model", + "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + }, + "ssdp_location": "https://fake_host:12345/tv_agent" +} diff --git a/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json b/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json new file mode 100644 index 00000000000..b2f5f27a8b4 --- /dev/null +++ b/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json @@ -0,0 +1,11 @@ +{ + "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:samsung.com:device:RemoteControlReceiver:1", + "ssdp_st": "urn:samsung.com:device:RemoteControlReceiver:1", + "upnp": { + "friendlyName": "[TV] fake_name", + "manufacturer": "Samsung fake_manufacturer", + "modelName": "fake_model", + "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + }, + "ssdp_location": "http://fake_host:7676/smp_7_" +} diff --git a/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json b/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json new file mode 100644 index 00000000000..4074a39703e --- /dev/null +++ b/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json @@ -0,0 +1,11 @@ +{ + "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:schemas-upnp-org:service:RenderingControl:1", + "ssdp_st": "urn:schemas-upnp-org:service:RenderingControl:1", + "upnp": { + "friendlyName": "[TV] fake_name", + "manufacturer": "Samsung fake_manufacturer", + "modelName": "fake_model", + "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + }, + "ssdp_location": "https://fake_host:12345/test" +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 12c222033e0..b15c007c109 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -51,8 +51,6 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_MANUFACTURER, - ATTR_UPNP_MODEL_NAME, - ATTR_UPNP_UDN, SsdpServiceInfo, ) from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo @@ -61,6 +59,7 @@ from homeassistant.setup import async_setup_component from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_ENTRYDATA_WS, + MOCK_SSDP_DATA, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) @@ -84,49 +83,7 @@ MOCK_IMPORT_WSDATA = { CONF_PORT: 8002, } MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} -MOCK_SSDP_DATA = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="https://fake_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", - }, -) -MOCK_SSDP_DATA_NO_MANUFACTURER = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="https://fake_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", - }, -) -MOCK_SSDP_DATA_NOPREFIX = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://fake2_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake2_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake2_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", - }, -) -MOCK_SSDP_DATA_WRONGMODEL = SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://fake2_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", - ATTR_UPNP_MODEL_NAME: "HW-Qfake", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", - }, -) MOCK_DHCP_DATA = DhcpServiceInfo( ip="fake_host", macaddress="aabbccddeeff", hostname="fake_hostname" ) @@ -540,13 +497,16 @@ async def test_ssdp(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote", "rest_api_failing") async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: """Test starting a flow from discovery when the manufacturer data is missing.""" + ssdp_data = deepcopy(MOCK_SSDP_DATA) + ssdp_data.upnp.pop(ATTR_UPNP_MANUFACTURER) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_SSDP_DATA_NO_MANUFACTURER, + data=ssdp_data, ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED @pytest.mark.parametrize( @@ -566,12 +526,17 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( @pytest.mark.usefixtures("remote", "rest_api_failing") async def test_ssdp_noprefix(hass: HomeAssistant) -> None: - """Test starting a flow from discovery without prefixes.""" + """Test starting a flow from discovery when friendly name doesn't start with [TV].""" + ssdp_data = deepcopy(MOCK_SSDP_DATA) + ssdp_data.upnp[ATTR_UPNP_FRIENDLY_NAME] = ssdp_data.upnp[ATTR_UPNP_FRIENDLY_NAME][ + 4: + ] + # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_SSDP_DATA_NOPREFIX, + data=ssdp_data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -580,12 +545,12 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) 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" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" - assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + assert result["title"] == "fake_model" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @pytest.mark.usefixtures("remotews", "rest_api_failing") @@ -797,14 +762,15 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") -async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: +async def test_ssdp_wrong_manufacturer(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" - + ssdp_data = deepcopy(MOCK_SSDP_DATA) + ssdp_data.upnp[ATTR_UPNP_MANUFACTURER] = ssdp_data.upnp[ATTR_UPNP_MANUFACTURER][7:] # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_SSDP_DATA_WRONGMODEL, + data=ssdp_data, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -1100,7 +1066,7 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED @pytest.mark.usefixtures("remote", "remotews", "remoteencws", "rest_api_failing") @@ -1113,7 +1079,7 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") @@ -1493,7 +1459,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "original" @@ -1752,7 +1718,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" assert entry.unique_id is None @@ -1905,7 +1871,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "not_supported" + assert result2["reason"] == RESULT_NOT_SUPPORTED @pytest.mark.usefixtures("remoteencws", "rest_api") From 9e4a20c267c9ec95830b811884dbf327dc4d3eea Mon Sep 17 00:00:00 2001 From: John Hillery <34005807+jrhillery@users.noreply.github.com> Date: Mon, 5 May 2025 06:40:37 -0400 Subject: [PATCH 0121/1175] Bump nexia to 2.9.0 (#144153) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa Co-authored-by: J. Nick Koston Co-authored-by: Robert Resch --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e8a1b53cc08..0c01820055e 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.7.0"] + "requirements": ["nexia==2.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8ebee15acc5..7fb2f03f158 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1496,7 +1496,7 @@ nettigo-air-monitor==4.1.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.7.0 +nexia==2.9.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cadd68bb101..6f3df86cc20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1260,7 +1260,7 @@ netmap==0.7.0.2 nettigo-air-monitor==4.1.0 # homeassistant.components.nexia -nexia==2.7.0 +nexia==2.9.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 From 445b38f25d8276da1ae8a42e89cc2b6dec7e734c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 12:51:19 +0200 Subject: [PATCH 0122/1175] Bump github/codeql-action from 3.28.16 to 3.28.17 (#144245) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.16 to 3.28.17. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3.28.16...v3.28.17) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.28.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] 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 c6181121043..7cc5ae34bee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.28.16 + uses: github/codeql-action/init@v3.28.17 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.28.16 + uses: github/codeql-action/analyze@v3.28.17 with: category: "/language:python" From 3390dc0dbb45b4b2be57a3e75fd8b619af4baeb4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 5 May 2025 04:13:08 -0700 Subject: [PATCH 0123/1175] Fix Office 365 calendars to be compatible with rfc5545 (#144230) --- .../components/remote_calendar/config_flow.py | 13 +---- .../components/remote_calendar/coordinator.py | 12 +--- .../components/remote_calendar/ics.py | 44 ++++++++++++++ .../snapshots/test_calendar.ambr | 19 ++++++ .../remote_calendar/test_calendar.py | 30 ++++++++++ .../testdata/office365_invalid_tzid.ics | 58 +++++++++++++++++++ 6 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/remote_calendar/ics.py create mode 100644 tests/components/remote_calendar/snapshots/test_calendar.ambr create mode 100644 tests/components/remote_calendar/testdata/office365_invalid_tzid.ics diff --git a/homeassistant/components/remote_calendar/config_flow.py b/homeassistant/components/remote_calendar/config_flow.py index 802a7eb7cea..558a3d668ae 100644 --- a/homeassistant/components/remote_calendar/config_flow.py +++ b/homeassistant/components/remote_calendar/config_flow.py @@ -5,8 +5,6 @@ import logging from typing import Any from httpx import HTTPError, InvalidURL -from ical.calendar_stream import IcsCalendarStream -from ical.exceptions import CalendarParseError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -14,6 +12,7 @@ from homeassistant.const import CONF_URL from homeassistant.helpers.httpx_client import get_async_client from .const import CONF_CALENDAR_NAME, DOMAIN +from .ics import InvalidIcsException, parse_calendar _LOGGER = logging.getLogger(__name__) @@ -64,15 +63,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.debug("An error occurred: %s", err) else: try: - await self.hass.async_add_executor_job( - IcsCalendarStream.calendar_from_ics, res.text - ) - except CalendarParseError as err: + await parse_calendar(self.hass, res.text) + except InvalidIcsException: errors["base"] = "invalid_ics_file" - _LOGGER.error("Error reading the calendar information: %s", err.message) - _LOGGER.debug( - "Additional calendar error detail: %s", str(err.detailed_error) - ) else: return self.async_create_entry( title=user_input[CONF_CALENDAR_NAME], data=user_input diff --git a/homeassistant/components/remote_calendar/coordinator.py b/homeassistant/components/remote_calendar/coordinator.py index 6caec297c1a..1eead7682d3 100644 --- a/homeassistant/components/remote_calendar/coordinator.py +++ b/homeassistant/components/remote_calendar/coordinator.py @@ -5,8 +5,6 @@ import logging from httpx import HTTPError, InvalidURL from ical.calendar import Calendar -from ical.calendar_stream import IcsCalendarStream -from ical.exceptions import CalendarParseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL @@ -15,6 +13,7 @@ from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +from .ics import InvalidIcsException, parse_calendar type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator] @@ -56,14 +55,9 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]): translation_placeholders={"err": str(err)}, ) from err try: - # 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 self.ics = res.text - return await self.hass.async_add_executor_job( - IcsCalendarStream.calendar_from_ics, self.ics - ) - except CalendarParseError as err: + return await parse_calendar(self.hass, res.text) + except InvalidIcsException as err: raise UpdateFailed( translation_domain=DOMAIN, translation_key="unable_to_parse", diff --git a/homeassistant/components/remote_calendar/ics.py b/homeassistant/components/remote_calendar/ics.py new file mode 100644 index 00000000000..d0920d7ae32 --- /dev/null +++ b/homeassistant/components/remote_calendar/ics.py @@ -0,0 +1,44 @@ +"""Module for parsing ICS content. + +This module exists to fix known issues where calendar providers return calendars +that do not follow rfcc5545. This module will attempt to fix the calendar and return +a valid calendar object. +""" + +import logging + +from ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.compat import enable_compat_mode +from ical.exceptions import CalendarParseError + +from homeassistant.core import HomeAssistant + +_LOGGER = logging.getLogger(__name__) + + +class InvalidIcsException(Exception): + """Exception to indicate that the ICS content is invalid.""" + + +def _compat_calendar_from_ics(ics: str) -> Calendar: + """Parse the ICS content and return a Calendar object. + + This function is called in a separate thread to avoid blocking the event + loop while loading packages or parsing the ICS content for large calendars. + + It uses the `enable_compat_mode` context manager to fix known issues with + calendar providers that return invalid calendars. + """ + with enable_compat_mode(ics) as compat_ics: + return IcsCalendarStream.calendar_from_ics(compat_ics) + + +async def parse_calendar(hass: HomeAssistant, ics: str) -> Calendar: + """Parse the ICS content and return a Calendar object.""" + try: + return await hass.async_add_executor_job(_compat_calendar_from_ics, ics) + except CalendarParseError as err: + _LOGGER.error("Error parsing calendar information: %s", err.message) + _LOGGER.debug("Additional calendar error detail: %s", str(err.detailed_error)) + raise InvalidIcsException(err.message) from err diff --git a/tests/components/remote_calendar/snapshots/test_calendar.ambr b/tests/components/remote_calendar/snapshots/test_calendar.ambr new file mode 100644 index 00000000000..e372be5255c --- /dev/null +++ b/tests/components/remote_calendar/snapshots/test_calendar.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_calendar_examples[office365_invalid_tzid] + list([ + dict({ + 'description': None, + 'end': dict({ + 'dateTime': '2024-04-26T15:00:00-06:00', + }), + 'location': '', + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'dateTime': '2024-04-26T14:00:00-06:00', + }), + 'summary': 'Uffe', + 'uid': '040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000010000000309AE93C8C3A94489F90ADBEA30C2F2B', + }), + ]) +# --- diff --git a/tests/components/remote_calendar/test_calendar.py b/tests/components/remote_calendar/test_calendar.py index 6ae817321c3..a0c18383369 100644 --- a/tests/components/remote_calendar/test_calendar.py +++ b/tests/components/remote_calendar/test_calendar.py @@ -1,11 +1,13 @@ """Tests for calendar platform of Remote Calendar.""" from datetime import datetime +import pathlib import textwrap from httpx import Response import pytest import respx +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -21,6 +23,13 @@ from .conftest import ( from tests.common import MockConfigEntry +# Test data files with known calendars from various sources. You can add a new file +# in the testdata directory and add it will be parsed and tested. +TESTDATA_FILES = sorted( + pathlib.Path("tests/components/remote_calendar/testdata/").glob("*.ics") +) +TESTDATA_IDS = [f.stem for f in TESTDATA_FILES] + @respx.mock async def test_empty_calendar( @@ -392,3 +401,24 @@ async def test_all_day_iter_order( events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z") assert [event["summary"] for event in events] == event_order + + +@respx.mock +@pytest.mark.parametrize("ics_filename", TESTDATA_FILES, ids=TESTDATA_IDS) +async def test_calendar_examples( + hass: HomeAssistant, + config_entry: MockConfigEntry, + get_events: GetEventsFn, + ics_filename: pathlib.Path, + snapshot: SnapshotAssertion, +) -> None: + """Test parsing known calendars form test data files.""" + respx.get(CALENDER_URL).mock( + return_value=Response( + status_code=200, + text=ics_filename.read_text(), + ) + ) + await setup_integration(hass, config_entry) + events = await get_events("1997-07-14T00:00:00", "2025-07-01T00:00:00") + assert events == snapshot diff --git a/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics b/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics new file mode 100644 index 00000000000..bfadba446d2 --- /dev/null +++ b/tests/components/remote_calendar/testdata/office365_invalid_tzid.ics @@ -0,0 +1,58 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:Microsoft Exchange Server 2010 +VERSION:2.0 +X-WR-CALNAME:Kalender +BEGIN:VTIMEZONE +TZID:W. Europe Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VTIMEZONE +TZID:UTC +BEGIN:STANDARD +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T000000 +TZOFFSETFROM:+0000 +TZOFFSETTO:+0000 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000 + 010000000309AE93C8C3A94489F90ADBEA30C2F2B +SUMMARY:Uffe +DTSTART;TZID=Customized Time Zone:20240426T140000 +DTEND;TZID=Customized Time Zone:20240426T150000 +CLASS:PUBLIC +PRIORITY:5 +DTSTAMP:20250417T155647Z +TRANSP:OPAQUE +STATUS:CONFIRMED +SEQUENCE:0 +LOCATION: +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:0 +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-ISRESPONSEREQUESTED:FALSE +END:VEVENT +END:VCALENDAR From 0713ac497727afffdbc472da51c2c502974a6f89 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 5 May 2025 13:47:07 +0200 Subject: [PATCH 0124/1175] Cleanup invalid CONF_ID from samsungtv tests (#144252) --- tests/components/samsungtv/test_config_flow.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index b15c007c109..79e24519a85 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -35,7 +35,6 @@ from homeassistant.components.samsungtv.const import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, - CONF_ID, CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, @@ -104,14 +103,12 @@ MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( ) MOCK_OLD_ENTRY = { CONF_HOST: "fake_host", - CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", CONF_IP_ADDRESS: EXISTING_IP, CONF_METHOD: "legacy", CONF_PORT: None, } MOCK_LEGACY_ENTRY = { CONF_HOST: EXISTING_IP, - CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", CONF_METHOD: "legacy", CONF_PORT: None, } @@ -1296,7 +1293,6 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert entry is config_entries_domain[0] - assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP assert not entry.unique_id @@ -1315,7 +1311,6 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: entry2 = config_entries_domain[0] # check updated device info - assert entry2.data.get(CONF_ID) is not None assert entry2.data.get(CONF_IP_ADDRESS) is not None assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" From a073a6b01efd433ef4d02d73ff0a2e0b9f27f515 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 5 May 2025 14:23:02 +0200 Subject: [PATCH 0125/1175] Fix hassfest expecting strings file for custom components (#135789) Co-authored-by: Joost Lekkerkerker Co-authored-by: Robert Resch --- script/hassfest/services.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 3a0ebed76fe..70f0a63ca76 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -233,7 +233,7 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa ) if service_schema is None: continue - if "name" not in service_schema: + if "name" not in service_schema and integration.core: try: strings["services"][service_name]["name"] except KeyError: @@ -242,7 +242,7 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa f"Service {service_name} has no name {error_msg_suffix}", ) - if "description" not in service_schema: + if "description" not in service_schema and integration.core: try: strings["services"][service_name]["description"] except KeyError: @@ -257,7 +257,7 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa if "fields" in field_schema: # This is a section continue - if "name" not in field_schema: + if "name" not in field_schema and integration.core: try: strings["services"][service_name]["fields"][field_name]["name"] except KeyError: @@ -266,7 +266,7 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa f"Service {service_name} has a field {field_name} with no name {error_msg_suffix}", ) - if "description" not in field_schema: + if "description" not in field_schema and integration.core: try: strings["services"][service_name]["fields"][field_name][ "description" @@ -296,13 +296,14 @@ def validate_services(config: Config, integration: Integration) -> None: # noqa if "fields" not in section_schema: # This is not a section continue - try: - strings["services"][service_name]["sections"][section_name]["name"] - except KeyError: - integration.add_error( - "services", - f"Service {service_name} has a section {section_name} with no name {error_msg_suffix}", - ) + if "name" not in section_schema and integration.core: + try: + strings["services"][service_name]["sections"][section_name]["name"] + except KeyError: + integration.add_error( + "services", + f"Service {service_name} has a section {section_name} with no name {error_msg_suffix}", + ) def validate(integrations: dict[str, Integration], config: Config) -> None: From c14ddedfae455c4a4cd61b03d64e0b316443c291 Mon Sep 17 00:00:00 2001 From: Luca De Petrillo <972242+lukakama@users.noreply.github.com> Date: Mon, 5 May 2025 14:30:36 +0200 Subject: [PATCH 0126/1175] Fix message corruption in picotts component (#141182) --- homeassistant/components/picotts/tts.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index 44d33145b3d..54caf1a2b26 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -56,10 +56,15 @@ class PicoProvider(Provider): with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf: fname = tmpf.name - cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message] - subprocess.call(cmd) + cmd = ["pico2wave", "--wave", fname, "-l", language] + result = subprocess.run(cmd, text=True, input=message, check=False) data = None try: + if result.returncode != 0: + _LOGGER.error( + "Error running pico2wave, return code: %s", result.returncode + ) + return (None, None) with open(fname, "rb") as voice: data = voice.read() except OSError: From 0043b18135fbc6675719782f0892f4b92cfa9f3f Mon Sep 17 00:00:00 2001 From: tronikos Date: Mon, 5 May 2025 05:36:58 -0700 Subject: [PATCH 0127/1175] Use names instead of statistic IDs in the Opower repair issue (#144018) * Use names instead of statistic IDs in the Opower repair issue * target_ids --- homeassistant/components/opower/coordinator.py | 2 +- homeassistant/components/opower/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opower/coordinator.py b/homeassistant/components/opower/coordinator.py index a8d24b68fd2..d03c30b7db0 100644 --- a/homeassistant/components/opower/coordinator.py +++ b/homeassistant/components/opower/coordinator.py @@ -443,7 +443,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]): "energy_settings": "/config/energy", "target_ids": "\n".join( { - v + str(metadata_map[v]["name"]) for k, v in migration_map.items() if k in need_migration_source_ids } diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index b0516f266a1..f65aeb011ee 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -35,7 +35,7 @@ "issues": { "return_to_grid_migration": { "title": "Return to grid statistics for account: {utility_account_id}", - "description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}" + "description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue." } } } From aa8dfa760dc7603e6688fa9260cb5ccb4ea4a36b Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Mon, 5 May 2025 10:40:48 -0400 Subject: [PATCH 0128/1175] Bump Roborock Map Parser to 0.1.4 (#144260) Bump to 0.1.4 --- 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 784d2c6ad27..444232b5843 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -20,6 +20,6 @@ "quality_scale": "silver", "requirements": [ "python-roborock==2.18.2", - "vacuum-map-parser-roborock==0.1.2" + "vacuum-map-parser-roborock==0.1.4" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 7fb2f03f158..d21445b678d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3007,7 +3007,7 @@ url-normalize==2.2.1 uvcclient==0.12.1 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.2 +vacuum-map-parser-roborock==0.1.4 # homeassistant.components.vallox vallox-websocket-api==5.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f3df86cc20..838c5f4285a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2430,7 +2430,7 @@ url-normalize==2.2.1 uvcclient==0.12.1 # homeassistant.components.roborock -vacuum-map-parser-roborock==0.1.2 +vacuum-map-parser-roborock==0.1.4 # homeassistant.components.vallox vallox-websocket-api==5.3.0 From d775e443f8eab468064d56def991749a6b12c327 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:58:39 +0200 Subject: [PATCH 0129/1175] Fix balboa mocks (#144264) --- tests/components/balboa/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/balboa/conftest.py b/tests/components/balboa/conftest.py index 18639b0c9be..7678a97305e 100644 --- a/tests/components/balboa/conftest.py +++ b/tests/components/balboa/conftest.py @@ -66,6 +66,7 @@ def client_fixture() -> Generator[MagicMock]: client.heat_state = 2 client.lights = [] client.pumps = [] + client.temperature_range.client = client client.temperature_range.state = LowHighRange.LOW client.fault = None From 14735cce265a535992bd3aa1b42ab46177b7f3c9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:58:48 +0200 Subject: [PATCH 0130/1175] Fix deako mocks (#144265) --- tests/components/deako/test_init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/deako/test_init.py b/tests/components/deako/test_init.py index c2291330feb..33428f4f81c 100644 --- a/tests/components/deako/test_init.py +++ b/tests/components/deako/test_init.py @@ -21,6 +21,7 @@ async def test_deako_async_setup_entry( "id1": {}, "id2": {}, } + pydeako_deako_mock.return_value.get_name.return_value = "some device" mock_config_entry.add_to_hass(hass) From 5e8def837ece9ebf998d14b325d2967298ab638b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:58:58 +0200 Subject: [PATCH 0131/1175] Fix imeon_inverter mocks (#144266) --- tests/components/imeon_inverter/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/imeon_inverter/conftest.py b/tests/components/imeon_inverter/conftest.py index 38fb0d90322..5d1dacc4e69 100644 --- a/tests/components/imeon_inverter/conftest.py +++ b/tests/components/imeon_inverter/conftest.py @@ -60,6 +60,7 @@ def mock_imeon_inverter() -> Generator[MagicMock]: inverter.__aenter__.return_value = inverter inverter.login.return_value = True inverter.get_serial.return_value = TEST_SERIAL + inverter.inverter.get.return_value = {"inverter": "blah", "software": "1.0"} inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN) yield inverter From 2e8e13bffbf3c80447ee7d7806d4e52f52bf3b84 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:59:08 +0200 Subject: [PATCH 0132/1175] Fix velbus mocks (#144267) --- tests/components/velbus/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index 65418790280..f7cbeb7a052 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -85,6 +85,7 @@ def mock_module_no_subdevices( module.get_type_name.return_value = "VMB4RYLD" module.get_addresses.return_value = [1, 2, 3, 4] module.get_name.return_value = "BedRoom" + module.get_serial.return_value = "a1b2c3d4e5f6" module.get_sw_version.return_value = "1.0.0" module.is_loaded.return_value = True module.get_channels.return_value = {} @@ -98,6 +99,7 @@ def mock_module_subdevices() -> AsyncMock: module.get_type_name.return_value = "VMB2BLE" module.get_addresses.return_value = [88] module.get_name.return_value = "Kitchen" + module.get_serial.return_value = "a1b2c3d4e5f6" module.get_sw_version.return_value = "2.0.0" module.is_loaded.return_value = True module.get_channels.return_value = {} From 135df5a24eb68b5ceea25384d3e7a11352a53ff7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:59:17 +0200 Subject: [PATCH 0133/1175] Fix palazzetti mocks (#144268) --- tests/components/palazzetti/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/palazzetti/conftest.py b/tests/components/palazzetti/conftest.py index d3694653cd4..ab1e6323247 100644 --- a/tests/components/palazzetti/conftest.py +++ b/tests/components/palazzetti/conftest.py @@ -65,6 +65,7 @@ def mock_palazzetti_client() -> Generator[AsyncMock]: mock_client.has_fan_auto = True mock_client.has_on_off_switch = True mock_client.has_pellet_level = False + mock_client.host = "XXXXXXXXXX" mock_client.connected = True mock_client.status = 6 mock_client.is_heating = True From 826d28974b7ee2b3816fad0402da0b0d0c239a1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:59:50 +0200 Subject: [PATCH 0134/1175] Fix fibaro mocks (#144270) --- tests/components/fibaro/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/fibaro/conftest.py b/tests/components/fibaro/conftest.py index 53cecd78bb6..fde92faa673 100644 --- a/tests/components/fibaro/conftest.py +++ b/tests/components/fibaro/conftest.py @@ -255,6 +255,8 @@ def mock_button_device() -> Mock: climate.central_scene_event = [SceneEvent(1, "Pressed")] climate.actions = {} climate.interfaces = ["zwaveCentralScene"] + climate.battery_level = 100 + climate.armed = False return climate From 633c770a48e3d9b76a2e0e87a7d841669659cd58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 17:59:57 +0200 Subject: [PATCH 0135/1175] Fix matter mocks (#144271) --- tests/components/matter/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index f61cb54cd0b..4f6bda14097 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -43,6 +43,7 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock]: pytest.fail("Listen was not cancelled!") client.connect = AsyncMock(side_effect=connect) + client.check_node_update = AsyncMock(return_value=None) client.start_listening = AsyncMock(side_effect=listen) client.server_info = ServerInfoMessage( fabric_id=MOCK_FABRIC_ID, From 8a95fffbab36bc90ddca416a9a37f0190a6dc6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 5 May 2025 18:41:45 +0200 Subject: [PATCH 0136/1175] Remove program phase sensor from miele vacuum robot (#144257) * Use device class transation * Remove program pghses sensor from robot vacuum cleaner --- homeassistant/components/miele/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 867de3d814b..2bf1fbd1202 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -144,7 +144,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( MieleAppliance.STEAM_OVEN, MieleAppliance.MICROWAVE, MieleAppliance.COFFEE_SYSTEM, - MieleAppliance.ROBOT_VACUUM_CLEANER, MieleAppliance.WASHER_DRYER, MieleAppliance.STEAM_OVEN_COMBI, MieleAppliance.STEAM_OVEN_MICRO, From 36a08d04c58707e96581238a311aaf2da6b55e1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 5 May 2025 19:06:54 +0200 Subject: [PATCH 0137/1175] Fail tests which JSON serialize mocks (#144261) * Fail tests which JSON serialize mocks * Patch JSON helper earlier * Check type instead of attribute --- tests/conftest.py | 11 ++++++++++- tests/patch_json.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/patch_json.py diff --git a/tests/conftest.py b/tests/conftest.py index 9b861d5bde5..2c23270daee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,11 +42,14 @@ import respx from syrupy.assertion import SnapshotAssertion from syrupy.session import SnapshotSession +# Setup patching of JSON functions before any other Home Assistant imports +from . import patch_json # isort:skip + from homeassistant import block_async_io from homeassistant.exceptions import ServiceNotFound # Setup patching of recorder functions before any other Home Assistant imports -from . import patch_recorder +from . import patch_recorder # isort:skip # Setup patching of dt_util time functions before any other Home Assistant imports from . import patch_time # noqa: F401, isort:skip @@ -449,6 +452,12 @@ def reset_globals() -> Generator[None]: frame.async_setup(None) frame._REPORTED_INTEGRATIONS.clear() + # Reset patch_json + if patch_json.mock_objects: + obj = patch_json.mock_objects.pop() + patch_json.mock_objects.clear() + pytest.fail(f"Test attempted to serialize mock object {obj}") + @pytest.fixture(autouse=True, scope="session") def bcrypt_cost() -> Generator[None]: diff --git a/tests/patch_json.py b/tests/patch_json.py new file mode 100644 index 00000000000..e741ba1a816 --- /dev/null +++ b/tests/patch_json.py @@ -0,0 +1,37 @@ +"""Patch JSON related functions.""" + +from __future__ import annotations + +import functools +from typing import Any +from unittest import mock + +import orjson + +from homeassistant.helpers import json as json_helper + +real_json_encoder_default = json_helper.json_encoder_default + +mock_objects = [] + + +def json_encoder_default(obj: Any) -> Any: + """Convert Home Assistant objects. + + Hand other objects to the original method. + """ + if isinstance(obj, mock.Base): + mock_objects.append(obj) + raise TypeError(f"Attempting to serialize mock object {obj}") + return real_json_encoder_default(obj) + + +json_helper.json_encoder_default = json_encoder_default +json_helper.json_bytes = functools.partial( + orjson.dumps, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default +) +json_helper.json_bytes_sorted = functools.partial( + orjson.dumps, + option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SORT_KEYS, + default=json_encoder_default, +) From c73383ded3badb04358583729751a3f976e8eda5 Mon Sep 17 00:00:00 2001 From: Eliz Date: Mon, 5 May 2025 18:11:41 +0100 Subject: [PATCH 0138/1175] Fix missing head forwarding in ingress (#144231) * Add support for connect, head and trace in ingress * added tests * update the testutil * fix * fix empty space * removed connect * remove trace --- homeassistant/components/hassio/ingress.py | 1 + tests/components/hassio/test_ingress.py | 43 ++++++++++++++++++++++ tests/test_util/aiohttp.py | 4 ++ 3 files changed, 48 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index a2f5a43b69c..e673c3a70e9 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -109,6 +109,7 @@ class HassIOIngress(HomeAssistantView): delete = _handle patch = _handle options = _handle + head = _handle async def _handle_websocket( self, request: web.Request, token: str, path: str diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 805b5292edb..069abaa8513 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -269,6 +269,49 @@ async def test_ingress_request_options( assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] +@pytest.mark.parametrize( + "build_type", + [ + ("a3_vl", "test/beer/ping?index=1"), + ("core", "index.html"), + ("local", "panel/config"), + ("jk_921", "editor.php?idx=3&ping=5"), + ("fsadjf10312", ""), + ], +) +async def test_ingress_request_head( + hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker +) -> None: + """Test no auth needed for .""" + aioclient_mock.head( + f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}", + text="test", + ) + + resp = await hassio_noauth_client.head( + f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}", + headers={"X-Test-Header": "beer"}, + ) + + # Check we got right response + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "" # head does not return a body + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3] + assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress" + assert ( + aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] + == f"/api/hassio_ingress/{build_type[0]}" + ) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + @pytest.mark.parametrize( "build_type", [ diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 633f98dc5b3..9207ba0904b 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -110,6 +110,10 @@ class AiohttpClientMocker: """Register a mock patch request.""" self.request("patch", *args, **kwargs) + def head(self, *args, **kwargs): + """Register a mock head request.""" + self.request("head", *args, **kwargs) + @property def call_count(self): """Return the number of requests made.""" From b98a27d3d0d61bcd9891abc307d8a9229542598b Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Mon, 5 May 2025 20:43:51 +0200 Subject: [PATCH 0139/1175] Bump pylamarzocco to 2.0.0 (#144275) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index ab5a77cad4c..572f70bc455 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.0b7"] + "requirements": ["pylamarzocco==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d21445b678d..43736c6ccd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b7 +pylamarzocco==2.0.0 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 838c5f4285a..10710a20415 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0b7 +pylamarzocco==2.0.0 # homeassistant.components.lastfm pylast==5.1.0 From e3ed9fac780dd39d4728a75f2c8ce50e28d96777 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 5 May 2025 20:47:15 +0200 Subject: [PATCH 0140/1175] Update frontend to 20250502.1 (#144276) --- 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 2cfa9572ff3..18e4d349122 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==20250502.0"] + "requirements": ["home-assistant-frontend==20250502.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 73415df8abd..a9788e03648 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 43736c6ccd1..cb271164cc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10710a20415..d6c2611d678 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.0 +home-assistant-frontend==20250502.1 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From 1879b8c27f6f875d8250efbe2faf422a198ec064 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 5 May 2025 21:02:20 +0200 Subject: [PATCH 0141/1175] Fix Z-Wave config flow forms (#144279) --- .../components/zwave_js/config_flow.py | 8 +++-- .../components/zwave_js/strings.json | 18 +++++++---- tests/components/zwave_js/test_config_flow.py | 32 +++++++++---------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 2d9bc0fa1cd..184a7724799 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -717,7 +717,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): data_schema = vol.Schema(schema) - return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + return self.async_show_form( + step_id="configure_addon_user", data_schema=data_schema + ) async def async_step_finish_addon_setup_user( self, user_input: dict[str, Any] | None = None @@ -1097,7 +1099,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): } ) - return self.async_show_form(step_id="configure_addon", data_schema=data_schema) + return self.async_show_form( + step_id="configure_addon_reconfigure", data_schema=data_schema + ) async def async_step_choose_serial_port( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 53615e84691..56ae4e12401 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -37,8 +37,10 @@ "restore_nvm": "Please wait while the network restore completes." }, "step": { - "configure_addon": { + "configure_addon_user": { "data": { + "lr_s2_access_control_key": "Long Range S2 Access Control Key", + "lr_s2_authenticated_key": "Long Range S2 Authenticated Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -52,14 +54,16 @@ "data": { "emulate_hardware": "Emulate Hardware", "log_level": "Log level", - "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]", - "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]", - "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]", - "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]", + "lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]", + "lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]", + "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]", + "s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_access_control_key%]", + "s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_authenticated_key%]", + "s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_unauthenticated_key%]", "usb_path": "[%key:common::config_flow::data::usb_path%]" }, - "description": "[%key:component::zwave_js::config::step::configure_addon::description%]", - "title": "[%key:component::zwave_js::config::step::configure_addon::title%]" + "description": "[%key:component::zwave_js::config::step::configure_addon_user::description%]", + "title": "[%key:component::zwave_js::config::step::configure_addon_user::title%]" }, "hassio_confirm": { "description": "Do you want to set up the Z-Wave integration with the Z-Wave add-on?" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 1d8b997ea4d..3778e36f897 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -682,7 +682,7 @@ async def test_usb_discovery( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -783,7 +783,7 @@ async def test_usb_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" # Make sure the discovered usb device is preferred. data_schema = result["data_schema"] @@ -1015,7 +1015,7 @@ async def test_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1117,7 +1117,7 @@ async def test_discovery_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1674,7 +1674,7 @@ async def test_addon_installed( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1777,7 +1777,7 @@ async def test_addon_installed_start_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1862,7 +1862,7 @@ async def test_addon_installed_failures( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -1943,7 +1943,7 @@ async def test_addon_installed_set_options_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2058,7 +2058,7 @@ async def test_addon_installed_already_configured( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2154,7 +2154,7 @@ async def test_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_user" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2600,7 +2600,7 @@ async def test_reconfigure_addon_running( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2735,7 +2735,7 @@ async def test_reconfigure_addon_running_no_changes( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -2916,7 +2916,7 @@ async def test_reconfigure_different_device( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3099,7 +3099,7 @@ async def test_reconfigure_addon_restart_failed( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3240,7 +3240,7 @@ async def test_reconfigure_addon_running_server_info_failure( ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3387,7 +3387,7 @@ async def test_reconfigure_addon_not_installed( assert install_addon.call_args == call("core_zwave_js") assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "configure_addon" + assert result["step_id"] == "configure_addon_reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], From 0bf807b96e771f855deeaa5d44008f7bf8eac771 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 5 May 2025 21:26:56 +0200 Subject: [PATCH 0142/1175] Fix default entity name not the device default entity when no name set on MQTT subentry entity (#144263) --- homeassistant/components/mqtt/config_flow.py | 12 +++++++++--- tests/components/mqtt/common.py | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 1f317d9f743..2bbfd9e3515 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -465,7 +465,7 @@ class PlatformField: required: bool validator: Callable[..., Any] error: str | None = None - default: str | int | bool | vol.Undefined = vol.UNDEFINED + default: str | int | bool | None | vol.Undefined = vol.UNDEFINED is_schema_default: bool = False exclude_from_reconfig: bool = False conditions: tuple[dict[str, Any], ...] | None = None @@ -515,6 +515,7 @@ COMMON_ENTITY_FIELDS = { required=False, validator=str, exclude_from_reconfig=True, + default=None, ), CONF_ENTITY_PICTURE: PlatformField( selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url" @@ -1324,7 +1325,10 @@ def data_schema_from_fields( vol.Required(field_name, default=field_details.default) if field_details.required else vol.Optional( - field_name, default=field_details.default + field_name, + default=field_details.default + if field_details.default is not None + else vol.UNDEFINED, ): field_details.selector(component_data_with_user_input) # type: ignore[operator] if field_details.custom_filtering else field_details.selector @@ -1375,12 +1379,14 @@ def data_schema_from_fields( @callback def subentry_schema_default_data_from_fields( data_schema_fields: dict[str, PlatformField], + component_data: dict[str, Any], ) -> dict[str, Any]: """Generate custom data schema from platform fields or device data.""" return { key: field.default for key, field in data_schema_fields.items() if field.is_schema_default + or (field.default is not vol.UNDEFINED and key not in component_data) } @@ -2206,7 +2212,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): for component_data in self._subentry_data["components"].values(): platform = component_data[CONF_PLATFORM] subentry_default_data = subentry_schema_default_data_from_fields( - PLATFORM_ENTITY_FIELDS[platform] + PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data ) component_data.update(subentry_default_data) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index d811b601036..4e402046e2c 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -87,6 +87,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = { MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = { "5269352dd9534c908d22812ea5d714cd": { "platform": "notify", + "name": None, "command_topic": "test-topic", "command_template": "{{ value }}", "entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd", From f3b23afc92185abe7156c27c4ce2f3a54556d8df Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 5 May 2025 23:15:24 +0200 Subject: [PATCH 0143/1175] Change "recognized" to international English spelling in `hive` (#144284) Change "recognized" to international English in `hive` --- homeassistant/components/hive/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 58ba949d325..2aa17f0e005 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -34,7 +34,7 @@ } }, "error": { - "invalid_username": "Failed to sign in to Hive. Your email address is not recognised.", + "invalid_username": "Failed to sign in to Hive. Your email address is not recognized.", "invalid_password": "Failed to sign in to Hive. Incorrect password, please try again.", "invalid_code": "Failed to sign in to Hive. Your two-factor authentication code was incorrect.", "no_internet_available": "An Internet connection is required to connect to Hive.", From 14f967cdd05bf472f8b333c212c297b8a600b95c Mon Sep 17 00:00:00 2001 From: Jamin Date: Mon, 5 May 2025 19:25:52 -0500 Subject: [PATCH 0144/1175] Improve Voip pipeline stability (#137620) * Improve Voip pipeline stability It appears the pipeline is being unexpectedly cancelled in some instances. In order to mitigate this issue hang ups will be detected using a separate task rather than relying on timeouts in the STT read method. Also reading STT events will be retried once if it is cancelled. The pipeline will also catch and log any CancelledErrors to help with further debugging. * Update Voip tests * Remove unnecessary changes Remove unnecessary logging and cancelled error handling in wyoming STT. * Remove comment about clearing system prompt The test no longer checks for clearing the system prompt. Since that logic exists completely in the assist_satellite component I think it is reasonable to only test that logic in the unit tests for that component. * Re-raise cancellation Re-raise CancelledError if the current task is cancelling in the check hangup task Co-authored-by: J. Nick Koston * Re-raise CancelledError in pipeline as well * Fix formatting issue * Remove unnecessary logging * Add MockResultStream import to tests This was presumably missed while merging * Cancel check hangup task on disconnect * Add myself as codeowner for VoIP * Update CODEOWNERS --------- Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 4 +- .../components/voip/assist_satellite.py | 119 +++++++++--- homeassistant/components/voip/manifest.json | 2 +- tests/components/voip/test_voip.py | 171 ++++++++---------- 4 files changed, 171 insertions(+), 125 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 752bbb31460..89b0cf16af0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1678,8 +1678,8 @@ build.json @home-assistant/supervisor /tests/components/vlc_telnet/ @rodripf @MartinHjelmare /homeassistant/components/vodafone_station/ @paoloantinori @chemelli74 /tests/components/vodafone_station/ @paoloantinori @chemelli74 -/homeassistant/components/voip/ @balloob @synesthesiam -/tests/components/voip/ @balloob @synesthesiam +/homeassistant/components/voip/ @balloob @synesthesiam @jaminh +/tests/components/voip/ @balloob @synesthesiam @jaminh /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund /homeassistant/components/volvooncall/ @molobrakos diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index a2364200ce2..7b34d7a11ba 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -51,9 +51,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) _PIPELINE_TIMEOUT_SEC: Final = 30 +_HANGUP_SEC: Final = 0.5 _ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5 _ANNOUNCEMENT_AFTER_DELAY: Final = 1.0 -_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5 _ANNOUNCEMENT_RING_TIMEOUT: Final = 30 @@ -132,9 +132,10 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol self._processing_tone_done = asyncio.Event() self._announcement: AssistSatelliteAnnouncement | None = None - self._announcement_future: asyncio.Future[Any] = asyncio.Future() self._announcment_start_time: float = 0.0 - self._check_announcement_ended_task: asyncio.Task | None = None + self._check_announcement_pickup_task: asyncio.Task | None = None + self._check_hangup_task: asyncio.Task | None = None + self._call_end_future: asyncio.Future[Any] = asyncio.Future() self._last_chunk_time: float | None = None self._rtp_port: int | None = None self._run_pipeline_after_announce: bool = False @@ -233,7 +234,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol translation_key="non_tts_announcement", ) - self._announcement_future = asyncio.Future() + self._call_end_future = asyncio.Future() self._run_pipeline_after_announce = run_pipeline_after if self._rtp_port is None: @@ -274,53 +275,77 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol rtp_port=self._rtp_port, ) - # Check if caller hung up or didn't pick up - self._check_announcement_ended_task = ( + # Check if caller didn't pick up + self._check_announcement_pickup_task = ( self.config_entry.async_create_background_task( self.hass, - self._check_announcement_ended(), - "voip_announcement_ended", + self._check_announcement_pickup(), + "voip_announcement_pickup", ) ) try: - await self._announcement_future + await self._call_end_future except TimeoutError: # Stop ringing + _LOGGER.debug("Caller did not pick up in time") sip_protocol.cancel_call(call_info) raise - async def _check_announcement_ended(self) -> None: + async def _check_announcement_pickup(self) -> None: """Continuously checks if an audio chunk was received within a time limit. - If not, the caller is presumed to have hung up and the announcement is ended. + If not, the caller is presumed to have not picked up the phone and the announcement is ended. """ - while self._announcement is not None: + while True: current_time = time.monotonic() if (self._last_chunk_time is None) and ( (current_time - self._announcment_start_time) > _ANNOUNCEMENT_RING_TIMEOUT ): # Ring timeout + _LOGGER.debug("Ring timeout") self._announcement = None - self._check_announcement_ended_task = None - self._announcement_future.set_exception( + self._check_announcement_pickup_task = None + self._call_end_future.set_exception( TimeoutError("User did not pick up in time") ) _LOGGER.debug("Timed out waiting for the user to pick up the phone") break - - if (self._last_chunk_time is not None) and ( - (current_time - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC - ): - # Caller hung up - self._announcement = None - self._announcement_future.set_result(None) - self._check_announcement_ended_task = None - _LOGGER.debug("Announcement ended") + if self._last_chunk_time is not None: + _LOGGER.debug("Picked up the phone") + self._check_announcement_pickup_task = None break - await asyncio.sleep(_ANNOUNCEMENT_HANGUP_SEC / 2) + await asyncio.sleep(_HANGUP_SEC / 2) + + async def _check_hangup(self) -> None: + """Continuously checks if an audio chunk was received within a time limit. + + If not, the caller is presumed to have hung up and the call is ended. + """ + try: + while True: + current_time = time.monotonic() + if (self._last_chunk_time is not None) and ( + (current_time - self._last_chunk_time) > _HANGUP_SEC + ): + # Caller hung up + _LOGGER.debug("Hang up") + self._announcement = None + if self._run_pipeline_task is not None: + _LOGGER.debug("Cancelling running pipeline") + self._run_pipeline_task.cancel() + self._call_end_future.set_result(None) + self.disconnect() + break + + await asyncio.sleep(_HANGUP_SEC / 2) + except asyncio.CancelledError: + # Don't swallow cancellation + if (current_task := asyncio.current_task()) and current_task.cancelling(): + raise + _LOGGER.debug("Check hangup cancelled") async def async_start_conversation( self, start_announcement: AssistSatelliteAnnouncement @@ -332,6 +357,24 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol # VoIP # ------------------------------------------------------------------------- + def disconnect(self): + """Server disconnected.""" + super().disconnect() + if self._check_hangup_task is not None: + self._check_hangup_task.cancel() + self._check_hangup_task = None + + def connection_made(self, transport): + """Server is ready.""" + super().connection_made(transport) + self._last_chunk_time = time.monotonic() + # Check if caller hung up + self._check_hangup_task = self.config_entry.async_create_background_task( + self.hass, + self._check_hangup(), + "voip_hangup", + ) + def on_chunk(self, audio_bytes: bytes) -> None: """Handle raw audio chunk.""" self._last_chunk_time = time.monotonic() @@ -368,13 +411,22 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol self.voip_device.set_is_active(True) async def stt_stream(): + retry: bool = True while True: - async with asyncio.timeout(self._audio_chunk_timeout): - chunk = await self._audio_queue.get() - if not chunk: - break + try: + async with asyncio.timeout(self._audio_chunk_timeout): + chunk = await self._audio_queue.get() + if not chunk: + _LOGGER.debug("STT stream got None") + break yield chunk + except TimeoutError: + _LOGGER.debug("STT Stream timed out") + if not retry: + _LOGGER.debug("No more retries, ending STT stream") + break + retry = False # Play listening tone at the start of each cycle await self._play_tone(Tones.LISTENING, silence_before=0.2) @@ -385,6 +437,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol ) if self._pipeline_had_error: + _LOGGER.debug("Pipeline error") self._pipeline_had_error = False await self._play_tone(Tones.ERROR) else: @@ -394,7 +447,14 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol # length of the TTS audio. await self._tts_done.wait() except TimeoutError: + # This shouldn't happen anymore, we are detecting hang ups with a separate task + _LOGGER.exception("Timeout error") self.disconnect() # caller hung up + except asyncio.CancelledError: + _LOGGER.debug("Pipeline cancelled") + # Don't swallow cancellation + if (current_task := asyncio.current_task()) and current_task.cancelling(): + raise finally: # Stop audio stream await self._audio_queue.put(None) @@ -433,8 +493,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol if self._run_pipeline_after_announce: # Clear announcement to allow pipeline to run + _LOGGER.debug("Clearing announcement") self._announcement = None - self._announcement_future.set_result(None) def _clear_audio_queue(self) -> None: """Ensure audio queue is empty.""" @@ -463,6 +523,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol ) else: # Empty TTS response + _LOGGER.debug("Empty TTS response") self._tts_done.set() elif event.type == PipelineEventType.ERROR: # Play error tone instead of wait for TTS when pipeline is finished. diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index dfd397fde14..09e1f112699 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -1,7 +1,7 @@ { "domain": "voip", "name": "Voice over IP", - "codeowners": ["@balloob", "@synesthesiam"], + "codeowners": ["@balloob", "@synesthesiam", "@jaminh"], "config_flow": true, "dependencies": ["assist_pipeline", "assist_satellite", "intent", "network"], "documentation": "https://www.home-assistant.io/integrations/voip", diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 345f0399645..65567c8e1d1 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -335,9 +335,8 @@ async def test_pipeline( patch.object(satellite, "tts_response_finished", tts_response_finished), ): satellite._tones = Tones(0) - satellite.transport = Mock() + satellite.connection_made(Mock()) - satellite.connection_made(satellite.transport) assert satellite.state == AssistSatelliteState.IDLE # Ensure audio queue is cleared before pipeline starts @@ -473,7 +472,7 @@ async def test_tts_timeout( for tone in Tones: satellite._tone_bytes[tone] = tone_bytes - satellite.transport = Mock() + satellite.connection_made(Mock()) satellite.send_audio = Mock() original_send_tts = satellite._send_tts @@ -511,6 +510,7 @@ async def test_tts_wrong_extension( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) done = asyncio.Event() @@ -559,8 +559,6 @@ async def test_tts_wrong_extension( "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - satellite.transport = Mock() - original_send_tts = satellite._send_tts async def send_tts(*args, **kwargs): @@ -572,6 +570,8 @@ async def test_tts_wrong_extension( satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + satellite.connection_made(Mock()) + # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -579,10 +579,18 @@ async def test_tts_wrong_extension( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to exhaust the audio stream - async with asyncio.timeout(1): + async with asyncio.timeout(3): await done.wait() @@ -595,6 +603,7 @@ async def test_tts_wrong_wav_format( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) done = asyncio.Event() @@ -643,8 +652,6 @@ async def test_tts_wrong_wav_format( "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", new=async_pipeline_from_audio_stream, ): - satellite.transport = Mock() - original_send_tts = satellite._send_tts async def send_tts(*args, **kwargs): @@ -656,6 +663,8 @@ async def test_tts_wrong_wav_format( satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign] + satellite.connection_made(Mock()) + # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -663,10 +672,18 @@ async def test_tts_wrong_wav_format( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to exhaust the audio stream - async with asyncio.timeout(1): + async with asyncio.timeout(3): await done.wait() @@ -679,6 +696,7 @@ async def test_empty_tts_output( assert await async_setup_component(hass, "voip", {}) satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) async def async_pipeline_from_audio_stream(*args, **kwargs): @@ -728,7 +746,7 @@ async def test_empty_tts_output( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() + satellite.connection_made(Mock()) # silence satellite.on_chunk(bytes(_ONE_SECOND)) @@ -737,10 +755,18 @@ async def test_empty_tts_output( satellite.on_chunk(bytes([255] * _ONE_SECOND * 2)) # silence (assumes relaxed VAD sensitivity) - satellite.on_chunk(bytes(_ONE_SECOND * 4)) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) # Wait for mock pipeline to finish - async with asyncio.timeout(1): + async with asyncio.timeout(2): await satellite._tts_done.wait() mock_send_tts.assert_not_called() @@ -785,7 +811,7 @@ async def test_pipeline_error( ), ): satellite._tones = Tones.ERROR - satellite.transport = Mock() + satellite.connection_made(Mock()) satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign] satellite.on_chunk(bytes(_ONE_SECOND)) @@ -845,16 +871,20 @@ async def test_announce( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() announce_task = hass.async_create_background_task( satellite.async_announce(announcement), "voip_announce" ) await asyncio.sleep(0) + satellite.connection_made(Mock()) mock_protocol.outgoing_call.assert_called_once() # Trigger announcement satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await announce_task mock_send_tts.assert_called_once_with( @@ -897,11 +927,11 @@ async def test_voip_id_is_ip_address( "homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts", ) as mock_send_tts, ): - satellite.transport = Mock() announce_task = hass.async_create_background_task( satellite.async_announce(announcement), "voip_announce" ) await asyncio.sleep(0) + satellite.connection_made(Mock()) mock_protocol.outgoing_call.assert_called_once() assert ( mock_protocol.outgoing_call.call_args.kwargs["destination"].host @@ -910,7 +940,11 @@ async def test_voip_id_is_ip_address( # Trigger announcement satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await announce_task mock_send_tts.assert_called_once_with( @@ -955,7 +989,7 @@ async def test_announce_timeout( 0.01, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) with pytest.raises(TimeoutError): await satellite.async_announce(announcement) @@ -1042,7 +1076,7 @@ async def test_start_conversation( new=async_pipeline_from_audio_stream, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) conversation_task = hass.async_create_background_task( satellite.async_start_conversation(announcement), "voip_start_conversation" ) @@ -1051,16 +1085,20 @@ async def test_start_conversation( # Trigger announcement and wait for it to finish satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(2): await tts_sent.wait() - tts_sent.clear() - # Trigger pipeline satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): - # Wait for TTS - await tts_sent.wait() + await asyncio.sleep(0.2) + satellite.on_chunk(bytes(_ONE_SECOND)) + await asyncio.sleep(3) + async with asyncio.timeout(3): + # Wait for Conversation end await conversation_task @@ -1073,21 +1111,8 @@ async def test_start_conversation_user_doesnt_pick_up( """Test start conversation when the user doesn't pick up.""" assert await async_setup_component(hass, "voip", {}) - pipeline = assist_pipeline.Pipeline( - conversation_engine="test engine", - conversation_language="en", - language="en", - name="test pipeline", - stt_engine="test stt", - stt_language="en", - tts_engine="test tts", - tts_language="en", - tts_voice=None, - wake_word_entity=None, - wake_word_id=None, - ) - satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + satellite.addr = ("192.168.1.1", 12345) assert isinstance(satellite, VoipAssistSatellite) assert ( satellite.supported_features @@ -1098,62 +1123,22 @@ async def test_start_conversation_user_doesnt_pick_up( mock_protocol: AsyncMock = hass.data[DOMAIN].protocol mock_protocol.outgoing_call = Mock() - pipeline_started = asyncio.Event() - - async def async_pipeline_from_audio_stream( - hass: HomeAssistant, - context: Context, - *args, - conversation_extra_system_prompt: str | None = None, - **kwargs, - ): - # System prompt should be not be set due to timeout (user not picking up) - assert conversation_extra_system_prompt is None - - pipeline_started.set() + announcement = assist_satellite.AssistSatelliteAnnouncement( + message="test announcement", + media_id=_MEDIA_ID, + tts_token="test-token", + original_media_id=_MEDIA_ID, + media_id_source="tts", + ) + # Very short timeout which will trigger because we don't send any audio in with ( patch( - "homeassistant.components.assist_satellite.entity.async_get_pipeline", - return_value=pipeline, - ), - patch( - "homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_start_conversation", - side_effect=TimeoutError, - ), - patch( - "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", - new=async_pipeline_from_audio_stream, - ), - patch( - "homeassistant.components.tts.generate_media_source_id", - return_value="media-source://bla", - ), - patch( - "homeassistant.components.tts.async_resolve_engine", - return_value="test tts", - ), - patch( - "homeassistant.components.tts.async_create_stream", - return_value=MockResultStream(hass, "wav", b""), + "homeassistant.components.voip.assist_satellite._ANNOUNCEMENT_RING_TIMEOUT", + 0.1, ), ): - satellite.transport = Mock() + satellite.connection_made(Mock()) - # Error should clear system prompt with pytest.raises(TimeoutError): - await hass.services.async_call( - assist_satellite.DOMAIN, - "start_conversation", - { - "entity_id": satellite.entity_id, - "start_message": "test announcement", - "extra_system_prompt": "test prompt", - }, - blocking=True, - ) - - # Trigger a pipeline so we can check if the system prompt was cleared - satellite.on_chunk(bytes(_ONE_SECOND)) - async with asyncio.timeout(1): - await pipeline_started.wait() + await satellite.async_start_conversation(announcement) From 0dd21f4c89262a7c08f054105a31f292f06aaca9 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Mon, 5 May 2025 22:37:54 -0400 Subject: [PATCH 0145/1175] Rehlko adjust timeouts for coordinator polls (#144297) --- homeassistant/components/rehlko/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index 49ceb8ac870..bda2704a206 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo """Set up Rehlko from a config entry.""" websession = async_get_clientsession(hass) rehlko = AioKem(session=websession) + # If requests take more than 20 seconds; timeout and let the setup retry. + rehlko.set_timeout(20) async def async_refresh_token_update(refresh_token: str) -> None: """Handle refresh token update.""" @@ -87,6 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Retrys enabled after successful connection to prevent blocking startup rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20]) + # Rehlko service can be slow to respond, increase timeout for polls. + rehlko.set_timeout(100) return True From 12f9a1171691508b8b0d1f513da47f780ab9cdfb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 04:39:03 +0200 Subject: [PATCH 0146/1175] Fix mqtt subentry device name is not required but should be (#144289) Fix mqtt subentry device name is not required --- homeassistant/components/mqtt/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 2bbfd9e3515..02c8a1cdc8a 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1151,7 +1151,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ } MQTT_DEVICE_PLATFORM_FIELDS = { - ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str), + ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str), ATTR_SW_VERSION: PlatformField( selector=TEXT_SELECTOR, required=False, validator=str ), From 92010e1fcae0749edda08551ed89dfed338fb68a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 6 May 2025 04:39:25 +0200 Subject: [PATCH 0147/1175] Fix un-/re-load of Feedreader integration (#144285) fix unload platforms call --- homeassistant/components/feedreader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 31617cb220b..57c58d3a2b1 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -45,7 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) # if this is the last entry, remove the storage if len(entries) == 1: hass.data.pop(MY_KEY) - return await hass.config_entries.async_unload_platforms(entry, Platform.EVENT) + return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT]) async def _async_update_listener( From edcb0902097dc85489808b64f7211df91b934353 Mon Sep 17 00:00:00 2001 From: Jamin Date: Mon, 5 May 2025 21:50:46 -0500 Subject: [PATCH 0148/1175] Bump VoIP utils to 0.3.2 (#144298) --- homeassistant/components/voip/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/voip/manifest.json b/homeassistant/components/voip/manifest.json index 09e1f112699..59e54bfefea 100644 --- a/homeassistant/components/voip/manifest.json +++ b/homeassistant/components/voip/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["voip_utils"], "quality_scale": "internal", - "requirements": ["voip-utils==0.3.1"] + "requirements": ["voip-utils==0.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cb271164cc9..fef1ccd48b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3025,7 +3025,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.1 +voip-utils==0.3.2 # homeassistant.components.volkszaehler volkszaehler==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6c2611d678..d6b6c92ed6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2448,7 +2448,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.5.0 # homeassistant.components.voip -voip-utils==0.3.1 +voip-utils==0.3.2 # homeassistant.components.volvooncall volvooncall==0.10.3 From 212c3ddcca57917c0bff8554110dba4c598c9f93 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 6 May 2025 08:34:40 +0200 Subject: [PATCH 0149/1175] Use international English spelling for "authorization" in `reolink` (#144305) Use international English for "authorization" in `reolink` --- homeassistant/components/reolink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 8b7d276a9e3..82941bd5af2 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -30,7 +30,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 authorization level \"{userlevel}\"", "password_incompatible": "Password contains incompatible special character or is too long, maximum 31 characters and only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}", "unknown": "[%key:common::config_flow::error::unknown%]", "update_needed": "Failed to log in because of outdated firmware, please update the firmware to version {needed_firmware} using the Reolink Download Center: {download_center_url}, currently version {current_firmware} is installed", From 0edfbded2322c9a61cfff2ce93c567be47f61b42 Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Mon, 5 May 2025 23:45:39 -0700 Subject: [PATCH 0150/1175] Fixes #140182 by checking file status before sending the prompt. (#144131) * Added unit tests * Addressed review comments * Fixed tests * PR comments --- .../__init__.py | 36 ++++++ .../const.py | 1 + .../snapshots/test_init.ambr | 17 +++ .../test_init.py | 112 ++++++++++++++++++ 4 files changed, 166 insertions(+) diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 88a51446cda..79d092a60c3 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -2,11 +2,13 @@ from __future__ import annotations +import asyncio import mimetypes from pathlib import Path from google.genai import Client from google.genai.errors import APIError, ClientError +from google.genai.types import File, FileState from requests.exceptions import Timeout import voluptuous as vol @@ -32,6 +34,8 @@ from .const import ( CONF_CHAT_MODEL, CONF_PROMPT, DOMAIN, + FILE_POLLING_INTERVAL_SECONDS, + LOGGER, RECOMMENDED_CHAT_MODEL, TIMEOUT_MILLIS, ) @@ -91,8 +95,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) prompt_parts.append(uploaded_file) + async def wait_for_file_processing(uploaded_file: File) -> None: + """Wait for file processing to complete.""" + while True: + uploaded_file = await client.aio.files.get( + name=uploaded_file.name, + config={"http_options": {"timeout": TIMEOUT_MILLIS}}, + ) + if uploaded_file.state not in ( + FileState.STATE_UNSPECIFIED, + FileState.PROCESSING, + ): + break + LOGGER.debug( + "Waiting for file `%s` to be processed, current state: %s", + uploaded_file.name, + uploaded_file.state, + ) + await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS) + + if uploaded_file.state == FileState.FAILED: + raise HomeAssistantError( + f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}" + ) + await hass.async_add_executor_job(append_files_to_prompt) + tasks = [ + asyncio.create_task(wait_for_file_processing(part)) + for part in prompt_parts + if isinstance(part, File) and part.state != FileState.ACTIVE + ] + async with asyncio.timeout(TIMEOUT_MILLIS / 1000): + await asyncio.gather(*tasks) + try: response = await client.aio.models.generate_content( model=RECOMMENDED_CHAT_MODEL, contents=prompt_parts diff --git a/homeassistant/components/google_generative_ai_conversation/const.py b/homeassistant/components/google_generative_ai_conversation/const.py index a7dd584ebee..239b3ff763e 100644 --- a/homeassistant/components/google_generative_ai_conversation/const.py +++ b/homeassistant/components/google_generative_ai_conversation/const.py @@ -26,3 +26,4 @@ CONF_USE_GOOGLE_SEARCH_TOOL = "enable_google_search_tool" RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False TIMEOUT_MILLIS = 10000 +FILE_POLLING_INTERVAL_SECONDS = 0.05 diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr index ce882adf6e6..d8e54b15f61 100644 --- a/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr +++ b/tests/components/google_generative_ai_conversation/snapshots/test_init.ambr @@ -1,4 +1,21 @@ # serializer version: 1 +# name: test_generate_content_file_processing_succeeds + list([ + tuple( + '', + tuple( + ), + dict({ + 'contents': list([ + 'Describe this image from my doorbell camera', + File(name='doorbell_snapshot.jpg', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + File(name='context.txt', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=, source=None, video_metadata=None, error=None), + ]), + 'model': 'models/gemini-2.0-flash', + }), + ), + ]) +# --- # name: test_generate_content_service_with_image list([ tuple( diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index a08acc0df3f..94308260f74 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock, mock_open, patch +from google.genai.types import File, FileState import pytest from requests.exceptions import Timeout from syrupy.assertion import SnapshotAssertion @@ -91,6 +92,117 @@ async def test_generate_content_service_with_image( assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_content_file_processing_succeeds( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test generate content service.""" + stubbed_generated_content = ( + "A mail carrier is at your front door delivering a package" + ) + + with ( + patch( + "google.genai.models.AsyncModels.generate_content", + return_value=Mock( + text=stubbed_generated_content, + prompt_feedback=None, + candidates=[Mock()], + ), + ) as mock_generate, + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("builtins.open", mock_open(read_data="this is an image")), + patch("mimetypes.guess_type", return_value=["image/jpeg"]), + patch( + "google.genai.files.Files.upload", + side_effect=[ + File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE), + File(name="context.txt", state=FileState.PROCESSING), + ], + ), + patch( + "google.genai.files.AsyncFiles.get", + side_effect=[ + File(name="context.txt", state=FileState.PROCESSING), + File(name="context.txt", state=FileState.ACTIVE), + ], + ), + ): + response = await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + { + "prompt": "Describe this image from my doorbell camera", + "filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"], + }, + blocking=True, + return_response=True, + ) + + assert response == { + "text": stubbed_generated_content, + } + assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot + + +@pytest.mark.usefixtures("mock_init_component") +async def test_generate_content_file_processing_fails( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test generate content service.""" + stubbed_generated_content = ( + "A mail carrier is at your front door delivering a package" + ) + + with ( + patch( + "google.genai.models.AsyncModels.generate_content", + return_value=Mock( + text=stubbed_generated_content, + prompt_feedback=None, + candidates=[Mock()], + ), + ), + patch("pathlib.Path.exists", return_value=True), + patch.object(hass.config, "is_allowed_path", return_value=True), + patch("builtins.open", mock_open(read_data="this is an image")), + patch("mimetypes.guess_type", return_value=["image/jpeg"]), + patch( + "google.genai.files.Files.upload", + side_effect=[ + File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE), + File(name="context.txt", state=FileState.PROCESSING), + ], + ), + patch( + "google.genai.files.AsyncFiles.get", + side_effect=[ + File(name="context.txt", state=FileState.PROCESSING), + File( + name="context.txt", + state=FileState.FAILED, + error={"message": "File processing failed"}, + ), + ], + ), + pytest.raises( + HomeAssistantError, + match="File `context.txt` processing failed, reason: File processing failed", + ), + ): + await hass.services.async_call( + "google_generative_ai_conversation", + "generate_content", + { + "prompt": "Describe this image from my doorbell camera", + "filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"], + }, + blocking=True, + return_response=True, + ) + + @pytest.mark.usefixtures("mock_init_component") async def test_generate_content_service_error( hass: HomeAssistant, From 73996fb916ecc313603a0c129c40b9c7b31ab9e0 Mon Sep 17 00:00:00 2001 From: Cerallin <66366855+Cerallin@users.noreply.github.com> Date: Tue, 6 May 2025 15:55:43 +0800 Subject: [PATCH 0151/1175] Bump xiaomi-ble to 0.38.0 (#143885) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index a908d4747ad..3f13c7921a8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.37.0"] + "requirements": ["xiaomi-ble==0.38.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index fef1ccd48b6..7c253acf5b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3101,7 +3101,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.37.0 +xiaomi-ble==0.38.0 # homeassistant.components.knx xknx==3.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6b6c92ed6c..d626ecc7b86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2509,7 +2509,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.37.0 +xiaomi-ble==0.38.0 # homeassistant.components.knx xknx==3.6.0 From 66c86c0461ecd93333e56291f10840c41b47e612 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 09:55:57 +0200 Subject: [PATCH 0152/1175] Drop alias from local DOMAIN import (#144311) --- .../components/bmw_connected_drive/button.py | 4 ++-- .../components/bmw_connected_drive/lock.py | 6 +++--- .../components/bmw_connected_drive/notify.py | 6 +++--- .../components/bmw_connected_drive/number.py | 4 ++-- .../components/bmw_connected_drive/select.py | 4 ++-- .../components/bmw_connected_drive/switch.py | 6 +++--- .../components/danfoss_air/binary_sensor.py | 4 ++-- homeassistant/components/danfoss_air/sensor.py | 4 ++-- homeassistant/components/danfoss_air/switch.py | 4 ++-- homeassistant/components/dovado/notify.py | 4 ++-- homeassistant/components/dovado/sensor.py | 4 ++-- .../components/geofency/device_tracker.py | 14 +++++++------- .../components/gpslogger/device_tracker.py | 12 ++++++------ homeassistant/components/iperf3/sensor.py | 4 ++-- .../components/locative/device_tracker.py | 8 ++++---- .../components/lutron_caseta/binary_sensor.py | 6 +++--- homeassistant/components/mailgun/notify.py | 4 ++-- .../components/owntracks/device_tracker.py | 12 ++++++------ .../components/qwikswitch/binary_sensor.py | 6 +++--- homeassistant/components/qwikswitch/light.py | 6 +++--- homeassistant/components/qwikswitch/sensor.py | 6 +++--- homeassistant/components/qwikswitch/switch.py | 6 +++--- homeassistant/components/tibber/notify.py | 8 ++++---- homeassistant/components/waterfurnace/sensor.py | 4 ++-- homeassistant/components/xs1/climate.py | 6 +++--- homeassistant/components/xs1/sensor.py | 6 +++--- homeassistant/components/xs1/switch.py | 4 ++-- .../components/zoneminder/binary_sensor.py | 4 ++-- homeassistant/components/zoneminder/camera.py | 4 ++-- homeassistant/components/zoneminder/sensor.py | 4 ++-- homeassistant/components/zoneminder/switch.py | 4 ++-- 31 files changed, 89 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index f8980201f3f..726c3ff3f6e 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry from .entity import BMWBaseEntity if TYPE_CHECKING: @@ -111,7 +111,7 @@ class BMWButton(BMWBaseEntity, ButtonEntity): await self.entity_description.remote_function(self.vehicle) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 9d8965d6ebf..149647a3397 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator from .entity import BMWBaseEntity @@ -71,7 +71,7 @@ class BMWLock(BMWBaseEntity, LockEntity): self._attr_is_locked = None self.async_write_ha_state() raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex @@ -95,7 +95,7 @@ class BMWLock(BMWBaseEntity, LockEntity): self._attr_is_locked = None self.async_write_ha_state() raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index dfa0939e81f..2a94cf42853 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry PARALLEL_UPDATES = 1 @@ -92,7 +92,7 @@ class BMWNotificationService(BaseNotificationService): except (vol.Invalid, TypeError, ValueError) as ex: raise ServiceValidationError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_poi", translation_placeholders={ "poi_exception": str(ex), @@ -107,7 +107,7 @@ class BMWNotificationService(BaseNotificationService): await vehicle.remote_services.trigger_send_poi(poi) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/bmw_connected_drive/number.py b/homeassistant/components/bmw_connected_drive/number.py index 8361306ba9d..a30775caf60 100644 --- a/homeassistant/components/bmw_connected_drive/number.py +++ b/homeassistant/components/bmw_connected_drive/number.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator from .entity import BMWBaseEntity @@ -110,7 +110,7 @@ class BMWNumber(BMWBaseEntity, NumberEntity): await self.entity_description.remote_service(self.vehicle, value) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/bmw_connected_drive/select.py b/homeassistant/components/bmw_connected_drive/select.py index f144d3a71df..81e01b2bfad 100644 --- a/homeassistant/components/bmw_connected_drive/select.py +++ b/homeassistant/components/bmw_connected_drive/select.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator from .entity import BMWBaseEntity @@ -124,7 +124,7 @@ class BMWSelect(BMWBaseEntity, SelectEntity): await self.entity_description.remote_service(self.vehicle, option) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/bmw_connected_drive/switch.py b/homeassistant/components/bmw_connected_drive/switch.py index f46969f3e9b..cedcf2a7364 100644 --- a/homeassistant/components/bmw_connected_drive/switch.py +++ b/homeassistant/components/bmw_connected_drive/switch.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry +from . import DOMAIN, BMWConfigEntry from .coordinator import BMWDataUpdateCoordinator from .entity import BMWBaseEntity @@ -112,7 +112,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity): await self.entity_description.remote_service_on(self.vehicle) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex @@ -124,7 +124,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity): await self.entity_description.remote_service_off(self.vehicle) except MyBMWAPIError as ex: raise HomeAssistantError( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="remote_service_error", translation_placeholders={"exception": str(ex)}, ) from ex diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index 358d6ca07ab..736604d7ea1 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DANFOSS_AIR_DOMAIN +from . import DOMAIN def setup_platform( @@ -22,7 +22,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the available Danfoss Air sensors etc.""" - data = hass.data[DANFOSS_AIR_DOMAIN] + data = hass.data[DOMAIN] sensors = [ [ diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 85b4e89d434..569ba21b234 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DANFOSS_AIR_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the available Danfoss Air sensors etc.""" - data = hass.data[DANFOSS_AIR_DOMAIN] + data = hass.data[DOMAIN] sensors = [ [ diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index dc3277078b0..5e7c5728d81 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DANFOSS_AIR_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Danfoss Air HRV switch platform.""" - data = hass.data[DANFOSS_AIR_DOMAIN] + data = hass.data[DOMAIN] switches = [ [ diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index 556848bf89f..0b74f97d06f 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -8,7 +8,7 @@ from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DOVADO_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> DovadoSMSNotificationService: """Get the Dovado Router SMS notification service.""" - return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client) + return DovadoSMSNotificationService(hass.data[DOMAIN].client) class DovadoSMSNotificationService(BaseNotificationService): diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index e35fdeb2dc0..0129b990435 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DOVADO_DOMAIN +from . import DOMAIN MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) @@ -90,7 +90,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dovado sensor platform.""" - dovado = hass.data[DOVADO_DOMAIN] + dovado = hass.data[DOMAIN] sensors = config[CONF_SENSORS] entities = [ diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index c74dad1cebb..9e2cad50533 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -10,7 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE +from . import DOMAIN, TRACKER_UPDATE async def async_setup_entry( @@ -23,14 +23,14 @@ async def async_setup_entry( @callback def _receive_data(device, gps, location_name, attributes): """Fire HA event to set location.""" - if device in hass.data[GF_DOMAIN]["devices"]: + if device in hass.data[DOMAIN]["devices"]: return - hass.data[GF_DOMAIN]["devices"].add(device) + hass.data[DOMAIN]["devices"].add(device) async_add_entities([GeofencyEntity(device, gps, location_name, attributes)]) - hass.data[GF_DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = ( + hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = ( async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) ) @@ -45,7 +45,7 @@ async def async_setup_entry( } if dev_ids: - hass.data[GF_DOMAIN]["devices"].update(dev_ids) + hass.data[DOMAIN]["devices"].update(dev_ids) async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids) @@ -66,7 +66,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): self._unsub_dispatcher = None self._attr_unique_id = device self._attr_device_info = DeviceInfo( - identifiers={(GF_DOMAIN, device)}, + identifiers={(DOMAIN, device)}, name=device, ) @@ -93,7 +93,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() - self.hass.data[GF_DOMAIN]["devices"].remove(self.unique_id) + self.hass.data[DOMAIN]["devices"].remove(self.unique_id) @callback def _async_receive_data(self, device, gps, location_name, attributes): diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index be38382098d..cf0515f5c41 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE +from . import DOMAIN, TRACKER_UPDATE from .const import ( ATTR_ACTIVITY, ATTR_ALTITUDE, @@ -35,14 +35,14 @@ async def async_setup_entry( @callback def _receive_data(device, gps, battery, accuracy, attrs): """Receive set location.""" - if device in hass.data[GPL_DOMAIN]["devices"]: + if device in hass.data[DOMAIN]["devices"]: return - hass.data[GPL_DOMAIN]["devices"].add(device) + hass.data[DOMAIN]["devices"].add(device) async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)]) - hass.data[GPL_DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( + hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) ) @@ -58,7 +58,7 @@ async def async_setup_entry( entities = [] for dev_id in dev_ids: - hass.data[GPL_DOMAIN]["devices"].add(dev_id) + hass.data[DOMAIN]["devices"].add(dev_id) entity = GPSLoggerEntity(dev_id, None, None, None, None) entities.append(entity) @@ -83,7 +83,7 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): self._unsub_dispatcher = None self._attr_unique_id = device self._attr_device_info = DeviceInfo( - identifiers={(GPL_DOMAIN, device)}, + identifiers={(DOMAIN, device)}, name=device, ) diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 27b3eac26b5..9ba3b55ed4f 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ATTR_VERSION, DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES +from . import ATTR_VERSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES ATTR_PROTOCOL = "Protocol" ATTR_REMOTE_HOST = "Remote Server" @@ -29,7 +29,7 @@ async def async_setup_platform( entities = [ Iperf3Sensor(iperf3_host, description) - for iperf3_host in hass.data[IPERF3_DOMAIN].values() + for iperf3_host in hass.data[DOMAIN].values() for description in SENSOR_TYPES if description.key in discovery_info[CONF_MONITORED_CONDITIONS] ] diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index f7ae9039729..9663efdd76e 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE +from . import DOMAIN, TRACKER_UPDATE async def async_setup_entry( @@ -19,14 +19,14 @@ async def async_setup_entry( @callback def _receive_data(device, location, location_name): """Receive set location.""" - if device in hass.data[LT_DOMAIN]["devices"]: + if device in hass.data[DOMAIN]["devices"]: return - hass.data[LT_DOMAIN]["devices"].add(device) + hass.data[DOMAIN]["devices"].add(device) async_add_entities([LocativeEntity(device, location, location_name)]) - hass.data[LT_DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( + hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) ) diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index cb0f0da5227..4a92eb5c3b7 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN +from . import DOMAIN from .const import CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA from .entity import LutronCasetaEntity from .models import LutronCasetaConfigEntry @@ -49,11 +49,11 @@ class LutronOccupancySensor(LutronCasetaEntity, BinarySensorEntity): name = f"{area} {device['device_name']}" self._attr_name = name self._attr_device_info = DeviceInfo( - identifiers={(CASETA_DOMAIN, self.unique_id)}, + identifiers={(DOMAIN, self.unique_id)}, manufacturer=MANUFACTURER, model="Lutron Occupancy", name=self.name, - via_device=(CASETA_DOMAIN, self._bridge_device["serial"]), + via_device=(DOMAIN, self._bridge_device["serial"]), configuration_url=CONFIG_URL, entry_type=DeviceEntryType.SERVICE, ) diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index 26ff13f2a6f..b839e184810 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -23,7 +23,7 @@ from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_RECIPIENT, CONF_ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_SANDBOX, DOMAIN as MAILGUN_DOMAIN +from . import CONF_SANDBOX, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ def get_service( discovery_info: DiscoveryInfoType | None = None, ) -> MailgunNotificationService | None: """Get the Mailgun notification service.""" - data = hass.data[MAILGUN_DOMAIN] + data = hass.data[DOMAIN] mailgun_service = MailgunNotificationService( data.get(CONF_DOMAIN), data.get(CONF_SANDBOX), diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 7ccbbb69aa1..80a06478506 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -19,7 +19,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN as OT_DOMAIN +from . import DOMAIN async def async_setup_entry( @@ -38,22 +38,22 @@ async def async_setup_entry( entities = [] for dev_id in dev_ids: - entity = hass.data[OT_DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id) + entity = hass.data[DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id) entities.append(entity) @callback def _receive_data(dev_id, **data): """Receive set location.""" - entity = hass.data[OT_DOMAIN]["devices"].get(dev_id) + entity = hass.data[DOMAIN]["devices"].get(dev_id) if entity is not None: entity.update_data(data) return - entity = hass.data[OT_DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id, data) + entity = hass.data[DOMAIN]["devices"][dev_id] = OwnTracksEntity(dev_id, data) async_add_entities([entity]) - hass.data[OT_DOMAIN]["context"].set_async_see(_receive_data) + hass.data[DOMAIN]["context"].set_async_see(_receive_data) async_add_entities(entities) @@ -121,7 +121,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - device_info = DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}) + device_info = DeviceInfo(identifiers={(DOMAIN, self._dev_id)}) if "host_name" in self._data: device_info["name"] = self._data["host_name"] return device_info diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index 195433ebc17..bbe8d309e50 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as QWIKSWITCH +from . import DOMAIN from .entity import QSEntity _LOGGER = logging.getLogger(__name__) @@ -27,9 +27,9 @@ async def async_setup_platform( if discovery_info is None: return - qsusb = hass.data[QWIKSWITCH] + qsusb = hass.data[DOMAIN] _LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s", qsusb, discovery_info) - devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]] + devs = [QSBinarySensor(sensor) for sensor in discovery_info[DOMAIN]] add_entities(devs) diff --git a/homeassistant/components/qwikswitch/light.py b/homeassistant/components/qwikswitch/light.py index 073f7bb873a..0f91faeedc8 100644 --- a/homeassistant/components/qwikswitch/light.py +++ b/homeassistant/components/qwikswitch/light.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as QWIKSWITCH +from . import DOMAIN from .entity import QSToggleEntity @@ -21,8 +21,8 @@ async def async_setup_platform( if discovery_info is None: return - qsusb = hass.data[QWIKSWITCH] - devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + qsusb = hass.data[DOMAIN] + devs = [QSLight(qsid, qsusb) for qsid in discovery_info[DOMAIN]] add_entities(devs) diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 64b95fb17f6..e87fae83464 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as QWIKSWITCH +from . import DOMAIN from .entity import QSEntity _LOGGER = logging.getLogger(__name__) @@ -28,9 +28,9 @@ async def async_setup_platform( if discovery_info is None: return - qsusb = hass.data[QWIKSWITCH] + qsusb = hass.data[DOMAIN] _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info) - devs = [QSSensor(sensor) for sensor in discovery_info[QWIKSWITCH]] + devs = [QSSensor(sensor) for sensor in discovery_info[DOMAIN]] add_entities(devs) diff --git a/homeassistant/components/qwikswitch/switch.py b/homeassistant/components/qwikswitch/switch.py index ec47b4d99f2..6131d9e595c 100644 --- a/homeassistant/components/qwikswitch/switch.py +++ b/homeassistant/components/qwikswitch/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as QWIKSWITCH +from . import DOMAIN from .entity import QSToggleEntity @@ -21,8 +21,8 @@ async def async_setup_platform( if discovery_info is None: return - qsusb = hass.data[QWIKSWITCH] - devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + qsusb = hass.data[DOMAIN] + devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[DOMAIN]] add_entities(devs) diff --git a/homeassistant/components/tibber/notify.py b/homeassistant/components/tibber/notify.py index df6541591e0..5a10d8e0890 100644 --- a/homeassistant/components/tibber/notify.py +++ b/homeassistant/components/tibber/notify.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DOMAIN as TIBBER_DOMAIN +from . import DOMAIN async def async_setup_entry( @@ -30,7 +30,7 @@ class TibberNotificationEntity(NotifyEntity): """Implement the notification entity service for Tibber.""" _attr_supported_features = NotifyEntityFeature.TITLE - _attr_name = TIBBER_DOMAIN + _attr_name = DOMAIN _attr_icon = "mdi:message-flash" def __init__(self, unique_id: str) -> None: @@ -39,12 +39,12 @@ class TibberNotificationEntity(NotifyEntity): async def async_send_message(self, message: str, title: str | None = None) -> None: """Send a message to Tibber devices.""" - tibber_connection: Tibber = self.hass.data[TIBBER_DOMAIN] + tibber_connection: Tibber = self.hass.data[DOMAIN] try: await tibber_connection.send_notification( title or ATTR_TITLE_DEFAULT, message ) except TimeoutError as exc: raise HomeAssistantError( - translation_domain=TIBBER_DOMAIN, translation_key="send_message_timeout" + translation_domain=DOMAIN, translation_key="send_message_timeout" ) from exc diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 1e03ad88cc8..9153520e703 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from . import DOMAIN as WF_DOMAIN, UPDATE_TOPIC, WaterFurnaceData +from . import DOMAIN, UPDATE_TOPIC, WaterFurnaceData SENSORS = [ SensorEntityDescription(name="Furnace Mode", key="mode", icon="mdi:gauge"), @@ -104,7 +104,7 @@ def setup_platform( if discovery_info is None: return - client = hass.data[WF_DOMAIN] + client = hass.data[DOMAIN] add_entities(WaterFurnaceSensor(client, description) for description in SENSORS) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 3bb80df25b2..3f44cb1504d 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS +from . import ACTUATORS, DOMAIN, SENSORS from .entity import XS1DeviceEntity MIN_TEMP = 8 @@ -30,8 +30,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 thermostat platform.""" - actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] - sensors = hass.data[COMPONENT_DOMAIN][SENSORS] + actuators = hass.data[DOMAIN][ACTUATORS] + sensors = hass.data[DOMAIN][SENSORS] thermostat_entities = [] for actuator in actuators: diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index b3895d67d82..26c009b15ee 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS +from . import ACTUATORS, DOMAIN, SENSORS from .entity import XS1DeviceEntity @@ -20,8 +20,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 sensor platform.""" - sensors = hass.data[COMPONENT_DOMAIN][SENSORS] - actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + sensors = hass.data[DOMAIN][SENSORS] + actuators = hass.data[DOMAIN][ACTUATORS] sensor_entities = [] for sensor in sensors: diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py index a8f66390a6d..5e107099515 100644 --- a/homeassistant/components/xs1/switch.py +++ b/homeassistant/components/xs1/switch.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ACTUATORS, DOMAIN as COMPONENT_DOMAIN +from . import ACTUATORS, DOMAIN from .entity import XS1DeviceEntity @@ -22,7 +22,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 switch platform.""" - actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + actuators = hass.data[DOMAIN][ACTUATORS] add_entities( XS1SwitchEntity(actuator) diff --git a/homeassistant/components/zoneminder/binary_sensor.py b/homeassistant/components/zoneminder/binary_sensor.py index 926780fc6da..f26f2351b5a 100644 --- a/homeassistant/components/zoneminder/binary_sensor.py +++ b/homeassistant/components/zoneminder/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ZONEMINDER_DOMAIN +from . import DOMAIN async def async_setup_platform( @@ -23,7 +23,7 @@ async def async_setup_platform( ) -> None: """Set up the ZoneMinder binary sensor platform.""" sensors = [] - for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items(): + for host_name, zm_client in hass.data[DOMAIN].items(): sensors.append(ZMAvailabilitySensor(host_name, zm_client)) add_entities(sensors) diff --git a/homeassistant/components/zoneminder/camera.py b/homeassistant/components/zoneminder/camera.py index 21513b4bed4..851b7492e06 100644 --- a/homeassistant/components/zoneminder/camera.py +++ b/homeassistant/components/zoneminder/camera.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ZONEMINDER_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ def setup_platform( filter_urllib3_logging() cameras = [] zm_client: ZoneMinder - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + for zm_client in hass.data[DOMAIN].values(): if not (monitors := zm_client.get_monitors()): raise PlatformNotReady( "Camera could not fetch any monitors from ZoneMinder" diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 4f79f8876e5..5663da0b308 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ZONEMINDER_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -77,7 +77,7 @@ def setup_platform( sensors: list[SensorEntity] = [] zm_client: ZoneMinder - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + for zm_client in hass.data[DOMAIN].values(): if not (monitors := zm_client.get_monitors()): raise PlatformNotReady( "Sensor could not fetch any monitors from ZoneMinder" diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index 13da0927196..7ab6f786cfb 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ZONEMINDER_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def setup_platform( switches: list[ZMSwitchMonitors] = [] zm_client: ZoneMinder - for zm_client in hass.data[ZONEMINDER_DOMAIN].values(): + for zm_client in hass.data[DOMAIN].values(): if not (monitors := zm_client.get_monitors()): raise PlatformNotReady( "Switch could not fetch any monitors from ZoneMinder" From 60846434d30558cc776042e78b44c47249d25e29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 10:05:12 +0200 Subject: [PATCH 0153/1175] Invert DOMAIN alias in telegram (#144313) --- homeassistant/components/telegram/notify.py | 28 +++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index adb947bcf6b..6b9cf43bf71 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -20,17 +20,17 @@ from homeassistant.components.telegram_bot import ( ATTR_MESSAGE_TAG, ATTR_MESSAGE_THREAD_ID, ATTR_PARSER, + DOMAIN as TELEGRAM_BOT_DOMAIN, ) from homeassistant.const import ATTR_LOCATION from homeassistant.core import HomeAssistant from homeassistant.helpers.reload import setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as TELEGRAM_DOMAIN, PLATFORMS +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -DOMAIN = "telegram_bot" ATTR_KEYBOARD = "keyboard" ATTR_INLINE_KEYBOARD = "inline_keyboard" ATTR_PHOTO = "photo" @@ -52,7 +52,7 @@ def get_service( ) -> TelegramNotificationService: """Get the Telegram notification service.""" - setup_reload_service(hass, TELEGRAM_DOMAIN, PLATFORMS) + setup_reload_service(hass, DOMAIN, PLATFORMS) chat_id = config.get(CONF_CHAT_ID) return TelegramNotificationService(hass, chat_id) @@ -115,37 +115,45 @@ class TelegramNotificationService(BaseNotificationService): photos = photos if isinstance(photos, list) else [photos] for photo_data in photos: service_data.update(photo_data) - self.hass.services.call(DOMAIN, "send_photo", service_data=service_data) + self.hass.services.call( + TELEGRAM_BOT_DOMAIN, "send_photo", service_data=service_data + ) return None if data is not None and ATTR_VIDEO in data: videos = data.get(ATTR_VIDEO) videos = videos if isinstance(videos, list) else [videos] for video_data in videos: service_data.update(video_data) - self.hass.services.call(DOMAIN, "send_video", service_data=service_data) + self.hass.services.call( + TELEGRAM_BOT_DOMAIN, "send_video", service_data=service_data + ) return None if data is not None and ATTR_VOICE in data: voices = data.get(ATTR_VOICE) voices = voices if isinstance(voices, list) else [voices] for voice_data in voices: service_data.update(voice_data) - self.hass.services.call(DOMAIN, "send_voice", service_data=service_data) + self.hass.services.call( + TELEGRAM_BOT_DOMAIN, "send_voice", service_data=service_data + ) return None if data is not None and ATTR_LOCATION in data: service_data.update(data.get(ATTR_LOCATION)) return self.hass.services.call( - DOMAIN, "send_location", service_data=service_data + TELEGRAM_BOT_DOMAIN, "send_location", service_data=service_data ) if data is not None and ATTR_DOCUMENT in data: service_data.update(data.get(ATTR_DOCUMENT)) return self.hass.services.call( - DOMAIN, "send_document", service_data=service_data + TELEGRAM_BOT_DOMAIN, "send_document", service_data=service_data ) # Send message _LOGGER.debug( - "TELEGRAM NOTIFIER calling %s.send_message with %s", DOMAIN, service_data + "TELEGRAM NOTIFIER calling %s.send_message with %s", + TELEGRAM_BOT_DOMAIN, + service_data, ) return self.hass.services.call( - DOMAIN, "send_message", service_data=service_data + TELEGRAM_BOT_DOMAIN, "send_message", service_data=service_data ) From 46df29b390717592a03c04a105cfece583335d0f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 10:23:19 +0200 Subject: [PATCH 0154/1175] Add MQTT binary_sensor as entity platform on MQTT subentries (#144142) Add MQTT binary_sensor subentry support --- .../components/mqtt/binary_sensor.py | 3 +- homeassistant/components/mqtt/config_flow.py | 68 ++++++++++++++++++- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/strings.json | 35 ++++++++++ tests/components/mqtt/common.py | 18 +++++ tests/components/mqtt/test_config_flow.py | 21 ++++++ 6 files changed, 141 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index a1e146d4e36..0ac3cb7f786 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -35,7 +35,7 @@ from homeassistant.util import dt as dt_util from . import subscription from .config import MQTT_RO_SCHEMA -from .const import CONF_STATE_TOPIC, PAYLOAD_NONE +from .const import CONF_OFF_DELAY, CONF_STATE_TOPIC, PAYLOAD_NONE from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper from .models import MqttValueTemplate, ReceiveMessage from .schemas import MQTT_ENTITY_COMMON_SCHEMA @@ -45,7 +45,6 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 DEFAULT_NAME = "MQTT Binary sensor" -CONF_OFF_DELAY = "off_delay" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 02c8a1cdc8a..9e1773fab62 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -25,6 +25,7 @@ from cryptography.hazmat.primitives.serialization import ( from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.components.light import ( @@ -157,6 +158,7 @@ from .const import ( CONF_LAST_RESET_VALUE_TEMPLATE, CONF_MAX_KELVIN, CONF_MIN_KELVIN, + CONF_OFF_DELAY, CONF_ON_COMMAND_TYPE, CONF_OPTIONS, CONF_PAYLOAD_AVAILABLE, @@ -305,7 +307,13 @@ KEY_UPLOAD_SELECTOR = FileSelector( ) # Subentry selectors -SUBENTRY_PLATFORMS = [Platform.LIGHT, Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH] +SUBENTRY_PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.NOTIFY, + Platform.SENSOR, + Platform.SWITCH, +] SUBENTRY_PLATFORM_SELECTOR = SelectSelector( SelectSelectorConfig( options=[platform.value for platform in SUBENTRY_PLATFORMS], @@ -337,6 +345,14 @@ SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector( sort=True, ) ) +BINARY_SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[device_class.value for device_class in BinarySensorDeviceClass], + mode=SelectSelectorMode.DROPDOWN, + translation_key="device_class_binary_sensor", + sort=True, + ) +) SENSOR_STATE_CLASS_SELECTOR = SelectSelector( SelectSelectorConfig( options=[device_class.value for device_class in SensorStateClass], @@ -354,7 +370,7 @@ OPTIONS_SELECTOR = SelectSelector( SUGGESTED_DISPLAY_PRECISION_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=9) ) -EXPIRE_AFTER_SELECTOR = NumberSelector( +TIMEOUT_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) @@ -523,6 +539,13 @@ COMMON_ENTITY_FIELDS = { } PLATFORM_ENTITY_FIELDS = { + Platform.BINARY_SENSOR.value: { + CONF_DEVICE_CLASS: PlatformField( + selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, + required=False, + validator=str, + ), + }, Platform.NOTIFY.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( @@ -573,6 +596,44 @@ PLATFORM_ENTITY_FIELDS = { }, } PLATFORM_MQTT_FIELDS = { + Platform.BINARY_SENSOR.value: { + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=cv.template, + error="invalid_template", + ), + CONF_PAYLOAD_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=str, + default=DEFAULT_PAYLOAD_OFF, + ), + CONF_PAYLOAD_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=str, + default=DEFAULT_PAYLOAD_ON, + ), + CONF_EXPIRE_AFTER: PlatformField( + selector=TIMEOUT_SELECTOR, + required=False, + validator=cv.positive_int, + section="advanced_settings", + ), + CONF_OFF_DELAY: PlatformField( + selector=TIMEOUT_SELECTOR, + required=False, + validator=cv.positive_int, + section="advanced_settings", + ), + }, Platform.NOTIFY.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -611,7 +672,7 @@ PLATFORM_MQTT_FIELDS = { conditions=({CONF_STATE_CLASS: "total"},), ), CONF_EXPIRE_AFTER: PlatformField( - selector=EXPIRE_AFTER_SELECTOR, + selector=TIMEOUT_SELECTOR, required=False, validator=cv.positive_int, section="advanced_settings", @@ -1144,6 +1205,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ str, Callable[[dict[str, Any]], dict[str, str]] | None, ] = { + Platform.BINARY_SENSOR.value: None, Platform.LIGHT.value: validate_light_platform_config, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 18107c5c939..b6dda0c0f8a 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -105,6 +105,7 @@ CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" +CONF_OFF_DELAY = "off_delay" CONF_ON_COMMAND_TYPE = "on_command_type" CONF_PAYLOAD_CLOSE = "payload_close" CONF_PAYLOAD_OPEN = "payload_open" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 23a2a888989..b3eede62332 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -300,6 +300,7 @@ "flash_time_short": "Flash time short", "max_kelvin": "Max Kelvin", "min_kelvin": "Min Kelvin", + "off_delay": "OFF delay", "transition": "Transition support" }, "data_description": { @@ -309,6 +310,7 @@ "flash_time_short": "The duration, in seconds, of a \"short\" flash.", "max_kelvin": "The maximum color temperature in Kelvin.", "min_kelvin": "The minimum color temperature in Kelvin.", + "off_delay": "For sensors that only send \"on\" state updates (like PIRs), this variable sets a delay in seconds after which the sensor’s state will be updated back to \"off\".", "transition": "Enable the transition feature for this light" } }, @@ -600,6 +602,38 @@ } }, "selector": { + "device_class_binary_sensor": { + "options": { + "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", + "battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]", + "carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]", + "cold": "[%key:component::binary_sensor::entity_component::cold::name%]", + "connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]", + "door": "[%key:component::binary_sensor::entity_component::door::name%]", + "garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]", + "gas": "[%key:component::binary_sensor::entity_component::gas::name%]", + "heat": "[%key:component::binary_sensor::entity_component::heat::name%]", + "light": "[%key:component::binary_sensor::entity_component::light::name%]", + "lock": "[%key:component::binary_sensor::entity_component::lock::name%]", + "moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]", + "motion": "[%key:component::binary_sensor::entity_component::motion::name%]", + "moving": "[%key:component::binary_sensor::entity_component::moving::name%]", + "occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]", + "opening": "[%key:component::binary_sensor::entity_component::opening::name%]", + "plug": "[%key:component::binary_sensor::entity_component::plug::name%]", + "power": "[%key:component::binary_sensor::entity_component::power::name%]", + "presence": "[%key:component::binary_sensor::entity_component::presence::name%]", + "problem": "[%key:component::binary_sensor::entity_component::problem::name%]", + "running": "[%key:component::binary_sensor::entity_component::running::name%]", + "safety": "[%key:component::binary_sensor::entity_component::safety::name%]", + "smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]", + "sound": "[%key:component::binary_sensor::entity_component::sound::name%]", + "tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]", + "update": "[%key:component::binary_sensor::entity_component::update::name%]", + "vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]", + "window": "[%key:component::binary_sensor::entity_component::window::name%]" + } + }, "device_class_sensor": { "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", @@ -682,6 +716,7 @@ }, "platform": { "options": { + "binary_sensor": "[%key:component::binary_sensor::title%]", "light": "[%key:component::light::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 4e402046e2c..283414cb96a 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -66,6 +66,20 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { "configuration_url": "http://example.com", } +MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT = { + "5b06357ef8654e8d9c54cee5bb0e939b": { + "platform": "binary_sensor", + "name": "Hatch", + "device_class": "door", + "state_topic": "test-topic", + "payload_on": "ON", + "payload_off": "OFF", + "expire_after": 1200, + "off_delay": 5, + "value_template": "{{ value_json.value }}", + "entity_picture": "https://example.com/5b06357ef8654e8d9c54cee5bb0e939b", + }, +} MOCK_SUBENTRY_NOTIFY_COMPONENT1 = { "363a7ecad6be4a19b939a016ea93e994": { "platform": "notify", @@ -187,6 +201,10 @@ MOCK_NOTIFY_SUBENTRY_DATA_MULTI = { "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1 | MOCK_SUBENTRY_NOTIFY_COMPONENT2, } | MOCK_SUBENTRY_AVAILABILITY_DATA +MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, + "components": MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT, +} MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index b3d2769de6a..50f718e332d 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -33,6 +33,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( + MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, @@ -2657,6 +2658,25 @@ async def test_migrate_of_incompatible_config_entry( "entity_name", ), [ + ( + MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, + {"name": "Hatch"}, + {"device_class": "door"}, + (), + { + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "advanced_settings": {"expire_after": 1200, "off_delay": 5}, + }, + ( + ( + {"state_topic": "test-topic#invalid"}, + {"state_topic": "invalid_subscribe_topic"}, + ), + ), + "Milk notifier Hatch", + ), ( MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, @@ -2832,6 +2852,7 @@ async def test_migrate_of_incompatible_config_entry( ), ], ids=[ + "binary_sensor", "notify_with_entity_name", "notify_no_entity_name", "sensor_options", From ec4f4a4a1f7aaecb3040d9d0b030067a888394db Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 10:33:58 +0200 Subject: [PATCH 0155/1175] Fix Z-Wave USB discovery to use serial by id path (#144314) --- homeassistant/components/zwave_js/config_flow.py | 10 +++++++++- tests/components/zwave_js/test_config_flow.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 184a7724799..c6624046a00 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -461,10 +461,18 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if vid == "10C4" and pid == "EA60" and description and "2652" in description: return self.async_abort(reason="not_zwave_device") + discovery_info.device = await self.hass.async_add_executor_job( + usb.get_serial_by_id, discovery_info.device + ) + addon_info = await self._async_get_addon_info() if ( addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.INSTALLING) - and addon_info.options.get(CONF_ADDON_DEVICE) == discovery_info.device + and (addon_device := addon_info.options.get(CONF_ADDON_DEVICE)) is not None + and await self.hass.async_add_executor_job( + usb.get_serial_by_id, addon_device + ) + == discovery_info.device ): return self.async_abort(reason="already_configured") diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3778e36f897..08f0ffad4bd 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -653,6 +653,7 @@ async def test_usb_discovery( install_addon, addon_options, get_addon_discovery_info, + mock_usb_serial_by_id: MagicMock, set_addon_options, start_addon, usb_discovery_info: UsbServiceInfo, @@ -668,6 +669,7 @@ async def test_usb_discovery( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -765,6 +767,7 @@ async def test_usb_discovery_addon_not_running( supervisor, addon_installed, addon_options, + mock_usb_serial_by_id: MagicMock, set_addon_options, start_addon, get_addon_discovery_info, @@ -779,6 +782,7 @@ async def test_usb_discovery_addon_not_running( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -876,6 +880,7 @@ async def test_usb_discovery_addon_not_running( async def test_usb_discovery_migration( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, set_addon_options: AsyncMock, restart_addon: AsyncMock, client: MagicMock, @@ -929,6 +934,7 @@ async def test_usb_discovery_migration( ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -1278,6 +1284,7 @@ async def test_abort_usb_discovery_addon_required( async def test_abort_usb_discovery_confirm_addon_required( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, ) -> None: """Test usb discovery confirm aborted when existing entry not using add-on.""" addon_options["device"] = "/dev/another_device" @@ -1301,6 +1308,7 @@ async def test_abort_usb_discovery_confirm_addon_required( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 hass.config_entries.async_update_entry( entry, @@ -1331,6 +1339,7 @@ async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: async def test_usb_discovery_same_device( hass: HomeAssistant, addon_options: dict[str, Any], + mock_usb_serial_by_id: MagicMock, ) -> None: """Test usb discovery flow is aborted when the add-on device is discovered.""" addon_options["device"] = USB_DISCOVERY_INFO.device @@ -1341,6 +1350,7 @@ async def test_usb_discovery_same_device( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + assert mock_usb_serial_by_id.call_count == 2 @pytest.mark.parametrize( From 5df3a9d76df044e22f228872b599757be6c587a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 10:41:56 +0200 Subject: [PATCH 0156/1175] Use runtime_data in geocaching (#144310) --- homeassistant/components/geocaching/__init__.py | 15 +++++---------- .../components/geocaching/coordinator.py | 10 ++++++++-- homeassistant/components/geocaching/sensor.py | 7 +++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py index aa2926df949..144249ac42f 100644 --- a/homeassistant/components/geocaching/__init__.py +++ b/homeassistant/components/geocaching/__init__.py @@ -1,6 +1,5 @@ """The Geocaching integration.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import ( @@ -8,13 +7,12 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( async_get_config_entry_implementation, ) -from .const import DOMAIN -from .coordinator import GeocachingDataUpdateCoordinator +from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool: """Set up Geocaching from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) @@ -25,15 +23,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - del hass.data[DOMAIN][entry.entry_id] - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/geocaching/coordinator.py b/homeassistant/components/geocaching/coordinator.py index 41b59d049af..fdf8f1340da 100644 --- a/homeassistant/components/geocaching/coordinator.py +++ b/homeassistant/components/geocaching/coordinator.py @@ -14,14 +14,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, ENVIRONMENT, LOGGER, UPDATE_INTERVAL +type GeocachingConfigEntry = ConfigEntry[GeocachingDataUpdateCoordinator] + class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): """Class to manage fetching Geocaching data from single endpoint.""" - config_entry: ConfigEntry + config_entry: GeocachingConfigEntry def __init__( - self, hass: HomeAssistant, *, entry: ConfigEntry, session: OAuth2Session + self, + hass: HomeAssistant, + *, + entry: GeocachingConfigEntry, + session: OAuth2Session, ) -> None: """Initialize global Geocaching data updater.""" self.session = session diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index c7894afc5ac..a8008229c91 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -9,14 +9,13 @@ from typing import cast from geocachingapi.models import GeocachingStatus from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import GeocachingDataUpdateCoordinator +from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator @dataclass(frozen=True, kw_only=True) @@ -65,11 +64,11 @@ SENSORS: tuple[GeocachingSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GeocachingConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up a Geocaching sensor entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( GeocachingSensor(coordinator, description) for description in SENSORS ) From 33da5465bd4a2ee73ac3a0092039c714bc52584e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 10:44:16 +0200 Subject: [PATCH 0157/1175] Use runtime_data in gdacs (#144309) --- homeassistant/components/gdacs/__init__.py | 26 +++++++------------ homeassistant/components/gdacs/const.py | 2 -- homeassistant/components/gdacs/diagnostics.py | 9 +++---- .../components/gdacs/geo_location.py | 9 +++---- homeassistant/components/gdacs/sensor.py | 12 ++++----- tests/components/gdacs/conftest.py | 2 +- tests/components/gdacs/test_config_flow.py | 2 +- tests/components/gdacs/test_geo_location.py | 7 ++--- tests/components/gdacs/test_init.py | 4 +-- tests/components/gdacs/test_sensor.py | 5 ++-- 10 files changed, 29 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index e96246b70bf..1a8f2fce236 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -25,22 +25,17 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from .const import ( # noqa: F401 - CONF_CATEGORIES, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - FEED, - PLATFORMS, -) +from .const import CONF_CATEGORIES, DEFAULT_SCAN_INTERVAL, PLATFORMS # noqa: F401 _LOGGER = logging.getLogger(__name__) +type GdacsConfigEntry = ConfigEntry[GdacsFeedEntityManager] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +async def async_setup_entry( + hass: HomeAssistant, config_entry: GdacsConfigEntry +) -> bool: """Set up the GDACS component as config entry.""" - hass.data.setdefault(DOMAIN, {}) - feeds = hass.data[DOMAIN].setdefault(FEED, {}) - radius = config_entry.data[CONF_RADIUS] if hass.config.units is US_CUSTOMARY_SYSTEM: radius = DistanceConverter.convert( @@ -48,16 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Create feed entity manager for all platforms. manager = GdacsFeedEntityManager(hass, config_entry, radius) - feeds[config_entry.entry_id] = manager + config_entry.runtime_data = manager _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) await manager.async_init() return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GdacsConfigEntry) -> bool: """Unload an GDACS component config entry.""" - manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED].pop(entry.entry_id) - await manager.async_stop() + await entry.runtime_data.async_stop() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -65,7 +59,7 @@ class GdacsFeedEntityManager: """Feed Entity Manager for GDACS feed.""" def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, radius_in_km: float + self, hass: HomeAssistant, config_entry: GdacsConfigEntry, radius_in_km: float ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py index d1028ed2d08..c040809a357 100644 --- a/homeassistant/components/gdacs/const.py +++ b/homeassistant/components/gdacs/const.py @@ -10,8 +10,6 @@ DOMAIN = "gdacs" PLATFORMS = [Platform.GEO_LOCATION, Platform.SENSOR] -FEED = "feed" - CONF_CATEGORIES = "categories" DEFAULT_ICON = "mdi:alert" diff --git a/homeassistant/components/gdacs/diagnostics.py b/homeassistant/components/gdacs/diagnostics.py index 435e28ca1ae..9501fb29dd2 100644 --- a/homeassistant/components/gdacs/diagnostics.py +++ b/homeassistant/components/gdacs/diagnostics.py @@ -7,26 +7,23 @@ from typing import Any from aio_georss_client.status_update import StatusUpdate from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from . import GdacsFeedEntityManager -from .const import DOMAIN, FEED +from . import GdacsConfigEntry TO_REDACT = {CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: GdacsConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" data: dict[str, Any] = { "info": async_redact_data(config_entry.data, TO_REDACT), } - manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][config_entry.entry_id] - status_info: StatusUpdate = manager.status_info() + status_info: StatusUpdate = config_entry.runtime_data.status_info() if status_info: data["service"] = { "status": status_info.status, diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index d277ee54f6b..e4057633101 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -10,7 +10,6 @@ from typing import Any from aio_georss_gdacs.feed_entry import GdacsFeedEntry from homeassistant.components.geo_location import GeolocationEvent -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfLength from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -19,8 +18,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import GdacsFeedEntityManager -from .const import DEFAULT_ICON, DOMAIN, FEED +from . import GdacsConfigEntry, GdacsFeedEntityManager +from .const import DEFAULT_ICON _LOGGER = logging.getLogger(__name__) @@ -53,11 +52,11 @@ SOURCE = "gdacs" async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GdacsConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GDACS Feed platform.""" - manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] + manager = entry.runtime_data @callback def async_add_geolocation( diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py index a204addd414..f23a02d92b0 100644 --- a/homeassistant/components/gdacs/sensor.py +++ b/homeassistant/components/gdacs/sensor.py @@ -10,15 +10,14 @@ from typing import Any from aio_georss_client.status_update import StatusUpdate from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from . import GdacsFeedEntityManager -from .const import DOMAIN, FEED +from . import GdacsConfigEntry, GdacsFeedEntityManager +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,12 +37,11 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GdacsConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GDACS Feed platform.""" - manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] - sensor = GdacsSensor(entry, manager) + sensor = GdacsSensor(entry, entry.runtime_data) async_add_entities([sensor]) @@ -57,7 +55,7 @@ class GdacsSensor(SensorEntity): _attr_translation_key = "alerts" def __init__( - self, config_entry: ConfigEntry, manager: GdacsFeedEntityManager + self, config_entry: GdacsConfigEntry, manager: GdacsFeedEntityManager ) -> None: """Initialize entity.""" assert config_entry.unique_id diff --git a/tests/components/gdacs/conftest.py b/tests/components/gdacs/conftest.py index 9d9a91aa407..46ac1d0aab2 100644 --- a/tests/components/gdacs/conftest.py +++ b/tests/components/gdacs/conftest.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN +from homeassistant.components.gdacs.const import CONF_CATEGORIES, DOMAIN from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index f11848162cd..da9b2f7c9bf 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.components.gdacs import CONF_CATEGORIES, DOMAIN +from homeassistant.components.gdacs.const import CONF_CATEGORIES, DOMAIN from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 68e2d061259..a6937f80d59 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -5,7 +5,7 @@ from unittest.mock import patch from freezegun import freeze_time -from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED +from homeassistant.components.gdacs.const import DEFAULT_SCAN_INTERVAL from homeassistant.components.gdacs.geo_location import ( ATTR_ALERT_LEVEL, ATTR_COUNTRY, @@ -251,10 +251,7 @@ async def test_setup_imperial( ) # Test conversion of 200 miles to kilometers. - feeds = hass.data[DOMAIN][FEED] - assert feeds is not None - assert len(feeds) == 1 - manager = list(feeds.values())[0] + manager = config_entry.runtime_data # Ensure that the filter value in km is correctly set. assert manager._feed_manager._feed._filter_radius == 321.8688 diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index 1da4b0d9b9f..bdd11242b25 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from homeassistant.components.gdacs import DOMAIN, FEED from homeassistant.core import HomeAssistant @@ -14,8 +13,7 @@ async def test_component_unload_config_entry(hass: HomeAssistant, config_entry) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert mock_feed_manager_update.call_count == 1 - assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index 01609cf485e..abc095fb4f5 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -4,9 +4,8 @@ from unittest.mock import patch from freezegun import freeze_time -from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL -from homeassistant.components.gdacs.const import CONF_CATEGORIES +from homeassistant.components.gdacs.const import CONF_CATEGORIES, DOMAIN from homeassistant.components.gdacs.sensor import ( ATTR_CREATED, ATTR_LAST_UPDATE, @@ -73,7 +72,7 @@ async def test_setup(hass: HomeAssistant) -> None: CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL.seconds, } config_entry = MockConfigEntry( - domain=gdacs.DOMAIN, + domain=DOMAIN, title=f"{latitude}, {longitude}", data=entry_data, unique_id="my_very_unique_id", From f3371bcf3907ecfa745fccc9bbbd133dfc1fdd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 6 May 2025 10:57:56 +0200 Subject: [PATCH 0158/1175] Add async_delete_repair_issue method to CloudClient (#144302) --- homeassistant/components/cloud/client.py | 10 ++++++- tests/components/cloud/test_client.py | 33 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index ea3d992e8f7..7308c2c3d0e 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -26,7 +26,11 @@ from homeassistant.core import Context, HassJob, HomeAssistant, callback from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE 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.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.util.aiohttp import MockRequest, serialize_response from . import alexa_config, google_config @@ -409,3 +413,7 @@ class CloudClient(Interface): severity=IssueSeverity(severity), is_fixable=False, ) + + async def async_delete_repair_issue(self, identifier: str) -> None: + """Delete a repair issue.""" + async_delete_issue(hass=self._hass, domain=DOMAIN, issue_id=identifier) diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 52457fe558c..7b842b24551 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -496,6 +496,39 @@ async def test_async_create_repair_issue_unknown( assert issue is None +async def test_async_delete_repair_issue( + cloud: MagicMock, + mock_cloud_setup: None, + issue_registry: ir.IssueRegistry, +) -> None: + """Test delete repair issue.""" + identifier = "test_identifier" + issue_registry.issues[(DOMAIN, identifier)] = ir.IssueEntry( + active=True, + breaks_in_ha_version=None, + created=dt_util.utcnow(), + data={}, + dismissed_version=None, + domain=DOMAIN, + is_fixable=False, + is_persistent=True, + issue_domain=None, + issue_id=identifier, + learn_more_url=None, + severity="warning", + translation_key="test_translation_key", + translation_placeholders=None, + ) + + issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier) + assert issue is not None + + await cloud.client.async_delete_repair_issue(identifier=identifier) + + issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier) + assert issue is None + + async def test_disconnected(hass: HomeAssistant) -> None: """Test cleanup when disconnected from the cloud.""" prefs = MagicMock( From babc183834159d99d254ad09f43d74d4ce9ba9c1 Mon Sep 17 00:00:00 2001 From: Arnie97 Date: Tue, 6 May 2025 16:59:09 +0800 Subject: [PATCH 0159/1175] Allow liter for gas sensor device class (#141518) --- homeassistant/components/energy/sensor.py | 1 + homeassistant/components/energy/validate.py | 1 + homeassistant/components/number/const.py | 3 ++- homeassistant/components/sensor/const.py | 3 ++- homeassistant/util/unit_system.py | 1 + tests/components/energy/test_sensor.py | 2 +- tests/components/energy/test_validate.py | 4 ++-- tests/util/test_unit_system.py | 7 ++++++- 8 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 062601eb4c5..3dc857d75d9 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -52,6 +52,7 @@ VALID_ENERGY_UNITS_GAS = { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, + UnitOfVolume.LITERS, *VALID_ENERGY_UNITS, } VALID_VOLUME_UNITS_WATER: set[str] = { diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index cfacbe48b97..0f46678994f 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -50,6 +50,7 @@ GAS_USAGE_UNITS: dict[str, tuple[UnitOfEnergy | UnitOfVolume, ...]] = { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, + UnitOfVolume.LITERS, ), } GAS_PRICE_UNITS = tuple( diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 280edb819d4..58fa8ed1012 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -196,7 +196,7 @@ class NumberDeviceClass(StrEnum): """Gas. Unit of measurement: - - SI / metric: `m³` + - SI / metric: `L`, `m³` - USCS / imperial: `ft³`, `CCF` """ @@ -472,6 +472,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, + UnitOfVolume.LITERS, }, NumberDeviceClass.HUMIDITY: {PERCENTAGE}, NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX}, diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index c845980e9df..2a8ac8099ab 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -225,7 +225,7 @@ class SensorDeviceClass(StrEnum): """Gas. Unit of measurement: - - SI / metric: `m³` + - SI / metric: `L`, `m³` - USCS / imperial: `ft³`, `CCF` """ @@ -571,6 +571,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, + UnitOfVolume.LITERS, }, SensorDeviceClass.HUMIDITY: {PERCENTAGE}, SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 055f435503f..31f74377a16 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -355,6 +355,7 @@ US_CUSTOMARY_SYSTEM = UnitSystem( ("distance", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES, # Convert non-USCS volumes of gas meters ("gas", UnitOfVolume.CUBIC_METERS): UnitOfVolume.CUBIC_FEET, + ("gas", UnitOfVolume.LITERS): UnitOfVolume.CUBIC_FEET, # Convert non-USCS precipitation ("precipitation", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES, ("precipitation", UnitOfLength.MILLIMETERS): UnitOfLength.INCHES, diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index a438842f8a5..a9a249a8498 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -994,7 +994,7 @@ async def test_cost_sensor_handle_late_price_sensor( @pytest.mark.parametrize( "unit", - [UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS], + [UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS, UnitOfVolume.LITERS], ) async def test_cost_sensor_handle_gas( setup_integration, hass: HomeAssistant, hass_storage: dict[str, Any], unit diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index d7f0485139f..6389ac0b372 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -856,7 +856,7 @@ async def test_validation_gas( "affected_entities": {("sensor.gas_consumption_1", "beers")}, "translation_placeholders": { "energy_units": "GJ, kWh, MJ, MWh, Wh", - "gas_units": "CCF, ft³, m³", + "gas_units": "CCF, ft³, m³, L", }, }, { @@ -885,7 +885,7 @@ async def test_validation_gas( "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, "translation_placeholders": { "price_units": ( - "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³, EUR/L" ) }, }, diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index ddefe92de42..87a9729700e 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -434,6 +434,7 @@ def test_get_unit_system_invalid(key: str) -> None: UnitOfVolume.CUBIC_METERS, ), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), + (SensorDeviceClass.GAS, UnitOfVolume.LITERS, None), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, None), (SensorDeviceClass.GAS, "very_much", None), # Test precipitation conversion @@ -573,7 +574,10 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = { UnitOfLength.METERS, UnitOfLength.MILLIMETERS, ), - SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS,), + SensorDeviceClass.GAS: ( + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.LITERS, + ), SensorDeviceClass.PRECIPITATION: ( UnitOfLength.CENTIMETERS, UnitOfLength.MILLIMETERS, @@ -687,6 +691,7 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None: # Test gas meter conversion (SensorDeviceClass.GAS, UnitOfVolume.CENTUM_CUBIC_FEET, None), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET), + (SensorDeviceClass.GAS, UnitOfVolume.LITERS, UnitOfVolume.CUBIC_FEET), (SensorDeviceClass.GAS, UnitOfVolume.CUBIC_FEET, None), (SensorDeviceClass.GAS, "very_much", None), # Test precipitation conversion From 241b6a0170b7917c997a451b18436e1fd8ddfd98 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 11:01:46 +0200 Subject: [PATCH 0160/1175] Improve type hints in gc100 (#144308) --- homeassistant/components/gc100/__init__.py | 5 ++++- homeassistant/components/gc100/binary_sensor.py | 14 +++++++------- homeassistant/components/gc100/switch.py | 14 +++++++------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index a43741b9249..34cbbdbbb1c 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,5 +1,7 @@ """Support for controlling Global Cache gc100.""" +from __future__ import annotations + import gc100 import voluptuous as vol @@ -7,13 +9,14 @@ from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.util.hass_dict import HassKey CONF_PORTS = "ports" DEFAULT_PORT = 4998 DOMAIN = "gc100" -DATA_GC100 = "gc100" +DATA_GC100: HassKey[GC100Device] = HassKey("gc100") CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/gc100/binary_sensor.py b/homeassistant/components/gc100/binary_sensor.py index cef798935cb..3dcbb355d3a 100644 --- a/homeassistant/components/gc100/binary_sensor.py +++ b/homeassistant/components/gc100/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_PORTS, DATA_GC100 +from . import CONF_PORTS, DATA_GC100, GC100Device _SENSORS_SCHEMA = vol.Schema({cv.string: cv.string}) @@ -31,7 +31,7 @@ def setup_platform( ) -> None: """Set up the GC100 devices.""" binary_sensors = [] - ports = config[CONF_PORTS] + ports: list[dict[str, str]] = config[CONF_PORTS] for port in ports: for port_addr, port_name in port.items(): binary_sensors.append( @@ -43,23 +43,23 @@ def setup_platform( class GC100BinarySensor(BinarySensorEntity): """Representation of a binary sensor from GC100.""" - def __init__(self, name, port_addr, gc100): + def __init__(self, name: str, port_addr: str, gc100: GC100Device) -> None: """Initialize the GC100 binary sensor.""" self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 - self._state = None + self._state: bool | None = None # Subscribe to be notified about state changes (PUSH) self._gc100.subscribe(self._port_addr, self.set_state) @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def is_on(self): + def is_on(self) -> bool | None: """Return the state of the entity.""" return self._state @@ -67,7 +67,7 @@ class GC100BinarySensor(BinarySensorEntity): """Update the sensor state.""" self._gc100.read_sensor(self._port_addr, self.set_state) - def set_state(self, state): + def set_state(self, state: int) -> None: """Set the current state.""" self._state = state == 1 self.schedule_update_ha_state() diff --git a/homeassistant/components/gc100/switch.py b/homeassistant/components/gc100/switch.py index 23b178cc647..bb4742bafdf 100644 --- a/homeassistant/components/gc100/switch.py +++ b/homeassistant/components/gc100/switch.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_PORTS, DATA_GC100 +from . import CONF_PORTS, DATA_GC100, GC100Device _SWITCH_SCHEMA = vol.Schema({cv.string: cv.string}) @@ -33,7 +33,7 @@ def setup_platform( ) -> None: """Set up the GC100 devices.""" switches = [] - ports = config[CONF_PORTS] + ports: list[dict[str, str]] = config[CONF_PORTS] for port in ports: for port_addr, port_name in port.items(): switches.append(GC100Switch(port_name, port_addr, hass.data[DATA_GC100])) @@ -43,20 +43,20 @@ def setup_platform( class GC100Switch(SwitchEntity): """Represent a switch/relay from GC100.""" - def __init__(self, name, port_addr, gc100): + def __init__(self, name: str, port_addr: str, gc100: GC100Device) -> None: """Initialize the GC100 switch.""" self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 - self._state = None + self._state: bool | None = None @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return self._name @property - def is_on(self): + def is_on(self) -> bool | None: """Return the state of the entity.""" return self._state @@ -72,7 +72,7 @@ class GC100Switch(SwitchEntity): """Update the sensor state.""" self._gc100.read_sensor(self._port_addr, self.set_state) - def set_state(self, state): + def set_state(self, state: int) -> None: """Set the current state.""" self._state = state == 1 self.schedule_update_ha_state() From 9479874bb4103ca6f9f65c6f2552583548fb1118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 6 May 2025 11:49:44 +0200 Subject: [PATCH 0161/1175] Remove ThingTalk server configuration and related websocket command from cloud integration (#144301) --- homeassistant/components/cloud/__init__.py | 2 - homeassistant/components/cloud/const.py | 1 - homeassistant/components/cloud/http_api.py | 22 +------- tests/components/cloud/test_http_api.py | 66 +--------------------- 4 files changed, 2 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 97210b4197c..2c7c6f80d49 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -61,7 +61,6 @@ from .const import ( CONF_RELAYER_SERVER, CONF_REMOTESTATE_SERVER, CONF_SERVICEHANDLERS_SERVER, - CONF_THINGTALK_SERVER, CONF_USER_POOL_ID, DATA_CLOUD, DATA_CLOUD_LOG_HANDLER, @@ -134,7 +133,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_CLOUDHOOK_SERVER): str, vol.Optional(CONF_RELAYER_SERVER): str, vol.Optional(CONF_REMOTESTATE_SERVER): str, - vol.Optional(CONF_THINGTALK_SERVER): str, vol.Optional(CONF_SERVICEHANDLERS_SERVER): str, } ) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 9a977d2a5b9..1f154832ef9 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -81,7 +81,6 @@ CONF_ACME_SERVER = "acme_server" CONF_CLOUDHOOK_SERVER = "cloudhook_server" CONF_RELAYER_SERVER = "relayer_server" CONF_REMOTESTATE_SERVER = "remotestate_server" -CONF_THINGTALK_SERVER = "thingtalk_server" CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server" MODE_DEV = "development" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 7c7cb925e4f..998f3fcd5bc 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -16,7 +16,7 @@ from typing import Any, Concatenate, cast import aiohttp from aiohttp import web import attr -from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk +from hass_nabucasa import AlreadyConnectedError, Cloud, auth from hass_nabucasa.const import STATE_DISCONNECTED from hass_nabucasa.voice_data import TTS_VOICES import voluptuous as vol @@ -104,7 +104,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, alexa_list) websocket_api.async_register_command(hass, alexa_sync) - websocket_api.async_register_command(hass, thingtalk_convert) websocket_api.async_register_command(hass, tts_info) hass.http.register_view(GoogleActionsSyncView) @@ -998,25 +997,6 @@ async def alexa_sync( ) -@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) -@websocket_api.async_response -async def thingtalk_convert( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Convert a query.""" - cloud = hass.data[DATA_CLOUD] - - async with asyncio.timeout(10): - try: - connection.send_result( - msg["id"], await thingtalk.async_convert(cloud, msg["query"]) - ) - except thingtalk.ThingTalkConversionError as err: - connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) - - @websocket_api.websocket_command({"type": "cloud/tts/info"}) def tts_info( hass: HomeAssistant, diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 2722445445e..b5cce286ba2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch import aiohttp from freezegun.api import FrozenDateTimeFactory -from hass_nabucasa import AlreadyConnectedError, thingtalk +from hass_nabucasa import AlreadyConnectedError from hass_nabucasa.auth import ( InvalidTotpCode, MFARequired, @@ -1745,70 +1745,6 @@ async def test_enable_alexa_state_report_fail( assert response["error"]["code"] == "alexa_relink" -async def test_thingtalk_convert( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - setup_cloud: None, -) -> None: - """Test that we can convert a query.""" - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.cloud.http_api.thingtalk.async_convert", - return_value={"hello": "world"}, - ): - await client.send_json( - {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} - ) - response = await client.receive_json() - - assert response["success"] - assert response["result"] == {"hello": "world"} - - -async def test_thingtalk_convert_timeout( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - setup_cloud: None, -) -> None: - """Test that we can convert a query.""" - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.cloud.http_api.thingtalk.async_convert", - side_effect=TimeoutError, - ): - await client.send_json( - {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} - ) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == "timeout" - - -async def test_thingtalk_convert_internal( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - setup_cloud: None, -) -> None: - """Test that we can convert a query.""" - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.cloud.http_api.thingtalk.async_convert", - side_effect=thingtalk.ThingTalkConversionError("Did not understand"), - ): - await client.send_json( - {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} - ) - response = await client.receive_json() - - assert not response["success"] - assert response["error"]["code"] == "unknown_error" - assert response["error"]["message"] == "Did not understand" - - async def test_tts_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, From 62877c2c58ff37bd0a347a0679e25eb97080e819 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 11:58:25 +0200 Subject: [PATCH 0162/1175] Use runtime_data in geonetnz_quakes (#144319) --- .../components/geonetnz_quakes/__init__.py | 19 ++++++++++--------- .../components/geonetnz_quakes/const.py | 2 -- .../components/geonetnz_quakes/diagnostics.py | 11 +++-------- .../geonetnz_quakes/geo_location.py | 8 +++----- .../components/geonetnz_quakes/sensor.py | 7 +++---- .../geonetnz_quakes/test_geo_location.py | 14 +++++--------- tests/components/geonetnz_quakes/test_init.py | 4 +--- 7 files changed, 25 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index b9443d4aed8..a1522862dca 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -31,7 +31,6 @@ from .const import ( DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, - FEED, PLATFORMS, ) @@ -59,6 +58,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +type GeonetnzQuakesConfigEntry = ConfigEntry[GeonetnzQuakesFeedEntityManager] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the GeoNet NZ Quakes component.""" @@ -89,11 +90,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: GeonetnzQuakesConfigEntry +) -> bool: """Set up the GeoNet NZ Quakes component as config entry.""" - hass.data.setdefault(DOMAIN, {}) - feeds = hass.data[DOMAIN].setdefault(FEED, {}) - radius = config_entry.data[CONF_RADIUS] if hass.config.units is US_CUSTOMARY_SYSTEM: radius = DistanceConverter.convert( @@ -101,16 +101,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Create feed entity manager for all platforms. manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius) - feeds[config_entry.entry_id] = manager + config_entry.runtime_data = manager _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) await manager.async_init() return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: GeonetnzQuakesConfigEntry +) -> bool: """Unload an GeoNet NZ Quakes component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) - await manager.async_stop() + await entry.runtime_data.async_stop() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py index db529a17fbe..9c0f1a08c6f 100644 --- a/homeassistant/components/geonetnz_quakes/const.py +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -11,8 +11,6 @@ PLATFORMS = [Platform.GEO_LOCATION, Platform.SENSOR] CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" CONF_MMI = "mmi" -FEED = "feed" - DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) DEFAULT_MINIMUM_MAGNITUDE = 0.0 DEFAULT_MMI = 3 diff --git a/homeassistant/components/geonetnz_quakes/diagnostics.py b/homeassistant/components/geonetnz_quakes/diagnostics.py index fbe9bf511aa..ebb6a2e9046 100644 --- a/homeassistant/components/geonetnz_quakes/diagnostics.py +++ b/homeassistant/components/geonetnz_quakes/diagnostics.py @@ -5,28 +5,23 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from . import GeonetnzQuakesFeedEntityManager -from .const import DOMAIN, FEED +from . import GeonetnzQuakesConfigEntry TO_REDACT = {CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: GeonetnzQuakesConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" data: dict[str, Any] = { "info": async_redact_data(config_entry.data, TO_REDACT), } - manager: GeonetnzQuakesFeedEntityManager = hass.data[DOMAIN][FEED][ - config_entry.entry_id - ] - status_info = manager.status_info() + status_info = config_entry.runtime_data.status_info() if status_info: data["service"] = { "status": status_info.status, diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 96a1c3c09b2..e67d22c850f 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -9,7 +9,6 @@ from typing import Any from aio_geojson_geonetnz_quakes.feed_entry import GeonetnzQuakesFeedEntry from homeassistant.components.geo_location import GeolocationEvent -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME, UnitOfLength from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -18,8 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.unit_conversion import DistanceConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM -from . import GeonetnzQuakesFeedEntityManager -from .const import DOMAIN, FEED +from . import GeonetnzQuakesConfigEntry, GeonetnzQuakesFeedEntityManager _LOGGER = logging.getLogger(__name__) @@ -39,11 +37,11 @@ SOURCE = "geonetnz_quakes" async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GeonetnzQuakesConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GeoNet NZ Quakes Feed platform.""" - manager: GeonetnzQuakesFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] + manager = entry.runtime_data @callback def async_add_geolocation( diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py index b8a1e2dd4db..cc4b4e16282 100644 --- a/homeassistant/components/geonetnz_quakes/sensor.py +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -5,13 +5,12 @@ from __future__ import annotations import logging from homeassistant.components.sensor import SensorEntity -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 AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN, FEED +from . import GeonetnzQuakesConfigEntry _LOGGER = logging.getLogger(__name__) @@ -32,11 +31,11 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GeonetnzQuakesConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GeoNet NZ Quakes Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager = entry.runtime_data sensor = GeonetnzQuakesSensor(entry.entry_id, entry.unique_id, entry.title, manager) async_add_entities([sensor]) _LOGGER.debug("Sensor setup done") diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index fd8ba81fca7..7373b207bab 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -5,9 +5,8 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory -from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE -from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED +from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.components.geonetnz_quakes.geo_location import ( ATTR_DEPTH, ATTR_EXTERNAL_ID, @@ -38,7 +37,7 @@ from . import _generate_mock_feed_entry from tests.common import async_fire_time_changed -CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} +CONFIG = {DOMAIN: {CONF_RADIUS: 200}} async def test_setup( @@ -74,7 +73,7 @@ async def test_setup( freezer.move_to(utcnow) with patch("aio_geojson_client.feed.GeoJsonFeed.update") as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] - assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -188,7 +187,7 @@ async def test_setup_imperial( patch("aio_geojson_client.feed.GeoJsonFeed.last_timestamp", create=True), ): mock_feed_update.return_value = "OK", [mock_entry_1] - assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + assert await async_setup_component(hass, DOMAIN, CONFIG) await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -201,10 +200,7 @@ async def test_setup_imperial( ) # Test conversion of 200 miles to kilometers. - feeds = hass.data[DOMAIN][FEED] - assert feeds is not None - assert len(feeds) == 1 - manager = list(feeds.values())[0] + manager = hass.config_entries.async_loaded_entries(DOMAIN)[0].runtime_data # Ensure that the filter value in km is correctly set. assert manager._feed_manager._feed._filter_radius == 321.8688 diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 6730fa53ece..fd334fa57ee 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from homeassistant.components.geonetnz_quakes import DOMAIN, FEED from homeassistant.core import HomeAssistant @@ -16,8 +15,7 @@ async def test_component_unload_config_entry(hass: HomeAssistant, config_entry) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert mock_feed_manager_update.call_count == 1 - assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None From 19a0a16915bdcfc83ab329ebb74875a11b74ad39 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 12:01:27 +0200 Subject: [PATCH 0163/1175] Revert "Disable S3 checksums" (#144092) (#144318) --- homeassistant/components/s3/__init__.py | 7 ------- tests/components/s3/test_init.py | 17 ----------------- 2 files changed, 24 deletions(-) diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/s3/__init__.py index ea6b8e244b1..95e5e7d738c 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/s3/__init__.py @@ -7,7 +7,6 @@ from typing import cast from aiobotocore.client import AioBaseClient as S3Client from aiobotocore.session import AioSession -from botocore.config import Config from botocore.exceptions import ClientError, ConnectionError, ParamValidationError from homeassistant.config_entries import ConfigEntry @@ -33,11 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: """Set up S3 from a config entry.""" data = cast(dict, entry.data) - # due to https://github.com/home-assistant/core/issues/143995 - config = Config( - request_checksum_calculation="when_required", - response_checksum_validation="when_required", - ) try: session = AioSession() # pylint: disable-next=unnecessary-dunder-call @@ -46,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool: endpoint_url=data.get(CONF_ENDPOINT_URL), aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY], aws_access_key_id=data[CONF_ACCESS_KEY_ID], - config=config, ).__aenter__() await client.head_bucket(Bucket=data[CONF_BUCKET]) except ClientError as err: diff --git a/tests/components/s3/test_init.py b/tests/components/s3/test_init.py index 8255bbd0c66..afa11f5cf72 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/s3/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from botocore.config import Config from botocore.exceptions import ( ClientError, EndpointConnectionError, @@ -74,19 +73,3 @@ async def test_setup_entry_head_bucket_error( ) await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR - - -async def test_checksum_settings_present( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> None: - """Test that checksum validation is set to be compatible with third-party S3 providers.""" - # due to https://github.com/home-assistant/core/issues/143995 - with patch( - "homeassistant.components.s3.AioSession.create_client" - ) as mock_create_client: - await setup_integration(hass, mock_config_entry) - - config_arg = mock_create_client.call_args[1]["config"] - assert isinstance(config_arg, Config) - assert config_arg.request_checksum_calculation == "when_required" - assert config_arg.response_checksum_validation == "when_required" From 5a01521ff831db08f0403c5b78361c607719b8cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 12:06:00 +0200 Subject: [PATCH 0164/1175] Use runtime_data in geonetnz_volcano (#144320) --- .../components/geonetnz_volcano/__init__.py | 19 ++++++++++--------- .../components/geonetnz_volcano/const.py | 2 -- .../components/geonetnz_volcano/sensor.py | 8 +++----- .../components/geonetnz_volcano/test_init.py | 4 +--- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index b08d6d62c55..c3ceeab33f8 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -29,7 +29,6 @@ from .const import ( DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, - FEED, IMPERIAL_UNITS, PLATFORMS, ) @@ -52,6 +51,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +type GeonetnzVolcanoConfigEntry = ConfigEntry[GeonetnzVolcanoFeedEntityManager] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the GeoNet NZ Volcano component.""" @@ -84,11 +85,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: GeonetnzVolcanoConfigEntry +) -> bool: """Set up the GeoNet NZ Volcano component as config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(FEED, {}) - radius = config_entry.data[CONF_RADIUS] unit_system = config_entry.data[CONF_UNIT_SYSTEM] if unit_system == IMPERIAL_UNITS: @@ -97,16 +97,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Create feed entity manager for all platforms. manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system) - hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + config_entry.runtime_data = manager _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) await manager.async_init() return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: GeonetnzVolcanoConfigEntry +) -> bool: """Unload an GeoNet NZ Volcano component config entry.""" - manager = hass.data[DOMAIN][FEED].pop(entry.entry_id) - await manager.async_stop() + await entry.runtime_data.async_stop() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py index be04a25d27a..98ac69fec19 100644 --- a/homeassistant/components/geonetnz_volcano/const.py +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -6,8 +6,6 @@ from homeassistant.const import Platform DOMAIN = "geonetnz_volcano" -FEED = "feed" - ATTR_ACTIVITY = "activity" ATTR_DISTANCE = "distance" ATTR_EXTERNAL_ID = "external_id" diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index bde04acb895..159806778ce 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -5,7 +5,6 @@ from __future__ import annotations import logging from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, UnitOfLength from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,14 +12,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import DistanceConverter +from . import GeonetnzVolcanoConfigEntry from .const import ( ATTR_ACTIVITY, ATTR_DISTANCE, ATTR_EXTERNAL_ID, ATTR_HAZARDS, DEFAULT_ICON, - DOMAIN, - FEED, IMPERIAL_UNITS, ) @@ -32,11 +30,11 @@ ATTR_LAST_UPDATE_SUCCESSFUL = "feed_last_update_successful" async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GeonetnzVolcanoConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GeoNet NZ Volcano Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager = entry.runtime_data @callback def async_add_sensor(feed_manager, external_id, unit_system): diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index fe113434dc6..49b4af2abec 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from homeassistant.components.geonetnz_volcano import DOMAIN, FEED from homeassistant.core import HomeAssistant @@ -17,8 +16,7 @@ async def test_component_unload_config_entry(hass: HomeAssistant, config_entry) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert mock_feed_manager_update.call_count == 1 - assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None From 57217b46ed18694ac14f625094b815c32d72c26c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 12:14:46 +0200 Subject: [PATCH 0165/1175] Use runtime_data in gogogate2 (#144322) --- .../components/gogogate2/__init__.py | 12 ++-- homeassistant/components/gogogate2/common.py | 58 ++++++++----------- homeassistant/components/gogogate2/const.py | 2 +- .../components/gogogate2/coordinator.py | 6 +- homeassistant/components/gogogate2/cover.py | 11 ++-- homeassistant/components/gogogate2/entity.py | 5 +- homeassistant/components/gogogate2/sensor.py | 13 ++--- 7 files changed, 50 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index ceb07c99849..1afb77a4f70 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,16 +1,16 @@ """The gogogate2 component.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant -from .common import get_data_update_coordinator +from .common import create_data_update_coordinator from .const import DEVICE_TYPE_GOGOGATE2 +from .coordinator import GogoGateConfigEntry PLATFORMS = [Platform.COVER, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GogoGateConfigEntry) -> bool: """Do setup of Gogogate2.""" # Update the config entry. @@ -24,14 +24,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if config_updates: hass.config_entries.async_update_entry(entry, data=config_updates) - data_update_coordinator = get_data_update_coordinator(hass, entry) + data_update_coordinator = create_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() + entry.runtime_data = data_update_coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GogoGateConfigEntry) -> bool: """Unload Gogogate2 config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 8506414ca33..a98e1194e5b 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -16,7 +16,6 @@ from ismartgate import ( ) from ismartgate.common import AbstractDoor -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE, CONF_IP_ADDRESS, @@ -27,8 +26,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import UpdateFailed -from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN -from .coordinator import DeviceDataUpdateCoordinator +from .const import DEVICE_TYPE_ISMARTGATE +from .coordinator import DeviceDataUpdateCoordinator, GogoGateConfigEntry _LOGGER = logging.getLogger(__name__) @@ -41,47 +40,40 @@ class StateData(NamedTuple): door: AbstractDoor | None -def get_data_update_coordinator( - hass: HomeAssistant, config_entry: ConfigEntry +def create_data_update_coordinator( + hass: HomeAssistant, config_entry: GogoGateConfigEntry ) -> DeviceDataUpdateCoordinator: """Get an update coordinator.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) - config_entry_data = hass.data[DOMAIN][config_entry.entry_id] + api = get_api(hass, config_entry.data) - if DATA_UPDATE_COORDINATOR not in config_entry_data: - api = get_api(hass, config_entry.data) + async def async_update_data() -> GogoGate2InfoResponse | ISmartGateInfoResponse: + try: + return await api.async_info() + except Exception as exception: + raise UpdateFailed( + f"Error communicating with API: {exception}" + ) from exception - async def async_update_data() -> GogoGate2InfoResponse | ISmartGateInfoResponse: - try: - return await api.async_info() - except Exception as exception: - raise UpdateFailed( - f"Error communicating with API: {exception}" - ) from exception - - config_entry_data[DATA_UPDATE_COORDINATOR] = DeviceDataUpdateCoordinator( - hass, - config_entry, - _LOGGER, - api, - # Name of the data. For logging purposes. - name="gogogate2", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=5), - ) - - return config_entry_data[DATA_UPDATE_COORDINATOR] + return DeviceDataUpdateCoordinator( + hass, + config_entry, + _LOGGER, + api, + # Name of the data. For logging purposes. + name="gogogate2", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=5), + ) -def cover_unique_id(config_entry: ConfigEntry, door: AbstractDoor) -> str: +def cover_unique_id(config_entry: GogoGateConfigEntry, door: AbstractDoor) -> str: """Generate a cover entity unique id.""" return f"{config_entry.unique_id}_{door.door_id}" def sensor_unique_id( - config_entry: ConfigEntry, door: AbstractDoor, sensor_type: str + config_entry: GogoGateConfigEntry, door: AbstractDoor, sensor_type: str ) -> str: """Generate a cover entity unique id.""" return f"{config_entry.unique_id}_{door.door_id}_{sensor_type}" diff --git a/homeassistant/components/gogogate2/const.py b/homeassistant/components/gogogate2/const.py index 2f6ac76122f..a5122b7e215 100644 --- a/homeassistant/components/gogogate2/const.py +++ b/homeassistant/components/gogogate2/const.py @@ -1,7 +1,7 @@ """Constants for integration.""" DOMAIN = "gogogate2" -DATA_UPDATE_COORDINATOR = "data_update_coordinator" + DEVICE_TYPE_GOGOGATE2 = "gogogate2" DEVICE_TYPE_ISMARTGATE = "ismartgate" MANUFACTURER = "Remsol" diff --git a/homeassistant/components/gogogate2/coordinator.py b/homeassistant/components/gogogate2/coordinator.py index c2e7cc47b46..5f5a082084c 100644 --- a/homeassistant/components/gogogate2/coordinator.py +++ b/homeassistant/components/gogogate2/coordinator.py @@ -13,18 +13,20 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +type GogoGateConfigEntry = ConfigEntry[DeviceDataUpdateCoordinator] + class DeviceDataUpdateCoordinator( DataUpdateCoordinator[GogoGate2InfoResponse | ISmartGateInfoResponse] ): """Manages polling for state changes from the device.""" - config_entry: ConfigEntry + config_entry: GogoGateConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, logger: logging.Logger, api: AbstractGateApi, *, diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 9492108d4b2..539e53598fb 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -16,22 +16,21 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .common import cover_unique_id, get_data_update_coordinator -from .coordinator import DeviceDataUpdateCoordinator +from .common import cover_unique_id +from .coordinator import DeviceDataUpdateCoordinator, GogoGateConfigEntry from .entity import GoGoGate2Entity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the config entry.""" - data_update_coordinator = get_data_update_coordinator(hass, config_entry) + data_update_coordinator = config_entry.runtime_data async_add_entities( [ @@ -48,7 +47,7 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, data_update_coordinator: DeviceDataUpdateCoordinator, door: AbstractDoor, ) -> None: diff --git a/homeassistant/components/gogogate2/entity.py b/homeassistant/components/gogogate2/entity.py index 8a699f6101b..a6879f038bc 100644 --- a/homeassistant/components/gogogate2/entity.py +++ b/homeassistant/components/gogogate2/entity.py @@ -4,13 +4,12 @@ from __future__ import annotations from ismartgate.common import AbstractDoor, get_door_by_id -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER -from .coordinator import DeviceDataUpdateCoordinator +from .coordinator import DeviceDataUpdateCoordinator, GogoGateConfigEntry class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]): @@ -18,7 +17,7 @@ class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]): def __init__( self, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, data_update_coordinator: DeviceDataUpdateCoordinator, door: AbstractDoor, unique_id: str, diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index ce86ca9ac43..c594671b34f 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -11,13 +11,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .common import get_data_update_coordinator, sensor_unique_id -from .coordinator import DeviceDataUpdateCoordinator +from .common import sensor_unique_id +from .coordinator import DeviceDataUpdateCoordinator, GogoGateConfigEntry from .entity import GoGoGate2Entity SENSOR_ID_WIRED = "WIRE" @@ -25,11 +24,11 @@ SENSOR_ID_WIRED = "WIRE" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the config entry.""" - data_update_coordinator = get_data_update_coordinator(hass, config_entry) + data_update_coordinator = config_entry.runtime_data sensors = chain( [ @@ -69,7 +68,7 @@ class DoorSensorBattery(DoorSensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, data_update_coordinator: DeviceDataUpdateCoordinator, door: AbstractDoor, ) -> None: @@ -97,7 +96,7 @@ class DoorSensorTemperature(DoorSensorEntity): def __init__( self, - config_entry: ConfigEntry, + config_entry: GogoGateConfigEntry, data_update_coordinator: DeviceDataUpdateCoordinator, door: AbstractDoor, ) -> None: From c9a9488ff5dd72ddd2830924f1ac323c26dd6bf5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 6 May 2025 13:20:04 +0300 Subject: [PATCH 0166/1175] Manage unsupported sources on Samsung TV (#144221) --- .../components/samsungtv/media_player.py | 6 ++++- .../components/samsungtv/strings.json | 3 +++ .../components/samsungtv/test_media_player.py | 22 ++++++++++++++----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cc3ca5f142e..fa4f04a97ec 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -386,4 +386,8 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity): await self._async_send_keys([SOURCES[source]]) return - LOGGER.error("Unsupported source") + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="source_unsupported", + translation_placeholders={"entity": self.entity_id, "source": source}, + ) diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index fc3be3fcc19..17fde5db5bf 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -68,6 +68,9 @@ "service_unsupported": { "message": "Entity {entity} does not support this action." }, + "source_unsupported": { + "message": "Entity {entity} does not support source {source}." + }, "error_set_volume": { "message": "Unable to set volume level on {host}: {error}" }, diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 7dc5c6489d8..d36ff8daeb3 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1105,17 +1105,27 @@ async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: async def test_select_source_invalid_source(hass: HomeAssistant) -> None: """Test for select_source with invalid source.""" + + source = "INVALID" + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: await setup_samsungtv_entry(hass, MOCK_CONFIG) remote.reset_mock() - await hass.services.async_call( - MP_DOMAIN, - SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, - True, - ) + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: source}, + True, + ) # control not called assert remote.control.call_count == 0 + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "source_unsupported" + assert exc_info.value.translation_placeholders == { + "entity": ENTITY_ID, + "source": source, + } @pytest.mark.usefixtures("rest_api") From 687c74ee4c380538aa9dae541e35227dd08054b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 12:21:48 +0200 Subject: [PATCH 0167/1175] Remove deprecated freebox reboot service (#144303) --- homeassistant/components/freebox/__init__.py | 22 ++------------------ homeassistant/components/freebox/const.py | 1 - tests/components/freebox/test_init.py | 20 ++---------------- 3 files changed, 4 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 90ebd53048a..46d68fc8f60 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,23 +1,20 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from datetime import timedelta -import logging from freebox_api.exceptions import HttpRequestError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, PLATFORMS, SERVICE_REBOOT +from .const import DOMAIN, PLATFORMS from .router import FreeboxRouter, get_api SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Freebox entry.""" @@ -40,20 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - # Services - async def async_reboot(call: ServiceCall) -> None: - """Handle reboot service call.""" - # The Freebox reboot service has been replaced by a - # dedicated button entity and marked as deprecated - _LOGGER.warning( - "The 'freebox.reboot' service is deprecated and " - "replaced by a dedicated reboot button entity; please " - "use that entity to reboot the freebox instead" - ) - await router.reboot() - - hass.services.async_register(DOMAIN, SERVICE_REBOOT, async_reboot) - async def async_close_connection(event: Event) -> None: """Close Freebox connection on HA Stop.""" await router.close() @@ -71,6 +54,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: router: FreeboxRouter = hass.data[DOMAIN].pop(entry.unique_id) await router.close() - hass.services.async_remove(DOMAIN, SERVICE_REBOOT) return unload_ok diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 13be45926b4..da5ae836be0 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -8,7 +8,6 @@ import socket from homeassistant.const import Platform DOMAIN = "freebox" -SERVICE_REBOOT = "reboot" APP_DESC = { "app_id": "hass", diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 4be58f247cd..c696ba838be 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -1,11 +1,11 @@ """Tests for the Freebox init.""" -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, Mock from pytest_unordered import unordered from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN -from homeassistant.components.freebox.const import DOMAIN, SERVICE_REBOOT +from homeassistant.components.freebox.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 @@ -33,19 +33,6 @@ async def test_setup(hass: HomeAssistant, router: Mock) -> None: assert router.call_count == 1 assert router().open.call_count == 1 - assert hass.services.has_service(DOMAIN, SERVICE_REBOOT) - - with patch( - "homeassistant.components.freebox.router.FreeboxRouter.reboot" - ) as mock_service: - await hass.services.async_call( - DOMAIN, - SERVICE_REBOOT, - blocking=True, - ) - await hass.async_block_till_done() - mock_service.assert_called_once() - async def test_setup_import(hass: HomeAssistant, router: Mock) -> None: """Test setup of integration from import.""" @@ -65,8 +52,6 @@ async def test_setup_import(hass: HomeAssistant, router: Mock) -> None: assert router.call_count == 1 assert router().open.call_count == 1 - assert hass.services.has_service(DOMAIN, SERVICE_REBOOT) - async def test_unload_remove(hass: HomeAssistant, router: Mock) -> None: """Test unload and remove of integration.""" @@ -106,7 +91,6 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock) -> None: assert state_switch.state == STATE_UNAVAILABLE assert router().close.call_count == 1 - assert not hass.services.has_service(DOMAIN, SERVICE_REBOOT) await hass.config_entries.async_remove(entry.entry_id) await hass.async_block_till_done() From 5475d7ef587ee8fba37de625547c88d4dfa17610 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 13:09:56 +0200 Subject: [PATCH 0168/1175] Use runtime_data in freebox (#144326) --- homeassistant/components/freebox/__init__.py | 20 +++++++------------ .../components/freebox/alarm_control_panel.py | 9 ++++----- .../components/freebox/binary_sensor.py | 9 ++++----- homeassistant/components/freebox/button.py | 8 +++----- homeassistant/components/freebox/camera.py | 9 ++++----- .../components/freebox/device_tracker.py | 9 ++++----- homeassistant/components/freebox/router.py | 4 +++- homeassistant/components/freebox/sensor.py | 7 +++---- homeassistant/components/freebox/switch.py | 8 +++----- 9 files changed, 35 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 46d68fc8f60..94ccae61088 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -4,19 +4,18 @@ from datetime import timedelta from freebox_api.exceptions import HttpRequestError -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, PLATFORMS -from .router import FreeboxRouter, get_api +from .const import PLATFORMS +from .router import FreeboxConfigEntry, FreeboxRouter, get_api SCAN_INTERVAL = timedelta(seconds=30) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool: """Set up Freebox entry.""" api = await get_api(hass, entry.data[CONF_HOST]) try: @@ -32,8 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_track_time_interval(hass, router.update_all, SCAN_INTERVAL) ) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = router + entry.runtime_data = router await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -44,15 +42,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) ) + entry.async_on_unload(router.close) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: FreeboxConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - router: FreeboxRouter = hass.data[DOMAIN].pop(entry.unique_id) - await router.close() - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/freebox/alarm_control_panel.py b/homeassistant/components/freebox/alarm_control_panel.py index 89462b33a2f..b0242a1b054 100644 --- a/homeassistant/components/freebox/alarm_control_panel.py +++ b/homeassistant/components/freebox/alarm_control_panel.py @@ -7,13 +7,12 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, AlarmControlPanelState, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, FreeboxHomeCategory +from .const import FreeboxHomeCategory from .entity import FreeboxHomeEntity -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter FREEBOX_TO_STATUS = { "alarm1_arming": AlarmControlPanelState.ARMING, @@ -29,11 +28,11 @@ FREEBOX_TO_STATUS = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up alarm panel.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data async_add_entities( ( diff --git a/homeassistant/components/freebox/binary_sensor.py b/homeassistant/components/freebox/binary_sensor.py index 9fc9929b869..75b7dded36a 100644 --- a/homeassistant/components/freebox/binary_sensor.py +++ b/homeassistant/components/freebox/binary_sensor.py @@ -10,15 +10,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, FreeboxHomeCategory +from .const import FreeboxHomeCategory from .entity import FreeboxHomeEntity -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -35,11 +34,11 @@ RAID_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up binary sensors.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data _LOGGER.debug("%s - %s - %s raid(s)", router.name, router.mac, len(router.raids)) diff --git a/homeassistant/components/freebox/button.py b/homeassistant/components/freebox/button.py index 4f676fd46a1..21a7b1c9990 100644 --- a/homeassistant/components/freebox/button.py +++ b/homeassistant/components/freebox/button.py @@ -10,13 +10,11 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter @dataclass(frozen=True, kw_only=True) @@ -45,11 +43,11 @@ BUTTON_DESCRIPTIONS: tuple[FreeboxButtonEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the buttons.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data entities = [ FreeboxButton(router, description) for description in BUTTON_DESCRIPTIONS ] diff --git a/homeassistant/components/freebox/camera.py b/homeassistant/components/freebox/camera.py index 45bb5a34063..d997908dd06 100644 --- a/homeassistant/components/freebox/camera.py +++ b/homeassistant/components/freebox/camera.py @@ -12,27 +12,26 @@ from homeassistant.components.ffmpeg.camera import ( DEFAULT_ARGUMENTS, FFmpegCamera, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ATTR_DETECTION, DOMAIN, FreeboxHomeCategory +from .const import ATTR_DETECTION, FreeboxHomeCategory from .entity import FreeboxHomeEntity -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up cameras.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data tracked: set[str] = set() @callback diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index dcb6eb104b2..243f0de315a 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -6,22 +6,21 @@ from datetime import datetime from typing import Any from homeassistant.components.device_tracker import ScannerEntity -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 AddConfigEntryEntitiesCallback -from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN -from .router import FreeboxRouter +from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS +from .router import FreeboxConfigEntry, FreeboxRouter async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up device tracker for Freebox component.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data tracked: set[str] = set() @callback diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 753bdff8cec..d6c45cd178b 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -38,6 +38,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type FreeboxConfigEntry = ConfigEntry[FreeboxRouter] + def is_json(json_str: str) -> bool: """Validate if a String is a JSON value or not.""" @@ -102,7 +104,7 @@ class FreeboxRouter: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, api: Freepybox, freebox_config: Mapping[str, Any], ) -> None: diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index cc62de9ae0d..7a176ca5fa7 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -20,7 +19,7 @@ from homeassistant.util import dt as dt_util from .const import DOMAIN from .entity import FreeboxHomeEntity -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -61,11 +60,11 @@ DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensors.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data entities: list[SensorEntity] = [] _LOGGER.debug( diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index c4618b014bf..9506a87b5fa 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -8,13 +8,11 @@ from typing import Any from freebox_api.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .router import FreeboxRouter +from .router import FreeboxConfigEntry, FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -30,11 +28,11 @@ SWITCH_DESCRIPTIONS = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: FreeboxConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the switch.""" - router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] + router = entry.runtime_data entities = [ FreeboxSwitch(router, entity_description) for entity_description in SWITCH_DESCRIPTIONS From ce95876d03dd5ee6208e069930cafdf024f03f85 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 13:29:37 +0200 Subject: [PATCH 0169/1175] Rename S3 to AWS_S3 (#144324) --- CODEOWNERS | 4 ++-- homeassistant/brands/amazon.json | 9 ++++++++- homeassistant/components/{s3 => aws_s3}/__init__.py | 2 +- homeassistant/components/{s3 => aws_s3}/backup.py | 2 +- .../components/{s3 => aws_s3}/config_flow.py | 2 +- homeassistant/components/{s3 => aws_s3}/const.py | 4 ++-- .../components/{s3 => aws_s3}/manifest.json | 6 +++--- .../components/{s3 => aws_s3}/quality_scale.yaml | 0 homeassistant/components/{s3 => aws_s3}/strings.json | 12 ++++++------ homeassistant/generated/config_flows.py | 2 +- homeassistant/generated/integrations.json | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/{s3 => aws_s3}/__init__.py | 2 +- tests/components/{s3 => aws_s3}/conftest.py | 8 ++++---- tests/components/{s3 => aws_s3}/const.py | 4 ++-- tests/components/{s3 => aws_s3}/test_backup.py | 10 +++++----- tests/components/{s3 => aws_s3}/test_config_flow.py | 4 ++-- tests/components/{s3 => aws_s3}/test_init.py | 2 +- 19 files changed, 48 insertions(+), 41 deletions(-) rename homeassistant/components/{s3 => aws_s3}/__init__.py (98%) rename homeassistant/components/{s3 => aws_s3}/backup.py (99%) rename homeassistant/components/{s3 => aws_s3}/config_flow.py (98%) rename homeassistant/components/{s3 => aws_s3}/const.py (90%) rename homeassistant/components/{s3 => aws_s3}/manifest.json (66%) rename homeassistant/components/{s3 => aws_s3}/quality_scale.yaml (100%) rename homeassistant/components/{s3 => aws_s3}/strings.json (73%) rename tests/components/{s3 => aws_s3}/__init__.py (90%) rename tests/components/{s3 => aws_s3}/conftest.py (93%) rename tests/components/{s3 => aws_s3}/const.py (78%) rename tests/components/{s3 => aws_s3}/test_backup.py (98%) rename tests/components/{s3 => aws_s3}/test_config_flow.py (96%) rename tests/components/{s3 => aws_s3}/test_init.py (98%) diff --git a/CODEOWNERS b/CODEOWNERS index 89b0cf16af0..997fd1b0981 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -171,6 +171,8 @@ build.json @home-assistant/supervisor /homeassistant/components/avea/ @pattyland /homeassistant/components/awair/ @ahayworth @danielsjf /tests/components/awair/ @ahayworth @danielsjf +/homeassistant/components/aws_s3/ @tomasbedrich +/tests/components/aws_s3/ @tomasbedrich /homeassistant/components/axis/ @Kane610 /tests/components/axis/ @Kane610 /homeassistant/components/azure_data_explorer/ @kaareseras @@ -1318,8 +1320,6 @@ 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/s3/ @tomasbedrich -/tests/components/s3/ @tomasbedrich /homeassistant/components/sabnzbd/ @shaiu @jpbede /tests/components/sabnzbd/ @shaiu @jpbede /homeassistant/components/saj/ @fredericvl diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index a7caea2b932..624a8a17b7d 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -1,5 +1,12 @@ { "domain": "amazon", "name": "Amazon", - "integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"] + "integrations": [ + "alexa", + "amazon_polly", + "aws", + "aws_s3", + "fire_tv", + "route53" + ] } diff --git a/homeassistant/components/s3/__init__.py b/homeassistant/components/aws_s3/__init__.py similarity index 98% rename from homeassistant/components/s3/__init__.py rename to homeassistant/components/aws_s3/__init__.py index 95e5e7d738c..b709595ae4a 100644 --- a/homeassistant/components/s3/__init__.py +++ b/homeassistant/components/aws_s3/__init__.py @@ -1,4 +1,4 @@ -"""The S3 integration.""" +"""The AWS S3 integration.""" from __future__ import annotations diff --git a/homeassistant/components/s3/backup.py b/homeassistant/components/aws_s3/backup.py similarity index 99% rename from homeassistant/components/s3/backup.py rename to homeassistant/components/aws_s3/backup.py index a58947d4c2d..7ef1289132d 100644 --- a/homeassistant/components/s3/backup.py +++ b/homeassistant/components/aws_s3/backup.py @@ -1,4 +1,4 @@ -"""Backup platform for the S3 integration.""" +"""Backup platform for the AWS S3 integration.""" from collections.abc import AsyncIterator, Callable, Coroutine import functools diff --git a/homeassistant/components/s3/config_flow.py b/homeassistant/components/aws_s3/config_flow.py similarity index 98% rename from homeassistant/components/s3/config_flow.py rename to homeassistant/components/aws_s3/config_flow.py index d721594b7bd..81ddd881f0f 100644 --- a/homeassistant/components/s3/config_flow.py +++ b/homeassistant/components/aws_s3/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for the S3 integration.""" +"""Config flow for the AWS S3 integration.""" from __future__ import annotations diff --git a/homeassistant/components/s3/const.py b/homeassistant/components/aws_s3/const.py similarity index 90% rename from homeassistant/components/s3/const.py rename to homeassistant/components/aws_s3/const.py index d992a92ac20..95d53c93a08 100644 --- a/homeassistant/components/s3/const.py +++ b/homeassistant/components/aws_s3/const.py @@ -1,11 +1,11 @@ -"""Constants for the S3 integration.""" +"""Constants for the AWS S3 integration.""" from collections.abc import Callable from typing import Final from homeassistant.util.hass_dict import HassKey -DOMAIN: Final = "s3" +DOMAIN: Final = "aws_s3" CONF_ACCESS_KEY_ID = "access_key_id" CONF_SECRET_ACCESS_KEY = "secret_access_key" diff --git a/homeassistant/components/s3/manifest.json b/homeassistant/components/aws_s3/manifest.json similarity index 66% rename from homeassistant/components/s3/manifest.json rename to homeassistant/components/aws_s3/manifest.json index 6a3026ff76d..8ab65b5883a 100644 --- a/homeassistant/components/s3/manifest.json +++ b/homeassistant/components/aws_s3/manifest.json @@ -1,9 +1,9 @@ { - "domain": "s3", - "name": "S3", + "domain": "aws_s3", + "name": "AWS S3", "codeowners": ["@tomasbedrich"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/s3", + "documentation": "https://www.home-assistant.io/integrations/aws_s3", "integration_type": "service", "iot_class": "cloud_push", "loggers": ["aiobotocore"], diff --git a/homeassistant/components/s3/quality_scale.yaml b/homeassistant/components/aws_s3/quality_scale.yaml similarity index 100% rename from homeassistant/components/s3/quality_scale.yaml rename to homeassistant/components/aws_s3/quality_scale.yaml diff --git a/homeassistant/components/s3/strings.json b/homeassistant/components/aws_s3/strings.json similarity index 73% rename from homeassistant/components/s3/strings.json rename to homeassistant/components/aws_s3/strings.json index 3404321be03..b5683aafa6e 100644 --- a/homeassistant/components/s3/strings.json +++ b/homeassistant/components/aws_s3/strings.json @@ -9,18 +9,18 @@ "endpoint_url": "Endpoint URL" }, "data_description": { - "access_key_id": "Access key ID to connect to S3 API", - "secret_access_key": "Secret access key to connect to S3 API", + "access_key_id": "Access key ID to connect to AWS S3 API", + "secret_access_key": "Secret access key to connect to AWS S3 API", "bucket": "Bucket must already exist and be writable by the provided credentials.", "endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs." }, - "title": "Add S3 bucket" + "title": "Add AWS S3 bucket" } }, "error": { - "cannot_connect": "[%key:component::s3::exceptions::cannot_connect::message%]", - "invalid_bucket_name": "[%key:component::s3::exceptions::invalid_bucket_name::message%]", - "invalid_credentials": "[%key:component::s3::exceptions::invalid_credentials::message%]", + "cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]", + "invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]", + "invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]", "invalid_endpoint_url": "Invalid endpoint URL" }, "abort": { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 680d0a7bb2c..cf80105a889 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -75,6 +75,7 @@ FLOWS = { "aussie_broadband", "autarco", "awair", + "aws_s3", "axis", "azure_data_explorer", "azure_devops", @@ -541,7 +542,6 @@ FLOWS = { "ruuvi_gateway", "ruuvitag_ble", "rympro", - "s3", "sabnzbd", "samsungtv", "sanix", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1b9e9216827..4cee3922cd4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -219,6 +219,12 @@ "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, + "aws_s3": { + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_push", + "name": "AWS S3" + }, "fire_tv": { "integration_type": "virtual", "config_flow": false, @@ -5622,12 +5628,6 @@ "config_flow": true, "iot_class": "cloud_polling" }, - "s3": { - "name": "S3", - "integration_type": "service", - "config_flow": true, - "iot_class": "cloud_push" - }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 7c253acf5b7..c9c358a5724 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -210,7 +210,7 @@ aioazuredevops==2.2.1 aiobafi6==0.9.0 # homeassistant.components.aws -# homeassistant.components.s3 +# homeassistant.components.aws_s3 aiobotocore==2.21.1 # homeassistant.components.comelit diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d626ecc7b86..71d12a5cf32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aioazuredevops==2.2.1 aiobafi6==0.9.0 # homeassistant.components.aws -# homeassistant.components.s3 +# homeassistant.components.aws_s3 aiobotocore==2.21.1 # homeassistant.components.comelit diff --git a/tests/components/s3/__init__.py b/tests/components/aws_s3/__init__.py similarity index 90% rename from tests/components/s3/__init__.py rename to tests/components/aws_s3/__init__.py index 570747e69d0..90e4652bb2b 100644 --- a/tests/components/s3/__init__.py +++ b/tests/components/aws_s3/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the S3 integration.""" +"""Tests for the AWS S3 integration.""" from homeassistant.core import HomeAssistant diff --git a/tests/components/s3/conftest.py b/tests/components/aws_s3/conftest.py similarity index 93% rename from tests/components/s3/conftest.py rename to tests/components/aws_s3/conftest.py index a2c2b9eb3dd..8f12ee17661 100644 --- a/tests/components/s3/conftest.py +++ b/tests/components/aws_s3/conftest.py @@ -1,4 +1,4 @@ -"""Common fixtures for the S3 tests.""" +"""Common fixtures for the AWS S3 tests.""" from collections.abc import AsyncIterator, Generator import json @@ -6,12 +6,12 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.backup import AgentBackup -from homeassistant.components.s3.backup import ( +from homeassistant.components.aws_s3.backup import ( MULTIPART_MIN_PART_SIZE_BYTES, suggested_filenames, ) -from homeassistant.components.s3.const import DOMAIN +from homeassistant.components.aws_s3.const import DOMAIN +from homeassistant.components.backup import AgentBackup from .const import USER_INPUT diff --git a/tests/components/s3/const.py b/tests/components/aws_s3/const.py similarity index 78% rename from tests/components/s3/const.py rename to tests/components/aws_s3/const.py index 92ebc080f2c..443275d0444 100644 --- a/tests/components/s3/const.py +++ b/tests/components/aws_s3/const.py @@ -1,6 +1,6 @@ -"""Consts for S3 tests.""" +"""Consts for AWS S3 tests.""" -from homeassistant.components.s3.const import ( +from homeassistant.components.aws_s3.const import ( CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_ENDPOINT_URL, diff --git a/tests/components/s3/test_backup.py b/tests/components/aws_s3/test_backup.py similarity index 98% rename from tests/components/s3/test_backup.py rename to tests/components/aws_s3/test_backup.py index 535e546dd21..a8b24ec1ab4 100644 --- a/tests/components/s3/test_backup.py +++ b/tests/components/aws_s3/test_backup.py @@ -1,4 +1,4 @@ -"""Test the S3 backup platform.""" +"""Test the AWS S3 backup platform.""" from collections.abc import AsyncGenerator from io import StringIO @@ -9,19 +9,19 @@ from unittest.mock import AsyncMock, Mock, patch from botocore.exceptions import ConnectTimeoutError import pytest -from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup -from homeassistant.components.s3.backup import ( +from homeassistant.components.aws_s3.backup import ( MULTIPART_MIN_PART_SIZE_BYTES, BotoCoreError, S3BackupAgent, async_register_backup_agents_listener, suggested_filenames, ) -from homeassistant.components.s3.const import ( +from homeassistant.components.aws_s3.const import ( CONF_ENDPOINT_URL, DATA_BACKUP_AGENT_LISTENERS, DOMAIN, ) +from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup from homeassistant.core import HomeAssistant from homeassistant.helpers.backup import async_initialize_backup from homeassistant.setup import async_setup_component @@ -362,7 +362,7 @@ async def test_agents_upload_network_failure( ) assert resp.status == 201 - assert "Upload failed for s3" in caplog.text + assert "Upload failed for aws_s3" in caplog.text async def test_agents_download( diff --git a/tests/components/s3/test_config_flow.py b/tests/components/aws_s3/test_config_flow.py similarity index 96% rename from tests/components/s3/test_config_flow.py rename to tests/components/aws_s3/test_config_flow.py index 1ea59a3aeb5..061d990140a 100644 --- a/tests/components/s3/test_config_flow.py +++ b/tests/components/aws_s3/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the S3 config flow.""" +"""Test the AWS S3 config flow.""" from unittest.mock import AsyncMock, patch @@ -10,7 +10,7 @@ from botocore.exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components.s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN +from homeassistant.components.aws_s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType diff --git a/tests/components/s3/test_init.py b/tests/components/aws_s3/test_init.py similarity index 98% rename from tests/components/s3/test_init.py rename to tests/components/aws_s3/test_init.py index afa11f5cf72..ee247bfce1d 100644 --- a/tests/components/s3/test_init.py +++ b/tests/components/aws_s3/test_init.py @@ -1,4 +1,4 @@ -"""Test the s3 storage integration.""" +"""Test the AWS S3 storage integration.""" from unittest.mock import AsyncMock, patch From deaaf2f082c568dbde14572eeff6cc1adc56076d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 13:35:27 +0200 Subject: [PATCH 0170/1175] Drop alias from local const DOMAIN import (#144312) --- .../components/advantage_air/light.py | 6 ++--- .../components/advantage_air/update.py | 6 ++--- .../components/agent_dvr/__init__.py | 4 ++-- .../agent_dvr/alarm_control_panel.py | 4 ++-- homeassistant/components/axis/entity.py | 4 ++-- homeassistant/components/deconz/entity.py | 10 ++++---- homeassistant/components/deconz/light.py | 6 ++--- .../components/fireservicerota/sensor.py | 4 ++-- .../components/fireservicerota/switch.py | 4 ++-- homeassistant/components/flo/coordinator.py | 4 ++-- homeassistant/components/flo/entity.py | 4 ++-- homeassistant/components/heos/media_player.py | 24 +++++++++---------- .../components/iaqualink/binary_sensor.py | 4 ++-- homeassistant/components/iaqualink/climate.py | 7 ++---- homeassistant/components/iaqualink/light.py | 4 ++-- homeassistant/components/iaqualink/sensor.py | 4 ++-- homeassistant/components/iaqualink/switch.py | 4 ++-- .../components/kaleidescape/entity.py | 4 ++-- .../components/kaleidescape/media_player.py | 4 ++-- .../components/kaleidescape/remote.py | 4 ++-- .../components/kaleidescape/sensor.py | 4 ++-- .../components/konnected/binary_sensor.py | 6 ++--- homeassistant/components/konnected/sensor.py | 6 ++--- .../components/lutron_caseta/scene.py | 4 ++-- .../components/nuki/binary_sensor.py | 4 ++-- homeassistant/components/nuki/sensor.py | 4 ++-- .../components/point/alarm_control_panel.py | 4 ++-- homeassistant/components/screenlogic/util.py | 4 ++-- .../components/tibber/coordinator.py | 6 ++--- homeassistant/components/tibber/sensor.py | 22 +++++++---------- homeassistant/components/unifi/__init__.py | 4 ++-- .../components/unifi/device_tracker.py | 8 +++---- homeassistant/components/unifi/services.py | 8 +++---- .../components/wemo/device_trigger.py | 4 ++-- homeassistant/components/wemo/light.py | 4 ++-- .../components/zha/device_trigger.py | 4 ++-- homeassistant/components/zha/logbook.py | 4 ++-- 37 files changed, 102 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index ffd502663b0..9708adbc1f7 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import AdvantageAirDataConfigEntry -from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN +from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN from .entity import AdvantageAirEntity, AdvantageAirThingEntity from .models import AdvantageAirData @@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): self._id: str = light["id"] self._attr_unique_id += f"-{self._id}" self._attr_device_info = DeviceInfo( - identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, - via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]), + identifiers={(DOMAIN, self._attr_unique_id)}, + via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), manufacturer="Advantage Air", model=light.get("moduleType"), name=light["name"], diff --git a/homeassistant/components/advantage_air/update.py b/homeassistant/components/advantage_air/update.py index 92a162303dd..68df31142e3 100644 --- a/homeassistant/components/advantage_air/update.py +++ b/homeassistant/components/advantage_air/update.py @@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import AdvantageAirDataConfigEntry -from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN +from .const import DOMAIN from .entity import AdvantageAirEntity from .models import AdvantageAirData @@ -32,9 +32,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): """Initialize the Advantage Air App.""" super().__init__(instance) self._attr_device_info = DeviceInfo( - identifiers={ - (ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]) - }, + identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])}, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], name=self.coordinator.data["system"]["name"], diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index 2cb32b6c80e..d504568869c 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL +from .const import DOMAIN, SERVER_URL ATTRIBUTION = "ispyconnect.com" DEFAULT_BRAND = "Agent DVR by ispyconnect.com" @@ -46,7 +46,7 @@ async def async_setup_entry( device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={(AGENT_DOMAIN, agent_client.unique)}, + identifiers={(DOMAIN, agent_client.unique)}, manufacturer="iSpyConnect", name=f"Agent {agent_client.name}", model="Agent DVR", diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 1ac808c87ad..0d9267e7739 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import AgentDVRConfigEntry -from .const import DOMAIN as AGENT_DOMAIN +from .const import DOMAIN CONF_HOME_MODE_NAME = "home" CONF_AWAY_MODE_NAME = "away" @@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity): self._client = client self._attr_unique_id = f"{client.unique}_CP" self._attr_device_info = DeviceInfo( - identifiers={(AGENT_DOMAIN, client.unique)}, + identifiers={(DOMAIN, client.unique)}, name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}", manufacturer="Agent", model=CONST_ALARM_CONTROL_PANEL_NAME, diff --git a/homeassistant/components/axis/entity.py b/homeassistant/components/axis/entity.py index b952000cca8..596d07de40f 100644 --- a/homeassistant/components/axis/entity.py +++ b/homeassistant/components/axis/entity.py @@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, EntityDescription -from .const import DOMAIN as AXIS_DOMAIN +from .const import DOMAIN if TYPE_CHECKING: from .hub import AxisHub @@ -61,7 +61,7 @@ class AxisEntity(Entity): self.hub = hub self._attr_device_info = DeviceInfo( - identifiers={(AXIS_DOMAIN, hub.unique_id)}, + identifiers={(DOMAIN, hub.unique_id)}, serial_number=hub.unique_id, ) diff --git a/homeassistant/components/deconz/entity.py b/homeassistant/components/deconz/entity.py index f45c35ada44..fef973d612c 100644 --- a/homeassistant/components/deconz/entity.py +++ b/homeassistant/components/deconz/entity.py @@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN as DECONZ_DOMAIN +from .const import DOMAIN from .hub import DeconzHub from .util import serial_from_unique_id @@ -59,12 +59,12 @@ class DeconzBase[_DeviceT: _DeviceType]: return DeviceInfo( connections={(CONNECTION_ZIGBEE, self.serial)}, - identifiers={(DECONZ_DOMAIN, self.serial)}, + identifiers={(DOMAIN, self.serial)}, manufacturer=self._device.manufacturer, model=self._device.model_id, name=self._device.name, sw_version=self._device.software_version, - via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), + via_device=(DOMAIN, self.hub.api.config.bridge_id), ) @@ -176,9 +176,9 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]): def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( - identifiers={(DECONZ_DOMAIN, self._group_identifier)}, + identifiers={(DOMAIN, self._group_identifier)}, manufacturer="Dresden Elektronik", model="deCONZ group", name=self.group.name, - via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), + via_device=(DOMAIN, self.hub.api.config.bridge_id), ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index b61a1d39333..1eb827f85d6 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -38,7 +38,7 @@ from homeassistant.util.color import ( ) from . import DeconzConfigEntry -from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS +from .const import DOMAIN, POWER_PLUGS from .entity import DeconzDevice from .hub import DeconzHub @@ -395,11 +395,11 @@ class DeconzGroup(DeconzBaseLight[Group]): def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( - identifiers={(DECONZ_DOMAIN, self.unique_id)}, + identifiers={(DOMAIN, self.unique_id)}, manufacturer="Dresden Elektronik", model="deCONZ group", name=self._device.name, - via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id), + via_device=(DOMAIN, self.hub.api.config.bridge_id), ) @property diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 5ed65609dc8..f7414d7e1bd 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -9,7 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN as FIRESERVICEROTA_DOMAIN +from .const import DOMAIN from .coordinator import FireServiceConfigEntry, FireServiceRotaClient _LOGGER = logging.getLogger(__name__) @@ -106,7 +106,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update", + f"{DOMAIN}_{self._entry_id}_update", self.client_update, ) ) diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index d9fe382e4b1..26dc3b27c19 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as FIRESERVICEROTA_DOMAIN +from .const import DOMAIN from .coordinator import ( FireServiceConfigEntry, FireServiceRotaClient, @@ -122,7 +122,7 @@ class ResponseSwitch(SwitchEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update", + f"{DOMAIN}_{self._entry_id}_update", self.client_update, ) ) diff --git a/homeassistant/components/flo/coordinator.py b/homeassistant/components/flo/coordinator.py index 9f540b230f4..0e50c8c6b03 100644 --- a/homeassistant/components/flo/coordinator.py +++ b/homeassistant/components/flo/coordinator.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import DOMAIN as FLO_DOMAIN, LOGGER +from .const import DOMAIN, LOGGER type FloConfigEntry = ConfigEntry[FloRuntimeData] @@ -55,7 +55,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): hass, LOGGER, config_entry=config_entry, - name=f"{FLO_DOMAIN}-{device_id}", + name=f"{DOMAIN}-{device_id}", update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 072afbae4f2..c9717b16059 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -5,7 +5,7 @@ from __future__ import annotations from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DOMAIN as FLO_DOMAIN +from .const import DOMAIN from .coordinator import FloDeviceDataUpdateCoordinator @@ -32,7 +32,7 @@ class FloEntity(Entity): """Return a device description for device registry.""" return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self._device.mac_address)}, - identifiers={(FLO_DOMAIN, self._device.id)}, + identifiers={(DOMAIN, self._device.id)}, serial_number=self._device.serial_number, manufacturer=self._device.manufacturer, model=self._device.model, diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 810244a815a..dd0cef0ec10 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -50,7 +50,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import services -from .const import DOMAIN as HEOS_DOMAIN +from .const import DOMAIN from .coordinator import HeosConfigEntry, HeosCoordinator PARALLEL_UPDATES = 0 @@ -151,7 +151,7 @@ def catch_action_error[**_P, _R]( return await func(*args, **kwargs) except (HeosError, ValueError) as ex: raise HomeAssistantError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="action_error", translation_placeholders={"action": action, "error": str(ex)}, ) from ex @@ -179,7 +179,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): manufacturer = model_parts[0] if len(model_parts) == 2 else "HEOS" model = model_parts[1] if len(model_parts) == 2 else player.model self._attr_device_info = DeviceInfo( - identifiers={(HEOS_DOMAIN, str(player.player_id))}, + identifiers={(DOMAIN, str(player.player_id))}, manufacturer=manufacturer, model=model, name=player.name, @@ -215,7 +215,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): for member_id in player_ids if ( entity_id := entity_registry.async_get_entity_id( - Platform.MEDIA_PLAYER, HEOS_DOMAIN, str(member_id) + Platform.MEDIA_PLAYER, DOMAIN, str(member_id) ) ) ] @@ -379,7 +379,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): return raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="unknown_source", translation_placeholders={"source": source}, ) @@ -406,7 +406,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): """Set group volume level.""" if self._player.group_id is None: raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="entity_not_grouped", translation_placeholders={"entity_id": self.entity_id}, ) @@ -419,7 +419,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): """Turn group volume down for media player.""" if self._player.group_id is None: raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="entity_not_grouped", translation_placeholders={"entity_id": self.entity_id}, ) @@ -430,7 +430,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): """Turn group volume up for media player.""" if self._player.group_id is None: raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="entity_not_grouped", translation_placeholders={"entity_id": self.entity_id}, ) @@ -446,13 +446,13 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): entity_entry = entity_registry.async_get(entity_id) if entity_entry is None: raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="entity_not_found", translation_placeholders={"entity_id": entity_id}, ) - if entity_entry.platform != HEOS_DOMAIN: + if entity_entry.platform != DOMAIN: raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="not_heos_media_player", translation_placeholders={"entity_id": entity_id}, ) @@ -648,7 +648,7 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity): if media_source.is_media_source_id(media_content_id): return await self._async_browse_media_source(media_content_id) raise ServiceValidationError( - translation_domain=HEOS_DOMAIN, + translation_domain=DOMAIN, translation_key="unsupported_media_content_id", translation_placeholders={"media_content_id": media_content_id}, ) diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 8fe9d77fbe8..5546e5e9006 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as AQUALINK_DOMAIN +from .const import DOMAIN from .entity import AqualinkEntity PARALLEL_UPDATES = 0 @@ -28,7 +28,7 @@ async def async_setup_entry( async_add_entities( ( HassAqualinkBinarySensor(dev) - for dev in hass.data[AQUALINK_DOMAIN][BINARY_SENSOR_DOMAIN] + for dev in hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] ), True, ) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index d30700898c8..fdd16205be4 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import refresh_system -from .const import DOMAIN as AQUALINK_DOMAIN +from .const import DOMAIN from .entity import AqualinkEntity from .utils import await_or_reraise @@ -37,10 +37,7 @@ async def async_setup_entry( ) -> None: """Set up discovered switches.""" async_add_entities( - ( - HassAqualinkThermostat(dev) - for dev in hass.data[AQUALINK_DOMAIN][CLIMATE_DOMAIN] - ), + (HassAqualinkThermostat(dev) for dev in hass.data[DOMAIN][CLIMATE_DOMAIN]), True, ) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index e515c482158..868480b0913 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import refresh_system -from .const import DOMAIN as AQUALINK_DOMAIN +from .const import DOMAIN from .entity import AqualinkEntity from .utils import await_or_reraise @@ -33,7 +33,7 @@ async def async_setup_entry( ) -> None: """Set up discovered lights.""" async_add_entities( - (HassAqualinkLight(dev) for dev in hass.data[AQUALINK_DOMAIN][LIGHT_DOMAIN]), + (HassAqualinkLight(dev) for dev in hass.data[DOMAIN][LIGHT_DOMAIN]), True, ) diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 1b453f28d8f..a28d527b239 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as AQUALINK_DOMAIN +from .const import DOMAIN from .entity import AqualinkEntity PARALLEL_UPDATES = 0 @@ -27,7 +27,7 @@ async def async_setup_entry( ) -> None: """Set up discovered sensors.""" async_add_entities( - (HassAqualinkSensor(dev) for dev in hass.data[AQUALINK_DOMAIN][SENSOR_DOMAIN]), + (HassAqualinkSensor(dev) for dev in hass.data[DOMAIN][SENSOR_DOMAIN]), True, ) diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index e746cbb4f4b..e01e294355f 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import refresh_system -from .const import DOMAIN as AQUALINK_DOMAIN +from .const import DOMAIN from .entity import AqualinkEntity from .utils import await_or_reraise @@ -26,7 +26,7 @@ async def async_setup_entry( ) -> None: """Set up discovered switches.""" async_add_entities( - (HassAqualinkSwitch(dev) for dev in hass.data[AQUALINK_DOMAIN][SWITCH_DOMAIN]), + (HassAqualinkSwitch(dev) for dev in hass.data[DOMAIN][SWITCH_DOMAIN]), True, ) diff --git a/homeassistant/components/kaleidescape/entity.py b/homeassistant/components/kaleidescape/entity.py index 667cba757d6..1c391b6600b 100644 --- a/homeassistant/components/kaleidescape/entity.py +++ b/homeassistant/components/kaleidescape/entity.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity -from .const import DOMAIN as KALEIDESCAPE_DOMAIN, NAME as KALEIDESCAPE_NAME +from .const import DOMAIN, NAME as KALEIDESCAPE_NAME if TYPE_CHECKING: from kaleidescape import Device as KaleidescapeDevice @@ -29,7 +29,7 @@ class KaleidescapeEntity(Entity): self._attr_unique_id = device.serial_number self._attr_device_info = DeviceInfo( - identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)}, + identifiers={(DOMAIN, self._device.serial_number)}, # Instead of setting the device name to the entity name, kaleidescape # should be updated to set has_entity_name = True name=f"{KALEIDESCAPE_NAME} {device.system.friendly_name}", diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index 88e2e16bef2..cd8aa9d4a8e 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.util.dt import utcnow -from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .const import DOMAIN from .entity import KaleidescapeEntity if TYPE_CHECKING: @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - entities = [KaleidescapeMediaPlayer(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + entities = [KaleidescapeMediaPlayer(hass.data[DOMAIN][entry.entry_id])] async_add_entities(entities) diff --git a/homeassistant/components/kaleidescape/remote.py b/homeassistant/components/kaleidescape/remote.py index ddafd52f220..2b341e0c429 100644 --- a/homeassistant/components/kaleidescape/remote.py +++ b/homeassistant/components/kaleidescape/remote.py @@ -9,7 +9,7 @@ from kaleidescape import const as kaleidescape_const from homeassistant.components.remote import RemoteEntity from homeassistant.exceptions import HomeAssistantError -from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .const import DOMAIN from .entity import KaleidescapeEntity if TYPE_CHECKING: @@ -27,7 +27,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - entities = [KaleidescapeRemote(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + entities = [KaleidescapeRemote(hass.data[DOMAIN][entry.entry_id])] async_add_entities(entities) diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py index 8bff5df2e70..ac0f6504daa 100644 --- a/homeassistant/components/kaleidescape/sensor.py +++ b/homeassistant/components/kaleidescape/sensor.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import PERCENTAGE, EntityCategory -from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .const import DOMAIN from .entity import KaleidescapeEntity if TYPE_CHECKING: @@ -136,7 +136,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform from a config entry.""" - device: KaleidescapeDevice = hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id] + device: KaleidescapeDevice = hass.data[DOMAIN][entry.entry_id] async_add_entities( KaleidescapeSensor(device, description) for description in SENSOR_TYPES ) diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 3f1a27302d8..d6bdab37a9c 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as KONNECTED_DOMAIN +from .const import DOMAIN async def async_setup_entry( @@ -24,7 +24,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up binary sensors attached to a Konnected device from a config entry.""" - data = hass.data[KONNECTED_DOMAIN] + data = hass.data[DOMAIN] device_id = config_entry.data["id"] sensors = [ KonnectedBinarySensor(device_id, pin_num, pin_data) @@ -48,7 +48,7 @@ class KonnectedBinarySensor(BinarySensorEntity): self._attr_unique_id = f"{device_id}-{zone_num}" self._attr_name = data.get(CONF_NAME) self._attr_device_info = DeviceInfo( - identifiers={(KONNECTED_DOMAIN, device_id)}, + identifiers={(DOMAIN, device_id)}, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index cd36c217627..155e99a7002 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -22,7 +22,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW +from .const import DOMAIN, SIGNAL_DS18B20_NEW SENSOR_TYPES: dict[str, SensorEntityDescription] = { "temperature": SensorEntityDescription( @@ -46,7 +46,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensors attached to a Konnected device from a config entry.""" - data = hass.data[KONNECTED_DOMAIN] + data = hass.data[DOMAIN] device_id = config_entry.data["id"] # Initialize all DHT sensors. @@ -121,7 +121,7 @@ class KonnectedSensor(SensorEntity): name += f" {description.name}" self._attr_name = name - self._attr_device_info = DeviceInfo(identifiers={(KONNECTED_DOMAIN, device_id)}) + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_id)}) async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 671df82d8e0..4838064eaaf 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN as CASETA_DOMAIN +from .const import DOMAIN from .util import serial_to_unique_id @@ -39,7 +39,7 @@ class LutronCasetaScene(Scene): self._bridge: Smartbridge = data.bridge bridge_unique_id = serial_to_unique_id(data.bridge_device["serial"]) self._attr_device_info = DeviceInfo( - identifiers={(CASETA_DOMAIN, data.bridge_device["serial"])}, + identifiers={(DOMAIN, data.bridge_device["serial"])}, ) self._attr_name = scene["name"] self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 2785c46ca17..4bdc2a15156 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import NukiEntryData -from .const import DOMAIN as NUKI_DOMAIN +from .const import DOMAIN from .entity import NukiEntity @@ -25,7 +25,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Nuki binary sensors.""" - entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id] + entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id] entities: list[NukiEntity] = [] diff --git a/homeassistant/components/nuki/sensor.py b/homeassistant/components/nuki/sensor.py index 4f3890a10cf..809e97d6ce9 100644 --- a/homeassistant/components/nuki/sensor.py +++ b/homeassistant/components/nuki/sensor.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import NukiEntryData -from .const import DOMAIN as NUKI_DOMAIN +from .const import DOMAIN from .entity import NukiEntity @@ -21,7 +21,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Nuki lock sensor.""" - entry_data: NukiEntryData = hass.data[NUKI_DOMAIN][entry.entry_id] + entry_data: NukiEntryData = hass.data[DOMAIN][entry.entry_id] async_add_entities( NukiBatterySensor(entry_data.coordinator, lock) for lock in entry_data.locks diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index fa56bf70546..2df26283624 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -17,7 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import PointConfigEntry -from .const import DOMAIN as POINT_DOMAIN, SIGNAL_WEBHOOK +from .const import DOMAIN, SIGNAL_WEBHOOK _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): self._attr_name = self._home["name"] self._attr_unique_id = f"point.{home_id}" self._attr_device_info = DeviceInfo( - identifiers={(POINT_DOMAIN, home_id)}, + identifiers={(DOMAIN, home_id)}, manufacturer="Minut", name=self._attr_name, ) diff --git a/homeassistant/components/screenlogic/util.py b/homeassistant/components/screenlogic/util.py index 781d0fcab24..44fc8966b20 100644 --- a/homeassistant/components/screenlogic/util.py +++ b/homeassistant/components/screenlogic/util.py @@ -6,7 +6,7 @@ from screenlogicpy.const.data import SHARED_VALUES from homeassistant.helpers import entity_registry as er -from .const import DOMAIN as SL_DOMAIN, SL_UNIT_TO_HA_UNIT, ScreenLogicDataPath +from .const import DOMAIN, SL_UNIT_TO_HA_UNIT, ScreenLogicDataPath from .coordinator import ScreenlogicDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def cleanup_excluded_entity( entity_registry = er.async_get(coordinator.hass) unique_id = f"{coordinator.config_entry.unique_id}_{generate_unique_id(*data_path)}" if entity_id := entity_registry.async_get_entity_id( - platform_domain, SL_DOMAIN, unique_id + platform_domain, DOMAIN, unique_id ): _LOGGER.debug( "Removing existing entity '%s' per data inclusion rule", entity_id diff --git a/homeassistant/components/tibber/coordinator.py b/homeassistant/components/tibber/coordinator.py index e565fdc7dd8..8335cc2d773 100644 --- a/homeassistant/components/tibber/coordinator.py +++ b/homeassistant/components/tibber/coordinator.py @@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util -from .const import DOMAIN as TIBBER_DOMAIN +from .const import DOMAIN FIVE_YEARS = 5 * 365 * 24 @@ -80,7 +80,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): for sensor_type, is_production, unit in sensors: statistic_id = ( - f"{TIBBER_DOMAIN}:energy_" + f"{DOMAIN}:energy_" f"{sensor_type.lower()}_" f"{home.home_id.replace('-', '')}" ) @@ -166,7 +166,7 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): mean_type=StatisticMeanType.NONE, has_sum=True, name=f"{home.name} {sensor_type}", - source=TIBBER_DOMAIN, + source=DOMAIN, statistic_id=statistic_id, unit_of_measurement=unit, ) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 9f87b8a8490..26b8f5400a0 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -41,7 +41,7 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util import Throttle, dt as dt_util -from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER +from .const import DOMAIN, MANUFACTURER from .coordinator import TibberDataCoordinator _LOGGER = logging.getLogger(__name__) @@ -267,7 +267,7 @@ async def async_setup_entry( ) -> None: """Set up the Tibber sensor.""" - tibber_connection = hass.data[TIBBER_DOMAIN] + tibber_connection = hass.data[DOMAIN] entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) @@ -309,21 +309,17 @@ async def async_setup_entry( continue # migrate to new device ids - old_entity_id = entity_registry.async_get_entity_id( - "sensor", TIBBER_DOMAIN, old_id - ) + old_entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, old_id) if old_entity_id is not None: entity_registry.async_update_entity( old_entity_id, new_unique_id=home.home_id ) # migrate to new device ids - device_entry = device_registry.async_get_device( - identifiers={(TIBBER_DOMAIN, old_id)} - ) + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, old_id)}) if device_entry and entry.entry_id in device_entry.config_entries: device_registry.async_update_device( - device_entry.id, new_identifiers={(TIBBER_DOMAIN, home.home_id)} + device_entry.id, new_identifiers={(DOMAIN, home.home_id)} ) async_add_entities(entities, True) @@ -352,7 +348,7 @@ class TibberSensor(SensorEntity): def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" device_info = DeviceInfo( - identifiers={(TIBBER_DOMAIN, self._tibber_home.home_id)}, + identifiers={(DOMAIN, self._tibber_home.home_id)}, name=self._device_name, manufacturer=MANUFACTURER, ) @@ -553,19 +549,19 @@ class TibberRtEntityCreator: if translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION_SIMPLE: entity_id = self._entity_registry.async_get_entity_id( "sensor", - TIBBER_DOMAIN, + DOMAIN, f"{home_id}_rt_{translation_key.replace('_', ' ')}", ) elif translation_key in RT_SENSORS_UNIQUE_ID_MIGRATION: entity_id = self._entity_registry.async_get_entity_id( "sensor", - TIBBER_DOMAIN, + DOMAIN, f"{home_id}_rt_{RT_SENSORS_UNIQUE_ID_MIGRATION[translation_key]}", ) elif translation_key != description_key: entity_id = self._entity_registry.async_get_entity_id( "sensor", - TIBBER_DOMAIN, + DOMAIN, f"{home_id}_rt_{translation_key}", ) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index b893b612f2a..71404ef4bc2 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN as UNIFI_DOMAIN, PLATFORMS, UNIFI_WIRELESS_CLIENTS +from .const import DOMAIN, PLATFORMS, UNIFI_WIRELESS_CLIENTS from .errors import AuthenticationRequired, CannotConnect from .hub import UnifiHub, get_unifi_api from .services import async_setup_services @@ -22,7 +22,7 @@ SAVE_DELAY = 10 STORAGE_KEY = "unifi_data" STORAGE_VERSION = 1 -CONFIG_SCHEMA = cv.config_entry_only_config_schema(UNIFI_DOMAIN) +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a26232664a8..1084c29e75f 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util from . import UnifiConfigEntry -from .const import DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN from .entity import ( HandlerT, UnifiEntity, @@ -204,14 +204,12 @@ def async_update_unique_id(hass: HomeAssistant, config_entry: UnifiConfigEntry) def update_unique_id(obj_id: str) -> None: """Rework unique ID.""" new_unique_id = f"{hub.site}-{obj_id}" - if ent_reg.async_get_entity_id( - DEVICE_TRACKER_DOMAIN, UNIFI_DOMAIN, new_unique_id - ): + if ent_reg.async_get_entity_id(DEVICE_TRACKER_DOMAIN, DOMAIN, new_unique_id): return unique_id = f"{obj_id}-{hub.site}" if entity_id := ent_reg.async_get_entity_id( - DEVICE_TRACKER_DOMAIN, UNIFI_DOMAIN, unique_id + DEVICE_TRACKER_DOMAIN, DOMAIN, unique_id ): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 9d4d92839fc..6cd652871d8 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from .const import DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN SERVICE_RECONNECT_CLIENT = "reconnect_client" SERVICE_REMOVE_CLIENTS = "remove_clients" @@ -42,7 +42,7 @@ def async_setup_services(hass: HomeAssistant) -> None: for service in SUPPORTED_SERVICES: hass.services.async_register( - UNIFI_DOMAIN, + DOMAIN, service, async_call_unifi_service, schema=SERVICE_TO_SCHEMA.get(service), @@ -66,7 +66,7 @@ async def async_reconnect_client(hass: HomeAssistant, data: Mapping[str, Any]) - if mac == "": return - for config_entry in hass.config_entries.async_loaded_entries(UNIFI_DOMAIN): + for config_entry in hass.config_entries.async_loaded_entries(DOMAIN): if ( (not (hub := config_entry.runtime_data).available) or (client := hub.api.clients.get(mac)) is None @@ -84,7 +84,7 @@ async def async_remove_clients(hass: HomeAssistant, data: Mapping[str, Any]) -> - Total time between first seen and last seen is less than 15 minutes. - Neither IP, hostname nor name is configured. """ - for config_entry in hass.config_entries.async_loaded_entries(UNIFI_DOMAIN): + for config_entry in hass.config_entries.async_loaded_entries(DOMAIN): if not (hub := config_entry.runtime_data).available: continue diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index 560c95523cd..353b0470476 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -12,7 +12,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT +from .const import DOMAIN, WEMO_SUBSCRIPTION_EVENT from .coordinator import async_get_coordinator TRIGGER_TYPES = {EVENT_TYPE_LONG_PRESS} @@ -32,7 +32,7 @@ async def async_get_triggers( wemo_trigger = { # Required fields of TRIGGER_BASE_SCHEMA CONF_PLATFORM: "device", - CONF_DOMAIN: WEMO_DOMAIN, + CONF_DOMAIN: DOMAIN, CONF_DEVICE_ID: device_id, } diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 838073be84a..6d032a0a7b6 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import color as color_util from . import async_wemo_dispatcher_connect -from .const import DOMAIN as WEMO_DOMAIN +from .const import DOMAIN from .coordinator import DeviceCoordinator from .entity import WemoBinaryStateEntity, WemoEntity @@ -110,7 +110,7 @@ class WemoLight(WemoEntity, LightEntity): """Return the device info.""" return DeviceInfo( connections={(CONNECTION_ZIGBEE, self._unique_id)}, - identifiers={(WEMO_DOMAIN, self._unique_id)}, + identifiers={(DOMAIN, self._unique_id)}, manufacturer="Belkin", model=self._model_name, name=self.name, diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 8e8509e62a5..75d22ce28a1 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN as ZHA_DOMAIN +from .const import DOMAIN from .helpers import async_get_zha_device_proxy, get_zha_data CONF_SUBTYPE = "subtype" @@ -104,7 +104,7 @@ async def async_get_triggers( return [ { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: ZHA_DOMAIN, + CONF_DOMAIN: DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 05539a063d2..38fe9f92e64 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -12,7 +12,7 @@ from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr -from .const import DOMAIN as ZHA_DOMAIN +from .const import DOMAIN from .helpers import async_get_zha_device_proxy if TYPE_CHECKING: @@ -84,4 +84,4 @@ def async_describe_events( LOGBOOK_ENTRY_MESSAGE: message, } - async_describe_event(ZHA_DOMAIN, ZHA_EVENT, async_describe_zha_event) + async_describe_event(DOMAIN, ZHA_EVENT, async_describe_zha_event) From d0ed8b67c4095a0b0247010b9c17a7a5dea23e98 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 13:57:27 +0200 Subject: [PATCH 0171/1175] Add MQTT button as entity platform on MQTT subentries (#144204) --- homeassistant/components/mqtt/button.py | 10 +++-- homeassistant/components/mqtt/config_flow.py | 43 ++++++++++++++++++++ homeassistant/components/mqtt/const.py | 2 + homeassistant/components/mqtt/strings.json | 10 +++++ tests/components/mqtt/common.py | 16 ++++++++ tests/components/mqtt/test_config_flow.py | 22 ++++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 5b2bcc8920f..f5821896071 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -14,7 +14,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import ConfigType from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA -from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_RETAIN +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_PAYLOAD_PRESS, + CONF_RETAIN, + DEFAULT_PAYLOAD_PRESS, +) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import MqttCommandTemplate from .schemas import MQTT_ENTITY_COMMON_SCHEMA @@ -22,9 +28,7 @@ from .util import valid_publish_topic PARALLEL_UPDATES = 0 -CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" -DEFAULT_PAYLOAD_PRESS = "PRESS" PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 9e1773fab62..0ccf7468cbf 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -26,6 +26,7 @@ from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certifica import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.button import ButtonDeviceClass from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.components.light import ( @@ -163,6 +164,7 @@ from .const import ( CONF_OPTIONS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_PAYLOAD_PRESS, CONF_QOS, CONF_RED_TEMPLATE, CONF_RETAIN, @@ -206,6 +208,7 @@ from .const import ( DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PAYLOAD_OFF, DEFAULT_PAYLOAD_ON, + DEFAULT_PAYLOAD_PRESS, DEFAULT_PORT, DEFAULT_PREFIX, DEFAULT_PROTOCOL, @@ -309,6 +312,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( # Subentry selectors SUBENTRY_PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.LIGHT, Platform.NOTIFY, Platform.SENSOR, @@ -353,6 +357,14 @@ BINARY_SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector( sort=True, ) ) +BUTTON_DEVICE_CLASS_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[device_class.value for device_class in ButtonDeviceClass], + mode=SelectSelectorMode.DROPDOWN, + translation_key="device_class_button", + sort=True, + ) +) SENSOR_STATE_CLASS_SELECTOR = SelectSelector( SelectSelectorConfig( options=[device_class.value for device_class in SensorStateClass], @@ -546,6 +558,13 @@ PLATFORM_ENTITY_FIELDS = { validator=str, ), }, + Platform.BUTTON.value: { + CONF_DEVICE_CLASS: PlatformField( + selector=BUTTON_DEVICE_CLASS_SELECTOR, + required=False, + validator=str, + ), + }, Platform.NOTIFY.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( @@ -634,6 +653,29 @@ PLATFORM_MQTT_FIELDS = { section="advanced_settings", ), }, + Platform.BUTTON.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=cv.template, + error="invalid_template", + ), + CONF_PAYLOAD_PRESS: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=str, + default=DEFAULT_PAYLOAD_PRESS, + ), + CONF_RETAIN: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=bool + ), + }, Platform.NOTIFY.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -1206,6 +1248,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ Callable[[dict[str, Any]], dict[str, str]] | None, ] = { Platform.BINARY_SENSOR.value: None, + Platform.BUTTON.value: None, Platform.LIGHT.value: validate_light_platform_config, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b6dda0c0f8a..89e721f022b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -109,6 +109,7 @@ CONF_OFF_DELAY = "off_delay" CONF_ON_COMMAND_TYPE = "on_command_type" CONF_PAYLOAD_CLOSE = "payload_close" CONF_PAYLOAD_OPEN = "payload_open" +CONF_PAYLOAD_PRESS = "payload_press" CONF_PAYLOAD_STOP = "payload_stop" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" @@ -188,6 +189,7 @@ DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OPEN = "OPEN" +DEFAULT_PAYLOAD_PRESS = "PRESS" DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_WS_HEADERS: dict[str, str] = {} diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index b3eede62332..fdf1ebd8089 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -258,6 +258,7 @@ "optimistic": "Optimistic", "payload_off": "Payload \"off\"", "payload_on": "Payload \"on\"", + "payload_press": "Payload \"press\"", "qos": "QoS", "red_template": "Red template", "retain": "Retain", @@ -282,6 +283,7 @@ "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", "payload_off": "The payload that represents the \"off\" state.", "payload_on": "The payload that represents the \"on\" state.", + "payload_press": "The payload to send when the button is triggered.", "qos": "The QoS value a {platform} entity should use.", "red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", @@ -634,6 +636,13 @@ "window": "[%key:component::binary_sensor::entity_component::window::name%]" } }, + "device_class_button": { + "options": { + "identify": "[%key:component::button::entity_component::identify::name%]", + "restart": "[%key:common::action::restart%]", + "update": "[%key:component::button::entity_component::update::name%]" + } + }, "device_class_sensor": { "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", @@ -717,6 +726,7 @@ "platform": { "options": { "binary_sensor": "[%key:component::binary_sensor::title%]", + "button": "[%key:component::button::title%]", "light": "[%key:component::light::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 283414cb96a..3c017de89f4 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -80,6 +80,18 @@ MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT = { "entity_picture": "https://example.com/5b06357ef8654e8d9c54cee5bb0e939b", }, } +MOCK_SUBENTRY_BUTTON_COMPONENT = { + "365d05e6607c4dfb8ae915cff71a954b": { + "platform": "button", + "name": "Restart", + "device_class": "restart", + "command_topic": "test-topic", + "payload_press": "PRESS", + "command_template": "{{ value }}", + "retain": False, + "entity_picture": "https://example.com/365d05e6607c4dfb8ae915cff71a954b", + }, +} MOCK_SUBENTRY_NOTIFY_COMPONENT1 = { "363a7ecad6be4a19b939a016ea93e994": { "platform": "notify", @@ -205,6 +217,10 @@ MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT, } +MOCK_BUTTON_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, + "components": MOCK_SUBENTRY_BUTTON_COMPONENT, +} MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 50f718e332d..81d4960be23 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -34,6 +34,7 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, + MOCK_BUTTON_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, @@ -2677,6 +2678,26 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Hatch", ), + ( + MOCK_BUTTON_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 2}}, + {"name": "Restart"}, + {"device_class": "restart"}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "payload_press": "PRESS", + "retain": False, + }, + ( + ( + {"command_topic": "test-topic#invalid"}, + {"command_topic": "invalid_publish_topic"}, + ), + ), + "Milk notifier Restart", + ), ( MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, @@ -2853,6 +2874,7 @@ async def test_migrate_of_incompatible_config_entry( ], ids=[ "binary_sensor", + "button", "notify_with_entity_name", "notify_no_entity_name", "sensor_options", From 313be7b30aa5b2afde936edbefc5fda59ab40175 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 6 May 2025 14:49:47 +0200 Subject: [PATCH 0172/1175] Update Home Assistant base image to 2025.05.0 (#144333) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 87dad1bf5ef..00df4196523 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From e2c02706a0c32bd5e73198f3d73aee790bddff29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 15:20:18 +0200 Subject: [PATCH 0173/1175] Use runtime_data in google_assistant (#144332) --- homeassistant/components/google_assistant/__init__.py | 6 ++++-- homeassistant/components/google_assistant/button.py | 6 +++--- .../components/google_assistant/diagnostics.py | 10 ++++------ tests/components/google_assistant/test_button.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 273e46040b7..cfcada03a5c 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -95,6 +95,8 @@ CONFIG_SCHEMA = vol.Schema( {vol.Optional(DOMAIN): GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA ) +type GoogleConfigEntry = ConfigEntry[GoogleConfig] + async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate Google Actions component.""" @@ -115,7 +117,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bool: """Set up from a config entry.""" config: ConfigType = {**hass.data[DOMAIN][DATA_CONFIG]} @@ -141,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: google_config = GoogleConfig(hass, config) await google_config.async_initialize() - hass.data[DOMAIN][entry.entry_id] = google_config + entry.runtime_data = google_config hass.http.register_view(GoogleAssistantView(google_config)) diff --git a/homeassistant/components/google_assistant/button.py b/homeassistant/components/google_assistant/button.py index 58560d7b8d1..00d809a851c 100644 --- a/homeassistant/components/google_assistant/button.py +++ b/homeassistant/components/google_assistant/button.py @@ -2,7 +2,6 @@ from __future__ import annotations -from homeassistant import config_entries from homeassistant.components.button import ButtonEntity from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant @@ -11,18 +10,19 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import ConfigType +from . import GoogleConfigEntry from .const import CONF_PROJECT_ID, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN from .http import GoogleConfig async def async_setup_entry( hass: HomeAssistant, - config_entry: config_entries.ConfigEntry, + config_entry: GoogleConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the platform.""" yaml_config: ConfigType = hass.data[DOMAIN][DATA_CONFIG] - google_config: GoogleConfig = hass.data[DOMAIN][config_entry.entry_id] + google_config = config_entry.runtime_data entities = [] diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py index 48902147b05..5121a68f35c 100644 --- a/homeassistant/components/google_assistant/diagnostics.py +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -5,13 +5,12 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import REDACTED, async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType +from . import GoogleConfigEntry from .const import CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN -from .http import GoogleConfig from .smart_home import ( async_devices_query_response, async_devices_sync_response, @@ -29,12 +28,11 @@ TO_REDACT = [ async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: GoogleConfigEntry ) -> dict[str, Any]: """Return diagnostic information.""" - data = hass.data[DOMAIN] - config: GoogleConfig = data[entry.entry_id] - yaml_config: ConfigType = data[DATA_CONFIG] + config = entry.runtime_data + yaml_config: ConfigType = hass.data[DOMAIN][DATA_CONFIG] devices = await async_devices_sync_response(hass, config, REDACTED) sync = create_sync_response(REDACTED, devices) query = await async_devices_query_response(hass, config, devices) diff --git a/tests/components/google_assistant/test_button.py b/tests/components/google_assistant/test_button.py index 6fdb94a5610..9e60576b3e6 100644 --- a/tests/components/google_assistant/test_button.py +++ b/tests/components/google_assistant/test_button.py @@ -29,7 +29,7 @@ async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser) -> No assert state config_entry = hass.config_entries.async_entries("google_assistant")[0] - google_config: ga.GoogleConfig = hass.data[ga.DOMAIN][config_entry.entry_id] + google_config: ga.GoogleConfig = config_entry.runtime_data with patch.object(google_config, "async_sync_entities") as mock_sync_entities: mock_sync_entities.return_value = 200 From 40e3038775dc35d2d39cf61a0f25661deeed1f66 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 15:26:45 +0200 Subject: [PATCH 0174/1175] Fix Z-Wave to reload config entry after migration nvm restore (#144338) --- .../components/zwave_js/config_flow.py | 17 +- tests/components/zwave_js/test_config_flow.py | 301 ++++++++++++++++++ 2 files changed, 317 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index c6624046a00..46d9e061f0b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from contextlib import suppress from datetime import datetime import logging from pathlib import Path @@ -77,6 +78,7 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" +RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { @@ -1317,15 +1319,28 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): event["bytesWritten"] / event["total"] * 0.5 + 0.5 ) - controller = self._get_driver().controller + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + driver = self._get_driver() + controller = driver.controller + wait_driver_ready = asyncio.Event() unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), + driver.once("driver ready", set_driver_ready), ] try: await controller.async_restore_nvm(self.backup_data) except FailedCommand as err: raise AbortFlow(f"Failed to restore network: {err}") from err + else: + with suppress(TimeoutError): + async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + await self.hass.config_entries.async_reload(config_entry.entry_id) finally: for unsub in unsubs: unsub() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 08f0ffad4bd..de76d9d9dc4 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -190,6 +190,19 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version +@pytest.fixture(name="driver_ready_timeout") +def mock_driver_ready_timeout() -> Generator[None]: + """Mock migration nvm restore driver ready timeout.""" + with patch( + ( + "homeassistant.components.zwave_js.config_flow." + "RESTORE_NVM_DRIVER_READY_TIMEOUT" + ), + new=0, + ): + yield + + async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -889,6 +902,144 @@ async def test_usb_discovery_migration( """Test usb discovery migration.""" addon_options["device"] = "/dev/ttyUSB0" entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USB}, + data=USB_DISCOVERY_INFO, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": USB_DISCOVERY_INFO.device}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert integration.data["url"] == "ws://host1:3001" + assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device + assert integration.data["use_addon"] is True + + +@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_usb_discovery_migration_driver_ready_timeout( + hass: HomeAssistant, + addon_options: dict[str, Any], + driver_ready_timeout: None, + mock_usb_serial_by_id: MagicMock, + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test driver ready timeout after nvm restore during usb discovery migration.""" + addon_options["device"] = "/dev/ttyUSB0" + entry = integration + assert client.connect.call_count == 1 hass.config_entries.async_update_entry( entry, unique_id="1234", @@ -976,8 +1127,10 @@ async def test_usb_discovery_migration( assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 await hass.async_block_till_done() + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3552,6 +3705,152 @@ async def test_reconfigure_migrate_with_addon( ) -> None: """Test migration flow with add-on.""" entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] == FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + assert result["data_schema"].schema[CONF_USB_PATH] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": "/test"}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert integration.data["url"] == "ws://host1:3001" + assert integration.data["usb_path"] == "/test" + assert integration.data["use_addon"] is True + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_reconfigure_migrate_driver_ready_timeout( + hass: HomeAssistant, + client, + supervisor, + integration, + addon_running, + driver_ready_timeout: None, + restart_addon, + set_addon_options, + get_addon_discovery_info, +) -> None: + """Test migration flow with driver ready timeout after nvm restore.""" + entry = integration + assert client.connect.call_count == 1 hass.config_entries.async_update_entry( entry, unique_id="1234", @@ -3648,8 +3947,10 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 await hass.async_block_till_done() + assert client.connect.call_count == 3 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 From 2c34712069a713d4d7e1476c5a68ea74596bdbe0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 15:37:10 +0200 Subject: [PATCH 0175/1175] Move service definitions to separate module in guardian (#144306) * Move service definitions to separate module in guardian * docstring --- homeassistant/components/guardian/__init__.py | 143 ++--------------- homeassistant/components/guardian/services.py | 145 ++++++++++++++++++ 2 files changed, 154 insertions(+), 134 deletions(-) create mode 100644 homeassistant/components/guardian/services.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 075c388c4e4..8aab09c9b4b 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,28 +3,16 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any from aioguardian import Client -from aioguardian.errors import GuardianError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_DEVICE_ID, - CONF_FILENAME, - CONF_IP_ADDRESS, - CONF_PORT, - CONF_URL, - Platform, -) -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import ConfigType from .const import ( API_SENSOR_PAIR_DUMP, @@ -39,40 +27,10 @@ from .const import ( SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) from .coordinator import GuardianDataUpdateCoordinator +from .services import setup_services -DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) -SERVICE_NAME_PAIR_SENSOR = "pair_sensor" -SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" -SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" - -SERVICES = ( - SERVICE_NAME_PAIR_SENSOR, - SERVICE_NAME_UNPAIR_SENSOR, - SERVICE_NAME_UPGRADE_FIRMWARE, -) - -SERVICE_BASE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - } -) - -SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(CONF_UID): cv.string, - } -) - -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Optional(CONF_URL): cv.url, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_FILENAME): cv.string, - }, -) PLATFORMS = [ Platform.BINARY_SENSOR, @@ -93,22 +51,10 @@ class GuardianData: paired_sensor_manager: PairedSensorManager -@callback -def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: - """Get the entry ID related to a service call (by device ID).""" - device_id = call.data[CONF_DEVICE_ID] - device_registry = dr.async_get(hass) - - if (device_entry := device_registry.async_get(device_id)) is None: - raise ValueError(f"Invalid Guardian device ID: {device_id}") - - for entry_id in device_entry.config_entries: - if (entry := hass.config_entries.async_get_entry(entry_id)) is None: - continue - if entry.domain == DOMAIN: - return entry_id - - raise ValueError(f"No config entry for device ID: {device_id}") +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Elexa Guardian component.""" + setup_services(hass) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -173,71 +119,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up all of the Guardian entity platforms: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - @callback - def call_with_data( - func: Callable[[ServiceCall, GuardianData], Coroutine[Any, Any, None]], - ) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]: - """Hydrate a service call with the appropriate GuardianData object.""" - - async def wrapper(call: ServiceCall) -> None: - """Wrap the service function.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - data = hass.data[DOMAIN][entry_id] - - try: - async with data.client: - await func(call, data) - except GuardianError as err: - raise HomeAssistantError( - f"Error while executing {func.__name__}: {err}" - ) from err - - return wrapper - - @call_with_data - async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: - """Add a new paired sensor.""" - uid = call.data[CONF_UID] - await data.client.sensor.pair_sensor(uid) - await data.paired_sensor_manager.async_pair_sensor(uid) - - @call_with_data - async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: - """Remove a paired sensor.""" - uid = call.data[CONF_UID] - await data.client.sensor.unpair_sensor(uid) - await data.paired_sensor_manager.async_unpair_sensor(uid) - - @call_with_data - async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None: - """Upgrade the device firmware.""" - await data.client.system.upgrade_firmware( - url=call.data[CONF_URL], - port=call.data[CONF_PORT], - filename=call.data[CONF_FILENAME], - ) - - for service_name, schema, method in ( - ( - SERVICE_NAME_PAIR_SENSOR, - SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, - async_pair_sensor, - ), - ( - SERVICE_NAME_UNPAIR_SENSOR, - SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, - async_unpair_sensor, - ), - ( - SERVICE_NAME_UPGRADE_FIRMWARE, - SERVICE_UPGRADE_FIRMWARE_SCHEMA, - async_upgrade_firmware, - ), - ): - if hass.services.has_service(DOMAIN, service_name): - continue - hass.services.async_register(DOMAIN, service_name, method, schema=schema) - return True @@ -247,12 +128,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - if not hass.config_entries.async_loaded_entries(DOMAIN): - # If this is the last loaded instance of Guardian, deregister any services - # defined during integration setup: - for service_name in SERVICES: - hass.services.async_remove(DOMAIN, service_name) - return unload_ok diff --git a/homeassistant/components/guardian/services.py b/homeassistant/components/guardian/services.py new file mode 100644 index 00000000000..68d2f5159fc --- /dev/null +++ b/homeassistant/components/guardian/services.py @@ -0,0 +1,145 @@ +"""Support for Guardian services.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from typing import TYPE_CHECKING, Any + +from aioguardian.errors import GuardianError +import voluptuous as vol + +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_DEVICE_ID, + CONF_FILENAME, + CONF_PORT, + CONF_URL, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import CONF_UID, DOMAIN + +if TYPE_CHECKING: + from . import GuardianData + +SERVICE_NAME_PAIR_SENSOR = "pair_sensor" +SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" +SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" + +SERVICES = ( + SERVICE_NAME_PAIR_SENSOR, + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_NAME_UPGRADE_FIRMWARE, +) + +SERVICE_BASE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + } +) + +SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(CONF_UID): cv.string, + } +) + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, +) + + +@callback +def async_get_entry_id_for_service_call(call: ServiceCall) -> str: + """Get the entry ID related to a service call (by device ID).""" + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(call.hass) + + if (device_entry := device_registry.async_get(device_id)) is None: + raise ValueError(f"Invalid Guardian device ID: {device_id}") + + for entry_id in device_entry.config_entries: + if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + return entry_id + + raise ValueError(f"No config entry for device ID: {device_id}") + + +@callback +def call_with_data( + func: Callable[[ServiceCall, GuardianData], Coroutine[Any, Any, None]], +) -> Callable[[ServiceCall], Coroutine[Any, Any, None]]: + """Hydrate a service call with the appropriate GuardianData object.""" + + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + entry_id = async_get_entry_id_for_service_call(call) + data = call.hass.data[DOMAIN][entry_id] + + try: + async with data.client: + await func(call, data) + except GuardianError as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}" + ) from err + + return wrapper + + +@call_with_data +async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: + """Add a new paired sensor.""" + uid = call.data[CONF_UID] + await data.client.sensor.pair_sensor(uid) + await data.paired_sensor_manager.async_pair_sensor(uid) + + +@call_with_data +async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: + """Remove a paired sensor.""" + uid = call.data[CONF_UID] + await data.client.sensor.unpair_sensor(uid) + await data.paired_sensor_manager.async_unpair_sensor(uid) + + +@call_with_data +async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None: + """Upgrade the device firmware.""" + await data.client.system.upgrade_firmware( + url=call.data[CONF_URL], + port=call.data[CONF_PORT], + filename=call.data[CONF_FILENAME], + ) + + +def setup_services(hass: HomeAssistant) -> None: + """Register the Renault services.""" + for service_name, schema, method in ( + ( + SERVICE_NAME_PAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_pair_sensor, + ), + ( + SERVICE_NAME_UNPAIR_SENSOR, + SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, + async_unpair_sensor, + ), + ( + SERVICE_NAME_UPGRADE_FIRMWARE, + SERVICE_UPGRADE_FIRMWARE_SCHEMA, + async_upgrade_firmware, + ), + ): + hass.services.async_register(DOMAIN, service_name, method, schema=schema) From fbae79fab261bf456129b9587af0dc5c04c34f36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 15:52:00 +0200 Subject: [PATCH 0176/1175] Use runtime_data in google_assistant_sdk (#144335) --- .../google_assistant_sdk/__init__.py | 33 +++++++++---------- .../google_assistant_sdk/config_flow.py | 11 ++----- .../components/google_assistant_sdk/const.py | 3 -- .../google_assistant_sdk/diagnostics.py | 5 +-- .../google_assistant_sdk/helpers.py | 29 ++++++++-------- .../components/google_assistant_sdk/notify.py | 11 +++++-- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index a08d7554516..94b0e0b8a25 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -10,7 +10,6 @@ from google.oauth2.credentials import Credentials import voluptuous as vol from homeassistant.components import conversation -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform from homeassistant.core import ( HomeAssistant, @@ -26,15 +25,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_LANGUAGE_CODE, - DATA_MEM_STORAGE, - DATA_SESSION, - DOMAIN, - SUPPORTED_LANGUAGE_CODES, -) +from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES from .helpers import ( GoogleAssistantSDKAudioView, + GoogleAssistantSDKConfigEntry, + GoogleAssistantSDKRuntimeData, InMemoryStorage, async_send_text_commands, best_matching_language_code, @@ -66,10 +61,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry +) -> bool: """Set up Google Assistant SDK from a config entry.""" - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} - implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) try: @@ -82,23 +77,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][entry.entry_id][DATA_SESSION] = session mem_storage = InMemoryStorage(hass) - hass.data[DOMAIN][entry.entry_id][DATA_MEM_STORAGE] = mem_storage hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage)) await async_setup_service(hass) + entry.runtime_data = GoogleAssistantSDKRuntimeData( + session=session, mem_storage=mem_storage + ) agent = GoogleAssistantConversationAgent(hass, entry) conversation.async_set_agent(hass, entry, agent) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry +) -> bool: """Unload a config entry.""" - hass.data[DOMAIN].pop(entry.entry_id) if not hass.config_entries.async_loaded_entries(DOMAIN): for service_name in hass.services.async_services_for_domain(DOMAIN): hass.services.async_remove(DOMAIN, service_name) @@ -141,7 +138,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): """Google Assistant SDK conversation agent.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry + ) -> None: """Initialize the agent.""" self.hass = hass self.entry = entry @@ -161,7 +160,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): if self.session: session = self.session else: - session = self.hass.data[DOMAIN][self.entry.entry_id][DATA_SESSION] + session = self.entry.runtime_data.session self.session = session if not session.valid_token: await session.async_ensure_token_valid() diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index 48c92832483..6c010d39c43 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -8,17 +8,12 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ( - SOURCE_REAUTH, - ConfigEntry, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult, OptionsFlow from homeassistant.core import callback from homeassistant.helpers import config_entry_oauth2_flow from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES -from .helpers import default_language_code +from .helpers import GoogleAssistantSDKConfigEntry, default_language_code _LOGGER = logging.getLogger(__name__) @@ -77,7 +72,7 @@ class OAuth2FlowHandler( @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: GoogleAssistantSDKConfigEntry, ) -> OptionsFlow: """Create the options flow.""" return OptionsFlowHandler() diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index 4059f006d4b..2ad5bbbfec8 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -8,9 +8,6 @@ DEFAULT_NAME: Final = "Google Assistant SDK" CONF_LANGUAGE_CODE: Final = "language_code" -DATA_MEM_STORAGE: Final = "mem_storage" -DATA_SESSION: Final = "session" - # https://developers.google.com/assistant/sdk/reference/rpc/languages SUPPORTED_LANGUAGE_CODES: Final = [ "de-DE", diff --git a/homeassistant/components/google_assistant_sdk/diagnostics.py b/homeassistant/components/google_assistant_sdk/diagnostics.py index eacded4e2e6..45600f5010e 100644 --- a/homeassistant/components/google_assistant_sdk/diagnostics.py +++ b/homeassistant/components/google_assistant_sdk/diagnostics.py @@ -5,14 +5,15 @@ 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 .helpers import GoogleAssistantSDKConfigEntry + TO_REDACT = {"access_token", "refresh_token"} async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" return async_redact_data( diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index f9d332cd735..ca774bed77e 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -28,13 +28,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.event import async_call_later -from .const import ( - CONF_LANGUAGE_CODE, - DATA_MEM_STORAGE, - DATA_SESSION, - DOMAIN, - SUPPORTED_LANGUAGE_CODES, -) +from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES _LOGGER = logging.getLogger(__name__) @@ -49,6 +43,16 @@ DEFAULT_LANGUAGE_CODES = { "pt": "pt-BR", } +type GoogleAssistantSDKConfigEntry = ConfigEntry[GoogleAssistantSDKRuntimeData] + + +@dataclass +class GoogleAssistantSDKRuntimeData: + """Runtime data for Google Assistant SDK.""" + + session: OAuth2Session + mem_storage: InMemoryStorage + @dataclass class CommandResponse: @@ -62,9 +66,9 @@ async def async_send_text_commands( ) -> list[CommandResponse]: """Send text commands to Google Assistant Service.""" # There can only be 1 entry (config_flow has single_instance_allowed) - entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - session: OAuth2Session = hass.data[DOMAIN][entry.entry_id][DATA_SESSION] + session = entry.runtime_data.session try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as err: @@ -84,11 +88,10 @@ async def async_send_text_commands( _LOGGER.debug("command: %s\nresponse: %s", command, text_response) audio_response = resp[2] if media_players and audio_response: - mem_storage: InMemoryStorage = hass.data[DOMAIN][entry.entry_id][ - DATA_MEM_STORAGE - ] audio_url = GoogleAssistantSDKAudioView.url.format( - filename=mem_storage.store_and_get_identifier(audio_response) + filename=entry.runtime_data.mem_storage.store_and_get_identifier( + audio_response + ) ) await hass.services.async_call( DOMAIN_MP, diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index ffe34eefdfd..067f222ca50 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -5,12 +5,15 @@ from __future__ import annotations from typing import Any from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_LANGUAGE_CODE, DOMAIN -from .helpers import async_send_text_commands, default_language_code +from .helpers import ( + GoogleAssistantSDKConfigEntry, + async_send_text_commands, + default_language_code, +) # https://support.google.com/assistant/answer/9071582?hl=en LANG_TO_BROADCAST_COMMAND = { @@ -59,7 +62,9 @@ class BroadcastNotificationService(BaseNotificationService): return # There can only be 1 entry (config_flow has single_instance_allowed) - entry: ConfigEntry = self.hass.config_entries.async_entries(DOMAIN)[0] + entry: GoogleAssistantSDKConfigEntry = self.hass.config_entries.async_entries( + DOMAIN + )[0] language_code = entry.options.get( CONF_LANGUAGE_CODE, default_language_code(self.hass) ) From bdf6f7f59047e975fbb4200bc2d1b4bef3ae5935 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 15:52:33 +0200 Subject: [PATCH 0177/1175] Use config entry title to name SamsungTV entities (#144254) --- homeassistant/components/samsungtv/entity.py | 2 -- tests/components/samsungtv/snapshots/test_init.ambr | 6 +++--- tests/components/samsungtv/test_device_trigger.py | 2 +- tests/components/samsungtv/test_init.py | 8 ++++---- tests/components/samsungtv/test_media_player.py | 9 +++------ tests/components/samsungtv/test_remote.py | 2 +- tests/components/samsungtv/test_trigger.py | 4 ++-- 7 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 2126dae82f4..a25b8ff2b14 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -12,7 +12,6 @@ from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MODEL, - CONF_NAME, ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr @@ -41,7 +40,6 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity) # Fallback for legacy models that doesn't have a API to retrieve MAC or SerialNumber self._attr_unique_id = config_entry.unique_id or config_entry.entry_id self._attr_device_info = DeviceInfo( - name=config_entry.data.get(CONF_NAME), manufacturer=config_entry.data.get(CONF_MANUFACTURER), model=config_entry.data.get(CONF_MODEL), model_id=config_entry.data.get(CONF_MODEL), diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index db175626d41..48201781004 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -89,7 +89,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'tv', - 'friendly_name': 'any', + 'friendly_name': 'Mock Title', 'is_volume_muted': False, 'source_list': list([ 'TV', @@ -98,7 +98,7 @@ 'supported_features': , }), 'context': , - 'entity_id': 'media_player.any', + 'entity_id': 'media_player.mock_title', 'last_changed': , 'last_reported': , 'last_updated': , @@ -123,7 +123,7 @@ 'disabled_by': None, 'domain': 'media_player', 'entity_category': None, - 'entity_id': 'media_player.any', + 'entity_id': 'media_player.mock_title', 'has_entity_name': True, 'hidden_by': None, 'icon': None, diff --git a/tests/components/samsungtv/test_device_trigger.py b/tests/components/samsungtv/test_device_trigger.py index e67f154cae1..f142339547c 100644 --- a/tests/components/samsungtv/test_device_trigger.py +++ b/tests/components/samsungtv/test_device_trigger.py @@ -54,7 +54,7 @@ async def test_if_fires_on_turn_on_request( ) -> None: """Test for turn_on and turn_off triggers firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - entity_id = "media_player.fake" + entity_id = "media_player.mock_title" device = device_registry.async_get_device( identifiers={(DOMAIN, "be9554b9-c9fb-41f4-8920-22da015376a4")} diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 59dbfad0552..a8b8debd4c0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -49,7 +49,7 @@ from .const import ( from tests.common import MockConfigEntry, load_json_object_fixture -ENTITY_ID = f"{MP_DOMAIN}.fake_name" +ENTITY_ID = f"{MP_DOMAIN}.mock_title" MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", @@ -65,7 +65,7 @@ async def test_setup(hass: HomeAssistant) -> None: # test name and turn_on assert state - assert state.name == "fake_name" + assert state.name == "Mock Title" assert ( state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | MediaPlayerEntityFeature.TURN_ON @@ -151,8 +151,8 @@ async def test_setup_updates_from_ssdp( await hass.async_block_till_done() await hass.async_block_till_done() - assert hass.states.get("media_player.any") == snapshot - assert entity_registry.async_get("media_player.any") == snapshot + assert hass.states.get("media_player.mock_title") == snapshot + assert entity_registry.async_get("media_player.mock_title") == snapshot assert ( entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "https://fake_host:12345/tv_agent" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index d36ff8daeb3..b4f48ed20b3 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -94,7 +94,7 @@ from tests.common import ( load_json_object_fixture, ) -ENTITY_ID = f"{MP_DOMAIN}.fake" +ENTITY_ID = f"{MP_DOMAIN}.mock_title" MOCK_CONFIGWS = { CONF_HOST: "fake_host", CONF_NAME: "fake", @@ -158,12 +158,9 @@ async def test_setup_websocket_2( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Test setup of platform from config entry.""" - entity_id = f"{MP_DOMAIN}.fake" - entry = MockConfigEntry( domain=DOMAIN, data=MOCK_ENTRY_WS, - unique_id=entity_id, ) entry.add_to_hass(hass) @@ -188,7 +185,7 @@ async def test_setup_websocket_2( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get(entity_id) + state = hass.states.get(ENTITY_ID) assert state remote_class.assert_called_once_with(**MOCK_CALLS_WS) @@ -640,7 +637,7 @@ async def test_name(hass: HomeAssistant) -> None: """Test for name property.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) - assert state.attributes[ATTR_FRIENDLY_NAME] == "fake" + assert state.attributes[ATTR_FRIENDLY_NAME] == "Mock Title" @pytest.mark.usefixtures("remote") diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 65474979968..4149352ba3f 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -21,7 +21,7 @@ from .const import MOCK_CONFIG, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED from tests.common import MockConfigEntry -ENTITY_ID = f"{REMOTE_DOMAIN}.fake" +ENTITY_ID = f"{REMOTE_DOMAIN}.mock_title" @pytest.mark.usefixtures("remoteencws", "rest_api") diff --git a/tests/components/samsungtv/test_trigger.py b/tests/components/samsungtv/test_trigger.py index d957e501775..dce64c5e580 100644 --- a/tests/components/samsungtv/test_trigger.py +++ b/tests/components/samsungtv/test_trigger.py @@ -28,7 +28,7 @@ async def test_turn_on_trigger_device_id( """Test for turn_on triggers by device_id firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - entity_id = f"{entity_domain}.fake" + entity_id = f"{entity_domain}.mock_title" device = device_registry.async_get_device( identifiers={(DOMAIN, "be9554b9-c9fb-41f4-8920-22da015376a4")} @@ -92,7 +92,7 @@ async def test_turn_on_trigger_entity_id( """Test for turn_on triggers by entity_id firing.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - entity_id = f"{entity_domain}.fake" + entity_id = f"{entity_domain}.mock_title" assert await async_setup_component( hass, From 0ec7dc56542ef5636baa17137effe86b2ebe53e3 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 6 May 2025 15:54:42 +0200 Subject: [PATCH 0178/1175] Add endpoint validation for AWS S3 (#144334) --- .../components/aws_s3/config_flow.py | 48 +++++++++++-------- homeassistant/components/aws_s3/const.py | 3 +- homeassistant/components/aws_s3/strings.json | 2 +- tests/components/aws_s3/const.py | 2 +- tests/components/aws_s3/test_config_flow.py | 27 ++++++++++- 5 files changed, 58 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/aws_s3/config_flow.py b/homeassistant/components/aws_s3/config_flow.py index 81ddd881f0f..a4de192e513 100644 --- a/homeassistant/components/aws_s3/config_flow.py +++ b/homeassistant/components/aws_s3/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from urllib.parse import urlparse from aiobotocore.session import AioSession from botocore.exceptions import ClientError, ConnectionError, ParamValidationError @@ -17,6 +18,7 @@ from homeassistant.helpers.selector import ( ) from .const import ( + AWS_DOMAIN, CONF_ACCESS_KEY_ID, CONF_BUCKET, CONF_ENDPOINT_URL, @@ -57,28 +59,34 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL], } ) - try: - session = AioSession() - async with session.create_client( - "s3", - endpoint_url=user_input.get(CONF_ENDPOINT_URL), - aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY], - aws_access_key_id=user_input[CONF_ACCESS_KEY_ID], - ) as client: - await client.head_bucket(Bucket=user_input[CONF_BUCKET]) - except ClientError: - errors["base"] = "invalid_credentials" - except ParamValidationError as err: - if "Invalid bucket name" in str(err): - errors[CONF_BUCKET] = "invalid_bucket_name" - except ValueError: + + if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith( + AWS_DOMAIN + ): errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url" - except ConnectionError: - errors[CONF_ENDPOINT_URL] = "cannot_connect" else: - return self.async_create_entry( - title=user_input[CONF_BUCKET], data=user_input - ) + try: + session = AioSession() + async with session.create_client( + "s3", + endpoint_url=user_input.get(CONF_ENDPOINT_URL), + aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY], + aws_access_key_id=user_input[CONF_ACCESS_KEY_ID], + ) as client: + await client.head_bucket(Bucket=user_input[CONF_BUCKET]) + except ClientError: + errors["base"] = "invalid_credentials" + except ParamValidationError as err: + if "Invalid bucket name" in str(err): + errors[CONF_BUCKET] = "invalid_bucket_name" + except ValueError: + errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url" + except ConnectionError: + errors[CONF_ENDPOINT_URL] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_BUCKET], data=user_input + ) return self.async_show_form( step_id="user", diff --git a/homeassistant/components/aws_s3/const.py b/homeassistant/components/aws_s3/const.py index 95d53c93a08..a6863e6c38a 100644 --- a/homeassistant/components/aws_s3/const.py +++ b/homeassistant/components/aws_s3/const.py @@ -12,7 +12,8 @@ CONF_SECRET_ACCESS_KEY = "secret_access_key" CONF_ENDPOINT_URL = "endpoint_url" CONF_BUCKET = "bucket" -DEFAULT_ENDPOINT_URL = "https://s3.eu-central-1.amazonaws.com/" +AWS_DOMAIN = "amazonaws.com" +DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/" DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey( f"{DOMAIN}.backup_agent_listeners" diff --git a/homeassistant/components/aws_s3/strings.json b/homeassistant/components/aws_s3/strings.json index b5683aafa6e..84a7f68c850 100644 --- a/homeassistant/components/aws_s3/strings.json +++ b/homeassistant/components/aws_s3/strings.json @@ -21,7 +21,7 @@ "cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]", "invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]", "invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]", - "invalid_endpoint_url": "Invalid endpoint URL" + "invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/tests/components/aws_s3/const.py b/tests/components/aws_s3/const.py index 443275d0444..ebffa11d956 100644 --- a/tests/components/aws_s3/const.py +++ b/tests/components/aws_s3/const.py @@ -10,6 +10,6 @@ from homeassistant.components.aws_s3.const import ( USER_INPUT = { CONF_ACCESS_KEY_ID: "TestTestTestTestTest", CONF_SECRET_ACCESS_KEY: "TestTestTestTestTestTestTestTestTestTest", - CONF_ENDPOINT_URL: "http://127.0.0.1:9000", + CONF_ENDPOINT_URL: "https://s3.eu-south-1.amazonaws.com", CONF_BUCKET: "test", } diff --git a/tests/components/aws_s3/test_config_flow.py b/tests/components/aws_s3/test_config_flow.py index 061d990140a..593eea5cdb9 100644 --- a/tests/components/aws_s3/test_config_flow.py +++ b/tests/components/aws_s3/test_config_flow.py @@ -21,8 +21,12 @@ from tests.common import MockConfigEntry async def _async_start_flow( hass: HomeAssistant, + user_input: dict[str, str] | None = None, ) -> FlowResultType: """Initialize the config flow.""" + if user_input is None: + user_input = USER_INPUT + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -30,7 +34,7 @@ async def _async_start_flow( return await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + user_input, ) @@ -116,3 +120,24 @@ async def test_abort_if_already_configured( result = await _async_start_flow(hass) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_flow_create_not_aws_endpoint( + hass: HomeAssistant, +) -> None: + """Test config flow with a not aws endpoint should raise an error.""" + result = await _async_start_flow( + hass, USER_INPUT | {CONF_ENDPOINT_URL: "http://example.com"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_ENDPOINT_URL: "invalid_endpoint_url"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test" + assert result["data"] == USER_INPUT From 32a6b8a0f8c66c357ee8089ac5c67ae45ed875ce Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 16:04:12 +0200 Subject: [PATCH 0179/1175] Use runtime_data in goodwe (#144325) --- homeassistant/components/goodwe/__init__.py | 40 ++++++------------- homeassistant/components/goodwe/button.py | 9 ++--- homeassistant/components/goodwe/const.py | 4 -- .../components/goodwe/coordinator.py | 17 +++++++- .../components/goodwe/diagnostics.py | 9 ++--- homeassistant/components/goodwe/number.py | 10 ++--- homeassistant/components/goodwe/select.py | 10 ++--- homeassistant/components/goodwe/sensor.py | 13 +++--- 8 files changed, 51 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py index 02c1d5beac7..b6637bc8b50 100644 --- a/homeassistant/components/goodwe/__init__.py +++ b/homeassistant/components/goodwe/__init__.py @@ -2,26 +2,17 @@ from goodwe import InverterError, connect -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import DeviceInfo -from .const import ( - CONF_MODEL_FAMILY, - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE_INFO, - KEY_INVERTER, - PLATFORMS, -) -from .coordinator import GoodweUpdateCoordinator +from .const import CONF_MODEL_FAMILY, DOMAIN, PLATFORMS +from .coordinator import GoodweConfigEntry, GoodweRuntimeData, GoodweUpdateCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GoodweConfigEntry) -> bool: """Set up the Goodwe components from a config entry.""" - hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] model_family = entry.data[CONF_MODEL_FAMILY] @@ -50,11 +41,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = { - KEY_INVERTER: inverter, - KEY_COORDINATOR: coordinator, - KEY_DEVICE_INFO: device_info, - } + entry.runtime_data = GoodweRuntimeData( + inverter=inverter, + coordinator=coordinator, + device_info=device_info, + ) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -63,18 +54,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: GoodweConfigEntry +) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def update_listener(hass: HomeAssistant, config_entry: GoodweConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/goodwe/button.py b/homeassistant/components/goodwe/button.py index e93b23570db..64d1e08276d 100644 --- a/homeassistant/components/goodwe/button.py +++ b/homeassistant/components/goodwe/button.py @@ -8,13 +8,12 @@ import logging from goodwe import Inverter, InverterError from homeassistant.components.button import ButtonEntity, ButtonEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER +from .coordinator import GoodweConfigEntry _LOGGER = logging.getLogger(__name__) @@ -36,12 +35,12 @@ SYNCHRONIZE_CLOCK = GoodweButtonEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoodweConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the inverter button entities from a config entry.""" - inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + inverter = config_entry.runtime_data.inverter + device_info = config_entry.runtime_data.device_info # read current time from the inverter try: diff --git a/homeassistant/components/goodwe/const.py b/homeassistant/components/goodwe/const.py index 730433c4a66..432d18e5867 100644 --- a/homeassistant/components/goodwe/const.py +++ b/homeassistant/components/goodwe/const.py @@ -12,7 +12,3 @@ DEFAULT_NAME = "GoodWe" SCAN_INTERVAL = timedelta(seconds=10) CONF_MODEL_FAMILY = "model_family" - -KEY_INVERTER = "inverter" -KEY_COORDINATOR = "coordinator" -KEY_DEVICE_INFO = "device_info" diff --git a/homeassistant/components/goodwe/coordinator.py b/homeassistant/components/goodwe/coordinator.py index 914ba3155b4..3236b95d9e0 100644 --- a/homeassistant/components/goodwe/coordinator.py +++ b/homeassistant/components/goodwe/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any @@ -9,22 +10,34 @@ from goodwe import Inverter, InverterError, RequestFailedException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) +type GoodweConfigEntry = ConfigEntry[GoodweRuntimeData] + + +@dataclass +class GoodweRuntimeData: + """Data class for runtime data.""" + + inverter: Inverter + coordinator: GoodweUpdateCoordinator + device_info: DeviceInfo + class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Gather data for the energy device.""" - config_entry: ConfigEntry + config_entry: GoodweConfigEntry def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: GoodweConfigEntry, inverter: Inverter, ) -> None: """Initialize update coordinator.""" diff --git a/homeassistant/components/goodwe/diagnostics.py b/homeassistant/components/goodwe/diagnostics.py index 66806d31589..ece5f3b6507 100644 --- a/homeassistant/components/goodwe/diagnostics.py +++ b/homeassistant/components/goodwe/diagnostics.py @@ -4,19 +4,16 @@ from __future__ import annotations from typing import Any -from goodwe import Inverter - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, KEY_INVERTER +from .coordinator import GoodweConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: GoodweConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - inverter: Inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] + inverter = config_entry.runtime_data.inverter return { "config_entry": config_entry.as_dict(), diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 0a61ac19d64..0d200c2725c 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -13,13 +13,13 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER +from .const import DOMAIN +from .coordinator import GoodweConfigEntry _LOGGER = logging.getLogger(__name__) @@ -86,12 +86,12 @@ NUMBERS = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoodweConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the inverter select entities from a config entry.""" - inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + inverter = config_entry.runtime_data.inverter + device_info = config_entry.runtime_data.device_info entities = [] diff --git a/homeassistant/components/goodwe/select.py b/homeassistant/components/goodwe/select.py index 340e10bfa0f..c26e8135b3f 100644 --- a/homeassistant/components/goodwe/select.py +++ b/homeassistant/components/goodwe/select.py @@ -5,13 +5,13 @@ import logging from goodwe import Inverter, InverterError, OperationMode from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, KEY_DEVICE_INFO, KEY_INVERTER +from .const import DOMAIN +from .coordinator import GoodweConfigEntry _LOGGER = logging.getLogger(__name__) @@ -39,12 +39,12 @@ OPERATION_MODE = SelectEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoodweConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the inverter select entities from a config entry.""" - inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + inverter = config_entry.runtime_data.inverter + device_info = config_entry.runtime_data.device_info supported_modes = await inverter.get_operation_modes(False) # read current operating mode from the inverter diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index d2dce2770e4..c51827712d4 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -17,7 +17,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -39,8 +38,8 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER -from .coordinator import GoodweUpdateCoordinator +from .const import DOMAIN +from .coordinator import GoodweConfigEntry, GoodweUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -165,14 +164,14 @@ TEXT_SENSOR = GoodweSensorEntityDescription( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoodweConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the GoodWe inverter from a config entry.""" entities: list[InverterSensor] = [] - inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO] + inverter = config_entry.runtime_data.inverter + coordinator = config_entry.runtime_data.coordinator + device_info = config_entry.runtime_data.device_info # Individual inverter sensors entities entities.extend( From 144739284749ad12eddca09477582b2e8df9a35a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 16:16:16 +0200 Subject: [PATCH 0180/1175] Use runtime_data in guardian (#144344) * Use runtime_data in guardian * Adjust tests --- homeassistant/components/guardian/__init__.py | 21 ++++++++----------- .../components/guardian/binary_sensor.py | 12 +++++------ homeassistant/components/guardian/button.py | 11 +++++----- .../components/guardian/coordinator.py | 10 +++++---- .../components/guardian/diagnostics.py | 9 ++++---- homeassistant/components/guardian/entity.py | 6 +++--- homeassistant/components/guardian/sensor.py | 8 +++---- homeassistant/components/guardian/services.py | 9 ++++---- homeassistant/components/guardian/switch.py | 11 +++++----- homeassistant/components/guardian/util.py | 4 ++-- homeassistant/components/guardian/valve.py | 11 +++++----- tests/components/guardian/test_diagnostics.py | 4 ++-- 12 files changed, 53 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 8aab09c9b4b..65f5525d587 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -40,12 +40,14 @@ PLATFORMS = [ Platform.VALVE, ] +type GuardianConfigEntry = ConfigEntry[GuardianData] + @dataclass class GuardianData: - """Define an object to be stored in `hass.data`.""" + """Define an object to be stored in `entry.runtime_data`.""" - entry: ConfigEntry + entry: GuardianConfigEntry client: Client valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] paired_sensor_manager: PairedSensorManager @@ -57,7 +59,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GuardianConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -108,8 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await paired_sensor_manager.async_initialize() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = GuardianData( + entry.runtime_data = GuardianData( entry=entry, client=client, valve_controller_coordinators=valve_controller_coordinators, @@ -122,13 +123,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GuardianConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) class PairedSensorManager: @@ -137,7 +134,7 @@ class PairedSensorManager: def __init__( self, hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, client: Client, api_lock: asyncio.Lock, sensor_pair_dump_coordinator: GuardianDataUpdateCoordinator, diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 7d5f97bdb65..d6583abd843 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -12,17 +12,15 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import GuardianData +from . import GuardianConfigEntry from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, - DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) from .coordinator import GuardianDataUpdateCoordinator @@ -87,11 +85,11 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Guardian switches based on a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data uid = entry.data[CONF_UID] async_finish_entity_domain_replacements( @@ -151,7 +149,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, coordinator: GuardianDataUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: @@ -173,7 +171,7 @@ class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, coordinators: dict[str, GuardianDataUpdateCoordinator], description: ValveControllerBinarySensorDescription, ) -> None: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index 01bac63c6e3..2ecdbed38ea 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -12,14 +12,13 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import GuardianData -from .const import API_SYSTEM_DIAGNOSTICS, DOMAIN +from . import GuardianConfigEntry, GuardianData +from .const import API_SYSTEM_DIAGNOSTICS from .entity import ValveControllerEntity, ValveControllerEntityDescription from .util import convert_exceptions_to_homeassistant_error @@ -69,11 +68,11 @@ BUTTON_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Guardian buttons based on a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_add_entities( GuardianButton(entry, data, description) for description in BUTTON_DESCRIPTIONS @@ -90,7 +89,7 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, data: GuardianData, description: ValveControllerButtonDescription, ) -> None: diff --git a/homeassistant/components/guardian/coordinator.py b/homeassistant/components/guardian/coordinator.py index 500b7c10784..a49bf6803d9 100644 --- a/homeassistant/components/guardian/coordinator.py +++ b/homeassistant/components/guardian/coordinator.py @@ -5,18 +5,20 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from datetime import timedelta -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER +if TYPE_CHECKING: + from . import GuardianConfigEntry + DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" @@ -25,13 +27,13 @@ SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" - config_entry: ConfigEntry + config_entry: GuardianConfigEntry def __init__( self, hass: HomeAssistant, *, - entry: ConfigEntry, + entry: GuardianConfigEntry, client: Client, api_name: str, api_coro: Callable[[], Coroutine[Any, Any, dict[str, Any]]], diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index 2f4287bea29..22a1bde7817 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -5,12 +5,11 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from . import GuardianData -from .const import CONF_UID, DOMAIN +from . import GuardianConfigEntry +from .const import CONF_UID CONF_BSSID = "bssid" CONF_PAIRED_UIDS = "paired_uids" @@ -29,10 +28,10 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: GuardianConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/guardian/entity.py b/homeassistant/components/guardian/entity.py index fca0afeda0e..c48c87afa01 100644 --- a/homeassistant/components/guardian/entity.py +++ b/homeassistant/components/guardian/entity.py @@ -4,11 +4,11 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import GuardianConfigEntry from .const import API_SYSTEM_DIAGNOSTICS, CONF_UID, DOMAIN from .coordinator import GuardianDataUpdateCoordinator @@ -32,7 +32,7 @@ class PairedSensorEntity(GuardianEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, coordinator: GuardianDataUpdateCoordinator, description: EntityDescription, ) -> None: @@ -62,7 +62,7 @@ class ValveControllerEntity(GuardianEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, coordinators: dict[str, GuardianDataUpdateCoordinator], description: ValveControllerEntityDescription, ) -> None: diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 13dd8e01296..da4a78d7b7e 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EntityCategory, UnitOfElectricCurrent, @@ -25,13 +24,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from . import GuardianData +from . import GuardianConfigEntry from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, API_VALVE_STATUS, CONF_UID, - DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) from .entity import ( @@ -138,11 +136,11 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Guardian switches based on a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data @callback def add_new_paired_sensor(uid: str) -> None: diff --git a/homeassistant/components/guardian/services.py b/homeassistant/components/guardian/services.py index 68d2f5159fc..288c6becbee 100644 --- a/homeassistant/components/guardian/services.py +++ b/homeassistant/components/guardian/services.py @@ -22,7 +22,7 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import CONF_UID, DOMAIN if TYPE_CHECKING: - from . import GuardianData + from . import GuardianConfigEntry, GuardianData SERVICE_NAME_PAIR_SENSOR = "pair_sensor" SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" @@ -58,7 +58,7 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( @callback -def async_get_entry_id_for_service_call(call: ServiceCall) -> str: +def async_get_entry_id_for_service_call(call: ServiceCall) -> GuardianConfigEntry: """Get the entry ID related to a service call (by device ID).""" device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(call.hass) @@ -70,7 +70,7 @@ def async_get_entry_id_for_service_call(call: ServiceCall) -> str: if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None: continue if entry.domain == DOMAIN: - return entry_id + return entry raise ValueError(f"No config entry for device ID: {device_id}") @@ -83,8 +83,7 @@ def call_with_data( async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" - entry_id = async_get_entry_id_for_service_call(call) - data = call.hass.data[DOMAIN][entry_id] + data = async_get_entry_id_for_service_call(call).runtime_data try: async with data.client: diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index a2c9ca282be..7640425d8c1 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -9,13 +9,12 @@ from typing import Any from aioguardian import Client from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import GuardianData -from .const import API_VALVE_STATUS, API_WIFI_STATUS, DOMAIN +from . import GuardianConfigEntry, GuardianData +from .const import API_VALVE_STATUS, API_WIFI_STATUS from .entity import ValveControllerEntity, ValveControllerEntityDescription from .util import convert_exceptions_to_homeassistant_error from .valve import GuardianValveState @@ -111,11 +110,11 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Guardian switches based on a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_add_entities( ValveControllerSwitch(entry, data, description) @@ -130,7 +129,7 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, data: GuardianData, description: ValveControllerSwitchDescription, ) -> None: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 69e79f6627e..d05b6ef98d9 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Concatenate from aioguardian.errors import GuardianError -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -18,6 +17,7 @@ from homeassistant.helpers import entity_registry as er from .const import LOGGER if TYPE_CHECKING: + from . import GuardianConfigEntry from .entity import GuardianEntity DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) @@ -36,7 +36,7 @@ class EntityDomainReplacementStrategy: @callback def async_finish_entity_domain_replacements( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy], ) -> None: """Remove old entities and create a repairs issue with info on their replacement.""" diff --git a/homeassistant/components/guardian/valve.py b/homeassistant/components/guardian/valve.py index 6847b3211c5..ad8cd9cae00 100644 --- a/homeassistant/components/guardian/valve.py +++ b/homeassistant/components/guardian/valve.py @@ -15,12 +15,11 @@ from homeassistant.components.valve import ( ValveEntityDescription, ValveEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import GuardianData -from .const import API_VALVE_STATUS, DOMAIN +from . import GuardianConfigEntry, GuardianData +from .const import API_VALVE_STATUS from .entity import ValveControllerEntity, ValveControllerEntityDescription from .util import convert_exceptions_to_homeassistant_error @@ -110,11 +109,11 @@ VALVE_CONTROLLER_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GuardianConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Guardian switches based on a config entry.""" - data: GuardianData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data async_add_entities( ValveControllerValve(entry, data, description) @@ -132,7 +131,7 @@ class ValveControllerValve(ValveControllerEntity, ValveEntity): def __init__( self, - entry: ConfigEntry, + entry: GuardianConfigEntry, data: GuardianData, description: ValveControllerValveDescription, ) -> None: diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index 4487d0b6ac6..8851b6589f6 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Guardian diagnostics.""" from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.guardian import DOMAIN, GuardianData +from homeassistant.components.guardian import GuardianData from homeassistant.core import HomeAssistant from tests.common import ANY, MockConfigEntry @@ -16,7 +16,7 @@ async def test_entry_diagnostics( setup_guardian: None, # relies on config_entry fixture ) -> None: """Test config entry diagnostics.""" - data: GuardianData = hass.data[DOMAIN][config_entry.entry_id] + data: GuardianData = config_entry.runtime_data # Simulate the pairing of a paired sensor: await data.paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") From 253217958ba9823ba4315f97e3df43cd0e168906 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 16:55:04 +0200 Subject: [PATCH 0181/1175] Use runtime_data in google (#144331) * Use runtime_data in google * Quality scale --- homeassistant/components/google/__init__.py | 28 +++++++------------ homeassistant/components/google/api.py | 4 +-- homeassistant/components/google/calendar.py | 13 ++++----- .../components/google/config_flow.py | 10 ++----- homeassistant/components/google/const.py | 2 -- .../components/google/coordinator.py | 11 ++++---- .../components/google/diagnostics.py | 11 ++++---- .../components/google/quality_scale.yaml | 6 +--- homeassistant/components/google/store.py | 13 +++++++++ 9 files changed, 45 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 2b7aeadc0ba..3c3d6577e6c 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -14,7 +14,6 @@ from gcal_sync.model import DateOrDatetime, Event import voluptuous as vol import yaml -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_ENTITIES, @@ -34,8 +33,6 @@ from homeassistant.helpers.entity import generate_entity_id from .api import ApiAuthImpl, get_feature_access from .const import ( - DATA_SERVICE, - DATA_STORE, DOMAIN, EVENT_DESCRIPTION, EVENT_END_DATE, @@ -50,7 +47,7 @@ from .const import ( EVENT_TYPES_CONF, FeatureAccess, ) -from .store import LocalCalendarStore +from .store import GoogleConfigEntry, GoogleRuntimeData, LocalCalendarStore _LOGGER = logging.getLogger(__name__) @@ -139,11 +136,8 @@ ADD_EVENT_SERVICE_SCHEMA = vol.All( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bool: """Set up Google from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {} - # Validate google_calendars.yaml (if present) as soon as possible to return # helpful error messages. try: @@ -181,9 +175,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: calendar_service = GoogleCalendarService( ApiAuthImpl(async_get_clientsession(hass), session) ) - hass.data[DOMAIN][entry.entry_id][DATA_SERVICE] = calendar_service - hass.data[DOMAIN][entry.entry_id][DATA_STORE] = LocalCalendarStore( - hass, entry.entry_id + entry.runtime_data = GoogleRuntimeData( + service=calendar_service, + store=LocalCalendarStore(hass, entry.entry_id), ) if entry.unique_id is None: @@ -207,27 +201,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def async_entry_has_scopes(entry: ConfigEntry) -> bool: +def async_entry_has_scopes(entry: GoogleConfigEntry) -> bool: """Verify that the config entry desired scope is present in the oauth token.""" access = get_feature_access(entry) token_scopes = entry.data.get("token", {}).get("scope", []) return access.scope in token_scopes -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> None: """Reload config entry if the access options change.""" if not async_entry_has_scopes(entry): await hass.config_entries.async_reload(entry.entry_id) -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> None: """Handle removal of a local storage.""" store = LocalCalendarStore(hass, entry.entry_id) await store.async_remove() diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 194c2a0b4a5..efbbec73017 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -17,7 +17,6 @@ from oauth2client.client import ( ) from homeassistant.components.application_credentials import AuthImplementation -from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import ( @@ -27,6 +26,7 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS, FeatureAccess +from .store import GoogleConfigEntry _LOGGER = logging.getLogger(__name__) @@ -155,7 +155,7 @@ class DeviceFlow: self._listener() -def get_feature_access(config_entry: ConfigEntry) -> FeatureAccess: +def get_feature_access(config_entry: GoogleConfigEntry) -> FeatureAccess: """Return the desired calendar feature access.""" if config_entry.options and CONF_CALENDAR_ACCESS in config_entry.options: return FeatureAccess[config_entry.options[CONF_CALENDAR_ACCESS]] diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index a62d2bf1d6b..6fef46395e8 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -37,7 +37,6 @@ from homeassistant.components.calendar import ( extract_offset, is_offset_reached, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError, PlatformNotReady @@ -52,7 +51,6 @@ from . import ( CONF_SEARCH, CONF_TRACK, DEFAULT_CONF_OFFSET, - DOMAIN, YAML_DEVICES, get_calendar_info, load_config, @@ -60,8 +58,6 @@ from . import ( ) from .api import get_feature_access from .const import ( - DATA_SERVICE, - DATA_STORE, EVENT_END_DATE, EVENT_END_DATETIME, EVENT_IN, @@ -72,6 +68,7 @@ from .const import ( FeatureAccess, ) from .coordinator import CalendarQueryUpdateCoordinator, CalendarSyncUpdateCoordinator +from .store import GoogleConfigEntry _LOGGER = logging.getLogger(__name__) @@ -109,7 +106,7 @@ class GoogleCalendarEntityDescription(CalendarEntityDescription): def _get_entity_descriptions( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoogleConfigEntry, calendar_item: Calendar, calendar_info: Mapping[str, Any], ) -> list[GoogleCalendarEntityDescription]: @@ -202,12 +199,12 @@ def _get_entity_descriptions( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoogleConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the google calendar platform.""" - calendar_service = hass.data[DOMAIN][config_entry.entry_id][DATA_SERVICE] - store = hass.data[DOMAIN][config_entry.entry_id][DATA_STORE] + calendar_service = config_entry.runtime_data.service + store = config_entry.runtime_data.store try: result = await calendar_service.async_list_calendars() except ApiException as err: diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index add75f5e95b..15b9ed1c0d8 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -11,12 +11,7 @@ from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException, ApiForbiddenException import voluptuous as vol -from homeassistant.config_entries import ( - SOURCE_REAUTH, - ConfigEntry, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult, OptionsFlow from homeassistant.core import callback from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,6 +33,7 @@ from .const import ( CredentialType, FeatureAccess, ) +from .store import GoogleConfigEntry _LOGGER = logging.getLogger(__name__) @@ -240,7 +236,7 @@ class OAuth2FlowHandler( @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: GoogleConfigEntry, ) -> OptionsFlow: """Create an options flow.""" return OptionsFlowHandler() diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index 1e0b2fc910b..6613668cf91 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -9,9 +9,7 @@ DOMAIN = "google" CONF_CALENDAR_ACCESS = "calendar_access" CONF_CREDENTIAL_TYPE = "credential_type" DATA_CALENDARS = "calendars" -DATA_SERVICE = "service" DATA_CONFIG = "config" -DATA_STORE = "store" class FeatureAccess(Enum): diff --git a/homeassistant/components/google/coordinator.py b/homeassistant/components/google/coordinator.py index 4a8a3d9f167..9f51c60b069 100644 --- a/homeassistant/components/google/coordinator.py +++ b/homeassistant/components/google/coordinator.py @@ -14,12 +14,13 @@ from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline from ical.iter import SortableItemValue -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util +from .store import GoogleConfigEntry + _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -47,12 +48,12 @@ def _truncate_timeline(timeline: Timeline, max_events: int) -> Timeline: class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): """Coordinator for calendar RPC calls that use an efficient sync.""" - config_entry: ConfigEntry + config_entry: GoogleConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoogleConfigEntry, sync: CalendarEventSyncManager, name: str, ) -> None: @@ -108,12 +109,12 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): for limitations in the calendar API for supporting search. """ - config_entry: ConfigEntry + config_entry: GoogleConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GoogleConfigEntry, calendar_service: GoogleCalendarService, name: str, calendar_id: str, diff --git a/homeassistant/components/google/diagnostics.py b/homeassistant/components/google/diagnostics.py index 1a6f498b4cd..6dc6e321a23 100644 --- a/homeassistant/components/google/diagnostics.py +++ b/homeassistant/components/google/diagnostics.py @@ -4,11 +4,10 @@ import datetime from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from .const import DATA_STORE, DOMAIN +from .store import GoogleConfigEntry TO_REDACT = { "id", @@ -40,7 +39,7 @@ def redact_store(data: dict[str, Any]) -> dict[str, Any]: async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: GoogleConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" payload: dict[str, Any] = { @@ -49,7 +48,7 @@ async def async_get_config_entry_diagnostics( "system_timezone": str(datetime.datetime.now().astimezone().tzinfo), } - store = hass.data[DOMAIN][config_entry.entry_id][DATA_STORE] - data = await store.async_load() - payload["store"] = redact_store(data) + store = config_entry.runtime_data.store + if data := await store.async_load(): + payload["store"] = redact_store(data) return payload diff --git a/homeassistant/components/google/quality_scale.yaml b/homeassistant/components/google/quality_scale.yaml index 9ef6abdba90..43c86c54e28 100644 --- a/homeassistant/components/google/quality_scale.yaml +++ b/homeassistant/components/google/quality_scale.yaml @@ -40,11 +40,7 @@ rules: to increase functionality such as checking for the specific contents of a unique id assigned to a config entry. docs-actions: done - runtime-data: - status: todo - comment: | - The integration stores config entry data in `hass.data` and should be - updated to use `runtime_data`. + runtime-data: done # Silver log-when-unavailable: done diff --git a/homeassistant/components/google/store.py b/homeassistant/components/google/store.py index c4d9e4c3e9c..4936a86f384 100644 --- a/homeassistant/components/google/store.py +++ b/homeassistant/components/google/store.py @@ -2,11 +2,14 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any +from gcal_sync.api import GoogleCalendarService from gcal_sync.store import CalendarStore +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import Store @@ -19,6 +22,16 @@ STORAGE_VERSION = 1 # Buffer writes every few minutes (plus guaranteed to be written at shutdown) STORAGE_SAVE_DELAY_SECONDS = 120 +type GoogleConfigEntry = ConfigEntry[GoogleRuntimeData] + + +@dataclass +class GoogleRuntimeData: + """Google runtime data.""" + + service: GoogleCalendarService + store: LocalCalendarStore + class LocalCalendarStore(CalendarStore): """Storage for local persistence of calendar and event data.""" From c3ce82d87410d05141b8aeb81799c71da00ba6c0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 6 May 2025 16:57:11 +0200 Subject: [PATCH 0182/1175] Fix Z-Wave migration flow to unload config entry before unplugging controller (#144343) * Fix Z-Wave migration unload config entry before unplugging controller * Remove typo --- homeassistant/components/zwave_js/config_flow.py | 9 +++++---- tests/components/zwave_js/test_config_flow.py | 9 ++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 46d9e061f0b..84717047fdd 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -907,10 +907,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Reset the current controller, and instruct the user to unplug it.""" if user_input is not None: - config_entry = self._reconfigure_config_entry - assert config_entry is not None - # Unload the config entry before stopping the add-on. - await self.hass.config_entries.async_unload(config_entry.entry_id) if self.usb_path: # USB discovery was used, so the device is already known. await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) @@ -925,6 +921,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to reset controller: %s", err) return self.async_abort(reason="reset_failed") + config_entry = self._reconfigure_config_entry + assert config_entry is not None + # Unload the config entry before asking the user to unplug the controller. + await self.hass.config_entries.async_unload(config_entry.entry_id) + return self.async_show_form( step_id="instruct_unplug", description_placeholders={ diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index de76d9d9dc4..15fd9fcbd30 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1109,10 +1109,10 @@ async def test_usb_discovery_migration_driver_ready_timeout( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -3776,6 +3776,7 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3790,7 +3791,6 @@ async def test_reconfigure_migrate_with_addon( }, ) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -3918,6 +3918,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3932,7 +3933,6 @@ async def test_reconfigure_migrate_driver_ready_timeout( }, ) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert result["type"] == FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( @@ -4108,6 +4108,7 @@ async def test_reconfigure_migrate_start_addon_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -4202,6 +4203,7 @@ async def test_reconfigure_migrate_restore_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -4367,6 +4369,7 @@ async def test_choose_serial_port_usb_ports_failure( assert result["type"] == FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED with patch( "homeassistant.components.zwave_js.config_flow.async_get_usb_ports", From 452e946509bd7dc655293aff2413c92e90ade905 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 May 2025 10:32:35 -0500 Subject: [PATCH 0183/1175] Bump bluemaestro-ble to 0.4.1 (#144345) changelog: https://github.com/Bluetooth-Devices/bluemaestro-ble/compare/v0.4.0...v0.4.1 fixes #https://github.com/home-assistant/core/issues/144339 --- homeassistant/components/bluemaestro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 5e3c43f4ff9..887b27239ef 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bluemaestro", "iot_class": "local_push", - "requirements": ["bluemaestro-ble==0.4.0"] + "requirements": ["bluemaestro-ble==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c9c358a5724..ef4d4f489bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ blockchain==1.4.4 bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro -bluemaestro-ble==0.4.0 +bluemaestro-ble==0.4.1 # homeassistant.components.decora # bluepy==1.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71d12a5cf32..4bd95e17b36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ blinkpy==0.23.0 bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro -bluemaestro-ble==0.4.0 +bluemaestro-ble==0.4.1 # homeassistant.components.bluetooth bluetooth-adapters==0.21.4 From 121e9e4e7f55aa24bf69e5fe04e43dae9d2357d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 6 May 2025 12:13:51 -0500 Subject: [PATCH 0184/1175] Bump aioesphomeapi to 30.2.0 (#144348) --- 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 beaf68decd9..d43ea86126a 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==30.1.0", + "aioesphomeapi==30.2.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.15.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index ef4d4f489bd..1a47b96cd62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==30.1.0 +aioesphomeapi==30.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bd95e17b36..baf12847707 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==30.1.0 +aioesphomeapi==30.2.0 # homeassistant.components.flo aioflo==2021.11.0 From a673bd7a9117de01a8499e6726929d97cdfe37c4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 6 May 2025 19:16:14 +0200 Subject: [PATCH 0185/1175] Use runtime_data in here_travel_time (#144340) --- .../components/here_travel_time/__init__.py | 19 +++++++------------ .../here_travel_time/coordinator.py | 12 ++++++++---- .../components/here_travel_time/sensor.py | 12 +++++++----- .../components/here_travel_time/test_init.py | 1 - 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 132b12de4ce..525da15bd74 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started @@ -18,10 +17,10 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - DOMAIN, TRAVEL_MODE_PUBLIC, ) from .coordinator import ( + HereConfigEntry, HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator, ) @@ -30,7 +29,7 @@ from .model import HERETravelTimeConfig PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry) -> bool: """Set up HERE Travel Time from a config entry.""" api_key = config_entry.data[CONF_API_KEY] @@ -57,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b cls = HERERoutingDataUpdateCoordinator data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data_coordinator + config_entry.runtime_data = data_coordinator async def _async_update_at_start(_: HomeAssistant) -> None: await data_coordinator.async_refresh() @@ -68,12 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: HereConfigEntry +) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index a3345e78e4e..aa36404c584 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -41,16 +41,20 @@ BACKOFF_MULTIPLIER = 1.1 _LOGGER = logging.getLogger(__name__) +type HereConfigEntry = ConfigEntry[ + HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator +] + class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): """here_routing DataUpdateCoordinator.""" - config_entry: ConfigEntry + config_entry: HereConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HereConfigEntry, api_key: str, config: HERETravelTimeConfig, ) -> None: @@ -173,12 +177,12 @@ class HERETransitDataUpdateCoordinator( ): """HERETravelTime DataUpdateCoordinator.""" - config_entry: ConfigEntry + config_entry: HereConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HereConfigEntry, api_key: str, config: HERETravelTimeConfig, ) -> None: diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 0f0cbb7d3cb..bbaabb56d46 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -40,6 +39,7 @@ from .const import ( ICONS, ) from .coordinator import ( + HereConfigEntry, HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator, ) @@ -77,14 +77,14 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HereConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add HERE travel time entities from a config_entry.""" entry_id = config_entry.entry_id name = config_entry.data[CONF_NAME] - coordinator = hass.data[DOMAIN][entry_id] + coordinator = config_entry.runtime_data sensors: list[HERETravelTimeSensor] = [ HERETravelTimeSensor( @@ -164,7 +164,8 @@ class OriginSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HERERoutingDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator + | HERETransitDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( @@ -192,7 +193,8 @@ class DestinationSensor(HERETravelTimeSensor): self, unique_id_prefix: str, name: str, - coordinator: HERERoutingDataUpdateCoordinator, + coordinator: HERERoutingDataUpdateCoordinator + | HERETransitDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" sensor_description = SensorEntityDescription( diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py index 682d8c560bb..ff09c7e6ae9 100644 --- a/tests/components/here_travel_time/test_init.py +++ b/tests/components/here_travel_time/test_init.py @@ -50,4 +50,3 @@ async def test_unload_entry(hass: HomeAssistant, options) -> None: await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert await hass.config_entries.async_unload(entry.entry_id) - assert not hass.data[DOMAIN] From da7e9f3ab6bfcbc9fba07174e88a7ea56c950335 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 6 May 2025 19:30:48 +0200 Subject: [PATCH 0186/1175] Ensure all default MQTT subentry option values are saved (#144347) * Ensure all default MQTT subentry option values are saved * Apply correct filter --- homeassistant/components/mqtt/config_flow.py | 12 +++++++++--- tests/components/mqtt/common.py | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 0ccf7468cbf..d83b88540b2 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1490,8 +1490,11 @@ def subentry_schema_default_data_from_fields( return { key: field.default for key, field in data_schema_fields.items() - if field.is_schema_default - or (field.default is not vol.UNDEFINED and key not in component_data) + if _check_conditions(field, component_data) + and ( + field.is_schema_default + or (field.default is not vol.UNDEFINED and key not in component_data) + ) } @@ -2317,7 +2320,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): for component_data in self._subentry_data["components"].values(): platform = component_data[CONF_PLATFORM] subentry_default_data = subentry_schema_default_data_from_fields( - PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data + COMMON_ENTITY_FIELDS + | PLATFORM_ENTITY_FIELDS[platform] + | PLATFORM_MQTT_FIELDS[platform], + component_data, ) component_data.update(subentry_default_data) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 3c017de89f4..f0952e7f821 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -179,6 +179,10 @@ MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = { "state_topic": "test-topic", "color_temp_kelvin": True, "state_value_template": "{{ value_json.value }}", + "brightness_scale": 255, + "max_kelvin": 6535, + "min_kelvin": 2000, + "white_scale": 255, "entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2", }, } From 76df7de0cf59eceee1cbd38a871ed0c60744c8da Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 May 2025 21:24:09 +0200 Subject: [PATCH 0187/1175] Update frontend to 20250506.0 (#144354) --- 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 18e4d349122..4abf9aa7814 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==20250502.1"] + "requirements": ["home-assistant-frontend==20250506.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a9788e03648..1838e552800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.96.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1a47b96cd62..90bc6166969 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baf12847707..e63967853d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250502.1 +home-assistant-frontend==20250506.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From 320df710a4d47cbeb204efd09a61ef3c9cbcc93f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 6 May 2025 15:24:32 -0400 Subject: [PATCH 0188/1175] Remove some media player intent checks for when paused (#144351) --- .../components/media_player/intent.py | 2 -- tests/components/media_player/test_intent.py | 24 ------------------- 2 files changed, 26 deletions(-) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index af37c0d68bb..4349362b13a 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -93,7 +93,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None: DOMAIN, SERVICE_VOLUME_SET, required_domains={DOMAIN}, - required_states={MediaPlayerState.PLAYING}, required_features=MediaPlayerEntityFeature.VOLUME_SET, required_slots={ ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo( @@ -159,7 +158,6 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): DOMAIN, SERVICE_MEDIA_PLAY, required_domains={DOMAIN}, - required_states={MediaPlayerState.PAUSED}, description="Resumes a media player", platforms={DOMAIN}, device_classes={MediaPlayerDeviceClass}, diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index 9ddf50d04f4..8e7211183e7 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -104,19 +104,6 @@ async def test_unpause_media_player_intent(hass: HomeAssistant) -> None: assert call.service == SERVICE_MEDIA_PLAY assert call.data == {"entity_id": entity_id} - # Test if not paused - hass.states.async_set( - entity_id, - STATE_PLAYING, - ) - - with pytest.raises(intent.MatchFailedError): - response = await intent.async_handle( - hass, - "test", - media_player_intent.INTENT_MEDIA_UNPAUSE, - ) - async def test_next_media_player_intent(hass: HomeAssistant) -> None: """Test HassMediaNext intent for media players.""" @@ -245,17 +232,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None: assert call.service == SERVICE_VOLUME_SET assert call.data == {"entity_id": entity_id, "volume_level": 0.5} - # Test if not playing - hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes) - - with pytest.raises(intent.MatchFailedError): - response = await intent.async_handle( - hass, - "test", - media_player_intent.INTENT_SET_VOLUME, - {"volume_level": {"value": 50}}, - ) - # Test feature not supported hass.states.async_set( entity_id, From 27913294601f6cba03afd3b6829d32d2fe8d38db Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 7 May 2025 07:45:07 +1000 Subject: [PATCH 0189/1175] Use config location for Homelink in Teslemetry (#144171) --- homeassistant/components/teslemetry/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/button.py b/homeassistant/components/teslemetry/button.py index 2de2868551b..cf1d6157ec1 100644 --- a/homeassistant/components/teslemetry/button.py +++ b/homeassistant/components/teslemetry/button.py @@ -51,8 +51,8 @@ DESCRIPTIONS: tuple[TeslemetryButtonEntityDescription, ...] = ( key="homelink", func=lambda self: handle_vehicle_command( self.api.trigger_homelink( - lat=self.coordinator.data["drive_state_latitude"], - lon=self.coordinator.data["drive_state_longitude"], + lat=self.hass.config.latitude, + lon=self.hass.config.longitude, ) ), ), From 946172d530036875ce46cf2d75a7afda2c9e69e5 Mon Sep 17 00:00:00 2001 From: John Hillery <34005807+jrhillery@users.noreply.github.com> Date: Wed, 7 May 2025 00:23:06 -0400 Subject: [PATCH 0190/1175] Bump nexia to 2.10.0 (#144363) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 0c01820055e..939b0b62284 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nexia", "iot_class": "cloud_polling", "loggers": ["nexia"], - "requirements": ["nexia==2.9.0"] + "requirements": ["nexia==2.10.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 90bc6166969..1dbae05889c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1496,7 +1496,7 @@ nettigo-air-monitor==4.1.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.9.0 +nexia==2.10.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e63967853d0..026ba6f488e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1260,7 +1260,7 @@ netmap==0.7.0.2 nettigo-air-monitor==4.1.0 # homeassistant.components.nexia -nexia==2.9.0 +nexia==2.10.0 # homeassistant.components.nextcloud nextcloudmonitor==1.5.1 From 6e7f57383ac72b653f8b9a35af6492bbd4190176 Mon Sep 17 00:00:00 2001 From: markhannon Date: Wed, 7 May 2025 16:34:32 +1000 Subject: [PATCH 0191/1175] Add switch entity to Zimi integration (#144236) * Import switch.py * Alignment to light.py * Use default switch attributes * Update homeassistant/components/zimi/switch.py --------- Co-authored-by: Josef Zweck --- homeassistant/components/zimi/__init__.py | 2 +- homeassistant/components/zimi/switch.py | 56 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zimi/switch.py diff --git a/homeassistant/components/zimi/__init__.py b/homeassistant/components/zimi/__init__.py index db91f7816c4..5827684cc3d 100644 --- a/homeassistant/components/zimi/__init__.py +++ b/homeassistant/components/zimi/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN from .helpers import async_connect_to_controller -PLATFORMS = [Platform.LIGHT] +PLATFORMS = [Platform.LIGHT, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zimi/switch.py b/homeassistant/components/zimi/switch.py new file mode 100644 index 00000000000..a5292602a6e --- /dev/null +++ b/homeassistant/components/zimi/switch.py @@ -0,0 +1,56 @@ +"""Switch platform for zcc integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ZimiConfigEntry +from .entity import ZimiEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ZimiConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Zimi Switch platform.""" + + api = config_entry.runtime_data + + outlets = [ZimiSwitch(device, api) for device in api.outlets] + + async_add_entities(outlets) + + +class ZimiSwitch(ZimiEntity, SwitchEntity): + """Representation of an Zimi Switch.""" + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the switch to turn on.""" + + _LOGGER.debug( + "Sending turn_on() for %s in %s", self._device.name, self._device.room + ) + + await self._device.turn_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the switch to turn off.""" + + _LOGGER.debug( + "Sending turn_off() for %s in %s", self._device.name, self._device.room + ) + + await self._device.turn_off() From 2a25dcd44e3aa031f3f8ff4569adb8c8e96ccad4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 08:44:55 +0200 Subject: [PATCH 0192/1175] Bump renault-api to 0.3.1 (#144366) * Bump renault-api to 0.3.1 * Adjust tests --- .../components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../renault/snapshots/test_binary_sensor.ambr | 576 ------------------ .../renault/snapshots/test_sensor.ambr | 188 ------ tests/components/renault/test_sensor.py | 4 +- 6 files changed, 5 insertions(+), 769 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 06acf4a3e49..2861c52c24a 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": "silver", - "requirements": ["renault-api==0.3.0"] + "requirements": ["renault-api==0.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1dbae05889c..ae417e551d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2631,7 +2631,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.0 +renault-api==0.3.1 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 026ba6f488e..b03e173146c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2138,7 +2138,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.0 +renault-api==0.3.1 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index d1547bc1bbc..e89873593e9 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -1005,102 +1005,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_driver_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Driver door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'driver_door_status', - 'unique_id': 'vf1twingoiiivin_driver_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Driver door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_driver_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_hatch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hatch', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hatch_status', - 'unique_id': 'vf1twingoiiivin_hatch_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Hatch', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_hatch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hvac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1148,102 +1052,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Lock', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'vf1twingoiiivin_lock_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'lock', - 'friendly_name': 'REG-TWINGO-III Lock', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Passenger door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'passenger_door_status', - 'unique_id': 'vf1twingoiiivin_passenger_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Passenger door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_plug-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1292,102 +1100,6 @@ 'state': 'on', }) # --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear left door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_left_door_status', - 'unique_id': 'vf1twingoiiivin_rear_left_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Rear left door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear right door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_right_door_status', - 'unique_id': 'vf1twingoiiivin_rear_right_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-TWINGO-III Rear right door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_binary_sensors[zoe_40][binary_sensor.reg_zoe_40_charging-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1579,102 +1291,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_driver_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Driver door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'driver_door_status', - 'unique_id': 'vf1zoe50vin_driver_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Driver door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_driver_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_hatch', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Hatch', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'hatch_status', - 'unique_id': 'vf1zoe50vin_hatch_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Hatch', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_hatch', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hvac-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1722,102 +1338,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Lock', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'vf1zoe50vin_lock_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'lock', - 'friendly_name': 'REG-ZOE-50 Lock', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_passenger_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Passenger door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'passenger_door_status', - 'unique_id': 'vf1zoe50vin_passenger_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Passenger door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_passenger_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_plug-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1866,99 +1386,3 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear left door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_left_door_status', - 'unique_id': 'vf1zoe50vin_rear_left_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Rear left door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Rear right door', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'rear_right_door_status', - 'unique_id': 'vf1zoe50vin_rear_right_door_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'REG-ZOE-50 Rear right door', - }), - 'context': , - 'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index e7300d2b003..b6c9569e0d3 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -3211,100 +3211,6 @@ 'state': 'unknown', }) # --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state', - 'unique_id': 'vf1twingoiiivin_res_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-TWINGO-III Remote engine start', - }), - 'context': , - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start code', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state_code', - 'unique_id': 'vf1twingoiiivin_res_state_code', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-TWINGO-III Remote engine start code', - }), - 'context': , - 'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_sensors[zoe_40][sensor.reg_zoe_40_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4737,97 +4643,3 @@ 'state': 'unplugged', }) # --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state', - 'unique_id': 'vf1zoe50vin_res_state', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-ZOE-50 Remote engine start', - }), - 'context': , - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Stopped, ready for RES', - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Remote engine start code', - 'platform': 'renault', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'res_state_code', - 'unique_id': 'vf1zoe50vin_res_state_code', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'REG-ZOE-50 Remote engine start code', - }), - 'context': , - 'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10', - }) -# --- diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 10fa2f0ffb0..e75d0558f19 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -197,7 +197,7 @@ async def test_sensor_throttling_after_init( @pytest.mark.parametrize( ("vehicle_type", "vehicle_count", "scan_interval"), [ - ("zoe_50", 1, 420), # 7 coordinators => 7 minutes interval + ("zoe_50", 1, 300), # 5 coordinators => 5 minutes interval ("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval ("multi", 2, 480), # 8 coordinators => 8 minutes interval ], @@ -236,7 +236,7 @@ async def test_dynamic_scan_interval( @pytest.mark.parametrize( ("vehicle_type", "vehicle_count", "scan_interval"), [ - ("zoe_50", 1, 300), # (7-2) coordinators => 5 minutes interval + ("zoe_50", 1, 240), # (6-2) coordinators => 4 minutes interval ("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval ("multi", 2, 360), # (8-2) coordinators => 6 minutes interval ], From dbffd8c0ffae2d04c54d5c07d593c50343e3f35c Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 7 May 2025 09:11:31 +0200 Subject: [PATCH 0193/1175] Bump uiprotect to version 7.6.0 (#144369) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a3f3b6fe2eb..e23568480ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.5.5", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.6.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index ae417e551d0..bcf228e5754 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2975,7 +2975,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.5.5 +uiprotect==7.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b03e173146c..a6204329d43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2404,7 +2404,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.5.5 +uiprotect==7.6.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 65278100a0f7a4fb8f1268d7a4382a009100ac5a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 09:13:14 +0200 Subject: [PATCH 0194/1175] Remove entity name input from Samsung TV config flow (#144372) --- .../components/samsungtv/config_flow.py | 19 ++++++----------- .../components/samsungtv/test_config_flow.py | 21 +++---------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 74915c9251b..9867e44254e 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.const import ( CONF_MAC, CONF_METHOD, CONF_MODEL, - CONF_NAME, CONF_PIN, CONF_PORT, CONF_TOKEN, @@ -63,7 +62,7 @@ from .const import ( UPNP_SVC_RENDERING_CONTROL, ) -DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) def _strip_uuid(udn: str) -> str: @@ -139,7 +138,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, CONF_METHOD: self._bridge.method, CONF_MODEL: self._model, - CONF_NAME: self._name, CONF_PORT: self._bridge.port, CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location, CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location, @@ -261,8 +259,7 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): ) except socket.gaierror as err: raise AbortFlow(RESULT_UNKNOWN_HOST) from err - self._name = user_input.get(CONF_NAME, self._host) or "" - self._title = self._name + self._title = self._host async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -534,10 +531,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle configuration by re-auth.""" - if entry_data.get(CONF_MODEL) and entry_data.get(CONF_NAME): - self._title = f"{entry_data[CONF_NAME]} ({entry_data[CONF_MODEL]})" - else: - self._title = entry_data.get(CONF_NAME) or entry_data[CONF_HOST] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -570,11 +563,11 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): # On websocket we will get RESULT_CANNOT_CONNECT when auth is missing errors = {"base": RESULT_AUTH_MISSING} - self.context["title_placeholders"] = {"device": self._title} + self.context["title_placeholders"] = {"device": reauth_entry.title} return self.async_show_form( step_id="reauth_confirm", errors=errors, - description_placeholders={"device": self._title}, + description_placeholders={"device": reauth_entry.title}, ) async def _async_start_encrypted_pairing(self, host: str) -> None: @@ -611,10 +604,10 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): errors = {"base": RESULT_INVALID_PIN} - self.context["title_placeholders"] = {"device": self._title} + self.context["title_placeholders"] = {"device": reauth_entry.title} return self.async_show_form( step_id="reauth_confirm_encrypted", errors=errors, - description_placeholders={"device": self._title}, + description_placeholders={"device": reauth_entry.title}, data_schema=vol.Schema({vol.Required(CONF_PIN): str}), ) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 79e24519a85..57c1a3b0bf4 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -81,7 +81,7 @@ MOCK_IMPORT_WSDATA = { CONF_NAME: "fake", CONF_PORT: 8002, } -MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} +MOCK_USER_DATA = {CONF_HOST: "fake_host"} MOCK_DHCP_DATA = DhcpServiceInfo( ip="fake_host", macaddress="aabbccddeeff", hostname="fake_hostname" @@ -176,9 +176,8 @@ async def test_user_legacy(hass: HomeAssistant) -> None: ) # legacy tv entry created assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "fake_name" + assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_METHOD] == "legacy" assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None @@ -211,9 +210,8 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: # legacy tv entry created assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "fake_name" + assert result3["title"] == "fake_host" assert result3["data"][CONF_HOST] == "fake_host" - assert result3["data"][CONF_NAME] == "fake_name" assert result3["data"][CONF_METHOD] == "legacy" assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result3["data"][CONF_MODEL] is None @@ -241,7 +239,6 @@ async def test_user_websocket(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" @@ -290,7 +287,6 @@ async def test_user_encrypted_websocket( 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" assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung" assert result4["data"][CONF_MODEL] == "UE48JU6400" @@ -422,7 +418,6 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -485,7 +480,6 @@ async def test_ssdp(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -544,7 +538,6 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -580,7 +573,6 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -620,7 +612,6 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( 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" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "82GXARRS" @@ -650,7 +641,6 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc 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" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "82GXARRS" @@ -702,7 +692,6 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l 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" assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result4["data"][CONF_MODEL] == "UE48JU6400" @@ -907,7 +896,6 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE48JU6400" @@ -938,7 +926,6 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: 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)" assert result["data"][CONF_MAC] == "aa:ee:tt:hh:ee:rr" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE43LS003" @@ -1036,7 +1023,6 @@ async def test_zeroconf(hass: HomeAssistant) -> None: 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" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" @@ -1255,7 +1241,6 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: ) 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 assert result["data"][CONF_PORT] == LEGACY_PORT From 358d904c2cf66bc35262c5a048734fed352a5a8c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 7 May 2025 09:24:51 +0200 Subject: [PATCH 0195/1175] Fix field validation for mqtt subentry options in sections (#144355) --- homeassistant/components/mqtt/config_flow.py | 8 +++++--- tests/components/mqtt/test_config_flow.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index d83b88540b2..b3c82dce65e 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -526,8 +526,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get( CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN ): - errors[CONF_MAX_KELVIN] = "max_below_min_kelvin" - errors[CONF_MIN_KELVIN] = "max_below_min_kelvin" + errors["advanced_settings"] = "max_below_min_kelvin" return errors @@ -1381,7 +1380,10 @@ def validate_user_input( try: validator(value) except (ValueError, vol.Error, vol.Invalid): - errors[field] = data_schema_fields[field].error or "invalid_input" + data_schema_field = data_schema_fields[field] + errors[data_schema_field.section or field] = ( + data_schema_field.error or "invalid_input" + ) if config_validator is not None: if TYPE_CHECKING: diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 81d4960be23..4cfc416c3c9 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -2858,14 +2858,22 @@ async def test_migrate_of_incompatible_config_entry( }, {"state_topic": "invalid_subscribe_topic"}, ), + ( + { + "command_topic": "test-topic", + "light_brightness_settings": { + "brightness_command_topic": "test-topic#invalid" + }, + }, + {"light_brightness_settings": "invalid_publish_topic"}, + ), ( { "command_topic": "test-topic", "advanced_settings": {"max_kelvin": 2000, "min_kelvin": 2000}, }, { - "max_kelvin": "max_below_min_kelvin", - "min_kelvin": "max_below_min_kelvin", + "advanced_settings": "max_below_min_kelvin", }, ), ), From 65ad39f5be0c080976984bfc07a3c1e83f854d55 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 7 May 2025 09:30:40 +0200 Subject: [PATCH 0196/1175] Modify require_admin decorator to take parameters for Unauthorized (#144346) --- .../components/config/config_entries.py | 36 +++++-------------- homeassistant/components/http/decorators.py | 8 +++-- .../components/repairs/websocket_api.py | 7 ++-- 3 files changed, 17 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 6e2d4a5da49..d20d4de881f 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -165,9 +165,7 @@ class ConfigManagerFlowIndexView( """Not implemented.""" raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add") @RequestDataValidator( vol.Schema( { @@ -218,16 +216,12 @@ class ConfigManagerFlowResourceView( url = "/api/config/config_entries/flow/{flow_id}" name = "api:config:config_entries:flow:resource" - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add") async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" return await super().get(request, flow_id) - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add") async def post(self, request: web.Request, flow_id: str) -> web.Response: """Handle a POST request.""" return await super().post(request, flow_id) @@ -262,9 +256,7 @@ class OptionManagerFlowIndexView( url = "/api/config/config_entries/options/flow" name = "api:config:config_entries:option:flow" - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) async def post(self, request: web.Request) -> web.Response: """Handle a POST request. @@ -281,16 +273,12 @@ class OptionManagerFlowResourceView( url = "/api/config/config_entries/options/flow/{flow_id}" name = "api:config:config_entries:options:flow:resource" - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" return await super().get(request, flow_id) - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) async def post(self, request: web.Request, flow_id: str) -> web.Response: """Handle a POST request.""" return await super().post(request, flow_id) @@ -304,9 +292,7 @@ class SubentryManagerFlowIndexView( url = "/api/config/config_entries/subentries/flow" name = "api:config:config_entries:subentries:flow" - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) @RequestDataValidator( vol.Schema( { @@ -341,16 +327,12 @@ class SubentryManagerFlowResourceView( url = "/api/config/config_entries/subentries/flow/{flow_id}" name = "api:config:config_entries:subentries:flow:resource" - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" return await super().get(request, flow_id) - @require_admin( - error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) - ) + @require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT) async def post(self, request: web.Request, flow_id: str) -> web.Response: """Handle a POST request.""" return await super().post(request, flow_id) diff --git a/homeassistant/components/http/decorators.py b/homeassistant/components/http/decorators.py index 1adc21be09f..19a0a5d1c55 100644 --- a/homeassistant/components/http/decorators.py +++ b/homeassistant/components/http/decorators.py @@ -27,7 +27,8 @@ def require_admin[ ]( _func: None = None, *, - error: Unauthorized | None = None, + perm_category: str | None = None, + permission: str | None = None, ) -> Callable[ [_FuncType[_HomeAssistantViewT, _P, _ResponseT]], _FuncType[_HomeAssistantViewT, _P, _ResponseT], @@ -51,7 +52,8 @@ def require_admin[ ]( _func: _FuncType[_HomeAssistantViewT, _P, _ResponseT] | None = None, *, - error: Unauthorized | None = None, + perm_category: str | None = None, + permission: str | None = None, ) -> ( Callable[ [_FuncType[_HomeAssistantViewT, _P, _ResponseT]], @@ -76,7 +78,7 @@ def require_admin[ """Check admin and call function.""" user: User = request["hass_user"] if not user.is_admin: - raise error or Unauthorized() + raise Unauthorized(perm_category=perm_category, permission=permission) return await func(self, request, *args, **kwargs) diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 4875a8f6cfa..4117b0ee35b 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -14,7 +14,6 @@ from homeassistant.components import websocket_api from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.decorators import require_admin from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, @@ -114,7 +113,7 @@ class RepairsFlowIndexView(FlowManagerIndexView): url = "/api/repairs/issues/fix" name = "api:repairs:issues:fix" - @require_admin(error=Unauthorized(permission=POLICY_EDIT)) + @require_admin(permission=POLICY_EDIT) @RequestDataValidator( vol.Schema( { @@ -149,12 +148,12 @@ class RepairsFlowResourceView(FlowManagerResourceView): url = "/api/repairs/issues/fix/{flow_id}" name = "api:repairs:issues:fix:resource" - @require_admin(error=Unauthorized(permission=POLICY_EDIT)) + @require_admin(permission=POLICY_EDIT) async def get(self, request: web.Request, /, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" return await super().get(request, flow_id) - @require_admin(error=Unauthorized(permission=POLICY_EDIT)) + @require_admin(permission=POLICY_EDIT) async def post(self, request: web.Request, flow_id: str) -> web.Response: """Handle a POST request.""" return await super().post(request, flow_id) From 9a332f19c26685ccf085b9f73b9174a461b397db Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 10:47:36 +0200 Subject: [PATCH 0197/1175] Use runtime_data in hko (#144368) --- homeassistant/components/hko/__init__.py | 16 ++++++---------- homeassistant/components/hko/coordinator.py | 6 ++++-- homeassistant/components/hko/weather.py | 8 +++----- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hko/__init__.py b/homeassistant/components/hko/__init__.py index b7e21f731d8..b99fc07bc2f 100644 --- a/homeassistant/components/hko/__init__.py +++ b/homeassistant/components/hko/__init__.py @@ -4,18 +4,17 @@ from __future__ import annotations from hko import LOCATIONS -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LOCATION, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_DISTRICT, DOMAIN, KEY_DISTRICT, KEY_LOCATION -from .coordinator import HKOUpdateCoordinator +from .const import DEFAULT_DISTRICT, KEY_DISTRICT, KEY_LOCATION +from .coordinator import HKOConfigEntry, HKOUpdateCoordinator PLATFORMS: list[Platform] = [Platform.WEATHER] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HKOConfigEntry) -> bool: """Set up Hong Kong Observatory from a config entry.""" location = entry.data[CONF_LOCATION] @@ -27,16 +26,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = HKOUpdateCoordinator(hass, entry, websession, district, location) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HKOConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hko/coordinator.py b/homeassistant/components/hko/coordinator.py index aede960e702..29746c20728 100644 --- a/homeassistant/components/hko/coordinator.py +++ b/homeassistant/components/hko/coordinator.py @@ -65,16 +65,18 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type HKOConfigEntry = ConfigEntry[HKOUpdateCoordinator] + class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """HKO Update Coordinator.""" - config_entry: ConfigEntry + config_entry: HKOConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HKOConfigEntry, session: ClientSession, district: str, location: str, diff --git a/homeassistant/components/hko/weather.py b/homeassistant/components/hko/weather.py index e746d4304d3..075090ecc3f 100644 --- a/homeassistant/components/hko/weather.py +++ b/homeassistant/components/hko/weather.py @@ -5,7 +5,6 @@ from homeassistant.components.weather import ( WeatherEntity, WeatherEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo @@ -22,19 +21,18 @@ from .const import ( DOMAIN, MANUFACTURER, ) -from .coordinator import HKOUpdateCoordinator +from .coordinator import HKOConfigEntry, HKOUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HKOConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add a HKO weather entity from a config_entry.""" assert config_entry.unique_id is not None unique_id = config_entry.unique_id - coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([HKOEntity(unique_id, coordinator)], False) + async_add_entities([HKOEntity(unique_id, config_entry.runtime_data)], False) class HKOEntity(CoordinatorEntity[HKOUpdateCoordinator], WeatherEntity): From c5ef8659a791756a6286133d837bcb4a308cc5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 May 2025 11:07:15 +0200 Subject: [PATCH 0198/1175] Allow no_subscription repair issue in cloud (#144380) --- homeassistant/components/cloud/client.py | 1 + homeassistant/components/cloud/strings.json | 4 ++++ tests/components/cloud/test_client.py | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 7308c2c3d0e..916bac4f73d 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -40,6 +40,7 @@ from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) VALID_REPAIR_TRANSLATION_KEYS = { + "no_subscription", "warn_bad_custom_domain_configuration", "reset_bad_custom_domain_configuration", } diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 6380ee9c312..2dc2acc82d0 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -62,6 +62,10 @@ } } }, + "no_subscription": { + "title": "No subscription detected", + "description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}." + }, "warn_bad_custom_domain_configuration": { "title": "Detected wrong custom domain configuration", "description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. Please check the DNS configuration of your domain and make sure it points to the correct CNAME." diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 7b842b24551..14fcbbd5e5b 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -468,7 +468,10 @@ async def test_async_create_repair_issue_known( await cloud.client.async_create_repair_issue( identifier=identifier, translation_key=translation_key, - placeholders={"custom_domains": "example.com"}, + placeholders={ + "account_url": "http://example.org", + "custom_domains": "example.com", + }, severity="warning", ) issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier) From 0b1875de143cfc782cfaba1098ea411e47ba22e8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 7 May 2025 12:32:27 +0200 Subject: [PATCH 0199/1175] Fix Z-Wave controller hard reset (#144389) --- homeassistant/components/zwave_js/api.py | 22 +++++- tests/components/zwave_js/test_api.py | 94 +++++++++++++++++------- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index eb86a344c6e..aa2219031d2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,7 +2,9 @@ from __future__ import annotations +import asyncio from collections.abc import Callable, Coroutine +from contextlib import suppress import dataclasses from functools import partial, wraps from typing import Any, Concatenate, Literal, cast @@ -182,6 +184,8 @@ STRATEGY = "strategy" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 MINIMUM_QR_STRING_LENGTH = 52 +HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60 + # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( @@ -2816,6 +2820,7 @@ async def websocket_hard_reset_controller( driver: Driver, ) -> None: """Hard reset controller.""" + unsubs: list[Callable[[], None]] @callback def async_cleanup() -> None: @@ -2831,13 +2836,28 @@ async def websocket_hard_reset_controller( connection.send_result(msg[ID], device.id) async_cleanup() + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + msg[DATA_UNSUBSCRIBE] = unsubs = [ async_dispatcher_connect( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, _handle_device_added - ) + ), + driver.once("driver ready", set_driver_ready), ] + await driver.async_hard_reset() + with suppress(TimeoutError): + async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + + await hass.config_entries.async_reload(entry.entry_id) + @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c63283fd220..2e3d8fd290a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5,7 +5,7 @@ from http import HTTPStatus from io import BytesIO import json from typing import Any -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch import pytest from zwave_js_server.const import ( @@ -5078,53 +5078,97 @@ async def test_subscribe_node_statistics( assert msg["error"]["code"] == ERR_NOT_LOADED -@pytest.mark.skip( - reason="The test needs to be updated to reflect what happens when resetting the controller" -) async def test_hard_reset_controller( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - client, - integration, - listen_block, + client: MagicMock, + integration: MockConfigEntry, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the hard_reset_controller WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) - device = device_registry.async_get_device( - identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} - ) + async def async_send_command_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + return {} - client.async_send_command.return_value = {} - await ws_client.send_json( + client.async_send_command.side_effect = async_send_command_driver_ready + + await ws_client.send_json_auto_id( { - ID: 1, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } ) - - listen_block.set() - listen_block.clear() - await hass.async_block_till_done() - msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None assert msg["result"] == device.id assert msg["success"] - assert len(client.async_send_command.call_args_list) == 1 - assert client.async_send_command.call_args[0][0] == {"command": "driver.hard_reset"} + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) + + client.async_send_command.reset_mock() + + # Test sending command with driver not ready and timeout. + + async def async_send_command_no_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + return {} + + client.async_send_command.side_effect = async_send_command_no_driver_ready + + with patch( + "homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT", + new=0, + ): + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None + assert msg["result"] == device.id + assert msg["success"] + + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.driver.Driver.async_hard_reset", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 2, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } @@ -5139,9 +5183,8 @@ async def test_hard_reset_controller( await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 3, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: entry.entry_id, } @@ -5151,9 +5194,8 @@ async def test_hard_reset_controller( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED - await ws_client.send_json( + await ws_client.send_json_auto_id( { - ID: 4, TYPE: "zwave_js/hard_reset_controller", ENTRY_ID: "INVALID", } From d6e85eef485b589d7cc82ee7c7bbf901f00a96b1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 12:37:53 +0200 Subject: [PATCH 0200/1175] Fix SmartThings machine operating state with no options (#144390) --- .../components/smartthings/select.py | 11 +- tests/components/smartthings/conftest.py | 1 + .../device_status/da_wm_wm_100001.json | 154 ++++++++++++++ .../fixtures/devices/da_wm_wm_100001.json | 84 ++++++++ .../snapshots/test_binary_sensor.ambr | 95 +++++++++ .../smartthings/snapshots/test_init.ambr | 33 +++ .../smartthings/snapshots/test_select.ambr | 58 ++++++ .../smartthings/snapshots/test_sensor.ambr | 192 ++++++++++++++++++ 8 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json create mode 100644 tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 63dcb90b019..16051cb08f1 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -26,6 +26,7 @@ class SmartThingsSelectDescription(SelectEntityDescription): options_attribute: Attribute status_attribute: Attribute command: Command + default_options: list[str] | None = None CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { @@ -46,6 +47,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { options_attribute=Attribute.SUPPORTED_MACHINE_STATES, status_attribute=Attribute.MACHINE_STATE, command=Command.SET_MACHINE_STATE, + default_options=["run", "pause", "stop"], ), Capability.WASHER_OPERATING_STATE: SmartThingsSelectDescription( key=Capability.WASHER_OPERATING_STATE, @@ -55,6 +57,7 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { options_attribute=Attribute.SUPPORTED_MACHINE_STATES, status_attribute=Attribute.MACHINE_STATE, command=Command.SET_MACHINE_STATE, + default_options=["run", "pause", "stop"], ), Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT: SmartThingsSelectDescription( key=Capability.SAMSUNG_CE_AUTO_DISPENSE_DETERGENT, @@ -114,8 +117,12 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list of options.""" - return self.get_attribute_value( - self.entity_description.key, self.entity_description.options_attribute + return ( + self.get_attribute_value( + self.entity_description.key, self.entity_description.options_attribute + ) + or self.entity_description.default_options + or [] ) @property diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 244b89ca06a..b3a58b17637 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -122,6 +122,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "da_wm_wd_000001", "da_wm_wd_000001_1", "da_wm_wm_01011", + "da_wm_wm_100001", "da_wm_wm_000001", "da_wm_wm_000001_1", "da_wm_sc_000001", diff --git a/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json b/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json new file mode 100644 index 00000000000..b3b01762099 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_wm_wm_100001.json @@ -0,0 +1,154 @@ +{ + "components": { + "main": { + "ocf": { + "st": { + "value": null, + "timestamp": "2020-10-06T23:01:03.011Z" + }, + "mndt": { + "value": null, + "timestamp": "2021-01-28T11:54:37.203Z" + }, + "mnfv": { + "value": null, + "timestamp": "2020-12-20T14:21:43.766Z" + }, + "mnhw": { + "value": null, + "timestamp": "2021-01-25T22:57:01.985Z" + }, + "di": { + "value": "C0972771-01D0-0000-0000-000000000000", + "timestamp": "2019-08-10T18:37:20.487Z" + }, + "mnsl": { + "value": null, + "timestamp": "2020-12-20T14:21:31.219Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2019-08-10T18:37:20.514Z" + }, + "n": { + "value": "Washer", + "timestamp": "2019-08-10T18:37:20.555Z" + }, + "mnmo": { + "value": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000", + "timestamp": "2019-08-10T18:37:20.409Z" + }, + "vid": { + "value": "DA-WM-WM-100001", + "timestamp": "2019-08-10T18:37:20.381Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2019-08-10T18:37:20.436Z" + }, + "mnml": { + "value": null, + "timestamp": "2021-01-28T11:54:37.092Z" + }, + "mnpv": { + "value": null, + "timestamp": "2021-01-26T20:55:28.663Z" + }, + "mnos": { + "value": null, + "timestamp": "2021-01-26T20:55:28.411Z" + }, + "pi": { + "value": "shp", + "timestamp": "2019-08-10T18:37:20.457Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2019-08-10T18:37:20.534Z" + } + }, + "remoteControlStatus": { + "remoteControlEnabled": { + "value": "false", + "timestamp": "2025-04-06T17:30:05.372Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 22100103, + "timestamp": "2022-11-01T11:53:01.255Z" + } + }, + "refresh": {}, + "samsungce.washerOperatingState": { + "washerJobState": { + "value": "none", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "operatingState": { + "value": "ready", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "supportedOperatingStates": { + "value": ["ready", "running", "paused"], + "timestamp": "2022-11-01T11:53:01.255Z" + }, + "scheduledJobs": { + "value": null + }, + "scheduledPhases": { + "value": null + }, + "progress": { + "value": null + }, + "remainingTimeStr": { + "value": "00:57", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "washerJobPhase": { + "value": null + }, + "operationTime": { + "value": null + }, + "remainingTime": { + "value": 57, + "unit": "min", + "timestamp": "2025-04-18T13:17:00.432Z" + } + }, + "execute": { + "data": { + "value": null, + "data": {}, + "timestamp": "2020-10-05T02:10:50.602Z" + } + }, + "washerOperatingState": { + "completionTime": { + "value": "2025-04-18T14:14:00Z", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "machineState": { + "value": "stop", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "washerJobState": { + "value": "none", + "timestamp": "2025-04-18T13:17:00.432Z" + }, + "supportedMachineStates": { + "value": null, + "timestamp": "2020-08-14T14:25:00.803Z" + } + }, + "switch": { + "switch": { + "value": null, + "timestamp": "2020-09-13T18:32:28.637Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json b/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json new file mode 100644 index 00000000000..c1a4cd12578 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_wm_wm_100001.json @@ -0,0 +1,84 @@ +{ + "items": [ + { + "deviceId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "name": "Washer", + "label": "Washer", + "manufacturerName": "Samsung Electronics", + "presentationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "ownerId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "deviceTypeName": "Samsung OCF Washer", + "components": [ + { + "id": "main", + "label": "Washer", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "remoteControlStatus", + "version": 1 + }, + { + "id": "washerOperatingState", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.washerOperatingState", + "version": 1 + } + ], + "categories": [ + { + "name": "Washer", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2019-08-10T18:37:20Z", + "profile": { + "id": "REDACTED" + }, + "ocf": { + "ocfDeviceType": "oic.d.washer", + "name": "Washer", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0", + "manufacturerName": "Samsung Electronics", + "modelNumber": "TP6X_WA54M8750AV|20183944|20000101001111000100000000000000", + "vendorId": "DA-WM-WM-100001", + "lastSignupTime": "2021-01-16T06:29:39.379382Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 14cdd1548fc..61cecdbd364 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -2089,6 +2089,101 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_switch_switch_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Washer Power', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washer_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][binary_sensor.washer_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.washer_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[ecobee_sensor][binary_sensor.child_bedroom_motion-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index c10f47289a9..d70d9a1dcfc 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -992,6 +992,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_wm_wm_100001] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'TP6X_WA54M8750AV', + 'model_id': None, + 'name': 'Washer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[ecobee_sensor] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index b6528edfebe..17d8e10d230 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -525,3 +525,61 @@ 'state': 'standard', }) # --- +# name: test_all_entities[da_wm_wm_100001][select.washer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'run', + 'pause', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.washer', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operating_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][select.washer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer', + 'options': list([ + 'run', + 'pause', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'select.washer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 0e9ddf2ea09..a8d4da9123c 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -8546,6 +8546,198 @@ 'state': '1642.2', }) # --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_completion_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Completion time', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'completion_time', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_completionTime_completionTime', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_completion_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Washer Completion time', + }), + 'context': , + 'entity_id': 'sensor.washer_completion_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-18T14:14:00+00:00', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_job_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Job state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'washer_job_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_washerJobState_washerJobState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_job_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Job state', + 'options': list([ + 'air_wash', + 'ai_rinse', + 'ai_spin', + 'ai_wash', + 'cooling', + 'delay_wash', + 'drying', + 'finish', + 'none', + 'pre_wash', + 'rinse', + 'spin', + 'wash', + 'weight_sensing', + 'wrinkle_prevent', + 'freeze_protection', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_job_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'none', + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washer_machine_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Machine state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'washer_machine_state', + 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_100001][sensor.washer_machine_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washer Machine state', + 'options': list([ + 'pause', + 'run', + 'stop', + ]), + }), + 'context': , + 'entity_id': 'sensor.washer_machine_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'stop', + }) +# --- # name: test_all_entities[ecobee_sensor][sensor.child_bedroom_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From dded1305ec1c5ac52b150eb4042fa0a4ce210c5e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 12:40:01 +0200 Subject: [PATCH 0201/1175] Cleanup old config flow IMPORT constants in samsungtv tests (#144394) --- tests/components/samsungtv/test_config_flow.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 57c1a3b0bf4..0acbfd319a2 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -39,7 +39,6 @@ from homeassistant.const import ( CONF_MAC, CONF_METHOD, CONF_MODEL, - CONF_NAME, CONF_PIN, CONF_PORT, CONF_TOKEN, @@ -68,19 +67,6 @@ from tests.common import MockConfigEntry, load_json_object_fixture RESULT_ALREADY_CONFIGURED = "already_configured" RESULT_ALREADY_IN_PROGRESS = "already_in_progress" -MOCK_IMPORT_DATA = { - CONF_HOST: "fake_host", - CONF_NAME: "fake", - CONF_PORT: 55000, -} -MOCK_IMPORT_DATA_WITHOUT_NAME = { - CONF_HOST: "fake_host", -} -MOCK_IMPORT_WSDATA = { - CONF_HOST: "fake_host", - CONF_NAME: "fake", - CONF_PORT: 8002, -} MOCK_USER_DATA = {CONF_HOST: "fake_host"} MOCK_DHCP_DATA = DhcpServiceInfo( From 92a19357d34ac7b5871b9c6dc03a88fcd353c10b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 12:55:28 +0200 Subject: [PATCH 0202/1175] Fix variables in MELCloud (#144396) --- homeassistant/components/melcloud/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index 682a28ea080..19c333e5825 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -57,8 +57,8 @@ ATA_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATA_HVAC_MODE_LOOKUP.items()} ATW_ZONE_HVAC_MODE_LOOKUP = { - atw.ZONE_OPERATION_MODE_HEAT: HVACMode.HEAT, - atw.ZONE_OPERATION_MODE_COOL: HVACMode.COOL, + atw.ZONE_STATUS_HEAT: HVACMode.HEAT, + atw.ZONE_STATUS_COOL: HVACMode.COOL, } ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP.items()} From 4cdb7a98877f954af9d32fdfe38683ab9d5fdc96 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 7 May 2025 13:55:43 +0300 Subject: [PATCH 0203/1175] Add missing device_class translations for template helper (#144392) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 66864a027ba..c27acc37ed9 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -292,6 +292,7 @@ "aqi": "[%key:component::sensor::entity_component::aqi::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", + "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", "carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]", "carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]", "conductivity": "[%key:component::sensor::entity_component::conductivity::name%]", @@ -302,6 +303,7 @@ "distance": "[%key:component::sensor::entity_component::distance::name%]", "duration": "[%key:component::sensor::entity_component::duration::name%]", "energy": "[%key:component::sensor::entity_component::energy::name%]", + "energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]", "energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]", "frequency": "[%key:component::sensor::entity_component::frequency::name%]", "gas": "[%key:component::sensor::entity_component::gas::name%]", From 0aa817e3005adb00c50fbdf4e762babfbe772c6e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 13:00:19 +0200 Subject: [PATCH 0204/1175] Bump pySmartThings to 3.2.1 (#144393) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 0f43c2f9790..043bdea71e2 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.0"] + "requirements": ["pysmartthings==3.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bcf228e5754..27578b5e555 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2326,7 +2326,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.0 +pysmartthings==3.2.1 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6204329d43..a555fdcef4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1899,7 +1899,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.0 +pysmartthings==3.2.1 # homeassistant.components.smarty pysmarty2==0.10.2 From 99f55665a59d9d13cc55551d1c01f09c56727668 Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Wed, 7 May 2025 13:04:46 +0200 Subject: [PATCH 0205/1175] Bump wh-python to 2025.4.29 for Weheat integration (#144384) --- homeassistant/components/weheat/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weheat/manifest.json b/homeassistant/components/weheat/manifest.json index 3a4cff6f295..cd631866fdb 100644 --- a/homeassistant/components/weheat/manifest.json +++ b/homeassistant/components/weheat/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/weheat", "iot_class": "cloud_polling", - "requirements": ["weheat==2025.3.7"] + "requirements": ["weheat==2025.4.29"] } diff --git a/requirements_all.txt b/requirements_all.txt index 27578b5e555..478ff874224 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3074,7 +3074,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.3.7 +weheat==2025.4.29 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a555fdcef4b..47ae1bd9077 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2485,7 +2485,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.3.7 +weheat==2025.4.29 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.20.0 From a2ab28286f4a3c62fde64424970da4d271289dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 7 May 2025 13:05:56 +0200 Subject: [PATCH 0206/1175] Bump hass-nabucasa from 0.96.0 to 0.100.0 (#144341) * Bump hass-nabucasa from 0.96.0 to 0.98.0 * Bump hass-nabucasa from 0.98.0 to 0.99.0 * Bump hass-nabucasa from 0.99.0 to 0.100.0 --- 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 30e3925a591..91423007b74 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.96.0"], + "requirements": ["hass-nabucasa==0.100.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1838e552800..9a0e366dab8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 habluetooth==3.48.2 -hass-nabucasa==0.96.0 +hass-nabucasa==0.100.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250506.0 diff --git a/pyproject.toml b/pyproject.toml index 8623d54b963..b51bb69e490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "ha-ffmpeg==3.2.2", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.96.0", + "hass-nabucasa==0.100.0", # hassil is indirectly imported from onboarding via the import chain # onboarding->cloud->assist_pipeline->conversation->hassil. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its diff --git a/requirements.txt b/requirements.txt index e8b9e12bfe0..a8c30dfacd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 ha-ffmpeg==3.2.2 -hass-nabucasa==0.96.0 +hass-nabucasa==0.100.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 478ff874224..91b8bb95aa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ habiticalib==0.3.7 habluetooth==3.48.2 # homeassistant.components.cloud -hass-nabucasa==0.96.0 +hass-nabucasa==0.100.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47ae1bd9077..cd8a9800472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -963,7 +963,7 @@ habiticalib==0.3.7 habluetooth==3.48.2 # homeassistant.components.cloud -hass-nabucasa==0.96.0 +hass-nabucasa==0.100.0 # homeassistant.components.conversation hassil==2.2.3 From f7d8e4e7b901a2b2d7b956b97ef883219ba6bc60 Mon Sep 17 00:00:00 2001 From: Wilbert Date: Wed, 7 May 2025 13:06:07 +0200 Subject: [PATCH 0207/1175] Add typing to smartthings climate target_temperature_low (#143713) Fix climate target_temperature_low --- homeassistant/components/smartthings/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index f2f9479584c..c594ca237a4 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -307,7 +307,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): return None @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" if self.hvac_mode == HVACMode.HEAT_COOL: return self.get_attribute_value( From ed1eea9b504a77b3a8c6e84901bf7e6e634c7dca Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 7 May 2025 13:06:53 +0200 Subject: [PATCH 0208/1175] Set SmartThings power energy state class to Total (#144395) --- .../components/smartthings/sensor.py | 2 +- .../smartthings/snapshots/test_sensor.ambr | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 09287448fe5..2d6451fa279 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -631,7 +631,7 @@ CAPABILITY_TO_SENSORS: dict[ SmartThingsSensorEntityDescription( key="powerEnergy_meter", translation_key="power_energy", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value_fn=lambda value: value["powerEnergy"] / 1000, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index a8d4da9123c..ad073a1d670 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -1364,7 +1364,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1402,7 +1402,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'AC Office Granit Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -1793,7 +1793,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1831,7 +1831,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Office AirFree Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -2222,7 +2222,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -2260,7 +2260,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Aire Dormitorio Principal Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4000,7 +4000,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4038,7 +4038,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Refrigerator Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4277,7 +4277,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4315,7 +4315,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Refrigerator Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -4554,7 +4554,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -4592,7 +4592,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Frigo Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5128,7 +5128,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -5166,7 +5166,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Eco Heating System Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -5637,7 +5637,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -5675,7 +5675,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Dishwasher Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6104,7 +6104,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -6142,7 +6142,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'AirDresser Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -6571,7 +6571,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -6609,7 +6609,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Dryer Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7038,7 +7038,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -7076,7 +7076,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Seca-Roupa Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7507,7 +7507,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -7545,7 +7545,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Washer Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -7976,7 +7976,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -8014,7 +8014,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Washing Machine Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , @@ -8445,7 +8445,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -8483,7 +8483,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy', 'friendly_name': 'Machine à Laver Power energy', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , From e2820787bf0c367afa91aea0da75fcf6e6302450 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 7 May 2025 13:09:43 +0200 Subject: [PATCH 0209/1175] Bump devolo_home_control_api to 0.19.0 (#144374) --- .../components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/devolo_home_control/mocks.py | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index a9715fffa84..983b2a33452 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -8,6 +8,6 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["devolo_home_control_api"], - "requirements": ["devolo-home-control-api==0.18.3"], + "requirements": ["devolo-home-control-api==0.19.0"], "zeroconf": ["_dvl-deviceapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 91b8bb95aa3..d4cd74e01de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -782,7 +782,7 @@ denonavr==1.1.0 devialet==1.5.7 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.3 +devolo-home-control-api==0.19.0 # homeassistant.components.devolo_home_network devolo-plc-api==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd8a9800472..16c982f4478 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ denonavr==1.1.0 devialet==1.5.7 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.3 +devolo-home-control-api==0.19.0 # homeassistant.components.devolo_home_network devolo-plc-api==1.5.1 diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index d611c73cf2c..24f4e64ffe6 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -1,5 +1,6 @@ """Mocks for tests.""" +from datetime import UTC from typing import Any from unittest.mock import MagicMock @@ -28,6 +29,7 @@ class BinarySensorPropertyMock(BinarySensorProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "Test" self.key_count = 1 self.sensor_type = "door" @@ -41,6 +43,7 @@ class BinarySwitchPropertyMock(BinarySwitchProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "Test" self.state = False @@ -51,6 +54,7 @@ class ConsumptionPropertyMock(ConsumptionProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.element_uid = "devolo.Meter:Test" self.current_unit = "W" self.total_unit = "kWh" @@ -68,6 +72,7 @@ class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): self._unit = "°C" self._value = 20 self._logger = MagicMock() + self._timezone = UTC class BrightnessSensorPropertyMock(MultiLevelSensorProperty): @@ -80,6 +85,7 @@ class BrightnessSensorPropertyMock(MultiLevelSensorProperty): self._unit = "%" self._value = 20 self._logger = MagicMock() + self._timezone = UTC class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): @@ -92,6 +98,7 @@ class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): self.max = 24 self._value = 20 self._logger = MagicMock() + self._timezone = UTC class SirenPropertyMock(MultiLevelSwitchProperty): @@ -105,6 +112,7 @@ class SirenPropertyMock(MultiLevelSwitchProperty): self.switch_type = "tone" self._value = 0 self._logger = MagicMock() + self._timezone = UTC class SettingsMock(SettingsProperty): @@ -113,6 +121,7 @@ class SettingsMock(SettingsProperty): def __init__(self, **kwargs: Any) -> None: # pylint: disable=super-init-not-called """Initialize the mock.""" self._logger = MagicMock() + self._timezone = UTC self.name = "Test" self.zone = "Test" self.tone = 1 From 293e01f2e920af208613cd5a7fbe28cf04bdec5d Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Wed, 7 May 2025 13:12:10 +0200 Subject: [PATCH 0210/1175] Improve activity logic in Husqvarna Automower (#144057) * Improve activity logic in Husqvarna Automower * add test --- homeassistant/components/husqvarna_automower/lawn_mower.py | 6 +++--- tests/components/husqvarna_automower/test_lawn_mower.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index ee6007f089b..9ae214524a7 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -110,10 +110,10 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): mower_attributes = self.mower_attributes if mower_attributes.mower.state in PAUSED_STATES: return LawnMowerActivity.PAUSED - if mower_attributes.mower.activity in MOWING_ACTIVITIES: + if mower_attributes.mower.state in MowerStates.IN_OPERATION: + if mower_attributes.mower.activity == MowerActivities.GOING_HOME: + return LawnMowerActivity.RETURNING return LawnMowerActivity.MOWING - if mower_attributes.mower.activity == MowerActivities.GOING_HOME: - return LawnMowerActivity.RETURNING if (mower_attributes.mower.state == "RESTRICTED") or ( mower_attributes.mower.activity in DOCKED_ACTIVITIES ): diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index a8c34a3fc79..12c53d709ca 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -32,6 +32,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed MowerStates.IN_OPERATION, LawnMowerActivity.RETURNING, ), + ( + MowerActivities.NOT_APPLICABLE, + MowerStates.IN_OPERATION, + LawnMowerActivity.MOWING, + ), ], ) async def test_lawn_mower_states( From 48a2dde16bfdbcb0a7abed9a5ab270c54364c3b2 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 7 May 2025 15:08:17 +0300 Subject: [PATCH 0211/1175] Add more missing device_class translations for template helper (#144399) --- homeassistant/components/template/strings.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index c27acc37ed9..0b431d661cd 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -290,6 +290,7 @@ "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", "aqi": "[%key:component::sensor::entity_component::aqi::name%]", + "area": "[%key:component::sensor::entity_component::area::name%]", "atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]", "battery": "[%key:component::sensor::entity_component::battery::name%]", "blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]", @@ -340,6 +341,7 @@ "volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]", "water": "[%key:component::sensor::entity_component::water::name%]", "weight": "[%key:component::sensor::entity_component::weight::name%]", + "wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]", "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, From 704e4221f72af9d67aeb20d3d1196825467e6819 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 14:13:38 +0200 Subject: [PATCH 0212/1175] Improve SamsungTV ssdp test fixtures (#144376) * Improve SamsungTV ssdp fixtures * More * More * More * More * Improve --- tests/components/samsungtv/const.py | 2 +- .../fixtures/ssdp_device_main_tv_agent.json | 55 ++++++++- .../ssdp_service_remote_control_receiver.json | 63 ++++++++++- .../ssdp_service_rendering_control.json | 106 +++++++++++++++++- .../components/samsungtv/test_config_flow.py | 72 ++++++------ tests/components/samsungtv/test_init.py | 5 +- 6 files changed, 244 insertions(+), 59 deletions(-) diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index e4977a536b0..7a367294581 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -40,7 +40,7 @@ MOCK_ENTRYDATA_ENCRYPTED_WS = { CONF_SESSION_ID: "2", } MOCK_ENTRYDATA_WS = { - CONF_HOST: "fake_host", + CONF_HOST: "10.10.12.34", CONF_METHOD: METHOD_WEBSOCKET, CONF_PORT: 8002, CONF_MODEL: "any", diff --git a/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json b/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json index 2970f14bf5f..252d352f514 100644 --- a/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json +++ b/tests/components/samsungtv/fixtures/ssdp_device_main_tv_agent.json @@ -1,11 +1,54 @@ { - "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:samsung.com:service:MainTVAgent2:1", + "ssdp_usn": "uuid:055d4a80-005a-1000-b872-84a4668d8423::urn:samsung.com:service:MainTVAgent2:1", "ssdp_st": "urn:samsung.com:service:MainTVAgent2:1", "upnp": { - "friendlyName": "[TV] fake_name", - "manufacturer": "Samsung fake_manufacturer", - "modelName": "fake_model", - "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + "deviceType": "urn:samsung.com:device:MainTVServer2:1", + "friendlyName": "[TV]Samsung LED55", + "manufacturer": "Samsung Electronics", + "manufacturerURL": "http://www.samsung.com", + "modelDescription": "Samsung DTV MainTVServer2", + "modelName": "UE55H6400", + "modelNumber": "1.0", + "modelURL": "http://www.samsung.com", + "serialNumber": "20100621", + "UDN": "uuid:055d4a80-005a-1000-b872-84a4668d8423", + "UPC": "123456789012", + "deviceID": "ZPCNHA5IWYRV6", + "ProductCap": "Y2013", + "serviceList": { + "service": { + "serviceType": "urn:samsung.com:service:MainTVAgent2:1", + "serviceId": "urn:samsung.com:serviceId:MainTVAgent2", + "controlURL": "/smp_4_", + "eventSubURL": "/smp_5_", + "SCPDURL": "/smp_3_" + } + } }, - "ssdp_location": "https://fake_host:12345/tv_agent" + "ssdp_location": "http://10.10.12.34:7676/smp_2_", + "ssdp_nt": null, + "ssdp_udn": "uuid:055d4a80-005a-1000-b872-84a4668d8423", + "ssdp_ext": "", + "ssdp_server": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ssdp_headers": { + "CACHE-CONTROL": "max-age:1800", + "Date": "Thu, 01 Jan 1970 00:06:48 GMT", + "EXT": "", + "LOCATION": "http://10.10.12.34:7676/smp_2_", + "SERVER": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ST": "urn:samsung.com:service:MainTVAgent2:1", + "USN": "uuid:055d4a80-005a-1000-b872-84a4668d8423::urn:samsung.com:service:MainTVAgent2:1", + "Content-Length": "0", + "_host": "10.10.12.34", + "_udn": "uuid:055d4a80-005a-1000-b872-84a4668d8423", + "_location_original": "http://10.10.12.34:7676/smp_2_", + "location": "http://10.10.12.34:7676/smp_2_", + "_timestamp": "2025-04-30T07:30:24.160549", + "_remote_addr": ["10.10.12.34", 58482], + "_port": 58482, + "_local_addr": ["0.0.0.0", 0], + "_source": "search" + }, + "ssdp_all_locations": ["http://10.10.12.34:7676/smp_2_"], + "x_homeassistant_matching_domains": ["samsungtv"] } diff --git a/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json b/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json index b2f5f27a8b4..21cd39a65a9 100644 --- a/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json +++ b/tests/components/samsungtv/fixtures/ssdp_service_remote_control_receiver.json @@ -1,11 +1,62 @@ { - "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:samsung.com:device:RemoteControlReceiver:1", + "ssdp_usn": "uuid:068e7781-006e-1000-bbbf-84a4668d8423::urn:samsung.com:device:RemoteControlReceiver:1", "ssdp_st": "urn:samsung.com:device:RemoteControlReceiver:1", "upnp": { - "friendlyName": "[TV] fake_name", - "manufacturer": "Samsung fake_manufacturer", - "modelName": "fake_model", - "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1", + "friendlyName": "[TV]Samsung LED55", + "manufacturer": "Samsung Electronics", + "manufacturerURL": "http://www.samsung.com/sec", + "modelDescription": "Samsung TV RCR", + "modelName": "UE55H6400", + "modelNumber": "1.0", + "modelURL": "http://www.samsung.com/sec", + "serialNumber": "20090804RCR", + "UDN": "uuid:068e7781-006e-1000-bbbf-84a4668d8423", + "deviceID": "ZPCNHA5IWYRV6", + "ProductCap": "Resolution:1920X1080,ImageZoom,ImageRotate,Y2014,ENC", + "serviceList": { + "service": { + "serviceType": "urn:samsung.com:service:MultiScreenService:1", + "serviceId": "urn:samsung.com:serviceId:MultiScreenService", + "controlURL": "/smp_9_", + "eventSubURL": "/smp_10_", + "SCPDURL": "/smp_8_" + } + }, + "Capabilities": { + "Capability": { + "@name": "samsung:multiscreen:1", + "@port": "8001", + "@location": "/ms/1.0/" + } + } }, - "ssdp_location": "http://fake_host:7676/smp_7_" + "ssdp_location": "http://10.10.12.34:7676/smp_7_", + "ssdp_nt": "urn:samsung.com:device:RemoteControlReceiver:1", + "ssdp_udn": "uuid:068e7781-006e-1000-bbbf-84a4668d8423", + "ssdp_ext": "", + "ssdp_server": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ssdp_headers": { + "CACHE-CONTROL": "max-age:1800", + "Date": "Thu, 01 Jan 1970 00:06:48 GMT", + "EXT": "", + "LOCATION": "http://10.10.12.34:7676/smp_7_", + "SERVER": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ST": "urn:samsung.com:device:RemoteControlReceiver:1", + "USN": "uuid:068e7781-006e-1000-bbbf-84a4668d8423::urn:samsung.com:device:RemoteControlReceiver:1", + "Content-Length": "0", + "_host": "10.10.12.34", + "_udn": "uuid:068e7781-006e-1000-bbbf-84a4668d8423", + "_location_original": "http://10.10.12.34:7676/smp_7_", + "location": "http://10.10.12.34:7676/smp_7_", + "_timestamp": "2025-04-30T07:30:24.384758", + "_remote_addr": ["10.10.12.34", 24234], + "_port": 24234, + "_local_addr": ["0.0.0.0", 1900], + "HOST": "239.255.255.250:1900", + "NT": "urn:samsung.com:device:RemoteControlReceiver:1", + "NTS": "ssdp:alive" + }, + "ssdp_all_locations": ["http://10.10.12.34:7676/smp_7_"], + "x_homeassistant_matching_domains": ["samsungtv"] } diff --git a/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json b/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json index 4074a39703e..31c0944e0ac 100644 --- a/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json +++ b/tests/components/samsungtv/fixtures/ssdp_service_rendering_control.json @@ -1,11 +1,105 @@ { - "ssdp_usn": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de::urn:schemas-upnp-org:service:RenderingControl:1", + "ssdp_usn": "uuid:09896802-00a0-1000-adfd-84a4668d8423::urn:schemas-upnp-org:service:RenderingControl:1", "ssdp_st": "urn:schemas-upnp-org:service:RenderingControl:1", "upnp": { - "friendlyName": "[TV] fake_name", - "manufacturer": "Samsung fake_manufacturer", - "modelName": "fake_model", - "UDN": "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de" + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "X_compatibleId": "MS_DigitalMediaDeviceClass_DMR_V001", + "X_deviceCategory": "Display.TV.LCD Multimedia.DMR", + "X_DLNADOC": "DMR-1.50", + "friendlyName": "[TV]Samsung LED55", + "manufacturer": "Samsung Electronics", + "manufacturerURL": "http://www.samsung.com/sec", + "modelDescription": "Samsung TV DMR", + "modelName": "UE55H6400", + "modelNumber": "AllShare1.0", + "modelURL": "http://www.samsung.com/sec", + "serialNumber": "20110517DMR", + "UDN": "uuid:09896802-00a0-1000-adfd-84a4668d8423", + "deviceID": "ZPCNHA5IWYRV6", + "iconList": { + "icon": [ + { + "mimetype": "image/jpeg", + "width": "48", + "height": "48", + "depth": "24", + "url": "/dmr/icon_SML.jpg" + }, + { + "mimetype": "image/jpeg", + "width": "120", + "height": "120", + "depth": "24", + "url": "/dmr/icon_LRG.jpg" + }, + { + "mimetype": "image/png", + "width": "48", + "height": "48", + "depth": "24", + "url": "/dmr/icon_SML.png" + }, + { + "mimetype": "image/png", + "width": "120", + "height": "120", + "depth": "24", + "url": "/dmr/icon_LRG.png" + } + ] + }, + "serviceList": { + "service": [ + { + "serviceType": "urn:schemas-upnp-org:service:RenderingControl:1", + "serviceId": "urn:upnp-org:serviceId:RenderingControl", + "controlURL": "/smp_17_", + "eventSubURL": "/smp_18_", + "SCPDURL": "/smp_16_" + }, + { + "serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1", + "serviceId": "urn:upnp-org:serviceId:ConnectionManager", + "controlURL": "/smp_20_", + "eventSubURL": "/smp_21_", + "SCPDURL": "/smp_19_" + }, + { + "serviceType": "urn:schemas-upnp-org:service:AVTransport:1", + "serviceId": "urn:upnp-org:serviceId:AVTransport", + "controlURL": "/smp_23_", + "eventSubURL": "/smp_24_", + "SCPDURL": "/smp_22_" + } + ] + }, + "ProductCap": "Y2014,WebURIPlayable,SeekTRACK_NR,NavigateInPause", + "X_hardwareId": "VEN_0105&DEV_VD0001" }, - "ssdp_location": "https://fake_host:12345/test" + "ssdp_location": "http://10.10.12.34:7676/smp_15_", + "ssdp_nt": null, + "ssdp_udn": "uuid:09896802-00a0-1000-adfd-84a4668d8423", + "ssdp_ext": "", + "ssdp_server": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ssdp_headers": { + "CACHE-CONTROL": "max-age:1800", + "Date": "Thu, 01 Jan 1970 00:06:48 GMT", + "EXT": "", + "LOCATION": "http://10.10.12.34:7676/smp_15_", + "SERVER": "SHP, UPnP/1.0, Samsung UPnP SDK/1.0", + "ST": "urn:schemas-upnp-org:service:RenderingControl:1", + "USN": "uuid:09896802-00a0-1000-adfd-84a4668d8423::urn:schemas-upnp-org:service:RenderingControl:1", + "Content-Length": "0", + "_host": "10.10.12.34", + "_udn": "uuid:09896802-00a0-1000-adfd-84a4668d8423", + "_location_original": "http://10.10.12.34:7676/smp_15_", + "location": "http://10.10.12.34:7676/smp_15_", + "_timestamp": "2025-04-30T07:30:24.146243", + "_remote_addr": ["10.10.12.34", 52226], + "_port": 52226, + "_local_addr": ["0.0.0.0", 0], + "_source": "search" + }, + "ssdp_all_locations": ["http://10.10.12.34:7676/smp_15_"], + "x_homeassistant_matching_domains": ["samsungtv"] } diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 0acbfd319a2..6eef80026f0 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -70,7 +70,7 @@ RESULT_ALREADY_IN_PROGRESS = "already_in_progress" MOCK_USER_DATA = {CONF_HOST: "fake_host"} MOCK_DHCP_DATA = DhcpServiceInfo( - ip="fake_host", macaddress="aabbccddeeff", hostname="fake_hostname" + ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname" ) EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( @@ -88,7 +88,7 @@ MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( type="mock_type", ) MOCK_OLD_ENTRY = { - CONF_HOST: "fake_host", + CONF_HOST: "10.10.12.34", CONF_IP_ADDRESS: EXISTING_IP, CONF_METHOD: "legacy", CONF_PORT: None, @@ -464,11 +464,11 @@ async def test_ssdp(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "fake_model" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert result["title"] == "UE55H6400" + assert result["data"][CONF_HOST] == "10.10.12.34" + assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" + assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @pytest.mark.usefixtures("remote", "rest_api_failing") @@ -522,11 +522,11 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "fake_model" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert result["title"] == "UE55H6400" + assert result["data"][CONF_HOST] == "10.10.12.34" + assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" + assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @pytest.mark.usefixtures("remotews", "rest_api_failing") @@ -557,11 +557,11 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "fake_model" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert result["title"] == "UE55H6400" + assert result["data"][CONF_HOST] == "10.10.12.34" + assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" + assert result["data"][CONF_MODEL] == "UE55H6400" + assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" @pytest.mark.usefixtures("remotews", "rest_api_failing") @@ -597,13 +597,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" assert ( result["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -626,13 +626,13 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result["data"][CONF_MODEL] == "82GXARRS" assert ( result["data"][CONF_SSDP_MAIN_TV_AGENT_LOCATION] - == "https://fake_host:12345/tv_agent" + == "http://10.10.12.34:7676/smp_2_" ) assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -677,13 +677,13 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" - assert result4["data"][CONF_HOST] == "fake_host" + assert result4["data"][CONF_HOST] == "10.10.12.34" assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" - assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result4["data"][CONF_MANUFACTURER] == "Samsung Electronics" assert result4["data"][CONF_MODEL] == "UE48JU6400" assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" @@ -881,7 +881,7 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == "10.10.12.34" assert result["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE48JU6400" @@ -911,7 +911,7 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: ) 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_HOST] == "10.10.12.34" assert result["data"][CONF_MAC] == "aa:ee:tt:hh:ee:rr" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "UE43LS003" @@ -1378,7 +1378,7 @@ async def test_update_missing_model_added_from_ssdp( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.data[CONF_MODEL] == "fake_model" + assert entry.data[CONF_MODEL] == "UE55H6400" @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") @@ -1492,7 +1492,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd # Correct ST, ssdp location should change assert ( entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1526,8 +1526,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Main TV Agent ST, ssdp location should change assert ( - entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] - == "https://fake_host:12345/tv_agent" + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" ) # Rendering control should not be affected assert ( @@ -1562,7 +1561,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( # Correct ST, ssdp location should be added assert ( entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1592,8 +1591,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" # Correct ST for MainTV, ssdp location should be added assert ( - entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] - == "https://fake_host:12345/tv_agent" + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" ) assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1743,7 +1741,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con # Correct st assert ( entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1918,7 +1916,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( entry = MockConfigEntry( domain=DOMAIN, data=MOCK_OLD_ENTRY, - unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + unique_id="068e7781-006e-1000-bbbf-84a4668d8423", ) entry.add_to_hass(hass) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index a8b8debd4c0..7e0d1c87fb1 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -154,12 +154,11 @@ async def test_setup_updates_from_ssdp( assert hass.states.get("media_player.mock_title") == snapshot assert entity_registry.async_get("media_player.mock_title") == snapshot assert ( - entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] - == "https://fake_host:12345/tv_agent" + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" ) assert ( entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] - == "https://fake_host:12345/test" + == "http://10.10.12.34:7676/smp_15_" ) From e6912b94dfbfbf66cbfde5e6c639766bdf5b3ce5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 7 May 2025 14:14:39 +0200 Subject: [PATCH 0213/1175] Update frontend to 20250507.0 (#144398) --- 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 4abf9aa7814..84062384bf5 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==20250506.0"] + "requirements": ["home-assistant-frontend==20250507.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9a0e366dab8..fef5f5d4149 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.100.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 home-assistant-intents==2025.4.30 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d4cd74e01de..b3be558a6a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16c982f4478..752a8859a41 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -991,7 +991,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250506.0 +home-assistant-frontend==20250507.0 # homeassistant.components.conversation home-assistant-intents==2025.4.30 From c4e4c52c6c26beacd10e6d44b89de5c67949baac Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 7 May 2025 14:36:28 +0200 Subject: [PATCH 0214/1175] Bump deebot-client to 13.1.0 (#144397) --- 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 2a332e498c7..e670a36cf72 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index b3be558a6a3..35c9883f68b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ debugpy==1.8.13 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.0.1 +deebot-client==13.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 752a8859a41..9daf3726a51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -653,7 +653,7 @@ dbus-fast==2.43.0 debugpy==1.8.13 # homeassistant.components.ecovacs -deebot-client==13.0.1 +deebot-client==13.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 2f7fcb4f5ebaf4c9d06aa7ff27c759d5b67de7a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 7 May 2025 14:50:18 +0200 Subject: [PATCH 0215/1175] Do not duplicate model and model_id in SamsungTV device info (#144402) --- homeassistant/components/samsungtv/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index a25b8ff2b14..80ebe461757 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -41,7 +41,6 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity) self._attr_unique_id = config_entry.unique_id or config_entry.entry_id self._attr_device_info = DeviceInfo( manufacturer=config_entry.data.get(CONF_MANUFACTURER), - model=config_entry.data.get(CONF_MODEL), model_id=config_entry.data.get(CONF_MODEL), ) if self.unique_id: From c26b3f519a9bf70beeecb6c877e36658398b34dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Wed, 7 May 2025 15:25:18 +0200 Subject: [PATCH 0216/1175] Add discovery schema for Matter CumulativeEnergyExported (#144061) * Update sensor.py to add CumulativeEnergyExported attribute * Report exported energy as positive value * Add snapshot * Add translation key --- homeassistant/components/matter/sensor.py | 19 ++++++ homeassistant/components/matter/strings.json | 3 + .../matter/snapshots/test_sensor.ambr | 58 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index f1704b45c50..e0d2050c833 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -744,6 +744,25 @@ DISCOVERY_SCHEMAS = [ clusters.ElectricalEnergyMeasurement.Attributes.CumulativeEnergyImported, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ElectricalEnergyMeasurementCumulativeEnergyExported", + translation_key="energy_exported", + device_class=SensorDeviceClass.ENERGY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + suggested_display_precision=3, + state_class=SensorStateClass.TOTAL_INCREASING, + # id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh) + measurement_to_ha=lambda x: x.energy, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.ElectricalEnergyMeasurement.Attributes.CumulativeEnergyExported, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index b8e8c63502c..129c6a3ab54 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -303,6 +303,9 @@ "current_phase": { "name": "Current phase" }, + "energy_exported": { + "name": "Energy exported" + }, "evse_fault_state": { "name": "Fault state", "state": { diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 4a3ac8d75f9..6c00dc5cede 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -4254,6 +4254,64 @@ 'state': '-3.62', }) # --- +# name: test_sensors[solar_power][sensor.solarpower_energy_exported-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.solarpower_energy_exported', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy exported', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_exported', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalEnergyMeasurementCumulativeEnergyExported-145-2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[solar_power][sensor.solarpower_energy_exported-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarPower Energy exported', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solarpower_energy_exported', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '42.279', + }) +# --- # name: test_sensors[solar_power][sensor.solarpower_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 1ce44800aba092037ef1a40e1228051a63aeaa05 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 7 May 2025 09:05:08 -0500 Subject: [PATCH 0217/1175] Bump intents to 2025.5.7 (#144404) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 3cf4d826a9d..2955bb96833 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==2.2.3", "home-assistant-intents==2025.4.30"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fef5f5d4149..3c9af6b035c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.100.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250507.0 -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index b51bb69e490..cf27bea85e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.4.30", + "home-assistant-intents==2025.5.7", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index a8c30dfacd9..219e1734072 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.100.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 35c9883f68b..0e5fee13dae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ holidays==0.70 home-assistant-frontend==20250507.0 # homeassistant.components.conversation -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9daf3726a51..99dd08c74ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ holidays==0.70 home-assistant-frontend==20250507.0 # homeassistant.components.conversation -home-assistant-intents==2025.4.30 +home-assistant-intents==2025.5.7 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 9248fd73cb3..306b5901370 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.25.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.4.30 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 5456cd0ac18519f59412844ee719952eed7feec8 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 18:06:46 +0200 Subject: [PATCH 0218/1175] Fix spelling in user-facing strings of `auth` component (#144412) --- homeassistant/components/auth/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index c8622880f0f..b1e80d716d8 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -5,7 +5,7 @@ "step": { "init": { "title": "Set up two-factor authentication using TOTP", - "description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." + "description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**." } }, "error": { @@ -13,7 +13,7 @@ } }, "notify": { - "title": "Notify One-Time Password", + "title": "Notify one-time password", "step": { "init": { "title": "Set up one-time password delivered by notify component", From dc0998d95d84be0970d5de850032321af71f911a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 7 May 2025 18:50:45 +0200 Subject: [PATCH 0219/1175] Fix Z-Wave restore nvm command to wait for driver ready (#144413) --- homeassistant/components/zwave_js/api.py | 15 ++ .../components/zwave_js/config_flow.py | 2 +- homeassistant/components/zwave_js/const.py | 4 + tests/components/zwave_js/test_api.py | 152 +++++++++++++----- 4 files changed, 129 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index aa2219031d2..f4397737234 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -88,6 +88,7 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( @@ -3063,14 +3064,28 @@ async def websocket_restore_nvm( ) ) + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + # Set up subscription for progress events connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("nvm convert progress", forward_progress), controller.on("nvm restore progress", forward_progress), + driver.once("driver ready", set_driver_ready), ] await controller.async_restore_nvm_base64(msg["data"]) + + with suppress(TimeoutError): + async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + await hass.config_entries.async_reload(entry.entry_id) + connection.send_message( websocket_api.event_message( msg[ID], diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 84717047fdd..407af9e902b 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -67,6 +67,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, + RESTORE_NVM_DRIVER_READY_TIMEOUT, ) _LOGGER = logging.getLogger(__name__) @@ -78,7 +79,6 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" -RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 16cf6f748bb..5792fca42a2 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -201,3 +201,7 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE, WindowCoveringPropertyKey.VERTICAL_SLATS_ANGLE_NO_POSITION, } + +# Other constants + +RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 2e3d8fd290a..c6ce3d9ac1b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5518,10 +5518,98 @@ async def test_restore_nvm( # Set up mocks for the controller events controller = client.driver.controller - # Test restore success - with patch.object( - controller, "async_restore_nvm_base64", return_value=None - ) as mock_restore: + async def async_send_command_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + return {} + + client.async_send_command.side_effect = async_send_command_driver_ready + + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": integration.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify the finished event first + msg = await ws_client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["event"] == "finished" + + # Verify subscription success + msg = await ws_client.receive_json() + assert msg["type"] == "result" + assert msg["success"] is True + + # Simulate progress events + event = Event( + "nvm restore progress", + { + "source": "controller", + "event": "nvm restore progress", + "bytesWritten": 25, + "total": 100, + }, + ) + controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 25 + assert msg["event"]["total"] == 100 + + event = Event( + "nvm restore progress", + { + "source": "controller", + "event": "nvm restore progress", + "bytesWritten": 50, + "total": 100, + }, + ) + controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "nvm restore progress" + assert msg["event"]["bytesWritten"] == 50 + assert msg["event"]["total"] == 100 + + await hass.async_block_till_done() + + # Verify the restore was called + # The first call is the relevant one for nvm restore. + assert client.async_send_command.call_count == 3 + assert client.async_send_command.call_args_list[0] == call( + { + "command": "controller.restore_nvm", + "nvmData": "dGVzdA==", + }, + require_schema=14, + ) + + client.async_send_command.reset_mock() + + # Test sending command with driver not ready and timeout. + + async def async_send_command_no_driver_ready( + message: dict[str, Any], + require_schema: int | None = None, + ) -> dict: + """Send a command and get a response.""" + return {} + + client.async_send_command.side_effect = async_send_command_no_driver_ready + + with patch( + "homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", + new=0, + ): # Send the subscription request await ws_client.send_json_auto_id( { @@ -5533,6 +5621,7 @@ async def test_restore_nvm( # Verify the finished event first msg = await ws_client.receive_json() + assert msg["type"] == "event" assert msg["event"]["event"] == "finished" @@ -5541,48 +5630,25 @@ async def test_restore_nvm( assert msg["type"] == "result" assert msg["success"] is True - # Simulate progress events - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 25, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 25 - assert msg["event"]["total"] == 100 - - event = Event( - "nvm restore progress", - { - "source": "controller", - "event": "nvm restore progress", - "bytesWritten": 50, - "total": 100, - }, - ) - controller.receive_event(event) - msg = await ws_client.receive_json() - assert msg["event"]["event"] == "nvm restore progress" - assert msg["event"]["bytesWritten"] == 50 - assert msg["event"]["total"] == 100 - - # Wait for the restore to complete await hass.async_block_till_done() - # Verify the restore was called - assert mock_restore.called + # Verify the restore was called + # The first call is the relevant one for nvm restore. + assert client.async_send_command.call_count == 3 + assert client.async_send_command.call_args_list[0] == call( + { + "command": "controller.restore_nvm", + "nvmData": "dGVzdA==", + }, + require_schema=14, + ) + + client.async_send_command.reset_mock() # Test restore failure - with patch.object( - controller, - "async_restore_nvm_base64", - side_effect=FailedCommand("failed_command", "Restore failed"), + with patch( + f"{CONTROLLER_PATCH_PREFIX}.async_restore_nvm_base64", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): # Send the subscription request await ws_client.send_json_auto_id( @@ -5596,7 +5662,7 @@ async def test_restore_nvm( # Verify error response msg = await ws_client.receive_json() assert not msg["success"] - assert msg["error"]["code"] == "Restore failed" + assert msg["error"]["code"] == "zwave_error" # Test entry_id not found await ws_client.send_json_auto_id( From e290829bc005cdd863785a35dccf1af758af28d3 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 18:53:38 +0200 Subject: [PATCH 0220/1175] Add missing hyphen to "eight-digit HomeKit pairing code" (#144416) --- homeassistant/components/homekit_controller/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index e857e1a7f01..15785a3947a 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -12,7 +12,7 @@ }, "pair": { "title": "Pair with a device via HomeKit Accessory Protocol", - "description": "HomeKit Device communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit Controller or iCloud. Enter your eight digit HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging, often close to a HomeKit bar code, next to the image of a small house.", + "description": "HomeKit Device communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit Controller or iCloud. Enter your eight-digit HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging, often close to a HomeKit bar code, next to the image of a small house.", "data": { "pairing_code": "Pairing code", "allow_insecure_setup_codes": "Allow pairing with insecure setup codes." From e1344fca6c22cc70d34f0127a3a6ca62dddba7fe Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 20:11:41 +0200 Subject: [PATCH 0221/1175] Fix spelling of "HomeKit" and "Gateway" in `tradfri` (#144420) --- homeassistant/components/tradfri/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 66c46dd482e..8b86a6df9ab 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -17,7 +17,7 @@ "invalid_security_code": "Failed to register with provided code. If this keeps happening, try restarting the gateway.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout": "Timeout validating the code.", - "cannot_authenticate": "Cannot authenticate, is Gateway paired with another server like e.g. Homekit?" + "cannot_authenticate": "Cannot authenticate, is your gateway paired with another server like e.g. HomeKit?" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", From 9ec5d90f4d85e170fb194a415f97e421a450a264 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 21:21:43 +0200 Subject: [PATCH 0222/1175] =?UTF-8?q?Add=20missing=20hyphen=20to=20"6-digi?= =?UTF-8?q?t=20=E2=80=A6=20codes"=20in=20`opower`=20(#144417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/opower/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opower/strings.json b/homeassistant/components/opower/strings.json index f65aeb011ee..3af968cf789 100644 --- a/homeassistant/components/opower/strings.json +++ b/homeassistant/components/opower/strings.json @@ -9,7 +9,7 @@ } }, "mfa": { - "description": "The TOTP secret below is not one of the 6 digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.", + "description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.", "data": { "totp_secret": "TOTP secret" } From f5c67e2fd17c1d9a90a926d66b004f66068e9d4b Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 21:48:40 +0200 Subject: [PATCH 0223/1175] Fix user-facing strings in `totalconnect` (#144411) - replace two inconsistent occurrences of camel-cased "TotalConnect" with "Total Connect" - apply sentence-casing to all strings - add a missing hyphen to "4-digit numer" --- homeassistant/components/totalconnect/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index daf720084a5..f3174b72a8e 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Total Connect 2.0 Account Credentials", + "title": "Total Connect 2.0 account credentials", "description": "It is highly recommended to use a 'standard' Total Connect user account with Home Assistant. The account should not have full administrative privileges.", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -14,13 +14,13 @@ } }, "locations": { - "title": "Location Usercodes", + "title": "Location usercodes", "description": "Enter the usercode for this user at location {location_id}", "data": { "usercodes": "Usercode" }, "data_description": { - "usercodes": "The usercode is usually a 4 digit number" + "usercodes": "The usercode is usually a 4-digit number" } }, "reauth_confirm": { @@ -41,13 +41,13 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "no_locations": "No locations are available for this user, check TotalConnect settings" + "no_locations": "No locations are available for this user, check Total Connect settings" } }, "options": { "step": { "init": { - "title": "TotalConnect Options", + "title": "Total Connect options", "data": { "auto_bypass_low_battery": "Auto bypass low battery", "code_required": "Require user to enter code for alarm actions" @@ -62,11 +62,11 @@ "services": { "arm_away_instant": { "name": "Arm away instant", - "description": "Arms Away with zero entry delay." + "description": "Arms away with zero entry delay." }, "arm_home_instant": { "name": "Arm home instant", - "description": "Arms Home with zero entry delay." + "description": "Arms home with zero entry delay." } }, "entity": { From 4cc538b5ae1442e35fdc5301ce49358ac1aad478 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 7 May 2025 21:48:52 +0200 Subject: [PATCH 0224/1175] Add sensor for brew start time to lamarzocco (#144423) * Add sensor for brew start time to lamarzocco * pytestmark --- .../components/lamarzocco/icons.json | 3 ++ homeassistant/components/lamarzocco/sensor.py | 13 +++++ .../components/lamarzocco/strings.json | 3 ++ tests/components/lamarzocco/conftest.py | 10 ++++ .../lamarzocco/fixtures/config_gs3.json | 2 +- .../snapshots/test_diagnostics.ambr | 4 +- .../lamarzocco/snapshots/test_sensor.ambr | 48 +++++++++++++++++++ .../lamarzocco/test_binary_sensor.py | 2 + tests/components/lamarzocco/test_sensor.py | 2 + 9 files changed, 84 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/icons.json b/homeassistant/components/lamarzocco/icons.json index a319384d7fd..fb61397575d 100644 --- a/homeassistant/components/lamarzocco/icons.json +++ b/homeassistant/components/lamarzocco/icons.json @@ -82,6 +82,9 @@ "steam_boiler_ready_time": { "default": "mdi:av-timer" }, + "brewing_start_time": { + "default": "mdi:clock-start" + }, "total_coffees_made": { "default": "mdi:coffee" }, diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 5dc0eb3dbef..087605315e5 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -11,6 +11,7 @@ from pylamarzocco.models import ( BaseWidgetOutput, CoffeeAndFlushCounter, CoffeeBoiler, + MachineStatus, SteamBoilerLevel, SteamBoilerTemperature, ) @@ -72,6 +73,18 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( in (ModelName.LINEA_MICRA, ModelName.LINEA_MINI_R) ), ), + LaMarzoccoSensorEntityDescription( + key="brewing_start_time", + translation_key="brewing_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=( + lambda config: cast( + MachineStatus, config[WidgetType.CM_MACHINE_STATUS] + ).brewing_start_time + ), + entity_category=EntityCategory.DIAGNOSTIC, + available_fn=(lambda coordinator: not coordinator.websocket_terminated), + ), LaMarzoccoSensorEntityDescription( key="steam_boiler_ready_time", translation_key="steam_boiler_ready_time", diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index 6383e931c22..8de62efd284 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -147,6 +147,9 @@ "steam_boiler_ready_time": { "name": "Steam boiler ready time" }, + "brewing_start_time": { + "name": "Brewing start time" + }, "total_coffees_made": { "name": "Total coffees made", "unit_of_measurement": "coffees" diff --git a/tests/components/lamarzocco/conftest.py b/tests/components/lamarzocco/conftest.py index c7530d464db..ccfea1243bc 100644 --- a/tests/components/lamarzocco/conftest.py +++ b/tests/components/lamarzocco/conftest.py @@ -128,3 +128,13 @@ def mock_ble_device() -> BLEDevice: return BLEDevice( "00:00:00:00:00:00", "GS_GS012345", details={"path": "path"}, rssi=50 ) + + +@pytest.fixture +def mock_websocket_terminated() -> Generator[bool]: + """Mock websocket terminated.""" + with patch( + "homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated", + new=False, + ) as mock_websocket_terminated: + yield mock_websocket_terminated diff --git a/tests/components/lamarzocco/fixtures/config_gs3.json b/tests/components/lamarzocco/fixtures/config_gs3.json index 8958bb90fc4..80f535328d5 100644 --- a/tests/components/lamarzocco/fixtures/config_gs3.json +++ b/tests/components/lamarzocco/fixtures/config_gs3.json @@ -25,7 +25,7 @@ "status": "StandBy", "startTime": 1742857195332 }, - "brewingStartTime": null + "brewingStartTime": 1746641060000 }, "tutorialUrl": null }, diff --git a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr index 33b4b4092f7..9dcef0fe0f0 100644 --- a/tests/components/lamarzocco/snapshots/test_diagnostics.ambr +++ b/tests/components/lamarzocco/snapshots/test_diagnostics.ambr @@ -90,7 +90,7 @@ 'BrewingMode', 'StandBy', ]), - 'brewing_start_time': None, + 'brewing_start_time': '2025-05-07T18:04:20+00:00', 'mode': 'BrewingMode', 'next_status': dict({ 'start_time': '2025-03-24T22:59:55.332000+00:00', @@ -297,7 +297,7 @@ 'BrewingMode', 'StandBy', ]), - 'brewing_start_time': None, + 'brewing_start_time': '2025-05-07T18:04:20+00:00', 'mode': 'BrewingMode', 'next_status': dict({ 'start_time': '2025-03-24T22:59:55.332000+00:00', diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index 46abb93dd2e..15eda23c094 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -1,4 +1,52 @@ # serializer version: 1 +# name: test_sensors[sensor.gs012345_brewing_start_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.gs012345_brewing_start_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Brewing start time', + 'platform': 'lamarzocco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'brewing_start_time', + 'unique_id': 'GS012345_brewing_start_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.gs012345_brewing_start_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'GS012345 Brewing start time', + }), + 'context': , + 'entity_id': 'sensor.gs012345_brewing_start_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-05-07T18:04:20+00:00', + }) +# --- # name: test_sensors[sensor.gs012345_coffee_boiler_ready_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 570b5aef8ec..47e5a96ecbc 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -17,6 +17,8 @@ from . import async_init_integration from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +pytestmark = pytest.mark.usefixtures("mock_websocket_terminated") + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_binary_sensors( diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index 0b050dd7788..d3aba1ef370 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -14,6 +14,8 @@ from . import async_init_integration from tests.common import MockConfigEntry, snapshot_platform +pytestmark = pytest.mark.usefixtures("mock_websocket_terminated") + async def test_sensors( hass: HomeAssistant, From 0b0a239ed43deee833aa2c18a242cd253cdb1c76 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 7 May 2025 22:28:08 +0200 Subject: [PATCH 0225/1175] Fix sentence-casing in user-facing strings of `isy994` (#144428) --- homeassistant/components/isy994/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 6594c030f08..73f6cc98b12 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -36,7 +36,7 @@ "options": { "step": { "init": { - "title": "ISY Options", + "title": "ISY options", "description": "Set the options for the ISY integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", "data": { "sensor_string": "Node Sensor String", @@ -49,10 +49,10 @@ }, "system_health": { "info": { - "host_reachable": "Host Reachable", - "device_connected": "ISY Connected", - "last_heartbeat": "Last Heartbeat Time", - "websocket_status": "Event Socket Status" + "host_reachable": "Host reachable", + "device_connected": "ISY connected", + "last_heartbeat": "Last heartbeat time", + "websocket_status": "Event socket status" } }, "services": { @@ -89,7 +89,7 @@ } }, "get_zwave_parameter": { - "name": "Get Z-Wave Parameter", + "name": "Get Z-Wave parameter", "description": "Requests a Z-Wave device parameter via the ISY. The parameter value will be returned as an entity extra state attribute with the name \"ZW_#\" where \"#\" is the parameter number.", "fields": { "parameter": { @@ -164,7 +164,7 @@ }, "command": { "name": "Command", - "description": "The ISY Program Command to be sent." + "description": "The ISY program command to be sent." }, "isy": { "name": "ISY", From 744d5f7bd4af983a32690244b22d99ecc57a7caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Thu, 8 May 2025 09:10:30 +0200 Subject: [PATCH 0226/1175] Matter Mounted dimmable load control fixture (#144097) * Create mounted_dimmable_load_control_fixture.json * Add fixture * Add snapshots --- tests/components/matter/conftest.py | 1 + ...mounted_dimmable_load_control_fixture.json | 308 ++++++++++++++++++ .../matter/snapshots/test_number.ambr | 56 ++++ .../matter/snapshots/test_select.ambr | 60 ++++ .../matter/snapshots/test_switch.ambr | 48 +++ 5 files changed, 473 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/mounted_dimmable_load_control_fixture.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 4f6bda14097..cbc01d132a1 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -97,6 +97,7 @@ async def integration_fixture( "leak_sensor", "light_sensor", "microwave_oven", + "mounted_dimmable_load_control_fixture", "multi_endpoint_light", "occupancy_sensor", "on_off_plugin_unit", diff --git a/tests/components/matter/fixtures/nodes/mounted_dimmable_load_control_fixture.json b/tests/components/matter/fixtures/nodes/mounted_dimmable_load_control_fixture.json new file mode 100644 index 00000000000..b19b97bc41c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/mounted_dimmable_load_control_fixture.json @@ -0,0 +1,308 @@ +{ + "node_id": 14, + "date_commissioned": "2025-05-02T08:15:29.450054", + "last_interview": "2025-05-02T08:15:29.450072", + "interview_version": 6, + "available": false, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 3 + } + ], + "0/29/1": [29, 31, 40, 43, 48, 49, 50, 51, 52, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Mock Mounted dimmable load control", + "0/40/4": 32768, + "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": "53AB7717C13D0DD2", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 5, + "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, 21, 22, + 24, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 2, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkK7ybsD", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZBQ8P5SEgahQg==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 13, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65532, 65533, 65528, 65529, 65531 + ], + "0/52/0": [ + { + "0": 2673, + "1": "2673" + }, + { + "0": 2672, + "1": "2672" + }, + { + "0": 2671, + "1": "2671" + }, + { + "0": 2670, + "1": "2670" + }, + { + "0": 2669, + "1": "2669" + }, + { + "0": 2668, + "1": "2668" + }, + { + "0": 2667, + "1": "2667" + } + ], + "0/52/1": 830464, + "0/52/2": 635904, + "0/52/3": 635904, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "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, 2], + "0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRDhgkBwEkCAEwCUEEdWlSeMU0X1DnfNwpCgYjMQOf/XgYW1AbAJCiYwSvbm6/9kZ1C97E9ah0h3vtKD4jZIQBDQGv3e1ffCuw2OlDuTcKNQEoARgkAgE2AwQCBAEYMAQUP1MVmuztpdJEPcw9p/9X9qok6iAwBRRqGquZZYwbDAaOinVVrS9sWTozoBgwC0BAw6CB9ukgfW1LKZHsr2h6G2JAQWjUPNaWQrFAgWA7GAbgY2wdsppjUJ6kXIOyO5Ci/vlQHI2NE6woRbS+6QOuGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyvr+z4yBxEDoiyCFg+i408LqC3j0UMvTszBv1051g2EMrAzBkj+0RZFsSl3eQ3D2c7mTcH6GERtlk4BqGvC1qDcKNQEpARgkAmAwBBRqGquZZYwbDAaOinVVrS9sWTozoDAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQCIuoikQZU9LkDKw7dcTVVXBDlTyBol3w070PIIw8BbaQD5qCeIv/3cI5/X5sAYTmemRq0ZPMjAw1dsN+wodzm8Y", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BIPshBqc9a7nNK00eRrviEzHfe/cfATY9VngqKv17+uAUpy3XujhZBjkAQyhYAaSKxVzSfVttY4FVQkpXIHZFlA=", + "2": 4939, + "3": 2, + "4": 14, + "5": "ha-freebox", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEg+yEGpz1ruc0rTR5Gu+ITMd979x8BNj1WeCoq/Xv64BSnLde6OFkGOQBDKFgBpIrFXNJ9W21jgVVCSlcgdkWUDcKNQEpARgkAmAwBBRPkvAMbwLEubfgETM7L7icezGlHzAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQIKyooBXllxj1uo4Zn4CBbZqECNdO3wwzlhl7ZEygrWa04gBa5rVqgg+JahrvXD6HPHu4XldWIULtqTCPPIm4OsY" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 6, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": 0, + "1/6/65532": 1, + "1/6/65533": 6, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65532, 65533, 65528, 65529, 65531 + ], + "1/8/0": 254, + "1/8/1": 0, + "1/8/15": 0, + "1/8/17": 0, + "1/8/16384": 0, + "1/8/65532": 3, + "1/8/65533": 6, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65531": [0, 1, 15, 17, 16384, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 272, + "1": 1 + } + ], + "1/29/1": [3, 4, 6, 8, 29], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index e1ee782cd3b..03006b2210c 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -567,6 +567,62 @@ 'state': '255', }) # --- +# name: test_numbers[mounted_dimmable_load_control_fixture][number.mock_mounted_dimmable_load_control_on_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 255, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_mounted_dimmable_load_control_on_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'On level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'on_level', + 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-on_level-8-17', + 'unit_of_measurement': None, + }) +# --- +# name: test_numbers[mounted_dimmable_load_control_fixture][number.mock_mounted_dimmable_load_control_on_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Mounted dimmable load control On level', + 'max': 255, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.mock_mounted_dimmable_load_control_on_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_numbers[multi_endpoint_light][number.inovelli_off_transition_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 2665e59bf33..e9faf39c9ab 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -845,6 +845,66 @@ 'state': 'Low', }) # --- +# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'on', + 'off', + 'toggle', + 'previous', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power-on behavior on startup', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'startup_on_off', + 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[mounted_dimmable_load_control_fixture][select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Mounted dimmable load control Power-on behavior on startup', + 'options': list([ + 'on', + 'off', + 'toggle', + 'previous', + ]), + }), + 'context': , + 'entity_id': 'select.mock_mounted_dimmable_load_control_power_on_behavior_on_startup', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_selects[multi_endpoint_light][select.inovelli_dimming_edge-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index 9204a2b8e3a..4bc56a04ba8 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -382,6 +382,54 @@ 'state': 'off', }) # --- +# name: test_switches[mounted_dimmable_load_control_fixture][switch.mock_mounted_dimmable_load_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_mounted_dimmable_load_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-MatterSwitch-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[mounted_dimmable_load_control_fixture][switch.mock_mounted_dimmable_load_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Mock Mounted dimmable load control', + }), + 'context': , + 'entity_id': 'switch.mock_mounted_dimmable_load_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_switches[on_off_plugin_unit][switch.mock_onoffpluginunit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 50d57852a61f04a7f409f684817ac5726408b95c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 8 May 2025 09:27:17 +0200 Subject: [PATCH 0227/1175] Include runner arch in CI cache key (#144038) --- .github/workflows/ci.yaml | 56 +++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 656b75eb054..804b0883976 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,9 +37,9 @@ on: type: boolean env: - CACHE_VERSION: 12 + CACHE_VERSION: 1 UV_CACHE_VERSION: 1 - MYPY_CACHE_VERSION: 9 + MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.6" DEFAULT_PYTHON: "3.13" ALL_PYTHON_VERSIONS: "['3.13']" @@ -259,7 +259,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' @@ -276,7 +276,7 @@ jobs: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' @@ -306,7 +306,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit @@ -315,7 +315,7 @@ jobs: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Run ruff-format run: | @@ -346,7 +346,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit @@ -355,7 +355,7 @@ jobs: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Run ruff run: | @@ -386,7 +386,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit @@ -395,7 +395,7 @@ jobs: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Register yamllint problem matcher @@ -501,7 +501,7 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' @@ -509,10 +509,10 @@ jobs: with: path: ${{ env.UV_CACHE_DIR }} key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-uv-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ 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 @@ -598,7 +598,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Run hassfest run: | @@ -631,7 +631,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Run gen_requirements_all.py run: | @@ -688,7 +688,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Extract license data run: | @@ -731,7 +731,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register pylint problem matcher run: | @@ -778,7 +778,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register pylint problem matcher run: | @@ -830,17 +830,17 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore mypy cache uses: actions/cache@v4.2.3 with: path: .mypy_cache key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ steps.generate-mypy-key.outputs.key }} restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{ env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{ env.HA_SHORT_VERSION }}- - name: Register mypy problem matcher @@ -900,7 +900,7 @@ jobs: path: venv fail-on-cache-miss: true key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Run split_tests.py run: | @@ -959,7 +959,8 @@ jobs: with: path: venv fail-on-cache-miss: true - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: >- + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register Python problem matcher run: | @@ -1084,7 +1085,8 @@ jobs: with: path: venv fail-on-cache-miss: true - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: >- + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register Python problem matcher run: | @@ -1218,7 +1220,8 @@ jobs: with: path: venv fail-on-cache-miss: true - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: >- + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register Python problem matcher run: | @@ -1369,7 +1372,8 @@ jobs: with: path: venv fail-on-cache-miss: true - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + key: >- + ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Register Python problem matcher run: | From e74a29c87a80a36d6f09c2b4b10e794166b10f58 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 09:58:07 +0200 Subject: [PATCH 0228/1175] Sentence-case "multi-factor authentication" in `sense` (#144450) --- homeassistant/components/sense/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sense/strings.json b/homeassistant/components/sense/strings.json index 4579c84f050..c9ff5527940 100644 --- a/homeassistant/components/sense/strings.json +++ b/homeassistant/components/sense/strings.json @@ -10,7 +10,7 @@ } }, "validation": { - "title": "Sense Multi-factor authentication", + "title": "Sense multi-factor authentication", "data": { "code": "Verification code" } From 4a556f89aaa998684ed6d26858a71a1d767b3201 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 09:58:16 +0200 Subject: [PATCH 0229/1175] Add missing hyphen to "two-factor authentication" in `nextcloud` (#144448) --- homeassistant/components/nextcloud/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json index ef4e3de0f62..75950e94211 100644 --- a/homeassistant/components/nextcloud/strings.json +++ b/homeassistant/components/nextcloud/strings.json @@ -259,7 +259,7 @@ "name": "Task updates" }, "nextcloud_system_apps_app_updates_twofactor_totp": { - "name": "Two factor authentication updates" + "name": "Two-factor authentication updates" }, "nextcloud_system_apps_num_installed": { "name": "Apps installed" From 1294918f5b7514005d0904e36476609ea14e003e Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 09:58:30 +0200 Subject: [PATCH 0230/1175] Add missing hyphen to "two-factor authentication" in `august` (#144447) --- homeassistant/components/august/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index e3c97535a55..fbc746e939e 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -18,7 +18,7 @@ }, "step": { "validation": { - "title": "Two factor authentication", + "title": "Two-factor authentication", "data": { "verification_code": "Verification code" }, From 066d0f4143c5e0d3fe6e959127ccb6ef03af11e6 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 09:59:58 +0200 Subject: [PATCH 0231/1175] Add missing hyphen to "two-factor authentication" in `subaru` (#144446) --- homeassistant/components/subaru/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index 7525e73f802..6aef0041874 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -12,7 +12,7 @@ }, "two_factor": { "title": "[%key:component::subaru::config::step::user::title%]", - "description": "Two factor authentication required", + "description": "Two-factor authentication required", "data": { "contact_method": "Please select a contact method:" } From ce4e51078fda0353b7147ec9cbaf2fa8c6b361c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 03:00:32 -0500 Subject: [PATCH 0232/1175] Add test coverage for inkbird IBS-P02B (#144433) Was reported not working in https://github.com/Bluetooth-Devices/inkbird-ble/issues/95#issuecomment-2860473798 but cannot reproduce the issue. The tests are still useful --- tests/components/inkbird/__init__.py | 10 +++++++ tests/components/inkbird/test_sensor.py | 38 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/components/inkbird/__init__.py b/tests/components/inkbird/__init__.py index f798fee292c..7228f64448b 100644 --- a/tests/components/inkbird/__init__.py +++ b/tests/components/inkbird/__init__.py @@ -103,3 +103,13 @@ IAM_T1_SERVICE_INFO = _make_bluetooth_service_info( service_data={}, source="local", ) + +IBS_P02B_SERVICE_INFO = _make_bluetooth_service_info( + name="IBS-P02B", + manufacturer_data={9289: bytes.fromhex("111800656e0100005f00000100000000")}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + address="49:24:11:18:00:65", + rssi=-60, + service_data={}, + source="local", +) diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index 1feb5f5b02c..2a95714df4b 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -30,6 +30,7 @@ from homeassistant.util import dt as dt_util from . import ( IAM_T1_SERVICE_INFO, + IBS_P02B_SERVICE_INFO, SPS_PASSIVE_SERVICE_INFO, SPS_SERVICE_INFO, SPS_WITH_CORRUPT_NAME_SERVICE_INFO, @@ -256,3 +257,40 @@ async def test_notify_sensor(hass: HomeAssistant) -> None: saved_device_data_changed_callback({"temp_unit": "C"}) assert entry.data[CONF_DEVICE_DATA] == {"temp_unit": "C"} + + +async def test_ibs_p02b_sensors(hass: HomeAssistant) -> None: + """Test setting up creates the sensors for an IBS-P02B.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="49:24:11:18:00:65", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info(hass, IBS_P02B_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.ibs_p02b_0065_battery") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "95" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-P02B 0065 Battery" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.ibs_p02b_0065_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "36.6" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-P02B 0065 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + # Make sure we remember the device type + # in case the name is corrupted later + assert entry.data[CONF_DEVICE_TYPE] == "IBS-P02B" + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 1cb813e0c58d740f705b353d2588d2ef02806457 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 10:01:14 +0200 Subject: [PATCH 0233/1175] Fix sentence-casing and missing hyphen in `electrasmart` (#144443) --- homeassistant/components/electrasmart/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/electrasmart/strings.json b/homeassistant/components/electrasmart/strings.json index 06c7dfd6bed..485bf766534 100644 --- a/homeassistant/components/electrasmart/strings.json +++ b/homeassistant/components/electrasmart/strings.json @@ -3,12 +3,12 @@ "step": { "user": { "data": { - "phone_number": "Phone Number" + "phone_number": "Phone number" } }, "one_time_password": { "data": { - "one_time_password": "One Time Password" + "one_time_password": "One-time password" } } }, From ff637ef04624224f34922355ec05f39ec95caa59 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 8 May 2025 10:12:22 +0200 Subject: [PATCH 0234/1175] Include channel in Reolink device URL (#144456) Include channel in device URL --- homeassistant/components/reolink/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index ec598de663d..3325eab6f42 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -192,7 +192,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): hw_version=self._host.api.camera_hardware_version(dev_ch), sw_version=self._host.api.camera_sw_version(dev_ch), serial_number=self._host.api.camera_uid(dev_ch), - configuration_url=self._conf_url, + configuration_url=f"{self._conf_url}/?ch={dev_ch}", ) @property From a6f91177b616e023e81cfff3f1a258dbd71d007f Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 10:13:00 +0200 Subject: [PATCH 0235/1175] Small fixes in user-facing strings of `nest` (#144444) - add the missing hyphen to "one-time setup fee" - capitalize one occurrence of "Cloud Project" (treated as name in all other strings) - sentence-case "name" as this can be translated --- homeassistant/components/nest/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 54f543aa845..4a8689ff04c 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -6,7 +6,7 @@ "step": { "create_cloud_project": { "title": "Nest: Create and configure Cloud Project", - "description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, select **Create Project** then **New Project**.\n1. Give your Cloud Project a Name and then select **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and select **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and select **Enable**.\n\nProceed when your cloud project is set up." + "description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one-time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, select **Create Project** then **New Project**.\n1. Give your Cloud Project a name and then select **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and select **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and select **Enable**.\n\nProceed when your Cloud Project is set up." }, "cloud_project": { "title": "Nest: Enter Cloud Project ID", @@ -29,7 +29,7 @@ "title": "Configure Cloud Pub/Sub topic", "description": "Nest devices publish updates on a Cloud Pub/Sub topic. You can select an existing topic if one exists, or choose to create a new topic and the next step will create it for you with the necessary permissions. See the integration documentation for [more info]({more_info_url}).", "data": { - "topic_name": "Pub/Sub topic Name" + "topic_name": "Pub/Sub topic name" } }, "pubsub_topic_confirm": { @@ -41,7 +41,7 @@ "title": "Configure Cloud Pub/Sub subscription", "description": "Home Assistant receives realtime Nest device updates with a Cloud Pub/Sub subscription for topic `{topic}`.\n\nSelect an existing subscription below if one already exists, or the next step will create a new one for you. See the integration documentation for [more info]({more_info_url}).", "data": { - "subscription_name": "Pub/Sub subscription Name" + "subscription_name": "Pub/Sub subscription name" } }, "reauth_confirm": { From 678e25d0b165515fec35f75a009e977b174b96b4 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 8 May 2025 10:13:42 +0200 Subject: [PATCH 0236/1175] Bump pylamarzocco to 2.0.1 (#144454) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 572f70bc455..fb6a3660c66 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.0"] + "requirements": ["pylamarzocco==2.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e5fee13dae..94def5a2e6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0 +pylamarzocco==2.0.1 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99dd08c74ba..544baccd4e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.0 +pylamarzocco==2.0.1 # homeassistant.components.lastfm pylast==5.1.0 From bbc3862fec39fac6c0f10552d39fe842f972d2fc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 May 2025 11:59:20 +0200 Subject: [PATCH 0237/1175] Fix Z-Wave reset accumulated values button entity category (#144459) --- homeassistant/components/zwave_js/discovery.py | 2 +- tests/components/zwave_js/test_discovery.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 5c79c668afc..b46735e4040 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1204,7 +1204,7 @@ DISCOVERY_SCHEMAS = [ property={RESET_METER_PROPERTY}, type={ValueType.BOOLEAN}, ), - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ), ZWaveDiscoverySchema( platform=Platform.BINARY_SENSOR, diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 0be0cca78c8..7ef5f0e480f 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -431,10 +431,11 @@ async def test_rediscovery( async def test_aeotec_smart_switch_7( hass: HomeAssistant, + entity_registry: er.EntityRegistry, aeotec_smart_switch_7: Node, integration: MockConfigEntry, ) -> None: - """Test that Smart Switch 7 has a light and a switch entity.""" + """Test Smart Switch 7 discovery.""" state = hass.states.get("light.smart_switch_7") assert state assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ @@ -443,3 +444,9 @@ async def test_aeotec_smart_switch_7( state = hass.states.get("switch.smart_switch_7") assert state + + state = hass.states.get("button.smart_switch_7_reset_accumulated_values") + assert state + entity_entry = entity_registry.async_get(state.entity_id) + assert entity_entry + assert entity_entry.entity_category is EntityCategory.CONFIG From 3c4c3dc08e306b75dce486f5f5236a731fd04cf4 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 8 May 2025 14:28:56 +0200 Subject: [PATCH 0238/1175] Fix point import error (#144462) * fix import error * fix failing tests * Apply suggestions from code review --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/point/coordinator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/point/coordinator.py b/homeassistant/components/point/coordinator.py index c0cb4e27646..93bd74955ea 100644 --- a/homeassistant/components/point/coordinator.py +++ b/homeassistant/components/point/coordinator.py @@ -6,7 +6,6 @@ import logging from typing import Any from pypoint import PointSession -from tempora.utc import fromtimestamp from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -62,7 +61,9 @@ class PointDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]] or device.device_id not in self.device_updates or self.device_updates[device.device_id] < last_updated ): - self.device_updates[device.device_id] = last_updated or fromtimestamp(0) + self.device_updates[device.device_id] = ( + last_updated or datetime.fromtimestamp(0) + ) self.data[device.device_id] = { k: await device.sensor(k) for k in ("temperature", "humidity", "sound_pressure") From 2fd678bb59a7016c79cbf7834e1a8e1c7fc2047a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 May 2025 16:45:26 +0200 Subject: [PATCH 0239/1175] Set Z-Wave platforms fixture in light tests (#144473) --- tests/components/zwave_js/test_light.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index 21a6c0a8fae..954d6422399 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -2,6 +2,7 @@ from copy import deepcopy +import pytest from zwave_js_server.event import Event from homeassistant.components.light import ( @@ -26,6 +27,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -42,6 +44,12 @@ ZDB5100_ENTITY = "light.matrix_office" HSM200_V1_ENTITY = "light.hsm200" +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.LIGHT] + + async def test_light( hass: HomeAssistant, client, bulb_6_multi_color, integration ) -> None: From a1599d5f7d02f617a352a336eacdac3fbf79c76f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 May 2025 16:46:00 +0200 Subject: [PATCH 0240/1175] Set Z-Wave platforms fixture in helpers tests (#144472) --- tests/components/zwave_js/test_helpers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py index 356707fb5f8..c163b8e8c75 100644 --- a/tests/components/zwave_js/test_helpers.py +++ b/tests/components/zwave_js/test_helpers.py @@ -23,6 +23,12 @@ from tests.common import MockConfigEntry CONTROLLER_PATCH_PREFIX = "zwave_js_server.model.controller.Controller" +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [] + + async def test_async_get_node_status_sensor_entity_id( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: From 014c5dc7643edbd1c55e04e683746ac22d746820 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 8 May 2025 16:46:41 +0200 Subject: [PATCH 0241/1175] Set Z-Wave platforms fixture in config flow tests (#144470) --- tests/components/zwave_js/test_config_flow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 15fd9fcbd30..3f1d894030f 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -62,6 +62,12 @@ CP2652_ZIGBEE_DISCOVERY_INFO = UsbServiceInfo( ) +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [] + + @pytest.fixture(name="setup_entry") def setup_entry_fixture() -> Generator[AsyncMock]: """Mock entry setup.""" From 9e94e94075cf9f38e2fab342f14f757bbb8f0118 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 8 May 2025 17:38:13 +0200 Subject: [PATCH 0242/1175] Remove RTSPtoWebRTC (#144328) --- .strict-typing | 1 - CODEOWNERS | 2 - .../components/rtsp_to_webrtc/__init__.py | 123 -------- .../components/rtsp_to_webrtc/config_flow.py | 149 ---------- .../components/rtsp_to_webrtc/diagnostics.py | 17 -- .../components/rtsp_to_webrtc/manifest.json | 11 - .../components/rtsp_to_webrtc/strings.json | 42 --- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/integrations.json | 6 - mypy.ini | 10 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/rtsp_to_webrtc/__init__.py | 1 - tests/components/rtsp_to_webrtc/conftest.py | 108 ------- .../rtsp_to_webrtc/test_config_flow.py | 274 ------------------ .../rtsp_to_webrtc/test_diagnostics.py | 27 -- tests/components/rtsp_to_webrtc/test_init.py | 199 ------------- 17 files changed, 977 deletions(-) delete mode 100644 homeassistant/components/rtsp_to_webrtc/__init__.py delete mode 100644 homeassistant/components/rtsp_to_webrtc/config_flow.py delete mode 100644 homeassistant/components/rtsp_to_webrtc/diagnostics.py delete mode 100644 homeassistant/components/rtsp_to_webrtc/manifest.json delete mode 100644 homeassistant/components/rtsp_to_webrtc/strings.json delete mode 100644 tests/components/rtsp_to_webrtc/__init__.py delete mode 100644 tests/components/rtsp_to_webrtc/conftest.py delete mode 100644 tests/components/rtsp_to_webrtc/test_config_flow.py delete mode 100644 tests/components/rtsp_to_webrtc/test_diagnostics.py delete mode 100644 tests/components/rtsp_to_webrtc/test_init.py diff --git a/.strict-typing b/.strict-typing index 6aaa5d32a58..5648bbe3dd2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -434,7 +434,6 @@ homeassistant.components.roku.* homeassistant.components.romy.* homeassistant.components.rpi_power.* homeassistant.components.rss_feed_template.* -homeassistant.components.rtsp_to_webrtc.* homeassistant.components.russound_rio.* homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvitag_ble.* diff --git a/CODEOWNERS b/CODEOWNERS index 997fd1b0981..cffe9416374 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1307,8 +1307,6 @@ build.json @home-assistant/supervisor /tests/components/rpi_power/ @shenxn @swetoast /homeassistant/components/rss_feed_template/ @home-assistant/core /tests/components/rss_feed_template/ @home-assistant/core -/homeassistant/components/rtsp_to_webrtc/ @allenporter -/tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565 /homeassistant/components/russound_rio/ @noahhusby diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py deleted file mode 100644 index 0fc257c463f..00000000000 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ /dev/null @@ -1,123 +0,0 @@ -"""RTSPtoWebRTC integration with an external RTSPToWebRTC Server. - -WebRTC uses a direct communication from the client (e.g. a web browser) to a -camera device. Home Assistant acts as the signal path for initial set up, -passing through the client offer and returning a camera answer, then the client -and camera communicate directly. - -However, not all cameras natively support WebRTC. This integration is a shim -for camera devices that support RTSP streams only, relying on an external -server RTSPToWebRTC that is a proxy. Home Assistant does not participate in -the offer/answer SDP protocol, other than as a signal path pass through. - -Other integrations may use this integration with these steps: -- Check if this integration is loaded -- Call is_supported_stream_source for compatibility -- Call async_offer_for_stream_source to get back an answer for a client offer -""" - -from __future__ import annotations - -import asyncio -import logging - -from rtsp_to_webrtc.client import get_adaptive_client -from rtsp_to_webrtc.exceptions import ClientError, ResponseError -from rtsp_to_webrtc.interface import WebRTCClientInterface -from webrtc_models import RTCIceServer - -from homeassistant.components import camera -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "rtsp_to_webrtc" -DATA_SERVER_URL = "server_url" -DATA_UNSUB = "unsub" -TIMEOUT = 10 -CONF_STUN_SERVER = "stun_server" - -_DEPRECATED = "deprecated" - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up RTSPtoWebRTC from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - ir.async_create_issue( - hass, - DOMAIN, - _DEPRECATED, - breaks_in_ha_version="2025.6.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key=_DEPRECATED, - translation_placeholders={ - "go2rtc": "[go2rtc](https://www.home-assistant.io/integrations/go2rtc/)", - }, - ) - - client: WebRTCClientInterface - try: - async with asyncio.timeout(TIMEOUT): - client = await get_adaptive_client( - async_get_clientsession(hass), entry.data[DATA_SERVER_URL] - ) - except ResponseError as err: - raise ConfigEntryNotReady from err - except (TimeoutError, ClientError) as err: - raise ConfigEntryNotReady from err - - hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER) - if server := entry.options.get(CONF_STUN_SERVER): - - @callback - def get_servers() -> list[RTCIceServer]: - return [RTCIceServer(urls=[server])] - - entry.async_on_unload(camera.async_register_ice_servers(hass, get_servers)) - - async def async_offer_for_stream_source( - stream_source: str, - offer_sdp: str, - stream_id: str, - ) -> str: - """Handle the signal path for a WebRTC stream. - - This signal path is used to route the offer created by the client to the - proxy server that translates a stream to WebRTC. The communication for - the stream itself happens directly between the client and proxy. - """ - try: - async with asyncio.timeout(TIMEOUT): - return await client.offer_stream_id(stream_id, offer_sdp, stream_source) - except TimeoutError as err: - raise HomeAssistantError("Timeout talking to RTSPtoWebRTC server") from err - except ClientError as err: - raise HomeAssistantError(str(err)) from err - - entry.async_on_unload( - camera.async_register_rtsp_to_web_rtc_provider( - hass, DOMAIN, async_offer_for_stream_source - ) - ) - entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - if DOMAIN in hass.data: - del hass.data[DOMAIN] - ir.async_delete_issue(hass, DOMAIN, _DEPRECATED) - return True - - -async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload config entry when options change.""" - if hass.data[DOMAIN][CONF_STUN_SERVER] != entry.options.get(CONF_STUN_SERVER): - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py deleted file mode 100644 index 22502659757..00000000000 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Config flow for RTSPtoWebRTC.""" - -from __future__ import annotations - -import logging -from typing import Any -from urllib.parse import urlparse - -import rtsp_to_webrtc -import voluptuous as vol - -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) -from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.service_info.hassio import HassioServiceInfo - -from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema({vol.Required(DATA_SERVER_URL): str}) - - -class RTSPToWebRTCConfigFlow(ConfigFlow, domain=DOMAIN): - """RTSPtoWebRTC config flow.""" - - _hassio_discovery: dict[str, Any] - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Configure the RTSPtoWebRTC server url.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - if user_input is None: - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) - - url = user_input[DATA_SERVER_URL] - result = urlparse(url) - if not all([result.scheme, result.netloc]): - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={DATA_SERVER_URL: "invalid_url"}, - ) - - if error_code := await self._test_connection(url): - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors={"base": error_code}, - ) - - await self.async_set_unique_id(DOMAIN) - return self.async_create_entry( - title=url, - data={DATA_SERVER_URL: url}, - ) - - async def _test_connection(self, url: str) -> str | None: - """Test the connection and return any relevant errors.""" - client = rtsp_to_webrtc.client.Client(async_get_clientsession(self.hass), url) - try: - await client.heartbeat() - except rtsp_to_webrtc.exceptions.ResponseError as err: - _LOGGER.error("RTSPtoWebRTC server failure: %s", str(err)) - return "server_failure" - except rtsp_to_webrtc.exceptions.ClientError as err: - _LOGGER.error("RTSPtoWebRTC communication failure: %s", str(err)) - return "server_unreachable" - return None - - async def async_step_hassio( - self, discovery_info: HassioServiceInfo - ) -> ConfigFlowResult: - """Prepare configuration for the RTSPtoWebRTC server add-on discovery.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self._hassio_discovery = discovery_info.config - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Confirm Add-on discovery.""" - errors = None - if user_input is not None: - # Validate server connection once user has confirmed - host = self._hassio_discovery[CONF_HOST] - port = self._hassio_discovery[CONF_PORT] - url = f"http://{host}:{port}" - if error_code := await self._test_connection(url): - return self.async_abort(reason=error_code) - - if user_input is None or errors: - # Show initial confirmation or errors from server validation - return self.async_show_form( - step_id="hassio_confirm", - description_placeholders={"addon": self._hassio_discovery["addon"]}, - errors=errors, - ) - - return self.async_create_entry( - title=self._hassio_discovery["addon"], - data={DATA_SERVER_URL: url}, - ) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> OptionsFlow: - """Create an options flow.""" - return OptionsFlowHandler() - - -class OptionsFlowHandler(OptionsFlow): - """RTSPtoWeb Options flow.""" - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Manage the options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_STUN_SERVER, - description={ - "suggested_value": self.config_entry.options.get( - CONF_STUN_SERVER - ), - }, - ): str, - } - ), - ) diff --git a/homeassistant/components/rtsp_to_webrtc/diagnostics.py b/homeassistant/components/rtsp_to_webrtc/diagnostics.py deleted file mode 100644 index ab13e0a64ee..00000000000 --- a/homeassistant/components/rtsp_to_webrtc/diagnostics.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Diagnostics support for Nest.""" - -from __future__ import annotations - -from typing import Any - -from rtsp_to_webrtc import client - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - - -async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dict[str, Any]: - """Return diagnostics for a config entry.""" - return dict(client.get_diagnostics()) diff --git a/homeassistant/components/rtsp_to_webrtc/manifest.json b/homeassistant/components/rtsp_to_webrtc/manifest.json deleted file mode 100644 index 27b9703d50e..00000000000 --- a/homeassistant/components/rtsp_to_webrtc/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "rtsp_to_webrtc", - "name": "RTSPtoWebRTC", - "codeowners": ["@allenporter"], - "config_flow": true, - "dependencies": ["camera"], - "documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc", - "iot_class": "local_push", - "loggers": ["rtsp_to_webrtc"], - "requirements": ["rtsp-to-webrtc==0.5.1"] -} diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json deleted file mode 100644 index c8dcbb7f462..00000000000 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Configure RTSPtoWebRTC", - "description": "The RTSPtoWebRTC integration requires a server to translate RTSP streams into WebRTC. Enter the URL to the RTSPtoWebRTC server.", - "data": { - "server_url": "RTSPtoWebRTC server URL e.g. https://example.com" - } - }, - "hassio_confirm": { - "title": "RTSPtoWebRTC via Home Assistant add-on", - "description": "Do you want to configure Home Assistant to connect to the RTSPtoWebRTC server provided by the add-on: {addon}?" - } - }, - "error": { - "invalid_url": "Must be a valid RTSPtoWebRTC server URL e.g. https://example.com", - "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", - "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "server_failure": "[%key:component::rtsp_to_webrtc::config::error::server_failure%]", - "server_unreachable": "[%key:component::rtsp_to_webrtc::config::error::server_unreachable%]" - } - }, - "issues": { - "deprecated": { - "title": "The RTSPtoWebRTC integration is deprecated", - "description": "The RTSPtoWebRTC integration is deprecated and will be removed. Please use the {go2rtc} integration instead, which is enabled by default and provides a better experience. You only need to remove the RTSPtoWebRTC config entry." - } - }, - "options": { - "step": { - "init": { - "data": { - "stun_server": "Stun server address (host:port)" - } - } - } - } -} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf80105a889..e4815c82543 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -536,7 +536,6 @@ FLOWS = { "roon", "rova", "rpi_power", - "rtsp_to_webrtc", "ruckus_unleashed", "russound_rio", "ruuvi_gateway", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4cee3922cd4..85f9ae5e8a9 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5576,12 +5576,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "rtsp_to_webrtc": { - "name": "RTSPtoWebRTC", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" - }, "ruckus_unleashed": { "name": "Ruckus", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index fbc8715f9b2..518d1953fb3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4096,16 +4096,6 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.rtsp_to_webrtc.*] -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.russound_rio.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 94def5a2e6a..2481a3288eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2675,9 +2675,6 @@ rova==0.4.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 -# homeassistant.components.rtsp_to_webrtc -rtsp-to-webrtc==0.5.1 - # homeassistant.components.russound_rnet russound==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 544baccd4e5..e13b157579a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2170,9 +2170,6 @@ rova==0.4.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 -# homeassistant.components.rtsp_to_webrtc -rtsp-to-webrtc==0.5.1 - # homeassistant.components.ruuvitag_ble ruuvitag-ble==0.1.2 diff --git a/tests/components/rtsp_to_webrtc/__init__.py b/tests/components/rtsp_to_webrtc/__init__.py deleted file mode 100644 index ee4206e357d..00000000000 --- a/tests/components/rtsp_to_webrtc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the RTSPtoWebRTC integration.""" diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py deleted file mode 100644 index 956825f6372..00000000000 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Tests for RTSPtoWebRTC initialization.""" - -from __future__ import annotations - -from collections.abc import AsyncGenerator, Awaitable, Callable -from typing import Any -from unittest.mock import patch - -import pytest -import rtsp_to_webrtc - -from homeassistant.components import camera -from homeassistant.components.rtsp_to_webrtc import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from tests.common import MockConfigEntry - -STREAM_SOURCE = "rtsp://example.com" -SERVER_URL = "http://127.0.0.1:8083" - -CONFIG_ENTRY_DATA = {"server_url": SERVER_URL} - -# Typing helpers -type ComponentSetup = Callable[[], Awaitable[None]] -type AsyncYieldFixture[_T] = AsyncGenerator[_T] - - -@pytest.fixture(autouse=True) -async def webrtc_server() -> None: - """Patch client library to force usage of RTSPtoWebRTC server.""" - with patch( - "rtsp_to_webrtc.client.WebClient.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ResponseError(), - ): - yield - - -@pytest.fixture -async def mock_camera(hass: HomeAssistant) -> AsyncGenerator[None]: - """Initialize a demo camera platform.""" - assert await async_setup_component( - hass, "camera", {camera.DOMAIN: {"platform": "demo"}} - ) - await hass.async_block_till_done() - with ( - patch( - "homeassistant.components.demo.camera.Path.read_bytes", - return_value=b"Test", - ), - patch( - "homeassistant.components.camera.Camera.stream_source", - return_value=STREAM_SOURCE, - ), - ): - yield - - -@pytest.fixture -async def config_entry_data() -> dict[str, Any]: - """Fixture for MockConfigEntry data.""" - return CONFIG_ENTRY_DATA - - -@pytest.fixture -def config_entry_options() -> dict[str, Any] | None: - """Fixture to set initial config entry options.""" - return None - - -@pytest.fixture -async def config_entry( - config_entry_data: dict[str, Any], - config_entry_options: dict[str, Any] | None, -) -> MockConfigEntry: - """Fixture for MockConfigEntry.""" - return MockConfigEntry( - domain=DOMAIN, data=config_entry_data, options=config_entry_options - ) - - -@pytest.fixture -async def rtsp_to_webrtc_client() -> None: - """Fixture for mock rtsp_to_webrtc client.""" - with patch("rtsp_to_webrtc.client.Client.heartbeat"): - yield - - -@pytest.fixture -async def setup_integration( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> AsyncYieldFixture[ComponentSetup]: - """Fixture for setting up the component.""" - config_entry.add_to_hass(hass) - - async def func() -> None: - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - - yield func - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - await hass.config_entries.async_unload(entries[0].entry_id) - await hass.async_block_till_done() - - assert not hass.data.get(DOMAIN) - assert entries[0].state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py deleted file mode 100644 index d3afa80b0b4..00000000000 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Test the RTSPtoWebRTC config flow.""" - -from __future__ import annotations - -from unittest.mock import patch - -import rtsp_to_webrtc - -from homeassistant import config_entries -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 homeassistant.helpers.service_info.hassio import HassioServiceInfo - -from .conftest import ComponentSetup - -from tests.common import MockConfigEntry - - -async def test_web_full_flow(hass: HomeAssistant) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert result.get("data_schema").schema.get("server_url") is str - assert not result.get("errors") - with ( - patch("rtsp_to_webrtc.client.Client.heartbeat"), - patch( - "homeassistant.components.rtsp_to_webrtc.async_setup_entry", - return_value=True, - ) as mock_setup, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"server_url": "https://example.com"} - ) - 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"} - - assert len(mock_setup.mock_calls) == 1 - - -async def test_single_config_entry(hass: HomeAssistant) -> None: - """Test that only a single config entry is allowed.""" - old_entry = MockConfigEntry(domain=DOMAIN, data={"example": True}) - old_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == "single_instance_allowed" - - -async def test_invalid_url(hass: HomeAssistant) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert result.get("data_schema").schema.get("server_url") is str - assert not result.get("errors") - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"server_url": "not-a-url"} - ) - - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert result.get("errors") == {"server_url": "invalid_url"} - - -async def test_server_unreachable(hass: HomeAssistant) -> None: - """Exercise case where the server is unreachable.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert not result.get("errors") - with patch( - "rtsp_to_webrtc.client.Client.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ClientError(), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"server_url": "https://example.com"} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert result.get("errors") == {"base": "server_unreachable"} - - -async def test_server_failure(hass: HomeAssistant) -> None: - """Exercise case where server returns a failure.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert not result.get("errors") - with patch( - "rtsp_to_webrtc.client.Client.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ResponseError(), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"server_url": "https://example.com"} - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "user" - assert result.get("errors") == {"base": "server_failure"} - - -async def test_hassio_discovery(hass: HomeAssistant) -> None: - """Test supervisor add-on discovery.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=HassioServiceInfo( - config={ - "addon": "RTSPtoWebRTC", - "host": "fake-server", - "port": 8083, - }, - name="RTSPtoWebRTC", - slug="rtsp-to-webrtc", - uuid="1234", - ), - context={"source": config_entries.SOURCE_HASSIO}, - ) - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "hassio_confirm" - assert result.get("description_placeholders") == {"addon": "RTSPtoWebRTC"} - - with ( - patch("rtsp_to_webrtc.client.Client.heartbeat"), - patch( - "homeassistant.components.rtsp_to_webrtc.async_setup_entry", - return_value=True, - ) as mock_setup, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - await hass.async_block_till_done() - - 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"} - - assert len(mock_setup.mock_calls) == 1 - - -async def test_hassio_single_config_entry(hass: HomeAssistant) -> None: - """Test supervisor add-on discovery only allows a single entry.""" - old_entry = MockConfigEntry(domain=DOMAIN, data={"example": True}) - old_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=HassioServiceInfo( - config={ - "addon": "RTSPtoWebRTC", - "host": "fake-server", - "port": 8083, - }, - name="RTSPtoWebRTC", - slug="rtsp-to-webrtc", - uuid="1234", - ), - context={"source": config_entries.SOURCE_HASSIO}, - ) - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == "single_instance_allowed" - - -async def test_hassio_ignored(hass: HomeAssistant) -> None: - """Test ignoring superversor add-on discovery.""" - old_entry = MockConfigEntry(domain=DOMAIN, source=config_entries.SOURCE_IGNORE) - old_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=HassioServiceInfo( - config={ - "addon": "RTSPtoWebRTC", - "host": "fake-server", - "port": 8083, - }, - name="RTSPtoWebRTC", - slug="rtsp-to-webrtc", - uuid="1234", - ), - context={"source": config_entries.SOURCE_HASSIO}, - ) - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == "single_instance_allowed" - - -async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: - """Test server failure during supvervisor add-on discovery shows an error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data=HassioServiceInfo( - config={ - "addon": "RTSPtoWebRTC", - "host": "fake-server", - "port": 8083, - }, - name="RTSPtoWebRTC", - slug="rtsp-to-webrtc", - uuid="1234", - ), - context={"source": config_entries.SOURCE_HASSIO}, - ) - - assert result.get("type") is FlowResultType.FORM - assert result.get("step_id") == "hassio_confirm" - assert not result.get("errors") - - with patch( - "rtsp_to_webrtc.client.Client.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ResponseError(), - ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result.get("type") is FlowResultType.ABORT - assert result.get("reason") == "server_failure" - - -async def test_options_flow( - hass: HomeAssistant, - config_entry: MockConfigEntry, - setup_integration: ComponentSetup, -) -> None: - """Test setting stun server in options flow.""" - with patch( - "homeassistant.components.rtsp_to_webrtc.async_setup_entry", - return_value=True, - ): - await setup_integration() - - assert config_entry.state is ConfigEntryState.LOADED - assert not config_entry.options - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "init" - data_schema = result["data_schema"].schema - assert set(data_schema) == {"stun_server"} - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "stun_server": "example.com:1234", - }, - ) - 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"] is FlowResultType.FORM - assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={} - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - await hass.async_block_till_done() - assert config_entry.options == {} diff --git a/tests/components/rtsp_to_webrtc/test_diagnostics.py b/tests/components/rtsp_to_webrtc/test_diagnostics.py deleted file mode 100644 index ad3522686b6..00000000000 --- a/tests/components/rtsp_to_webrtc/test_diagnostics.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Test nest diagnostics.""" - -from typing import Any - -from homeassistant.core import HomeAssistant - -from .conftest import ComponentSetup - -from tests.common import MockConfigEntry -from tests.components.diagnostics import get_diagnostics_for_config_entry -from tests.typing import ClientSessionGenerator - -THERMOSTAT_TYPE = "sdm.devices.types.THERMOSTAT" - - -async def test_entry_diagnostics( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - config_entry: MockConfigEntry, - rtsp_to_webrtc_client: Any, - setup_integration: ComponentSetup, -) -> None: - """Test config entry diagnostics.""" - await setup_integration() - - result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - assert "webrtc" in result diff --git a/tests/components/rtsp_to_webrtc/test_init.py b/tests/components/rtsp_to_webrtc/test_init.py deleted file mode 100644 index 985e76fa1d1..00000000000 --- a/tests/components/rtsp_to_webrtc/test_init.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Tests for RTSPtoWebRTC initialization.""" - -from __future__ import annotations - -import base64 -from typing import Any -from unittest.mock import patch - -import aiohttp -import pytest -import rtsp_to_webrtc - -from homeassistant.components.rtsp_to_webrtc import DOMAIN -from homeassistant.components.websocket_api import TYPE_RESULT -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 - -from .conftest import SERVER_URL, STREAM_SOURCE, ComponentSetup - -from tests.common import MockConfigEntry -from tests.test_util.aiohttp import AiohttpClientMocker -from tests.typing import WebSocketGenerator - -# The webrtc component does not inspect the details of the offer and answer, -# and is only a pass through. -OFFER_SDP = "v=0\r\no=carol 28908764872 28908764872 IN IP4 100.3.6.6\r\n..." -ANSWER_SDP = "v=0\r\no=bob 2890844730 2890844730 IN IP4 host.example.com\r\n..." - - -@pytest.fixture(autouse=True) -async def setup_homeassistant(hass: HomeAssistant): - """Set up the homeassistant integration.""" - await async_setup_component(hass, "homeassistant", {}) - - -@pytest.mark.usefixtures("rtsp_to_webrtc_client") -async def test_setup_success( - hass: HomeAssistant, - config_entry: MockConfigEntry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test successful setup and unload.""" - config_entry.add_to_hass(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - assert issue_registry.async_get_issue(DOMAIN, "deprecated") - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.LOADED - await hass.config_entries.async_unload(entries[0].entry_id) - await hass.async_block_till_done() - - assert not hass.data.get(DOMAIN) - assert entries[0].state is ConfigEntryState.NOT_LOADED - assert not issue_registry.async_get_issue(DOMAIN, "deprecated") - - -@pytest.mark.parametrize("config_entry_data", [{}]) -async def test_invalid_config_entry( - hass: HomeAssistant, rtsp_to_webrtc_client: Any, setup_integration: ComponentSetup -) -> None: - """Test a config entry with missing required fields.""" - await setup_integration() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.SETUP_ERROR - - -async def test_setup_server_failure( - hass: HomeAssistant, setup_integration: ComponentSetup -) -> None: - """Test server responds with a failure on startup.""" - with patch( - "rtsp_to_webrtc.client.Client.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ResponseError(), - ): - await setup_integration() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.SETUP_RETRY - - -async def test_setup_communication_failure( - hass: HomeAssistant, setup_integration: ComponentSetup -) -> None: - """Test unable to talk to server on startup.""" - with patch( - "rtsp_to_webrtc.client.Client.heartbeat", - side_effect=rtsp_to_webrtc.exceptions.ClientError(), - ): - await setup_integration() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].state is ConfigEntryState.SETUP_RETRY - - -@pytest.mark.usefixtures("mock_camera", "rtsp_to_webrtc_client") -async def test_offer_for_stream_source( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - hass_ws_client: WebSocketGenerator, - setup_integration: ComponentSetup, -) -> None: - """Test successful response from RTSPtoWebRTC server.""" - await setup_integration() - - aioclient_mock.post( - f"{SERVER_URL}/stream", - json={"sdp64": base64.b64encode(ANSWER_SDP.encode("utf-8")).decode("utf-8")}, - ) - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": OFFER_SDP, - } - ) - - response = await client.receive_json() - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "answer", - "answer": ANSWER_SDP, - } - - # Validate request parameters were sent correctly - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][2] == { - "sdp64": base64.b64encode(OFFER_SDP.encode("utf-8")).decode("utf-8"), - "url": STREAM_SOURCE, - } - - -@pytest.mark.usefixtures("mock_camera", "rtsp_to_webrtc_client") -async def test_offer_failure( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - hass_ws_client: WebSocketGenerator, - setup_integration: ComponentSetup, -) -> None: - """Test a transient failure talking to RTSPtoWebRTC server.""" - await setup_integration() - - aioclient_mock.post( - f"{SERVER_URL}/stream", - exc=aiohttp.ClientError, - ) - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": OFFER_SDP, - } - ) - - response = await client.receive_json() - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "error", - "code": "webrtc_offer_failed", - "message": "RTSPtoWebRTC server communication failure: ", - } From 04867f6ecc56c78bc48c0bc52e76d31b55daaa34 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 19:16:37 +0200 Subject: [PATCH 0243/1175] Fix capitalization and grammar in `simplefin` (#144246) --- homeassistant/components/simplefin/strings.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplefin/strings.json b/homeassistant/components/simplefin/strings.json index 3ac03fe2cc0..b3750a96b1e 100644 --- a/homeassistant/components/simplefin/strings.json +++ b/homeassistant/components/simplefin/strings.json @@ -2,22 +2,22 @@ "config": { "step": { "user": { - "description": "Please enter either a Claim Token or an Access URL.", + "description": "Please enter a SimpleFIN setup token.", "data": { - "api_token": "Claim Token or Access URL" + "api_token": "Setup token" } } }, "error": { "invalid_auth": "Authentication failed: This could be due to revoked access or incorrect credentials", - "claim_error": "The claim token either does not exist or has already been used claimed by someone else. Receiving this could mean that the user\u2019s transaction information has been compromised", - "invalid_claim_token": "The claim token is invalid and could not be decoded", - "payment_required": "You presented a valid access url, however payment is required before you can obtain data", - "url_error": "There was an issue parsing the Account URL" + "claim_error": "The setup token either does not exist or has already been used by someone else. Receiving this could mean that the user\u2019s transaction information has been compromised", + "invalid_claim_token": "The setup token is invalid and could not be decoded", + "payment_required": "You presented a valid access URL, however payment is required before you can obtain data", + "url_error": "There was an issue parsing the access URL" }, "abort": { - "missing_access_url": "Access URL or Claim Token missing", - "already_configured": "This Access URL is already configured." + "missing_access_url": "Access URL or setup token missing", + "already_configured": "This access URL is already configured." } }, "entity": { From cb6847b64c0604edff43c432ec0924def8283ecc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 May 2025 20:02:24 +0200 Subject: [PATCH 0244/1175] Remove deprecated services in SABnzbd (#144405) Co-authored-by: Franck Nijhof --- homeassistant/components/sabnzbd/__init__.py | 134 +----------------- homeassistant/components/sabnzbd/const.py | 9 -- homeassistant/components/sabnzbd/icons.json | 11 -- .../components/sabnzbd/quality_scale.yaml | 12 +- .../components/sabnzbd/services.yaml | 23 --- homeassistant/components/sabnzbd/strings.json | 52 +------ tests/components/sabnzbd/conftest.py | 2 +- tests/components/sabnzbd/test_config_flow.py | 2 +- tests/components/sabnzbd/test_init.py | 42 ------ 9 files changed, 11 insertions(+), 276 deletions(-) delete mode 100644 homeassistant/components/sabnzbd/services.yaml delete mode 100644 tests/components/sabnzbd/test_init.py diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 1f68781a3a2..4241f39778c 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -2,148 +2,18 @@ from __future__ import annotations -from collections.abc import Callable, Coroutine import logging -from typing import Any - -import voluptuous as vol from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady -from .const import ( - ATTR_API_KEY, - ATTR_SPEED, - DEFAULT_SPEED_LIMIT, - DOMAIN, - SERVICE_PAUSE, - SERVICE_RESUME, - SERVICE_SET_SPEED, -) from .coordinator import SabnzbdConfigEntry, SabnzbdUpdateCoordinator from .helpers import get_client PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.NUMBER, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -SERVICES = ( - SERVICE_PAUSE, - SERVICE_RESUME, - SERVICE_SET_SPEED, -) - -SERVICE_BASE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_API_KEY): cv.string, - } -) - -SERVICE_SPEED_SCHEMA = SERVICE_BASE_SCHEMA.extend( - { - vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, - } -) - -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) - - -@callback -def async_get_entry_for_service_call( - hass: HomeAssistant, call: ServiceCall -) -> SabnzbdConfigEntry: - """Get the entry ID related to a service call (by device ID).""" - call_data_api_key = call.data[ATTR_API_KEY] - - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[ATTR_API_KEY] == call_data_api_key: - return entry - - raise ValueError(f"No api for API key: {call_data_api_key}") - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SabNzbd Component.""" - - @callback - def extract_api( - 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.""" - config_entry = async_get_entry_for_service_call(hass, call) - coordinator = config_entry.runtime_data - - try: - await func(call, coordinator) - except Exception as err: - raise HomeAssistantError( - f"Error while executing {func.__name__}: {err}" - ) from err - - return wrapper - - @extract_api - async def async_pause_queue( - call: ServiceCall, coordinator: SabnzbdUpdateCoordinator - ) -> None: - ir.async_create_issue( - hass, - DOMAIN, - "pause_action_deprecated", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - breaks_in_ha_version="2025.6", - translation_key="pause_action_deprecated", - ) - await coordinator.sab_api.pause_queue() - - @extract_api - async def async_resume_queue( - call: ServiceCall, coordinator: SabnzbdUpdateCoordinator - ) -> None: - ir.async_create_issue( - hass, - DOMAIN, - "resume_action_deprecated", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - breaks_in_ha_version="2025.6", - translation_key="resume_action_deprecated", - ) - await coordinator.sab_api.resume_queue() - - @extract_api - async def async_set_queue_speed( - call: ServiceCall, coordinator: SabnzbdUpdateCoordinator - ) -> None: - ir.async_create_issue( - hass, - DOMAIN, - "set_speed_action_deprecated", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - breaks_in_ha_version="2025.6", - translation_key="set_speed_action_deprecated", - ) - speed = call.data.get(ATTR_SPEED) - await coordinator.sab_api.set_speed_limit(speed) - - for service, method, schema in ( - (SERVICE_PAUSE, async_pause_queue, SERVICE_BASE_SCHEMA), - (SERVICE_RESUME, async_resume_queue, SERVICE_BASE_SCHEMA), - (SERVICE_SET_SPEED, async_set_queue_speed, SERVICE_SPEED_SCHEMA), - ): - hass.services.async_register(DOMAIN, service, method, schema=schema) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: SabnzbdConfigEntry) -> bool: """Set up the SabNzbd Component.""" diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py index f05b3f19e98..66c71089b72 100644 --- a/homeassistant/components/sabnzbd/const.py +++ b/homeassistant/components/sabnzbd/const.py @@ -1,12 +1,3 @@ """Constants for the Sabnzbd component.""" DOMAIN = "sabnzbd" - -ATTR_SPEED = "speed" -ATTR_API_KEY = "api_key" - -DEFAULT_SPEED_LIMIT = "100" - -SERVICE_PAUSE = "pause" -SERVICE_RESUME = "resume" -SERVICE_SET_SPEED = "set_speed" diff --git a/homeassistant/components/sabnzbd/icons.json b/homeassistant/components/sabnzbd/icons.json index b0a72040b4b..b06a1e316a1 100644 --- a/homeassistant/components/sabnzbd/icons.json +++ b/homeassistant/components/sabnzbd/icons.json @@ -13,16 +13,5 @@ "default": "mdi:speedometer" } } - }, - "services": { - "pause": { - "service": "mdi:pause" - }, - "resume": { - "service": "mdi:play" - }, - "set_speed": { - "service": "mdi:speedometer" - } } } diff --git a/homeassistant/components/sabnzbd/quality_scale.yaml b/homeassistant/components/sabnzbd/quality_scale.yaml index a1d6fc076b2..7e2a8fe9e26 100644 --- a/homeassistant/components/sabnzbd/quality_scale.yaml +++ b/homeassistant/components/sabnzbd/quality_scale.yaml @@ -1,6 +1,9 @@ rules: # Bronze - action-setup: done + action-setup: + status: exempt + comment: | + The integration does not provide any actions. appropriate-polling: done brands: done common-modules: done @@ -10,7 +13,7 @@ rules: docs-actions: status: exempt comment: | - The integration has deprecated the actions, thus the documentation has been removed. + The integration does not provide any actions. docs-high-level-description: done docs-installation-instructions: done docs-removal-instructions: done @@ -26,10 +29,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: todo - comment: | - Raise ServiceValidationError in async_get_entry_for_service_call. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml deleted file mode 100644 index f1eea1c9469..00000000000 --- a/homeassistant/components/sabnzbd/services.yaml +++ /dev/null @@ -1,23 +0,0 @@ -pause: - fields: - api_key: - required: true - selector: - text: -resume: - fields: - api_key: - required: true - selector: - text: -set_speed: - fields: - api_key: - required: true - selector: - text: - speed: - example: 100 - default: 100 - selector: - text: diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 0ac8b93c57f..601f1153b82 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -32,7 +32,7 @@ "name": "[%key:common::action::pause%]" }, "resume": { - "name": "[%key:component::sabnzbd::services::resume::name%]" + "name": "Resume" } }, "number": { @@ -76,56 +76,6 @@ } } }, - "services": { - "pause": { - "name": "[%key:common::action::pause%]", - "description": "Pauses downloads.", - "fields": { - "api_key": { - "name": "SABnzbd API key", - "description": "The SABnzbd API key to pause downloads." - } - } - }, - "resume": { - "name": "Resume", - "description": "Resumes downloads.", - "fields": { - "api_key": { - "name": "[%key:component::sabnzbd::services::pause::fields::api_key::name%]", - "description": "The SABnzbd API key to resume downloads." - } - } - }, - "set_speed": { - "name": "Set speed", - "description": "Sets the download speed limit.", - "fields": { - "api_key": { - "name": "[%key:component::sabnzbd::services::pause::fields::api_key::name%]", - "description": "The SABnzbd API key to set speed limit." - }, - "speed": { - "name": "Speed", - "description": "Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely." - } - } - } - }, - "issues": { - "pause_action_deprecated": { - "title": "SABnzbd pause action deprecated", - "description": "The 'Pause' action is deprecated and will be removed in a future version. Please use the 'Pause' button instead. To remove this issue, please adjust automations accordingly and restart Home Assistant." - }, - "resume_action_deprecated": { - "title": "SABnzbd resume action deprecated", - "description": "The 'Resume' action is deprecated and will be removed in a future version. Please use the 'Resume' button instead. To remove this issue, please adjust automations accordingly and restart Home Assistant." - }, - "set_speed_action_deprecated": { - "title": "SABnzbd set_speed action deprecated", - "description": "The 'Set speed' action is deprecated and will be removed in a future version. Please use the 'Speedlimit' number entity instead. To remove this issue, please adjust automations accordingly and restart Home Assistant." - } - }, "exceptions": { "service_call_exception": { "message": "Unable to send command to SABnzbd due to a connection error, try again later" diff --git a/tests/components/sabnzbd/conftest.py b/tests/components/sabnzbd/conftest.py index 6fa3d14e880..4d85055bb58 100644 --- a/tests/components/sabnzbd/conftest.py +++ b/tests/components/sabnzbd/conftest.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch import pytest -from homeassistant.components.sabnzbd import DOMAIN +from homeassistant.components.sabnzbd.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 797af63c096..98422e931ec 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -6,7 +6,7 @@ from pysabnzbd import SabnzbdApiException import pytest from homeassistant import config_entries -from homeassistant.components.sabnzbd import DOMAIN +from homeassistant.components.sabnzbd.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py deleted file mode 100644 index 9b833875bbc..00000000000 --- a/tests/components/sabnzbd/test_init.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for the SABnzbd Integration.""" - -import pytest - -from homeassistant.components.sabnzbd.const import ( - ATTR_API_KEY, - DOMAIN, - SERVICE_PAUSE, - SERVICE_RESUME, - SERVICE_SET_SPEED, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - - -@pytest.mark.parametrize( - ("service", "issue_id"), - [ - (SERVICE_RESUME, "resume_action_deprecated"), - (SERVICE_PAUSE, "pause_action_deprecated"), - (SERVICE_SET_SPEED, "set_speed_action_deprecated"), - ], -) -@pytest.mark.usefixtures("setup_integration") -async def test_deprecated_service_creates_issue( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - service: str, - issue_id: str, -) -> None: - """Test that deprecated actions creates an issue.""" - await hass.services.async_call( - DOMAIN, - service, - {ATTR_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0"}, - blocking=True, - ) - - issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id) - assert issue - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.breaks_in_ha_version == "2025.6" From 7ee9f0af2de9ddec673a7ef73dc25df781ddf4ca Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 May 2025 20:41:04 +0200 Subject: [PATCH 0245/1175] Add cooktop operating state to SmartThings (#144500) --- .../components/smartthings/icons.json | 11 ++ .../components/smartthings/sensor.py | 10 ++ .../components/smartthings/strings.json | 9 ++ .../smartthings/snapshots/test_sensor.ambr | 116 ++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 3125bd65548..b6e33e1a142 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -48,6 +48,17 @@ "default": "mdi:car-coolant-level" } }, + "sensor": { + "cooktop_operating_state": { + "default": "mdi:stove", + "state": { + "ready": "mdi:play-speed", + "run": "mdi:play", + "paused": "mdi:pause", + "finished": "mdi:food-turkey" + } + } + }, "switch": { "bubble_soak": { "default": "mdi:water-off", diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 2d6451fa279..3c32df271a9 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -265,6 +265,16 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, + Capability.CUSTOM_COOKTOP_OPERATING_STATE: { + Attribute.COOKTOP_OPERATING_STATE: [ + SmartThingsSensorEntityDescription( + key=Attribute.COOKTOP_OPERATING_STATE, + translation_key="cooktop_operating_state", + device_class=SensorDeviceClass.ENUM, + options_attribute=Attribute.SUPPORTED_COOKTOP_OPERATING_STATE, + ) + ] + }, Capability.DISHWASHER_OPERATING_STATE: { Attribute.MACHINE_STATE: [ SmartThingsSensorEntityDescription( diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 81f4d34c8bb..828948910c2 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -172,6 +172,15 @@ "tested": "Tested" } }, + "cooktop_operating_state": { + "name": "Operating state", + "state": { + "ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]", + "run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]", + "paused": "[%key:common::state::paused%]", + "finished": "[%key:component::smartthings::entity::sensor::oven_job_state::state::finished%]" + } + }, "dishwasher_machine_state": { "name": "Machine state", "state": { diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index ad073a1d670..8c631ed6983 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -2578,6 +2578,65 @@ 'state': '27', }) # --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_operating_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ready', + 'run', + 'paused', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_operating_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operating state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooktop_operating_state', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_main_custom.cooktopOperatingState_cooktopOperatingState_cooktopOperatingState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_operating_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Operating state', + 'options': list([ + 'ready', + 'run', + 'paused', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_operating_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ready', + }) +# --- # name: test_all_entities[da_ks_microwave_0101x][sensor.microwave_completion_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3568,6 +3627,63 @@ 'state': 'running', }) # --- +# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_operating_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'run', + 'ready', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vulcan_operating_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operating state', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooktop_operating_state', + 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_custom.cooktopOperatingState_cooktopOperatingState_cooktopOperatingState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_operating_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Vulcan Operating state', + 'options': list([ + 'run', + 'ready', + ]), + }), + 'context': , + 'entity_id': 'sensor.vulcan_operating_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ready', + }) +# --- # name: test_all_entities[da_ks_range_0101x][sensor.vulcan_oven_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 34dbd1fb108ce29510160165c8a3c805f205ac9d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 May 2025 21:03:41 +0200 Subject: [PATCH 0246/1175] Add hob support to SmartThings (#144493) * Add hob support to SmartThings * Add hob support to SmartThings * Add hob support to SmartThings * Fix * Update homeassistant/components/smartthings/icons.json --- .../components/smartthings/__init__.py | 5 + .../components/smartthings/icons.json | 18 + .../components/smartthings/sensor.py | 157 +++++-- .../components/smartthings/strings.json | 16 + .../device_status/da_ks_cooktop_31001.json | 10 +- .../smartthings/snapshots/test_sensor.ambr | 424 ++++++++++++++++++ 6 files changed, 577 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index cec71f91750..fe1b965db30 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -493,6 +493,11 @@ def process_status(status: dict[str, ComponentStatus]) -> dict[str, ComponentSta ) if disabled_components is not None: for component in disabled_components: + # Burner components are named burner-06 + # but disabledComponents contain burner-6 + if "burner" in component: + burner_id = int(component.split("-")[-1]) + component = f"burner-0{burner_id}" if component in status: del status[component] for component_status in status.values(): diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index b6e33e1a142..51978590e2e 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -57,6 +57,24 @@ "paused": "mdi:pause", "finished": "mdi:food-turkey" } + }, + "manual_level": { + "default": "mdi:radiator", + "state": { + "0": "mdi:radiator-off" + } + }, + "heating_mode": { + "state": { + "off": "mdi:power", + "manual": "mdi:cog", + "boost": "mdi:flash", + "keep_warm": "mdi:fire", + "quick_preheat": "mdi:heat-wave", + "defrost": "mdi:car-defrost-rear", + "melt": "mdi:snowflake-melt", + "simmer": "mdi:fire" + } } }, "switch": { diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 3c32df271a9..fac503399a9 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -45,6 +45,17 @@ THERMOSTAT_CAPABILITIES = { Capability.THERMOSTAT_MODE, } +COOKTOP_HEATING_MODES = { + "off": "off", + "manual": "manual", + "boost": "boost", + "keepWarm": "keep_warm", + "quickPreheat": "quick_preheat", + "defrost": "defrost", + "melt": "melt", + "simmer": "simmer", +} + JOB_STATE_MAP = { "airWash": "air_wash", "airwash": "air_wash", @@ -133,6 +144,9 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): extra_state_attributes_fn: Callable[[Any], dict[str, Any]] | None = None capability_ignore_list: list[set[Capability]] | None = None options_attribute: Attribute | None = None + options_map: dict[str, str] | None = None + translation_placeholders_fn: Callable[[str], dict[str, str]] | None = None + component_fn: Callable[[str], bool] | None = None exists_fn: Callable[[Status], bool] | None = None use_temperature_unit: bool = False deprecated: Callable[[ComponentStatus], str | None] | None = None @@ -265,6 +279,31 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, + Capability.SAMSUNG_CE_COOKTOP_HEATING_POWER: { + Attribute.MANUAL_LEVEL: [ + SmartThingsSensorEntityDescription( + key=Attribute.MANUAL_LEVEL, + translation_key="manual_level", + translation_placeholders_fn=lambda component: { + "burner_id": component.split("-0")[-1] + }, + component_fn=lambda component: component.startswith("burner-0"), + ) + ], + Attribute.HEATING_MODE: [ + SmartThingsSensorEntityDescription( + key=Attribute.HEATING_MODE, + translation_key="heating_mode", + options_attribute=Attribute.SUPPORTED_HEATING_MODES, + options_map=COOKTOP_HEATING_MODES, + device_class=SensorDeviceClass.ENUM, + translation_placeholders_fn=lambda component: { + "burner_id": component.split("-0")[-1] + }, + component_fn=lambda component: component.startswith("burner-0"), + ) + ], + }, Capability.CUSTOM_COOKTOP_OPERATING_STATE: { Attribute.COOKTOP_OPERATING_STATE: [ SmartThingsSensorEntityDescription( @@ -1038,59 +1077,72 @@ async def async_setup_entry( for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks for capability, attributes in CAPABILITY_TO_SENSORS.items(): - if capability in device.status[MAIN]: - for attribute, descriptions in attributes.items(): - for description in descriptions: - if ( - not description.capability_ignore_list - or not any( - all( - capability in device.status[MAIN] - for capability in capability_list - ) - for capability_list in description.capability_ignore_list - ) - ) and ( - not description.exists_fn - or description.exists_fn( - device.status[MAIN][capability][attribute] - ) - ): + for component, capabilities in device.status.items(): + if capability in capabilities: + for attribute, descriptions in attributes.items(): + for description in descriptions: if ( - description.deprecated - and ( - reason := description.deprecated( - device.status[MAIN] + ( + not description.capability_ignore_list + or not any( + all( + capability in device.status[MAIN] + for capability in capability_list + ) + for capability_list in description.capability_ignore_list + ) + ) + and ( + not description.exists_fn + or description.exists_fn( + device.status[MAIN][capability][attribute] + ) + ) + and ( + component == MAIN + or ( + description.component_fn is not None + and description.component_fn(component) ) ) - is not None ): - if deprecate_entity( - hass, - entity_registry, - SENSOR_DOMAIN, - f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}", - f"deprecated_{reason}", - ): - entities.append( - SmartThingsSensor( - entry_data.client, - device, - description, - capability, - attribute, + if ( + description.deprecated + and ( + reason := description.deprecated( + device.status[MAIN] ) ) - continue - entities.append( - SmartThingsSensor( - entry_data.client, - device, - description, - capability, - attribute, + is not None + ): + if deprecate_entity( + hass, + entity_registry, + SENSOR_DOMAIN, + f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}", + f"deprecated_{reason}", + ): + entities.append( + SmartThingsSensor( + entry_data.client, + device, + description, + MAIN, + capability, + attribute, + ) + ) + continue + entities.append( + SmartThingsSensor( + entry_data.client, + device, + description, + component, + capability, + attribute, + ) ) - ) async_add_entities(entities) @@ -1105,6 +1157,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): client: SmartThings, device: FullDevice, entity_description: SmartThingsSensorEntityDescription, + component: str, capability: Capability, attribute: Attribute, ) -> None: @@ -1112,16 +1165,22 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): capabilities_to_subscribe = {capability} if entity_description.use_temperature_unit: capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT) - super().__init__(client, device, capabilities_to_subscribe) - self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{entity_description.key}" + super().__init__(client, device, capabilities_to_subscribe, component=component) + self._attr_unique_id = f"{device.device.device_id}_{component}_{capability}_{attribute}_{entity_description.key}" self._attribute = attribute self.capability = capability self.entity_description = entity_description + if self.entity_description.translation_placeholders_fn: + self._attr_translation_placeholders = ( + self.entity_description.translation_placeholders_fn(component) + ) @property def native_value(self) -> str | float | datetime | int | None: """Return the state of the sensor.""" res = self.get_attribute_value(self.capability, self._attribute) + if options_map := self.entity_description.options_map: + return options_map.get(res) return self.entity_description.value_fn(res) @property @@ -1158,5 +1217,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): ) ) is None: return [] + if options_map := self.entity_description.options_map: + return [options_map[option] for option in options] return [option.lower() for option in options] return super().options diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 828948910c2..9cec158b5a9 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -181,6 +181,22 @@ "finished": "[%key:component::smartthings::entity::sensor::oven_job_state::state::finished%]" } }, + "manual_level": { + "name": "Burner {burner_id} level" + }, + "heating_mode": { + "name": "Burner {burner_id} heating mode", + "state": { + "off": "[%key:common::state::off%]", + "manual": "[%key:common::state::manual%]", + "boost": "Boost", + "keep_warm": "Keep warm", + "quick_preheat": "Quick preheat", + "defrost": "Defrost", + "melt": "Melt", + "simmer": "Simmer" + } + }, "dishwasher_machine_state": { "name": "Machine state", "state": { diff --git a/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json b/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json index 5ca8f56fbbf..ab836de52ad 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json @@ -9,11 +9,11 @@ }, "samsungce.cooktopHeatingPower": { "manualLevel": { - "value": 0, + "value": 5, "timestamp": "2025-03-26T05:57:23.203Z" }, "heatingMode": { - "value": "manual", + "value": "boost", "timestamp": "2025-03-25T18:18:28.550Z" }, "manualLevelMin": { @@ -95,7 +95,7 @@ "main": { "custom.disabledComponents": { "disabledComponents": { - "value": ["burner-6"], + "value": ["burner-05", "burner-6"], "timestamp": "2025-03-25T18:18:28.464Z" } }, @@ -467,11 +467,11 @@ }, "samsungce.cooktopHeatingPower": { "manualLevel": { - "value": 0, + "value": 2, "timestamp": "2025-03-26T07:27:58.652Z" }, "heatingMode": { - "value": "manual", + "value": "keepWarm", "timestamp": "2025-03-25T18:18:28.550Z" }, "manualLevelMin": { diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 8c631ed6983..df943079fe2 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -2578,6 +2578,430 @@ 'state': '27', }) # --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_1_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 1 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 1 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_1_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_1_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 1 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 1 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_1_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_2_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 2 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 2 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_2_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'boost', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_2_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 2 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 2 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_2_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_3_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 3 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 3 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_3_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'keep_warm', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_3_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 3 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 3 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_3_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_4_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 4 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 4 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_4_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_4_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 4 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 4 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_4_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_operating_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 337c64d69df2c1b3d0e58a818de720e0ea44e2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Thu, 8 May 2025 21:20:02 +0200 Subject: [PATCH 0247/1175] Add miele devices dynamically (#144216) * Use device class transation * WIP Cleanup tests * First 3 * First 3 * Button * Climate * Light * Switch * New and cleaner variant * Update homeassistant/components/miele/entity.py --------- Co-authored-by: Josef Zweck --- .../components/miele/binary_sensor.py | 22 +++- homeassistant/components/miele/button.py | 21 ++- homeassistant/components/miele/climate.py | 30 +++-- homeassistant/components/miele/coordinator.py | 14 ++ homeassistant/components/miele/fan.py | 21 ++- homeassistant/components/miele/light.py | 21 ++- homeassistant/components/miele/sensor.py | 60 +++++---- homeassistant/components/miele/switch.py | 40 ++++-- tests/components/miele/conftest.py | 2 +- .../components/miele/fixtures/5_devices.json | 124 +++++++++++++++++- .../fixtures/action_washing_machine.json | 8 +- .../miele/snapshots/test_diagnostics.ambr | 25 ++++ tests/components/miele/test_binary_sensor.py | 5 +- tests/components/miele/test_button.py | 9 +- tests/components/miele/test_climate.py | 9 +- tests/components/miele/test_fan.py | 11 +- tests/components/miele/test_init.py | 53 +++++++- tests/components/miele/test_light.py | 9 +- tests/components/miele/test_sensor.py | 5 +- tests/components/miele/test_switch.py | 9 +- 20 files changed, 389 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/miele/binary_sensor.py b/homeassistant/components/miele/binary_sensor.py index 5eb9eccc5df..9b0868beed4 100644 --- a/homeassistant/components/miele/binary_sensor.py +++ b/homeassistant/components/miele/binary_sensor.py @@ -263,13 +263,23 @@ async def async_setup_entry( ) -> None: """Set up the binary sensor platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - async_add_entities( - MieleBinarySensor(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in BINARY_SENSOR_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices() -> None: + nonlocal added_devices + + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices + + async_add_entities( + MieleBinarySensor(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in BINARY_SENSOR_TYPES + if device_id in new_devices_set and device.device_type in definition.types + ) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleBinarySensor(MieleEntity, BinarySensorEntity): diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index 70d4489e9be..b749ce364f0 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -111,13 +111,22 @@ async def async_setup_entry( ) -> None: """Set up the button platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - async_add_entities( - MieleButton(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in BUTTON_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices() -> None: + nonlocal added_devices + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices + + async_add_entities( + MieleButton(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in BUTTON_TYPES + if device_id in new_devices_set and device.device_type in definition.types + ) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleButton(MieleEntity, ButtonEntity): diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 22257448e3a..4324444d987 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -131,16 +131,30 @@ async def async_setup_entry( ) -> None: """Set up the climate platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - async_add_entities( - MieleClimate(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in CLIMATE_TYPES - if ( - device.device_type in definition.types - and (definition.description.value_fn(device) not in DISABLED_TEMP_ENTITIES) + def _async_add_new_devices() -> None: + nonlocal added_devices + + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices + + async_add_entities( + MieleClimate(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in CLIMATE_TYPES + if ( + device_id in new_devices_set + and device.device_type in definition.types + and ( + definition.description.value_fn(device) + not in DISABLED_TEMP_ENTITIES + ) + ) ) - ) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleClimate(MieleEntity, ClimateEntity): diff --git a/homeassistant/components/miele/coordinator.py b/homeassistant/components/miele/coordinator.py index 8902f0f173a..27456ffe04c 100644 --- a/homeassistant/components/miele/coordinator.py +++ b/homeassistant/components/miele/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio.timeouts +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging @@ -33,6 +34,11 @@ class MieleCoordinatorData: class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): """Coordinator for Miele data.""" + config_entry: MieleConfigEntry + new_device_callbacks: list[Callable[[dict[str, MieleDevice]], None]] = [] + known_devices: set[str] = set() + devices: dict[str, MieleDevice] = {} + def __init__( self, hass: HomeAssistant, @@ -56,12 +62,20 @@ class MieleDataUpdateCoordinator(DataUpdateCoordinator[MieleCoordinatorData]): device_id: MieleDevice(device) for device_id, device in devices_json.items() } + self.devices = devices actions = {} for device_id in devices: actions_json = await self.api.get_actions(device_id) actions[device_id] = MieleAction(actions_json) return MieleCoordinatorData(devices=devices, actions=actions) + def async_add_devices(self, added_devices: set[str]) -> tuple[set[str], set[str]]: + """Add devices.""" + current_devices = set(self.devices) + new_devices: set[str] = current_devices - added_devices + + return (new_devices, current_devices) + async def callback_update_data(self, devices_json: dict[str, dict]) -> None: """Handle data update from the API.""" devices = { diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index fcd74a93bfb..e8bea197f58 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -65,13 +65,22 @@ async def async_setup_entry( ) -> None: """Set up the fan platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - async_add_entities( - MieleFan(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in FAN_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices() -> None: + nonlocal added_devices + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices + + async_add_entities( + MieleFan(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in FAN_TYPES + if device_id in new_devices_set and device.device_type in definition.types + ) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleFan(MieleEntity, FanEntity): diff --git a/homeassistant/components/miele/light.py b/homeassistant/components/miele/light.py index 0fbc8124be8..678c2f92382 100644 --- a/homeassistant/components/miele/light.py +++ b/homeassistant/components/miele/light.py @@ -85,13 +85,22 @@ async def async_setup_entry( ) -> None: """Set up the light platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - async_add_entities( - MieleLight(coordinator, device_id, definition.description) - for device_id, device in coordinator.data.devices.items() - for definition in LIGHT_TYPES - if device.device_type in definition.types - ) + def _async_add_new_devices() -> None: + nonlocal added_devices + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices + + async_add_entities( + MieleLight(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in LIGHT_TYPES + if device_id in new_devices_set and device.device_type in definition.types + ) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleLight(MieleEntity, LightEntity): diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 2bf1fbd1202..12f035485be 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -426,33 +426,43 @@ async def async_setup_entry( ) -> None: """Set up the sensor platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - entities: list = [] - entity_class: type[MieleSensor] - for device_id, device in coordinator.data.devices.items(): - for definition in SENSOR_TYPES: - if device.device_type in definition.types: - match definition.description.key: - case "state_status": - entity_class = MieleStatusSensor - case "state_program_id": - entity_class = MieleProgramIdSensor - case "state_program_phase": - entity_class = MielePhaseSensor - case _: - entity_class = MieleSensor - if ( - definition.description.device_class == SensorDeviceClass.TEMPERATURE - and definition.description.value_fn(device) - == DISABLED_TEMPERATURE / 100 - ): - # Don't create entity if API signals that datapoint is disabled - continue - entities.append( - entity_class(coordinator, device_id, definition.description) - ) + def _async_add_new_devices() -> None: + nonlocal added_devices + entities: list = [] + entity_class: type[MieleSensor] + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices - async_add_entities(entities) + for device_id, device in coordinator.data.devices.items(): + for definition in SENSOR_TYPES: + if device.device_type in definition.types: + match definition.description.key: + case "state_status": + entity_class = MieleStatusSensor + case "state_program_id": + entity_class = MieleProgramIdSensor + case "state_program_phase": + entity_class = MielePhaseSensor + case _: + entity_class = MieleSensor + if ( + device_id in new_devices_set + and definition.description.device_class + == SensorDeviceClass.TEMPERATURE + and definition.description.value_fn(device) + == DISABLED_TEMPERATURE / 100 + ): + # Don't create entity if API signals that datapoint is disabled + continue + entities.append( + entity_class(coordinator, device_id, definition.description) + ) + async_add_entities(entities) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() APPLIANCE_ICONS = { diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index 427d90968b7..4cd237aa724 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -116,22 +116,34 @@ async def async_setup_entry( ) -> None: """Set up the switch platform.""" coordinator = config_entry.runtime_data + added_devices: set[str] = set() - entities: list = [] - entity_class: type[MieleSwitch] - for device_id, device in coordinator.data.devices.items(): - for definition in SWITCH_TYPES: - if device.device_type in definition.types: - match definition.description.key: - case "poweronoff": - entity_class = MielePowerSwitch - case "supercooling" | "superfreezing": - entity_class = MieleSuperSwitch + def _async_add_new_devices() -> None: + nonlocal added_devices + new_devices_set, current_devices = coordinator.async_add_devices(added_devices) + added_devices = current_devices - entities.append( - entity_class(coordinator, device_id, definition.description) - ) - async_add_entities(entities) + entities = [] + for device_id, device in coordinator.data.devices.items(): + for definition in SWITCH_TYPES: + if ( + device_id in new_devices_set + and device.device_type in definition.types + ): + entity_class: type[MieleSwitch] = MieleSwitch + match definition.description.key: + case "poweronoff": + entity_class = MielePowerSwitch + case "supercooling" | "superfreezing": + entity_class = MieleSuperSwitch + + entities.append( + entity_class(coordinator, device_id, definition.description) + ) + async_add_entities(entities) + + config_entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices)) + _async_add_new_devices() class MieleSwitch(MieleEntity, SwitchEntity): diff --git a/tests/components/miele/conftest.py b/tests/components/miele/conftest.py index 6df5b73ccc2..8e3b5628ed4 100644 --- a/tests/components/miele/conftest.py +++ b/tests/components/miele/conftest.py @@ -141,7 +141,7 @@ async def setup_platform( with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - yield + yield mock_config_entry @pytest.fixture diff --git a/tests/components/miele/fixtures/5_devices.json b/tests/components/miele/fixtures/5_devices.json index 93b5bf9f887..1753d982fb6 100644 --- a/tests/components/miele/fixtures/5_devices.json +++ b/tests/components/miele/fixtures/5_devices.json @@ -356,6 +356,106 @@ "batteryLevel": null } }, + "DummyAppliance_18": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 18, + "value_localized": "Cooker Hood" + }, + "deviceName": "", + "protocolVersion": 2, + "deviceIdentLabel": { + "fabNumber": "", + "fabIndex": "64", + "techType": "Fläkt", + "matNumber": "", + "swids": ["", "", "", "<...>"] + }, + "xkmIdentLabel": { + "techType": "EK039W", + "releaseVersion": "02.72" + } + }, + "state": { + "ProgramID": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 4608, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": 2, + "light": 1, + "elapsedTime": {}, + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": 0, + "value_localized": "0", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + }, "Dummy_Appliance_4": { "ident": { "type": { @@ -402,10 +502,28 @@ { "value_raw": -32768, "value_localized": null, "unit": "Celsius" }, { "value_raw": -32768, "value_localized": null, "unit": "Celsius" } ], + "coreTargetTemperature": [ + { "value_raw": 7500, "value_localized": "75.0", "unit": "Celsius" } + ], + "coreTemperature": [ + { "value_raw": 5200, "value_localized": "52.0", "unit": "Celsius" } + ], "temperature": [ - { "value_raw": -32768, "value_localized": null, "unit": "Celsius" }, - { "value_raw": -32768, "value_localized": null, "unit": "Celsius" }, - { "value_raw": -32768, "value_localized": null, "unit": "Celsius" } + { + "value_raw": 17500, + "value_localized": "175.0", + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } ], "signalInfo": false, "signalFailure": false, diff --git a/tests/components/miele/fixtures/action_washing_machine.json b/tests/components/miele/fixtures/action_washing_machine.json index 5e8e00306f4..363d3ae6c63 100644 --- a/tests/components/miele/fixtures/action_washing_machine.json +++ b/tests/components/miele/fixtures/action_washing_machine.json @@ -5,7 +5,13 @@ "startTime": [], "ventilationStep": [], "programId": [], - "targetTemperature": [], + "targetTemperature": [ + { + "zone": 1, + "min": -28, + "max": -14 + } + ], "deviceName": true, "powerOn": true, "powerOff": false, diff --git a/tests/components/miele/snapshots/test_diagnostics.ambr b/tests/components/miele/snapshots/test_diagnostics.ambr index aa564205867..92e312f8d73 100644 --- a/tests/components/miele/snapshots/test_diagnostics.ambr +++ b/tests/components/miele/snapshots/test_diagnostics.ambr @@ -36,6 +36,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -64,6 +69,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -92,6 +102,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -120,6 +135,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), @@ -689,6 +709,11 @@ 'startTime': list([ ]), 'targetTemperature': list([ + dict({ + 'max': -14, + 'min': -28, + 'zone': 1, + }), ]), 'ventilationStep': list([ ]), diff --git a/tests/components/miele/test_binary_sensor.py b/tests/components/miele/test_binary_sensor.py index fe1f4b896c5..d56128a1a76 100644 --- a/tests/components/miele/test_binary_sensor.py +++ b/tests/components/miele/test_binary_sensor.py @@ -17,11 +17,10 @@ from tests.common import MockConfigEntry, snapshot_platform async def test_binary_sensor_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test binary sensor state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/miele/test_button.py b/tests/components/miele/test_button.py index 9bf5f2f3f54..f4331bc40c5 100644 --- a/tests/components/miele/test_button.py +++ b/tests/components/miele/test_button.py @@ -24,21 +24,20 @@ ENTITY_ID = "button.washing_machine_start" async def test_button_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test button entity state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_button_press( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test button press.""" @@ -54,7 +53,7 @@ async def test_button_press( async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test handling of exception from API.""" mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index f03edada841..29124eda893 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -33,20 +33,19 @@ SERVICE_SET_TEMPERATURE = "set_temperature" async def test_climate_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test climate entity state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) async def test_set_target( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test the climate can be turned on/off.""" @@ -64,7 +63,7 @@ async def test_set_target( async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test handling of exception from API.""" mock_miele_client.set_target_temperature.side_effect = ClientError diff --git a/tests/components/miele/test_fan.py b/tests/components/miele/test_fan.py index 87f80614551..ce0a4936b41 100644 --- a/tests/components/miele/test_fan.py +++ b/tests/components/miele/test_fan.py @@ -25,14 +25,13 @@ ENTITY_ID = "fan.hood_fan" async def test_fan_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test fan entity state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.parametrize("load_device_file", ["fan_devices.json"]) @@ -46,7 +45,7 @@ async def test_fan_states( async def test_fan_control( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, expected_argument: dict[str, Any], ) -> None: @@ -74,7 +73,7 @@ async def test_fan_control( async def test_fan_set_speed( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, percentage: int, expected_argument: dict[str, Any], @@ -102,7 +101,7 @@ async def test_fan_set_speed( async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, ) -> None: """Test handling of exception from API.""" diff --git a/tests/components/miele/test_init.py b/tests/components/miele/test_init.py index 7a81ef78065..37ea5a57ed4 100644 --- a/tests/components/miele/test_init.py +++ b/tests/components/miele/test_init.py @@ -1,10 +1,12 @@ """Tests for init module.""" +from datetime import timedelta import http import time from unittest.mock import MagicMock from aiohttp import ClientConnectionError +from freezegun.api import FrozenDateTimeFactory from pymiele import OAUTH2_TOKEN import pytest from syrupy import SnapshotAssertion @@ -17,7 +19,11 @@ from homeassistant.setup import async_setup_component from . import setup_integration -from tests.common import MockConfigEntry +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import WebSocketGenerator @@ -157,3 +163,48 @@ async def test_device_remove_devices( old_device_entry.id, mock_config_entry.entry_id ) assert response["success"] + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_setup_all_platforms( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + load_device_file: str, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that all platforms can be set up.""" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("binary_sensor.freezer_door").state == "off" + assert hass.states.get("binary_sensor.hood_problem").state == "off" + + assert ( + hass.states.get("button.washing_machine_start").object_id + == "washing_machine_start" + ) + + assert hass.states.get("climate.freezer").state == "cool" + assert hass.states.get("light.hood_light").state == "on" + + assert hass.states.get("sensor.freezer_temperature").state == "-18.0" + assert hass.states.get("sensor.washing_machine").state == "off" + + assert hass.states.get("switch.washing_machine_power").state == "off" + + # Add two devices and let the clock tick for 130 seconds + freezer.tick(timedelta(seconds=130)) + mock_miele_client.get_devices.return_value = load_json_object_fixture( + "5_devices.json", DOMAIN + ) + + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 6 + + # Check a sample sensor for each new device + assert hass.states.get("sensor.dishwasher").state == "in_use" + assert hass.states.get("sensor.oven_temperature").state == "175.0" diff --git a/tests/components/miele/test_light.py b/tests/components/miele/test_light.py index 286c2df0dd8..9da6f5c686a 100644 --- a/tests/components/miele/test_light.py +++ b/tests/components/miele/test_light.py @@ -23,14 +23,13 @@ ENTITY_ID = "light.hood_light" async def test_light_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test light entity state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.parametrize( @@ -43,7 +42,7 @@ async def test_light_states( async def test_light_toggle( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, light_state: int, ) -> None: @@ -67,7 +66,7 @@ async def test_light_toggle( async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, ) -> None: """Test handling of exception from API.""" diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index 0a12a9e85e4..b87a165735f 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -17,11 +17,10 @@ from tests.common import MockConfigEntry, snapshot_platform async def test_sensor_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test sensor state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/miele/test_switch.py b/tests/components/miele/test_switch.py index fa5e9360da6..038dc781d40 100644 --- a/tests/components/miele/test_switch.py +++ b/tests/components/miele/test_switch.py @@ -23,14 +23,13 @@ ENTITY_ID = "switch.freezer_superfreezing" async def test_switch_states( hass: HomeAssistant, mock_miele_client: MagicMock, - mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, - setup_platform: None, + setup_platform: MockConfigEntry, ) -> None: """Test switch entity state.""" - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) @pytest.mark.parametrize( @@ -51,7 +50,7 @@ async def test_switch_states( async def test_switching( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, entity: str, ) -> None: @@ -81,7 +80,7 @@ async def test_switching( async def test_api_failure( hass: HomeAssistant, mock_miele_client: MagicMock, - setup_platform: None, + setup_platform: MockConfigEntry, service: str, entity: str, ) -> None: From 5df09c4f13ed6c306db73239e68d81049bf9f1f2 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 8 May 2025 21:54:09 +0200 Subject: [PATCH 0248/1175] Add missing hyphen to "single-board computers" in `homekit` (#144505) --- homeassistant/components/homekit/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index dcdf6892dc2..e6507c4a912 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -39,7 +39,7 @@ "camera_copy": "Cameras that support native H.264 streams", "camera_audio": "Cameras that support audio" }, - "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", + "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single-board computers.", "title": "Camera configuration" }, "advanced": { From 374b3ac6c612a5abefaebbe8972ff6d6b0f62d2b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 8 May 2025 21:54:49 +0200 Subject: [PATCH 0249/1175] Fix removing of smarthome templates on startup of AVM Fritz!SmartHome integration (#144506) --- .../components/fritzbox/coordinator.py | 2 +- tests/components/fritzbox/test_coordinator.py | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index adc63dd2c2e..8a37ebf63e4 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -92,7 +92,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat available_main_ains = [ ain - for ain, dev in data.devices.items() + for ain, dev in data.devices.items() | data.templates.items() if dev.device_and_unit_id[1] is None ] device_reg = dr.async_get(self.hass) diff --git a/tests/components/fritzbox/test_coordinator.py b/tests/components/fritzbox/test_coordinator.py index f4f4da90181..4c329daa640 100644 --- a/tests/components/fritzbox/test_coordinator.py +++ b/tests/components/fritzbox/test_coordinator.py @@ -15,7 +15,7 @@ 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 . import FritzDeviceCoverMock, FritzDeviceSwitchMock, FritzEntityBaseMock from .const import MOCK_CONFIG from tests.common import MockConfigEntry, async_fire_time_changed @@ -84,6 +84,8 @@ async def test_coordinator_automatic_registry_cleanup( entity_registry: er.EntityRegistry, ) -> None: """Test automatic registry cleanup.""" + + # init with 2 devices and 1 template fritz().get_devices.return_value = [ FritzDeviceSwitchMock( ain="fake ain switch", @@ -96,6 +98,13 @@ async def test_coordinator_automatic_registry_cleanup( name="fake_cover", ), ] + fritz().get_templates.return_value = [ + FritzEntityBaseMock( + ain="fake ain template", + device_and_unit_id=("fake ain template", None), + name="fake_template", + ) + ] entry = MockConfigEntry( domain=FB_DOMAIN, data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], @@ -105,9 +114,10 @@ async def test_coordinator_automatic_registry_cleanup( 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)) == 19 - assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 20 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 3 + # remove one device, keep the template fritz().get_devices.return_value = [ FritzDeviceSwitchMock( ain="fake ain switch", @@ -119,5 +129,14 @@ async def test_coordinator_automatic_registry_cleanup( 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)) == 13 + assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 2 + + # remove the template, keep the device + fritz().get_templates.return_value = [] + + 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)) == 12 assert len(dr.async_entries_for_config_entry(device_registry, entry.entry_id)) == 1 From 2396b1e73c85c4126a4d33b6b32445274af6d983 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 14:55:03 -0500 Subject: [PATCH 0250/1175] Bump forecast-solar to 4.2.0 (#144502) --- 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 769bda56adc..66796a44dc4 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/forecast_solar", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["forecast-solar==4.1.0"] + "requirements": ["forecast-solar==4.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2481a3288eb..c510414d92e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ fnv-hash-fast==1.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.1.0 +forecast-solar==4.2.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e13b157579a..0f3622fe0b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -818,7 +818,7 @@ fnv-hash-fast==1.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==4.1.0 +forecast-solar==4.2.0 # homeassistant.components.freebox freebox-api==1.2.2 From d13f9be9d81c5531a61655fbc62e5f23a6fff308 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Thu, 8 May 2025 21:57:20 +0200 Subject: [PATCH 0251/1175] Remove unused OpenWeatherMap const values (#144510) --- homeassistant/components/openweathermap/const.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index fbd2cb1aee2..0bc804a5b42 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -54,11 +54,6 @@ ATTR_API_DAILY_FORECAST = "daily_forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -FORECAST_MODE_HOURLY = "hourly" -FORECAST_MODE_DAILY = "daily" -FORECAST_MODE_FREE_DAILY = "freedaily" -FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly" -FORECAST_MODE_ONECALL_DAILY = "onecall_daily" OWM_MODE_FREE_CURRENT = "current" OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" From e0fb612e82b033999e5ec07c554124dc96f07e0e Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Thu, 8 May 2025 23:21:43 +0300 Subject: [PATCH 0252/1175] Show warning message for Z-Wave devices in interview stage (#144483) * Show warning message for devices in interview stage * remove debug code --- homeassistant/components/zwave_js/api.py | 10 +++++++++- tests/components/zwave_js/test_api.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index f4397737234..5eb59c6c288 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -674,10 +674,18 @@ async def websocket_node_alerts( connection.send_error(msg[ID], ERR_NOT_LOADED, str(err)) return + comments = node.device_config.metadata.comments + if node.in_interview: + comments.append( + { + "level": "warning", + "text": "This device is currently being interviewed and may not be fully operational.", + } + ) connection.send_result( msg[ID], { - "comments": node.device_config.metadata.comments, + "comments": comments, "is_embedded": node.device_config.is_embedded, }, ) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index c6ce3d9ac1b..150ee39925b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -505,6 +505,22 @@ async def test_node_alerts( assert result["comments"] == [{"level": "info", "text": "test"}] assert result["is_embedded"] + # Test with node in interview + with patch("zwave_js_server.model.node.Node.in_interview", return_value=True): + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/node_alerts", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["comments"]) == 2 + assert msg["result"]["comments"][1] == { + "level": "warning", + "text": "This device is currently being interviewed and may not be fully operational.", + } + # Test with provisioned device valid_qr_info = { VERSION: 1, From 42f53ff91722ad69ad1dd053271a98f24fe7129e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 8 May 2025 22:30:35 +0200 Subject: [PATCH 0253/1175] Don't encrypt or decrypt unknown files in backup archives (#144495) --- homeassistant/components/backup/http.py | 16 ++- homeassistant/components/backup/util.py | 92 +++++++++++++----- .../backup/fixtures/test_backups/c0cb53bd.tar | Bin 10240 -> 10240 bytes .../test_backups/c0cb53bd.tar.decrypted | Bin 10240 -> 10240 bytes tests/components/backup/test_http.py | 4 +- tests/components/backup/test_util.py | 64 ++++++++---- 6 files changed, 129 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py index 8f241e6363d..11d8199bdc5 100644 --- a/homeassistant/components/backup/http.py +++ b/homeassistant/components/backup/http.py @@ -22,7 +22,7 @@ from . import util from .agent import BackupAgent from .const import DATA_MANAGER from .manager import BackupManager -from .models import BackupNotFound +from .models import AgentBackup, BackupNotFound @callback @@ -85,7 +85,15 @@ class DownloadBackupView(HomeAssistantView): request, headers, backup_id, agent_id, agent, manager ) return await self._send_backup_with_password( - hass, request, headers, backup_id, agent_id, password, agent, manager + hass, + backup, + request, + headers, + backup_id, + agent_id, + password, + agent, + manager, ) except BackupNotFound: return Response(status=HTTPStatus.NOT_FOUND) @@ -116,6 +124,7 @@ class DownloadBackupView(HomeAssistantView): async def _send_backup_with_password( self, hass: HomeAssistant, + backup: AgentBackup, request: Request, headers: dict[istr, str], backup_id: str, @@ -144,7 +153,8 @@ class DownloadBackupView(HomeAssistantView): stream = util.AsyncIteratorWriter(hass) worker = threading.Thread( - target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []] + target=util.decrypt_backup, + args=[backup, reader, stream, password, on_done, 0, []], ) try: worker.start() diff --git a/homeassistant/components/backup/util.py b/homeassistant/components/backup/util.py index bd77880738e..8112faf4459 100644 --- a/homeassistant/components/backup/util.py +++ b/homeassistant/components/backup/util.py @@ -295,13 +295,26 @@ def validate_password_stream( raise BackupEmpty +def _get_expected_archives(backup: AgentBackup) -> set[str]: + """Get the expected archives in the backup.""" + expected_archives = set() + if backup.homeassistant_included: + expected_archives.add("homeassistant") + for addon in backup.addons: + expected_archives.add(addon.slug) + for folder in backup.folders: + expected_archives.add(folder.value) + return expected_archives + + def decrypt_backup( + backup: AgentBackup, input_stream: IO[bytes], output_stream: IO[bytes], password: str | None, on_done: Callable[[Exception | None], None], minimum_size: int, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Decrypt a backup.""" error: Exception | None = None @@ -315,7 +328,7 @@ def decrypt_backup( fileobj=output_stream, mode="w|", bufsize=BUF_SIZE ) as output_tar, ): - _decrypt_backup(input_tar, output_tar, password) + _decrypt_backup(backup, input_tar, output_tar, password) except (DecryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error decrypting backup: %s", err) error = err @@ -333,15 +346,18 @@ def decrypt_backup( def _decrypt_backup( + backup: AgentBackup, input_tar: tarfile.TarFile, output_tar: tarfile.TarFile, password: str | None, ) -> None: """Decrypt a backup.""" + expected_archives = _get_expected_archives(backup) for obj in input_tar: # We compare with PurePath to avoid issues with different path separators, # for example when backup.json is added as "./backup.json" - if PurePath(obj.name) == PurePath("backup.json"): + object_path = PurePath(obj.name) + if object_path == PurePath("backup.json"): # Rewrite the backup.json file to indicate that the backup is decrypted if not (reader := input_tar.extractfile(obj)): raise DecryptError @@ -352,7 +368,13 @@ def _decrypt_backup( metadata_obj.size = len(updated_metadata_b) output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b)) continue - if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): + prefix, _, suffix = object_path.name.partition(".") + if suffix not in ("tar", "tgz", "tar.gz"): + LOGGER.debug("Unknown file %s will not be decrypted", obj.name) + output_tar.addfile(obj, input_tar.extractfile(obj)) + continue + if prefix not in expected_archives: + LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name) output_tar.addfile(obj, input_tar.extractfile(obj)) continue istf = SecureTarFile( @@ -371,12 +393,13 @@ def _decrypt_backup( def encrypt_backup( + backup: AgentBackup, input_stream: IO[bytes], output_stream: IO[bytes], password: str | None, on_done: Callable[[Exception | None], None], minimum_size: int, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Encrypt a backup.""" error: Exception | None = None @@ -390,7 +413,7 @@ def encrypt_backup( fileobj=output_stream, mode="w|", bufsize=BUF_SIZE ) as output_tar, ): - _encrypt_backup(input_tar, output_tar, password, nonces) + _encrypt_backup(backup, input_tar, output_tar, password, nonces) except (EncryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error encrypting backup: %s", err) error = err @@ -408,17 +431,20 @@ def encrypt_backup( def _encrypt_backup( + backup: AgentBackup, input_tar: tarfile.TarFile, output_tar: tarfile.TarFile, password: str | None, - nonces: list[bytes], + nonces: NonceGenerator, ) -> None: """Encrypt a backup.""" inner_tar_idx = 0 + expected_archives = _get_expected_archives(backup) for obj in input_tar: # We compare with PurePath to avoid issues with different path separators, # for example when backup.json is added as "./backup.json" - if PurePath(obj.name) == PurePath("backup.json"): + object_path = PurePath(obj.name) + if object_path == PurePath("backup.json"): # Rewrite the backup.json file to indicate that the backup is encrypted if not (reader := input_tar.extractfile(obj)): raise EncryptError @@ -429,16 +455,21 @@ def _encrypt_backup( metadata_obj.size = len(updated_metadata_b) output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b)) continue - if not obj.name.endswith((".tar", ".tgz", ".tar.gz")): + prefix, _, suffix = object_path.name.partition(".") + if suffix not in ("tar", "tgz", "tar.gz"): + LOGGER.debug("Unknown file %s will not be encrypted", obj.name) output_tar.addfile(obj, input_tar.extractfile(obj)) continue + if prefix not in expected_archives: + LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name) + continue istf = SecureTarFile( None, # Not used gzip=False, key=password_to_key(password) if password is not None else None, mode="r", fileobj=input_tar.extractfile(obj), - nonce=nonces[inner_tar_idx], + nonce=nonces.get(inner_tar_idx), ) inner_tar_idx += 1 with istf.encrypt(obj) as encrypted: @@ -456,17 +487,33 @@ class _CipherWorkerStatus: writer: AsyncIteratorWriter +class NonceGenerator: + """Generate nonces for encryption.""" + + def __init__(self) -> None: + """Initialize the generator.""" + self._nonces: dict[int, bytes] = {} + + def get(self, index: int) -> bytes: + """Get a nonce for the given index.""" + if index not in self._nonces: + # Generate a new nonce for the given index + self._nonces[index] = os.urandom(16) + return self._nonces[index] + + class _CipherBackupStreamer: """Encrypt or decrypt a backup.""" _cipher_func: Callable[ [ + AgentBackup, IO[bytes], IO[bytes], str | None, Callable[[Exception | None], None], int, - list[bytes], + NonceGenerator, ], None, ] @@ -484,7 +531,7 @@ class _CipherBackupStreamer: self._hass = hass self._open_stream = open_stream self._password = password - self._nonces: list[bytes] = [] + self._nonces = NonceGenerator() def size(self) -> int: """Return the maximum size of the decrypted or encrypted backup.""" @@ -508,7 +555,15 @@ class _CipherBackupStreamer: writer = AsyncIteratorWriter(self._hass) worker = threading.Thread( target=self._cipher_func, - args=[reader, writer, self._password, on_done, self.size(), self._nonces], + args=[ + self._backup, + reader, + writer, + self._password, + on_done, + self.size(), + self._nonces, + ], ) worker_status = _CipherWorkerStatus( done=asyncio.Event(), reader=reader, thread=worker, writer=writer @@ -538,17 +593,6 @@ class DecryptedBackupStreamer(_CipherBackupStreamer): class EncryptedBackupStreamer(_CipherBackupStreamer): """Encrypt a backup.""" - def __init__( - self, - hass: HomeAssistant, - backup: AgentBackup, - open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]], - password: str | None, - ) -> None: - """Initialize.""" - super().__init__(hass, backup, open_stream, password) - self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())] - _cipher_func = staticmethod(encrypt_backup) def backup(self) -> AgentBackup: diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar index f3b2845d5eb19b9708ae6d4fd68f7d220fb39c45..29e61d5e4c11683b126dcc08b0736dca19eda9f4 100644 GIT binary patch delta 283 zcmZn&Xb70lB57`F!eD>^3w}8B;bhGKMkL>nJECrljQO6)RaOL{}>n z=ai-cSxU+IMX82LK*_ws+*FW&Gf+SQEK-(QRGgWg2NE>UGXSY6&a48d0rF~f6j04D z!Y~6Y0yjepn<25a69q*Uv9O3_7`dq6bzW0!ePX2WMFIr l_X#I6JBJ}m!NQ2iDFUV}hHzOyX7w}8B;bhGKMiR)&iL;7O}8Q5@4Ck%Mm_N rfpc<-fC;MsLKP=7`(_pi8K%t&LQhy3jUlQyfT~PcjNwK}{^tV#9Tpno diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted index c97533fc1afb35fafb7be57651fc72e4069f44b3..386ea021247a5b5f9658a1e2009e956f73dcca9d 100644 GIT binary patch delta 282 zcmZn&Xb70lB57`F%3y#13w}8B;bhGKMqN>nJECrljQO6)RaOL{}>n z=ai-cSxU+IMX82LK*_ws+*FW&Gf+SQEK-(QRGgWg2NE>UGXSY6&a48d0rF~f6j04D z!Y~6Y0yjepn<25a69t7Av9O3_7`dq6bzW0!ePX2WMFIz k_X#I6JBJ}m!NQ2iDFUV}MsQg{X7w}8B;bhGKMoT)&iN!7qPHR5@4Ck%Mm_N qfpc<-fC;MsLKP=7`(_r2Kg^p%SXda1A&NMFicDEd;3i4_=K}x=rW(lr diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py index 92bf454095e..b3845b1209a 100644 --- a/tests/components/backup/test_http.py +++ b/tests/components/backup/test_http.py @@ -177,7 +177,7 @@ async def _test_downloading_encrypted_backup( enc_metadata = json.loads(outer_tar.extractfile("./backup.json").read()) assert enc_metadata["protected"] is True with ( - outer_tar.extractfile("core.tar.gz") as inner_tar_file, + outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file, pytest.raises(tarfile.ReadError, match="file could not be opened"), ): # pylint: disable-next=consider-using-with @@ -209,7 +209,7 @@ async def _test_downloading_encrypted_backup( dec_metadata = json.loads(outer_tar.extractfile("./backup.json").read()) assert dec_metadata == enc_metadata | {"protected": False} with ( - outer_tar.extractfile("core.tar.gz") as inner_tar_file, + outer_tar.extractfile("homeassistant.tar.gz") as inner_tar_file, tarfile.open(fileobj=inner_tar_file, mode="r") as inner_tar, ): assert inner_tar.getnames() == [ diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index 97e94eafb73..a999672e7f6 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -174,7 +174,10 @@ async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None: ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -218,7 +221,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_reader( """Test the decrypted backup streamer.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -253,7 +259,10 @@ async def test_decrypted_backup_streamer_interrupt_stuck_writer( """Test the decrypted backup streamer.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -283,7 +292,10 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) -> """Test the decrypted backup streamer with wrong password.""" encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -320,7 +332,10 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -353,15 +368,16 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: bytes.fromhex("00000000000000000000000000000000"), ) encryptor = EncryptedBackupStreamer(hass, backup, open_backup, "hunter2") - assert encryptor.backup() == dataclasses.replace( - backup, protected=True, size=backup.size + len(expected_padding) - ) - encrypted_stream = await encryptor.open_stream() - encrypted_output = b"" - async for chunk in encrypted_stream: - encrypted_output += chunk - await encryptor.wait() + assert encryptor.backup() == dataclasses.replace( + backup, protected=True, size=backup.size + len(expected_padding) + ) + + encrypted_stream = await encryptor.open_stream() + encrypted_output = b"" + async for chunk in encrypted_stream: + encrypted_output += chunk + await encryptor.wait() # Expect the output to match the stored encrypted backup file, with additional # padding. @@ -377,7 +393,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_reader( "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -414,7 +433,10 @@ async def test_encrypted_backup_streamer_interrupt_stuck_writer( "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -447,7 +469,10 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No ) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -490,7 +515,7 @@ async def test_encrypted_backup_streamer_random_nonce(hass: HomeAssistant) -> No await encryptor1.wait() await encryptor2.wait() - # Output from the two streames should differ but have the same length. + # Output from the two streams should differ but have the same length. assert encrypted_output1 != encrypted_output3 assert len(encrypted_output1) == len(encrypted_output3) @@ -508,7 +533,10 @@ async def test_encrypted_backup_streamer_error(hass: HomeAssistant) -> None: "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) backup = AgentBackup( - addons=["addon_1", "addon_2"], + addons=[ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, From 4c43640d0d2cb740d33ee278e667c4ddb27737d8 Mon Sep 17 00:00:00 2001 From: DeerMaximum <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 8 May 2025 22:36:22 +0200 Subject: [PATCH 0254/1175] Bump pynina to 0.3.6 (#144494) --- homeassistant/components/nina/manifest.json | 2 +- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nina/manifest.json b/homeassistant/components/nina/manifest.json index 8bb9a347373..7383bd5932a 100644 --- a/homeassistant/components/nina/manifest.json +++ b/homeassistant/components/nina/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/nina", "iot_class": "cloud_polling", "loggers": ["pynina"], - "requirements": ["PyNINA==0.3.5"], + "requirements": ["pynina==0.3.6"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index c510414d92e..ebd9dbc3e2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,9 +69,6 @@ PyMetno==0.13.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.17 -# homeassistant.components.nina -PyNINA==0.3.5 - # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.5.0 @@ -2164,6 +2161,9 @@ pynetgear==0.10.10 # homeassistant.components.netio pynetio==0.1.9.1 +# homeassistant.components.nina +pynina==0.3.6 + # homeassistant.components.nobo_hub pynobo==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f3622fe0b1..6ef4f1e75f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -66,9 +66,6 @@ PyMetno==0.13.0 # homeassistant.components.keymitt_ble PyMicroBot==0.0.17 -# homeassistant.components.nina -PyNINA==0.3.5 - # homeassistant.components.mobile_app # homeassistant.components.owntracks PyNaCl==1.5.0 @@ -1767,6 +1764,9 @@ pynecil==4.1.0 # homeassistant.components.netgear pynetgear==0.10.10 +# homeassistant.components.nina +pynina==0.3.6 + # homeassistant.components.nobo_hub pynobo==1.8.1 From 7100481abc4c2cc74b3c0d46f6de05ee930bf2b6 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 8 May 2025 22:38:26 +0200 Subject: [PATCH 0255/1175] Improve Husqvarna Automower tests (#143113) --- .../husqvarna_automower/conftest.py | 56 +++++++------------ .../husqvarna_automower/test_button.py | 6 +- .../husqvarna_automower/test_lawn_mower.py | 14 ++--- .../husqvarna_automower/test_number.py | 2 +- .../husqvarna_automower/test_switch.py | 3 +- 5 files changed, 29 insertions(+), 52 deletions(-) diff --git a/tests/components/husqvarna_automower/conftest.py b/tests/components/husqvarna_automower/conftest.py index 871f108bfd0..1cd6f9b393e 100644 --- a/tests/components/husqvarna_automower/conftest.py +++ b/tests/components/husqvarna_automower/conftest.py @@ -3,10 +3,10 @@ import asyncio from collections.abc import Generator import time -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, create_autospec, patch +from aioautomower.commands import MowerCommands, WorkAreaSettings from aioautomower.model import MowerAttributes -from aioautomower.session import AutomowerSession, MowerCommands from aioautomower.utils import mower_list_to_dictionary_dataclass from aiohttp import ClientWebSocketResponse import pytest @@ -108,7 +108,9 @@ async def setup_credentials(hass: HomeAssistant) -> None: @pytest.fixture -def mock_automower_client(values) -> Generator[AsyncMock]: +def mock_automower_client( + values: dict[str, MowerAttributes], +) -> Generator[AsyncMock]: """Mock a Husqvarna Automower client.""" async def listen() -> None: @@ -117,37 +119,21 @@ def mock_automower_client(values) -> Generator[AsyncMock]: await listen_block.wait() pytest.fail("Listen was not cancelled!") - mock = AsyncMock(spec=AutomowerSession) - mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) - mock.commands = AsyncMock(spec_set=MowerCommands) - mock.get_status.return_value = values - mock.start_listening = AsyncMock(side_effect=listen) - with patch( "homeassistant.components.husqvarna_automower.AutomowerSession", - return_value=mock, - ): - yield mock - - -@pytest.fixture -def mock_automower_client_one_mower(values) -> Generator[AsyncMock]: - """Mock a Husqvarna Automower client.""" - - async def listen() -> None: - """Mock listen.""" - listen_block = asyncio.Event() - await listen_block.wait() - pytest.fail("Listen was not cancelled!") - - mock = AsyncMock(spec=AutomowerSession) - mock.auth = AsyncMock(side_effect=ClientWebSocketResponse) - mock.commands = AsyncMock(spec_set=MowerCommands) - mock.get_status.return_value = values - mock.start_listening = AsyncMock(side_effect=listen) - - with patch( - "homeassistant.components.husqvarna_automower.AutomowerSession", - return_value=mock, - ): - yield mock + autospec=True, + spec_set=True, + ) as mock: + mock_instance = mock.return_value + mock_instance.auth = AsyncMock(side_effect=ClientWebSocketResponse) + mock_instance.get_status = AsyncMock(return_value=values) + mock_instance.start_listening = AsyncMock(side_effect=listen) + mock_instance.commands = create_autospec( + MowerCommands, instance=True, spec_set=True + ) + mock_instance.commands.workarea_settings.return_value = create_autospec( + WorkAreaSettings, + instance=True, + spec_set=True, + ) + yield mock_instance diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py index 5bef810150d..b76bc7c9d73 100644 --- a/tests/components/husqvarna_automower/test_button.py +++ b/tests/components/husqvarna_automower/test_button.py @@ -64,8 +64,7 @@ async def test_button_states_and_commands( target={ATTR_ENTITY_ID: entity_id}, blocking=True, ) - mocked_method = getattr(mock_automower_client.commands, "error_confirm") - mocked_method.assert_called_once_with(TEST_MOWER_ID) + mock_automower_client.commands.error_confirm.assert_called_once_with(TEST_MOWER_ID) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "2023-06-05T00:16:00+00:00" @@ -106,8 +105,7 @@ async def test_sync_clock( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - mocked_method = mock_automower_client.commands.set_datetime - mocked_method.assert_called_once_with(TEST_MOWER_ID) + mock_automower_client.commands.set_datetime.assert_called_once_with(TEST_MOWER_ID) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "2024-02-29T11:00:00+00:00" diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 12c53d709ca..42b737652b7 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -90,9 +90,7 @@ async def test_lawn_mower_commands( mocked_method = getattr(mock_automower_client.commands, aioautomower_command) mocked_method.assert_called_once_with(TEST_MOWER_ID) - getattr( - mock_automower_client.commands, aioautomower_command - ).side_effect = ApiError("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -139,8 +137,7 @@ async def test_lawn_mower_service_commands( ) -> None: """Test lawn_mower commands.""" await setup_integration(hass, mock_config_entry) - mocked_method = AsyncMock() - setattr(mock_automower_client.commands, aioautomower_command, mocked_method) + mocked_method = getattr(mock_automower_client.commands, aioautomower_command) await hass.services.async_call( domain=DOMAIN, service=service, @@ -150,9 +147,7 @@ async def test_lawn_mower_service_commands( ) mocked_method.assert_called_once_with(TEST_MOWER_ID, extra_data) - getattr( - mock_automower_client.commands, aioautomower_command - ).side_effect = ApiError("Test error") + mocked_method.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", @@ -193,8 +188,7 @@ async def test_lawn_mower_override_work_area_command( ) -> None: """Test lawn_mower work area override commands.""" await setup_integration(hass, mock_config_entry) - mocked_method = AsyncMock() - setattr(mock_automower_client.commands, aioautomower_command, mocked_method) + mocked_method = getattr(mock_automower_client.commands, aioautomower_command) await hass.services.async_call( domain=DOMAIN, service=service, diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index 628011e3f15..005d294954c 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -96,7 +96,7 @@ async def test_number_workarea_commands( service_data={"value": "75"}, blocking=True, ) - assert len(mocked_method.mock_calls) == 2 + assert mock_automower_client.commands.workarea_settings.call_count == 2 @pytest.mark.usefixtures("entity_registry_enabled_by_default") diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 00b04ce9903..06efb8c45c0 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -133,8 +133,7 @@ async def test_stay_out_zone_switch_commands( ) values[TEST_MOWER_ID].stay_out_zones.zones[TEST_ZONE_ID].enabled = boolean mock_automower_client.get_status.return_value = values - mocked_method = AsyncMock() - setattr(mock_automower_client.commands, "switch_stay_out_zone", mocked_method) + mocked_method = mock_automower_client.commands.switch_stay_out_zone await hass.services.async_call( domain=SWITCH_DOMAIN, service=service, From b1392e1fc854f9b551469ee8de164bb55191b25c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 May 2025 15:43:45 -0500 Subject: [PATCH 0256/1175] Bump aiodns to 3.4.0 (#144511) --- homeassistant/components/dnsip/config_flow.py | 2 +- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/components/dnsip/sensor.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index 9e98178e718..e7b60d5bd6f 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -68,7 +68,7 @@ async def async_validate_hostname( result = False with contextlib.suppress(DNSError): result = bool( - await aiodns.DNSResolver( + await aiodns.DNSResolver( # type: ignore[call-overload] nameservers=[resolver], udp_port=port, tcp_port=port ).query(hostname, qtype) ) diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 35802adb7f3..e004b386e02 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.3.0"] + "requirements": ["aiodns==3.4.0"] } diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 6708baefe8c..6cdb67dd80d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -106,7 +106,7 @@ class WanIpSensor(SensorEntity): async def async_update(self) -> None: """Get the current DNS IP address for hostname.""" try: - response = await self.resolver.query(self.hostname, self.querytype) + response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload] except DNSError as err: _LOGGER.warning("Exception while resolving host: %s", err) response = None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3c9af6b035c..0e69000fa9d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 -aiodns==3.3.0 +aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index cf27bea85e0..5c24e227648 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.13.2" dependencies = [ - "aiodns==3.3.0", + "aiodns==3.4.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index 219e1734072..283651940f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiodns==3.3.0 +aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp==3.11.18 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index ebd9dbc3e2d..96635f94b65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.3.0 +aiodns==3.4.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ef4f1e75f8..df125ecadb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ aiodhcpwatcher==1.1.1 aiodiscover==2.6.1 # homeassistant.components.dnsip -aiodns==3.3.0 +aiodns==3.4.0 # homeassistant.components.duke_energy aiodukeenergy==0.3.0 From 6b2a4c975c258ae4645c0e0d4224a37589c47467 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 May 2025 23:44:06 +0200 Subject: [PATCH 0257/1175] Cleanup unused CONF_IP_ADDRESS from SamsungTV tests (#144379) --- tests/components/samsungtv/const.py | 3 --- tests/components/samsungtv/snapshots/test_diagnostics.ambr | 3 --- tests/components/samsungtv/test_config_flow.py | 6 +----- tests/components/samsungtv/test_media_player.py | 2 -- 4 files changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 7a367294581..7078f088217 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -8,7 +8,6 @@ from homeassistant.components.samsungtv.const import ( ) from homeassistant.const import ( CONF_HOST, - CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, CONF_MODEL, @@ -33,7 +32,6 @@ MOCK_CONFIG_ENCRYPTED_WS = { } MOCK_ENTRYDATA_ENCRYPTED_WS = { **MOCK_CONFIG_ENCRYPTED_WS, - CONF_IP_ADDRESS: "test", CONF_METHOD: "encrypted", CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: "037739871315caef138547b03e348b72", @@ -47,7 +45,6 @@ MOCK_ENTRYDATA_WS = { CONF_NAME: "any", } MOCK_ENTRY_WS_WITH_MAC = { - CONF_IP_ADDRESS: "test", CONF_HOST: "fake_host", CONF_METHOD: "websocket", CONF_MAC: "aa:bb:cc:dd:ee:ff", diff --git a/tests/components/samsungtv/snapshots/test_diagnostics.ambr b/tests/components/samsungtv/snapshots/test_diagnostics.ambr index dd1b3654186..7650623a4fb 100644 --- a/tests/components/samsungtv/snapshots/test_diagnostics.ambr +++ b/tests/components/samsungtv/snapshots/test_diagnostics.ambr @@ -14,7 +14,6 @@ 'entry': dict({ 'data': dict({ 'host': 'fake_host', - 'ip_address': 'test', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'websocket', 'model': '82GXARRS', @@ -47,7 +46,6 @@ 'entry': dict({ 'data': dict({ 'host': 'fake_host', - 'ip_address': 'test', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'encrypted', 'name': 'fake', @@ -107,7 +105,6 @@ 'entry': dict({ 'data': dict({ 'host': 'fake_host', - 'ip_address': 'test', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'encrypted', 'model': 'UE48JU6400', diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 6eef80026f0..504eccc4f12 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -35,7 +35,6 @@ from homeassistant.components.samsungtv.const import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, - CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, CONF_MODEL, @@ -89,7 +88,6 @@ MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( ) MOCK_OLD_ENTRY = { CONF_HOST: "10.10.12.34", - CONF_IP_ADDRESS: EXISTING_IP, CONF_METHOD: "legacy", CONF_PORT: None, } @@ -1257,14 +1255,13 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_old_entry(hass: HomeAssistant) -> None: - """Test update of old entry.""" + """Test update of old entry sets unique id.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) entry.add_to_hass(hass) config_entries_domain = hass.config_entries.async_entries(DOMAIN) assert len(config_entries_domain) == 1 assert entry is config_entries_domain[0] - assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP assert not entry.unique_id assert await async_setup_component(hass, DOMAIN, {}) is True @@ -1282,7 +1279,6 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: entry2 = config_entries_domain[0] # check updated device info - assert entry2.data.get(CONF_IP_ADDRESS) is not None assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index b4f48ed20b3..9171c49ef06 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -53,7 +53,6 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, CONF_HOST, - CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, CONF_MODEL, @@ -111,7 +110,6 @@ MOCK_CALLS_WS = { } MOCK_ENTRY_WS = { - CONF_IP_ADDRESS: "test", CONF_HOST: "fake_host", CONF_METHOD: "websocket", CONF_NAME: "fake", From fbe63e8d0349d13ef6a44cda6dfc91724e6547eb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 May 2025 23:44:44 +0200 Subject: [PATCH 0258/1175] Use runtime_data in hlk_sw16 (#144370) --- homeassistant/components/hlk_sw16/__init__.py | 26 ++++++------------- homeassistant/components/hlk_sw16/entity.py | 6 +++-- homeassistant/components/hlk_sw16/switch.py | 16 ++++++------ 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index ebd92908b93..f55535d9be0 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -3,6 +3,7 @@ import logging from hlk_sw16 import create_hlk_sw16_connection +from hlk_sw16.protocol import SW16Client import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -24,9 +25,6 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SWITCH] -DATA_DEVICE_REGISTER = "hlk_sw16_device_register" -DATA_DEVICE_LISTENER = "hlk_sw16_device_listener" - SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) RELAY_ID = vol.All( @@ -52,6 +50,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +type HlkConfigEntry = ConfigEntry[SW16Client] + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Component setup, do nothing.""" @@ -70,15 +70,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HlkConfigEntry) -> bool: """Set up the HLK-SW16 switch.""" - hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] address = f"{host}:{port}" - hass.data[DOMAIN][entry.entry_id] = {} - @callback def disconnected(): """Schedule reconnect after connection has been lost.""" @@ -106,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, ) - hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client + entry.runtime_data = client # Load entities await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -116,14 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HlkConfigEntry) -> bool: """Unload a config entry.""" - client = hass.data[DOMAIN][entry.entry_id].pop(DATA_DEVICE_REGISTER) - client.stop() - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - if hass.data[DOMAIN][entry.entry_id]: - hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - return unload_ok + entry.runtime_data.stop() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hlk_sw16/entity.py b/homeassistant/components/hlk_sw16/entity.py index 91510760968..d3784fef5ee 100644 --- a/homeassistant/components/hlk_sw16/entity.py +++ b/homeassistant/components/hlk_sw16/entity.py @@ -2,6 +2,8 @@ import logging +from hlk_sw16.protocol import SW16Client + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -17,12 +19,12 @@ class SW16Entity(Entity): _attr_should_poll = False - def __init__(self, device_port, entry_id, client): + def __init__(self, device_port: str, entry_id: str, client: SW16Client) -> None: """Initialize the device.""" # HLK-SW16 specific attributes for every component type self._entry_id = entry_id self._device_port = device_port - self._is_on = None + self._is_on: bool | None = None self._client = client self._attr_name = device_port self._attr_unique_id = f"{self._entry_id}_{self._device_port}" diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index c6e6f7f5201..795f3dc68ea 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,22 +1,22 @@ """Support for HLK-SW16 switches.""" +from __future__ import annotations + from typing import Any from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DATA_DEVICE_REGISTER -from .const import DOMAIN +from . import HlkConfigEntry from .entity import SW16Entity PARALLEL_UPDATES = 0 -def devices_from_entities(hass, entry): +def devices_from_entities(entry: HlkConfigEntry) -> list[SW16Switch]: """Parse configuration and add HLK-SW16 switch devices.""" - device_client = hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] + device_client = entry.runtime_data devices = [] for i in range(16): device_port = f"{i:01x}" @@ -27,18 +27,18 @@ def devices_from_entities(hass, entry): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HlkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HLK-SW16 platform.""" - async_add_entities(devices_from_entities(hass, entry)) + async_add_entities(devices_from_entities(entry)) class SW16Switch(SW16Entity, SwitchEntity): """Representation of a HLK-SW16 switch.""" @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._is_on From 1322d5437160c2c01ef3fcdb20830d3d5d6950f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 8 May 2025 23:47:24 +0200 Subject: [PATCH 0259/1175] Use runtime_data in hive (#144367) --- homeassistant/components/hive/__init__.py | 20 +++++++--------- .../components/hive/alarm_control_panel.py | 7 +++--- .../components/hive/binary_sensor.py | 7 +++--- homeassistant/components/hive/climate.py | 14 ++++------- homeassistant/components/hive/config_flow.py | 23 ++++++++++--------- homeassistant/components/hive/light.py | 16 ++++++------- homeassistant/components/hive/sensor.py | 7 +++--- homeassistant/components/hive/switch.py | 9 ++++---- homeassistant/components/hive/water_heater.py | 8 +++---- 9 files changed, 47 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index ac008b857af..c45ecd24ea3 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -24,11 +24,11 @@ from .entity import HiveEntity _LOGGER = logging.getLogger(__name__) +type HiveConfigEntry = ConfigEntry[Hive] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool: """Set up Hive from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - web_session = aiohttp_client.async_get_clientsession(hass) hive_config = dict(entry.data) hive = Hive(web_session) @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hive_config["options"].update( {CONF_SCAN_INTERVAL: dict(entry.options).get(CONF_SCAN_INTERVAL, 120)} ) - hass.data[DOMAIN][entry.entry_id] = hive + entry.runtime_data = hive try: devices = await hive.session.startSession(hive_config) @@ -59,16 +59,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> None: """Remove a config entry.""" hive = Auth(entry.data["username"], entry.data["password"]) await hive.forget_device( @@ -78,7 +74,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry + hass: HomeAssistant, config_entry: HiveConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove a config entry from a device.""" return True diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index c2fe47642a0..338cc6bcf0a 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -9,11 +9,10 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, AlarmControlPanelState, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from . import HiveConfigEntry from .entity import HiveEntity PARALLEL_UPDATES = 0 @@ -28,12 +27,12 @@ HIVETOHA = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data if devices := hive.session.deviceList.get("alarm_control_panel"): async_add_entities( [HiveAlarmControlPanelEntity(hive, dev) for dev in devices], True diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 2076d592a7c..cdf6c253916 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -10,11 +10,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from . import HiveConfigEntry from .entity import HiveEntity PARALLEL_UPDATES = 0 @@ -69,12 +68,12 @@ SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data sensors: list[BinarySensorEntity] = [] diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index bd7553faa1a..28062adb0e3 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -15,19 +15,13 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system -from .const import ( - ATTR_TIME_PERIOD, - DOMAIN, - SERVICE_BOOST_HEATING_OFF, - SERVICE_BOOST_HEATING_ON, -) +from . import HiveConfigEntry, refresh_system +from .const import ATTR_TIME_PERIOD, SERVICE_BOOST_HEATING_OFF, SERVICE_BOOST_HEATING_ON from .entity import HiveEntity HIVE_TO_HASS_STATE = { @@ -59,12 +53,12 @@ _LOGGER = logging.getLogger() async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data devices = hive.session.deviceList.get("climate") if devices: async_add_entities((HiveClimateEntity(hive, dev) for dev in devices), True) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index e3180dc9734..41dba27c3a5 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -16,7 +16,6 @@ import voluptuous as vol from homeassistant.config_entries import ( SOURCE_REAUTH, - ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -24,6 +23,7 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback +from . import HiveConfigEntry from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN @@ -37,7 +37,6 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self.data: dict[str, Any] = {} self.tokens: dict[str, str] = {} - self.entry: ConfigEntry | None = None self.device_registration: bool = False self.device_name = "Home Assistant" @@ -54,7 +53,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): ) # Get user from existing entry and abort if already setup - self.entry = await self.async_set_unique_id(self.data[CONF_USERNAME]) + await self.async_set_unique_id(self.data[CONF_USERNAME]) if self.context["source"] != SOURCE_REAUTH: self._abort_if_unique_id_configured() @@ -145,12 +144,12 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): # Setup the config entry self.data["tokens"] = self.tokens if self.source == SOURCE_REAUTH: - assert self.entry - self.hass.config_entries.async_update_entry( - self.entry, title=self.data["username"], data=self.data + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + title=self.data["username"], + data=self.data, + reason="reauth_successful", ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self.data["username"], data=self.data) async def async_step_reauth( @@ -166,7 +165,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: HiveConfigEntry, ) -> HiveOptionsFlowHandler: """Hive options callback.""" return HiveOptionsFlowHandler(config_entry) @@ -175,7 +174,9 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN): class HiveOptionsFlowHandler(OptionsFlow): """Config flow options for Hive.""" - def __init__(self, config_entry: ConfigEntry) -> None: + config_entry: HiveConfigEntry + + def __init__(self, config_entry: HiveConfigEntry) -> None: """Initialize Hive options flow.""" self.hive = None self.interval = config_entry.options.get(CONF_SCAN_INTERVAL, 120) @@ -190,7 +191,7 @@ class HiveOptionsFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user.""" - self.hive = self.hass.data["hive"][self.config_entry.entry_id] + self.hive = self.config_entry.runtime_data errors: dict[str, str] = {} if user_input is not None: new_interval = user_input.get(CONF_SCAN_INTERVAL) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 80a81583429..f89d23b8513 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -3,7 +3,9 @@ from __future__ import annotations from datetime import timedelta -from typing import TYPE_CHECKING, Any +from typing import Any + +from apyhiveapi import Hive from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -12,30 +14,26 @@ from homeassistant.components.light import ( ColorMode, LightEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import color as color_util -from . import refresh_system -from .const import ATTR_MODE, DOMAIN +from . import HiveConfigEntry, refresh_system +from .const import ATTR_MODE from .entity import HiveEntity -if TYPE_CHECKING: - from apyhiveapi import Hive - PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive: Hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data devices = hive.session.deviceList.get("light") if not devices: return diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 0609e43c4a9..70a21038d67 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -24,7 +23,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN +from . import HiveConfigEntry from .entity import HiveEntity PARALLEL_UPDATES = 0 @@ -90,11 +89,11 @@ SENSOR_TYPES: tuple[HiveSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data devices = hive.session.deviceList.get("sensor") if not devices: return diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index d4fefea5a56..0640436d105 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -8,13 +8,12 @@ from typing import Any from apyhiveapi import Hive from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system -from .const import ATTR_MODE, DOMAIN +from . import HiveConfigEntry, refresh_system +from .const import ATTR_MODE from .entity import HiveEntity PARALLEL_UPDATES = 0 @@ -34,12 +33,12 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data devices = hive.session.deviceList.get("switch") if not devices: return diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 5f0a3d0f3fa..104c4f62f9c 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -10,17 +10,15 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system +from . import HiveConfigEntry, refresh_system from .const import ( ATTR_ONOFF, ATTR_TIME_PERIOD, - DOMAIN, SERVICE_BOOST_HOT_WATER, WATER_HEATER_MODES, ) @@ -46,12 +44,12 @@ SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HiveConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive = entry.runtime_data devices = hive.session.deviceList.get("water_heater") if devices: async_add_entities((HiveWaterHeater(hive, dev) for dev in devices), True) From bdf4a21976b86a693a6ea8ad5fc4f63cf61c77f4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 May 2025 09:52:43 +1200 Subject: [PATCH 0260/1175] Use async_release_notes in ESPHome update entity (#144440) --- homeassistant/components/esphome/entity.py | 16 +++ homeassistant/components/esphome/update.py | 16 ++- tests/components/esphome/test_update.py | 138 ++++++++++++++++++--- 3 files changed, 150 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 7b02680afee..8eded610194 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -134,6 +134,22 @@ def esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]]( return _wrapper +def async_esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]]( + func: Callable[[_EntityT], Awaitable[_R | None]], +) -> Callable[[_EntityT], Coroutine[Any, Any, _R | None]]: + """Wrap a state property of an esphome entity. + + This checks if the state object in the entity is set + and returns None if it is not set. + """ + + @functools.wraps(func) + async def _wrapper(self: _EntityT) -> _R | None: + return await func(self) if self._has_state else None + + return _wrapper + + def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]]( func: Callable[[_EntityT], float | None], ) -> Callable[[_EntityT], float | None]: diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index d24d8919461..cc886f2ba4c 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -31,6 +31,7 @@ from .coordinator import ESPHomeDashboardCoordinator from .dashboard import async_get_dashboard from .entity import ( EsphomeEntity, + async_esphome_state_property, convert_api_error_ha_error, esphome_state_property, platform_async_setup_entry, @@ -270,7 +271,9 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): """A update implementation for esphome.""" _attr_supported_features = ( - UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.RELEASE_NOTES ) @callback @@ -300,11 +303,12 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): """Return the latest version.""" return self._state.latest_version - @property - @esphome_state_property - def release_summary(self) -> str: - """Return the release summary.""" - return self._state.release_summary + @async_esphome_state_property + async def async_release_notes(self) -> str | None: + """Return the release notes.""" + if self._state.release_summary: + return self._state.release_summary + return None @property @esphome_state_property diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 63294a6ad69..a612f44c07f 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -13,6 +13,8 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.update import ( + ATTR_IN_PROGRESS, + ATTR_UPDATE_PERCENTAGE, DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, UpdateEntityFeature, @@ -29,6 +31,12 @@ from homeassistant.exceptions import HomeAssistantError from .conftest import MockESPHomeDeviceType, MockGenericDeviceEntryType +from tests.typing import WebSocketGenerator + +RELEASE_SUMMARY = "This is a release summary" +RELEASE_URL = "https://esphome.io/changelog" +ENTITY_ID = "update.test_myupdate" + @pytest.fixture(autouse=True) def enable_entity(entity_registry_enabled_by_default: None) -> None: @@ -461,8 +469,8 @@ async def test_generic_device_update_entity( current_version="2024.6.0", latest_version="2024.6.0", title="ESPHome Project", - release_summary="This is a release summary", - release_url="https://esphome.io/changelog", + release_summary=RELEASE_SUMMARY, + release_url=RELEASE_URL, ) ] user_service = [] @@ -472,7 +480,7 @@ async def test_generic_device_update_entity( user_service=user_service, states=states, ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == STATE_OFF @@ -497,8 +505,8 @@ async def test_generic_device_update_entity_has_update( current_version="2024.6.0", latest_version="2024.6.1", title="ESPHome Project", - release_summary="This is a release summary", - release_url="https://esphome.io/changelog", + release_summary=RELEASE_SUMMARY, + release_url=RELEASE_URL, ) ] user_service = [] @@ -508,14 +516,14 @@ async def test_generic_device_update_entity_has_update( user_service=user_service, states=states, ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == STATE_ON await hass.services.async_call( UPDATE_DOMAIN, SERVICE_INSTALL, - {ATTR_ENTITY_ID: "update.test_myupdate"}, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) @@ -528,27 +536,129 @@ async def test_generic_device_update_entity_has_update( current_version="2024.6.0", latest_version="2024.6.1", title="ESPHome Project", - release_summary="This is a release summary", - release_url="https://esphome.io/changelog", + release_summary=RELEASE_SUMMARY, + release_url=RELEASE_URL, ) ) - state = hass.states.get("update.test_myupdate") + state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == STATE_ON - assert state.attributes["in_progress"] is True - assert state.attributes["update_percentage"] == 50 - + assert state.attributes[ATTR_IN_PROGRESS] is True + assert state.attributes[ATTR_UPDATE_PERCENTAGE] == 50 await hass.services.async_call( HOMEASSISTANT_DOMAIN, SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: "update.test_myupdate"}, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) + mock_device.set_state( + UpdateState( + key=1, + in_progress=True, + has_progress=False, + current_version="2024.6.0", + latest_version="2024.6.1", + title="ESPHome Project", + release_summary=RELEASE_SUMMARY, + release_url=RELEASE_URL, + ) + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_IN_PROGRESS] is True + assert state.attributes[ATTR_UPDATE_PERCENTAGE] is None + mock_client.update_command.assert_called_with(key=1, command=UpdateCommand.CHECK) +async def test_update_entity_release_notes( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test ESPHome update entity release notes.""" + entity_info = [ + UpdateInfo( + object_id="myupdate", + key=1, + name="my update", + unique_id="my_update", + ) + ] + + user_service = [] + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=[], + ) + + # release notes + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 1, + "type": "update/release_notes", + "entity_id": ENTITY_ID, + } + ) + + result = await client.receive_json() + assert result["result"] is None + + mock_device.set_state( + UpdateState( + key=1, + current_version="2024.6.0", + latest_version="2024.6.1", + title="ESPHome Project", + release_summary="", + release_url=RELEASE_URL, + ) + ) + + await client.send_json( + { + "id": 2, + "type": "update/release_notes", + "entity_id": ENTITY_ID, + } + ) + + result = await client.receive_json() + assert result["result"] is None + + mock_device.set_state( + UpdateState( + key=1, + current_version="2024.6.0", + latest_version="2024.6.1", + title="ESPHome Project", + release_summary=RELEASE_SUMMARY, + release_url=RELEASE_URL, + ) + ) + + await client.send_json( + { + "id": 3, + "type": "update/release_notes", + "entity_id": ENTITY_ID, + } + ) + + result = await client.receive_json() + assert result["result"] == RELEASE_SUMMARY + + async def test_attempt_to_update_twice( hass: HomeAssistant, mock_client: APIClient, From a37f8b1f4ec9318eeda368b427de7c938c12874a Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 9 May 2025 01:00:28 +0300 Subject: [PATCH 0261/1175] Jewish calendar entity translations (#144414) * Move strings from entity descriptions to strings.json * Use the original name values * Fix casing * Use "real" english names as well as transliterated names --- .../jewish_calendar/binary_sensor.py | 6 +- .../components/jewish_calendar/sensor.py | 47 +++++----- .../components/jewish_calendar/strings.json | 78 +++++++++++++++ .../components/jewish_calendar/test_sensor.py | 94 +++++++++---------- 4 files changed, 151 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index d8672e8a4a3..8d06526c322 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -38,19 +38,19 @@ class JewishCalendarBinarySensorEntityDescription( BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( JewishCalendarBinarySensorEntityDescription( key="issur_melacha_in_effect", - name="Issur Melacha in Effect", + translation_key="issur_melacha_in_effect", icon="mdi:power-plug-off", is_on=lambda state, now: bool(state.issur_melacha_in_effect(now)), ), JewishCalendarBinarySensorEntityDescription( key="erev_shabbat_hag", - name="Erev Shabbat/Hag", + translation_key="erev_shabbat_hag", is_on=lambda state, now: bool(state.erev_shabbat_chag(now)), entity_registry_enabled_default=False, ), JewishCalendarBinarySensorEntityDescription( key="motzei_shabbat_hag", - name="Motzei Shabbat/Hag", + translation_key="motzei_shabbat_hag", is_on=lambda state, now: bool(state.motzei_shabbat_chag(now)), entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index f6c1978be21..deaae64547a 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -28,31 +28,30 @@ _LOGGER = logging.getLogger(__name__) INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="date", - name="Date", - icon="mdi:star-david", translation_key="hebrew_date", + icon="mdi:star-david", ), SensorEntityDescription( key="weekly_portion", - name="Parshat Hashavua", + translation_key="weekly_portion", icon="mdi:book-open-variant", device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="holiday", - name="Holiday", + translation_key="holiday", icon="mdi:calendar-star", device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="omer_count", - name="Day of the Omer", + translation_key="omer_count", icon="mdi:counter", entity_registry_enabled_default=False, ), SensorEntityDescription( key="daf_yomi", - name="Daf Yomi", + translation_key="daf_yomi", icon="mdi:book-open-variant", entity_registry_enabled_default=False, ), @@ -61,106 +60,106 @@ INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="alot_hashachar", - name="Alot Hashachar", # codespell:ignore alot + translation_key="alot_hashachar", icon="mdi:weather-sunset-up", entity_registry_enabled_default=False, ), SensorEntityDescription( key="talit_and_tefillin", - name="Talit and Tefillin", + translation_key="talit_and_tefillin", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="netz_hachama", - name="Hanetz Hachama", + translation_key="netz_hachama", icon="mdi:calendar-clock", ), SensorEntityDescription( key="sof_zman_shema_gra", - name='Latest time for Shma Gr"a', + translation_key="sof_zman_shema_gra", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_shema_mga", - name='Latest time for Shma MG"A', + translation_key="sof_zman_shema_mga", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_tfilla_gra", - name='Latest time for Tefilla Gr"a', + translation_key="sof_zman_tfilla_gra", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_tfilla_mga", - name='Latest time for Tefilla MG"A', + translation_key="sof_zman_tfilla_mga", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="chatzot_hayom", - name="Chatzot Hayom", + translation_key="chatzot_hayom", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="mincha_gedola", - name="Mincha Gedola", + translation_key="mincha_gedola", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="mincha_ketana", - name="Mincha Ketana", + translation_key="mincha_ketana", icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="plag_hamincha", - name="Plag Hamincha", + translation_key="plag_hamincha", icon="mdi:weather-sunset-down", entity_registry_enabled_default=False, ), SensorEntityDescription( key="shkia", - name="Shkia", + translation_key="shkia", icon="mdi:weather-sunset", ), SensorEntityDescription( key="tset_hakohavim_tsom", - name="T'set Hakochavim", + translation_key="tset_hakohavim_tsom", icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="tset_hakohavim_shabbat", - name="T'set Hakochavim, 3 stars", + translation_key="tset_hakohavim_shabbat", icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_candle_lighting", - name="Upcoming Shabbat Candle Lighting", + translation_key="upcoming_shabbat_candle_lighting", icon="mdi:candle", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_havdalah", - name="Upcoming Shabbat Havdalah", + translation_key="upcoming_shabbat_havdalah", icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_candle_lighting", - name="Upcoming Candle Lighting", + translation_key="upcoming_candle_lighting", icon="mdi:candle", ), SensorEntityDescription( key="upcoming_havdalah", - name="Upcoming Havdalah", + translation_key="upcoming_havdalah", icon="mdi:weather-night", ), ) diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json index dcdfb05f10c..33d58ea3487 100644 --- a/homeassistant/components/jewish_calendar/strings.json +++ b/homeassistant/components/jewish_calendar/strings.json @@ -1,12 +1,90 @@ { "entity": { + "binary_sensor": { + "issur_melacha_in_effect": { + "name": "Issur Melacha in effect" + }, + "erev_shabbat_hag": { + "name": "Erev Shabbat/Hag" + }, + "motzei_shabbat_hag": { + "name": "Motzei Shabbat/Hag" + } + }, "sensor": { "hebrew_date": { + "name": "Date", "state_attributes": { "hebrew_year": { "name": "Hebrew year" }, "hebrew_month_name": { "name": "Hebrew month name" }, "hebrew_day": { "name": "Hebrew day" } } + }, + "weekly_portion": { + "name": "Weekly Torah portion" + }, + "holiday": { + "name": "Holiday" + }, + "omer_count": { + "name": "Day of the Omer" + }, + "daf_yomi": { + "name": "Daf Yomi" + }, + "alot_hashachar": { + "name": "Halachic dawn (Alot Hashachar)" + }, + "talit_and_tefillin": { + "name": "Earliest time for Talit and Tefillin" + }, + "netz_hachama": { + "name": "Halachic sunrise (Netz Hachama)" + }, + "sof_zman_shema_gra": { + "name": "Latest time for Shma Gr\"a" + }, + "sof_zman_shema_mga": { + "name": "Latest time for Shma MG\"A" + }, + "sof_zman_tfilla_gra": { + "name": "Latest time for Tefilla Gr\"a" + }, + "sof_zman_tfilla_mga": { + "name": "Latest time for Tefilla MG\"A" + }, + "chatzot_hayom": { + "name": "Halachic midday (Chatzot Hayom)" + }, + "mincha_gedola": { + "name": "Mincha Gedola" + }, + "mincha_ketana": { + "name": "Mincha Ketana" + }, + "plag_hamincha": { + "name": "Plag Hamincha" + }, + "shkia": { + "name": "Sunset (Shkia)" + }, + "tset_hakohavim_tsom": { + "name": "Nightfall (T'set Hakochavim)" + }, + "tset_hakohavim_shabbat": { + "name": "Nightfall (T'set Hakochavim, 3 stars)" + }, + "upcoming_shabbat_candle_lighting": { + "name": "Upcoming Shabbat candle lighting" + }, + "upcoming_shabbat_havdalah": { + "name": "Upcoming Shabbat Havdalah" + }, + "upcoming_candle_lighting": { + "name": "Upcoming candle lighting" + }, + "upcoming_havdalah": { + "name": "Upcoming Havdalah" } } }, diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index d38d20ab4d6..b33d8f3e84b 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -94,21 +94,21 @@ TEST_PARAMS = [ "state": "נצבים", "attr": { "device_class": "enum", - "friendly_name": "Jewish Calendar Parshat Hashavua", + "friendly_name": "Jewish Calendar Weekly Torah portion", "icon": "mdi:book-open-variant", "options": list(Parasha), }, }, "he", - "parshat_hashavua", - id="torah_reading", + "weekly_torah_portion", + id="torah_portion", ), pytest.param( "New York", dt(2018, 9, 8), {"state": dt(2018, 9, 8, 19, 47)}, "he", - "t_set_hakochavim", + "nightfall_t_set_hakochavim", id="first_stars_ny", ), pytest.param( @@ -116,7 +116,7 @@ TEST_PARAMS = [ dt(2018, 9, 8), {"state": dt(2018, 9, 8, 19, 21)}, "he", - "t_set_hakochavim", + "nightfall_t_set_hakochavim", id="first_stars_jerusalem", ), pytest.param( @@ -124,8 +124,8 @@ TEST_PARAMS = [ dt(2018, 10, 14), {"state": "לך לך"}, "he", - "parshat_hashavua", - id="torah_reading_weekday", + "weekly_torah_portion", + id="torah_portion_weekday", ), pytest.param( "Jerusalem", @@ -185,8 +185,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 1, 20, 10), "en_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12), "en_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 10), - "en_parshat_hashavua": "Ki Tavo", - "he_parshat_hashavua": "כי תבוא", + "en_weekly_torah_portion": "Ki Tavo", + "he_weekly_torah_portion": "כי תבוא", }, None, id="currently_first_shabbat", @@ -199,8 +199,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 1, 20, 18), "en_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 12), "en_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 18), - "en_parshat_hashavua": "Ki Tavo", - "he_parshat_hashavua": "כי תבוא", + "en_weekly_torah_portion": "Ki Tavo", + "he_weekly_torah_portion": "כי תבוא", }, 50, # Havdalah offset id="currently_first_shabbat_with_havdalah_offset", @@ -213,8 +213,8 @@ SHABBAT_PARAMS = [ "en_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 10), "en_upcoming_candle_lighting": dt(2018, 8, 31, 19, 12), "en_upcoming_havdalah": dt(2018, 9, 1, 20, 10), - "en_parshat_hashavua": "Ki Tavo", - "he_parshat_hashavua": "כי תבוא", + "en_weekly_torah_portion": "Ki Tavo", + "he_weekly_torah_portion": "כי תבוא", }, None, id="currently_first_shabbat_bein_hashmashot_lagging_date", @@ -227,8 +227,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 8, 19, 58), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19), "en_upcoming_shabbat_havdalah": dt(2018, 9, 8, 19, 58), - "en_parshat_hashavua": "Nitzavim", - "he_parshat_hashavua": "נצבים", + "en_weekly_torah_portion": "Nitzavim", + "he_weekly_torah_portion": "נצבים", }, None, id="after_first_shabbat", @@ -241,8 +241,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 8, 19, 58), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19), "en_upcoming_shabbat_havdalah": dt(2018, 9, 8, 19, 58), - "en_parshat_hashavua": "Nitzavim", - "he_parshat_hashavua": "נצבים", + "en_weekly_torah_portion": "Nitzavim", + "he_weekly_torah_portion": "נצבים", }, None, id="friday_upcoming_shabbat", @@ -255,8 +255,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 11, 19, 53), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), "en_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), - "en_parshat_hashavua": "Vayeilech", - "he_parshat_hashavua": "וילך", + "en_weekly_torah_portion": "Vayeilech", + "he_weekly_torah_portion": "וילך", "en_holiday": "Erev Rosh Hashana", "he_holiday": "ערב ראש השנה", }, @@ -271,8 +271,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 11, 19, 53), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), "en_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), - "en_parshat_hashavua": "Vayeilech", - "he_parshat_hashavua": "וילך", + "en_weekly_torah_portion": "Vayeilech", + "he_weekly_torah_portion": "וילך", "en_holiday": "Rosh Hashana I", "he_holiday": "א' ראש השנה", }, @@ -287,8 +287,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 11, 19, 53), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 48), "en_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 46), - "en_parshat_hashavua": "Vayeilech", - "he_parshat_hashavua": "וילך", + "en_weekly_torah_portion": "Vayeilech", + "he_weekly_torah_portion": "וילך", "en_holiday": "Rosh Hashana II", "he_holiday": "ב' ראש השנה", }, @@ -303,8 +303,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 9, 29, 19, 22), "en_upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 25), "en_upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 22), - "en_parshat_hashavua": "none", - "he_parshat_hashavua": "none", + "en_weekly_torah_portion": "none", + "he_weekly_torah_portion": "none", }, None, id="currently_shabbat_chol_hamoed", @@ -317,8 +317,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 2, 19, 17), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", "en_holiday": "Hoshana Raba", "he_holiday": "הושענא רבה", }, @@ -333,8 +333,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 2, 19, 17), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", "en_holiday": "Shmini Atzeret", "he_holiday": "שמיני עצרת", }, @@ -349,8 +349,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 2, 19, 17), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 13), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 11), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", "en_holiday": "Simchat Torah", "he_holiday": "שמחת תורה", }, @@ -365,8 +365,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 1, 19, 1), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 17, 39), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", "en_holiday": "Hoshana Raba", "he_holiday": "הושענא רבה", }, @@ -381,8 +381,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 1, 19, 1), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 17, 39), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", "en_holiday": "Shmini Atzeret, Simchat Torah", "he_holiday": "שמיני עצרת, שמחת תורה", }, @@ -397,8 +397,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2018, 10, 6, 18, 54), "en_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 17, 39), "en_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 54), - "en_parshat_hashavua": "Bereshit", - "he_parshat_hashavua": "בראשית", + "en_weekly_torah_portion": "Bereshit", + "he_weekly_torah_portion": "בראשית", }, None, id="after_one_day_yom_tov_in_israel", @@ -411,8 +411,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2016, 6, 13, 21, 19), "en_upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 9), "en_upcoming_shabbat_havdalah": "unknown", - "en_parshat_hashavua": "Bamidbar", - "he_parshat_hashavua": "במדבר", + "en_weekly_torah_portion": "Bamidbar", + "he_weekly_torah_portion": "במדבר", "en_holiday": "Erev Shavuot", "he_holiday": "ערב שבועות", }, @@ -427,8 +427,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2016, 6, 13, 21, 19), "en_upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 12), "en_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 21), - "en_parshat_hashavua": "Nasso", - "he_parshat_hashavua": "נשא", + "en_weekly_torah_portion": "Nasso", + "he_weekly_torah_portion": "נשא", "en_holiday": "Shavuot", "he_holiday": "שבועות", }, @@ -443,8 +443,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2017, 9, 23, 19, 11), "en_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 17, 56), "en_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), - "en_parshat_hashavua": "Ha'Azinu", - "he_parshat_hashavua": "האזינו", + "en_weekly_torah_portion": "Ha'Azinu", + "he_weekly_torah_portion": "האזינו", "en_holiday": "Rosh Hashana I", "he_holiday": "א' ראש השנה", }, @@ -459,8 +459,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2017, 9, 23, 19, 11), "en_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 17, 56), "en_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), - "en_parshat_hashavua": "Ha'Azinu", - "he_parshat_hashavua": "האזינו", + "en_weekly_torah_portion": "Ha'Azinu", + "he_weekly_torah_portion": "האזינו", "en_holiday": "Rosh Hashana II", "he_holiday": "ב' ראש השנה", }, @@ -475,8 +475,8 @@ SHABBAT_PARAMS = [ "en_upcoming_havdalah": dt(2017, 9, 23, 19, 11), "en_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 17, 56), "en_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 11), - "en_parshat_hashavua": "Ha'Azinu", - "he_parshat_hashavua": "האזינו", + "en_weekly_torah_portion": "Ha'Azinu", + "he_weekly_torah_portion": "האזינו", "en_holiday": "", "he_holiday": "", }, From d1b85cd452ddaae0c81b77190f81676dfb5c04f2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 9 May 2025 00:26:43 +0200 Subject: [PATCH 0262/1175] Fix voip test RuntimeWarning (#144519) --- tests/components/voip/test_voip.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 65567c8e1d1..364c4d3dd5a 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -1119,9 +1119,10 @@ async def test_start_conversation_user_doesnt_pick_up( & assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION ) - # Protocol has already been mocked, but "outgoing_call" is not async + # Protocol has already been mocked, but "outgoing_call" and "cancel_call" are not async mock_protocol: AsyncMock = hass.data[DOMAIN].protocol mock_protocol.outgoing_call = Mock() + mock_protocol.cancel_call = Mock() announcement = assist_satellite.AssistSatelliteAnnouncement( message="test announcement", From 96a8902365ba8e076caa6f4bb8dc323c13114e98 Mon Sep 17 00:00:00 2001 From: Tamer Wahba Date: Thu, 8 May 2025 18:41:14 -0400 Subject: [PATCH 0263/1175] fix homekit air purifier temperature sensor to convert unit (#144435) --- .../components/homekit/type_air_purifiers.py | 22 ++++++++++++++++--- .../homekit/test_type_air_purifiers.py | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_air_purifiers.py b/homeassistant/components/homekit/type_air_purifiers.py index 25d305a0aa9..feb75f4a856 100644 --- a/homeassistant/components/homekit/type_air_purifiers.py +++ b/homeassistant/components/homekit/type_air_purifiers.py @@ -8,7 +8,13 @@ from pyhap.const import CATEGORY_AIR_PURIFIER from pyhap.service import Service from pyhap.util import callback as pyhap_callback -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfTemperature, +) from homeassistant.core import ( Event, EventStateChangedData, @@ -43,7 +49,12 @@ from .const import ( THRESHOLD_FILTER_CHANGE_NEEDED, ) from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan -from .util import cleanup_name_for_homekit, convert_to_float, density_to_air_quality +from .util import ( + cleanup_name_for_homekit, + convert_to_float, + density_to_air_quality, + temperature_to_homekit, +) _LOGGER = logging.getLogger(__name__) @@ -345,8 +356,13 @@ class AirPurifier(Fan): ): return + unit = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS + ) + current_temperature = temperature_to_homekit(current_temperature, unit) + _LOGGER.debug( - "%s: Linked temperature sensor %s changed to %d", + "%s: Linked temperature sensor %s changed to %d °C", self.entity_id, self.linked_temperature_sensor, current_temperature, diff --git a/tests/components/homekit/test_type_air_purifiers.py b/tests/components/homekit/test_type_air_purifiers.py index 90b0e0047de..acc7838652d 100644 --- a/tests/components/homekit/test_type_air_purifiers.py +++ b/tests/components/homekit/test_type_air_purifiers.py @@ -34,9 +34,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + UnitOfTemperature, ) from homeassistant.core import Event, HomeAssistant @@ -437,6 +439,22 @@ async def test_expose_linked_sensors( assert acc.char_air_quality.value == 1 assert len(broker.mock_calls) == 0 + # Updated temperature with different unit should reflect in HomeKit + broker = MagicMock() + acc.char_current_temperature.broker = broker + hass.states.async_set( + temperature_entity_id, + 60, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, + }, + ) + await hass.async_block_till_done() + assert acc.char_current_temperature.value == 15.6 + assert len(broker.mock_calls) == 2 + broker.reset_mock() + # Updated temperature should reflect in HomeKit broker = MagicMock() acc.char_current_temperature.broker = broker From 1342dc142ca8e41fd5cd29e776fd27ef816f3a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 9 May 2025 09:50:32 +0200 Subject: [PATCH 0264/1175] Update test fixture for Miele dishwasher (#144537) --- tests/components/miele/fixtures/5_devices.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/miele/fixtures/5_devices.json b/tests/components/miele/fixtures/5_devices.json index 1753d982fb6..113babbd3f7 100644 --- a/tests/components/miele/fixtures/5_devices.json +++ b/tests/components/miele/fixtures/5_devices.json @@ -577,7 +577,7 @@ }, "state": { "ProgramID": { - "value_raw": 99938, + "value_raw": 38, "value_localized": "QuickPowerWash", "key_localized": "Program name" }, @@ -587,12 +587,12 @@ "key_localized": "status" }, "programType": { - "value_raw": 9992, + "value_raw": 2, "value_localized": "Automatic programme", "key_localized": "Program type" }, "programPhase": { - "value_raw": 9991799, + "value_raw": 1799, "value_localized": "Drying", "key_localized": "Program phase" }, From 7287f302f6fdc817b46dd6b36e3542a37677b9b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 09:51:56 +0200 Subject: [PATCH 0265/1175] Bump actions/dependency-review-action from 4.6.0 to 4.7.0 (#144532) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 804b0883976..ae732ef4912 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -653,7 +653,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Dependency review - uses: actions/dependency-review-action@v4.6.0 + uses: actions/dependency-review-action@v4.7.0 with: license-check: false # We use our own license audit checks From 2c8e33558e0deab44399c3d371574b8475e3cc62 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 May 2025 09:55:32 +0200 Subject: [PATCH 0266/1175] Catch and log unexpected backup ciphering errors (#144531) --- homeassistant/components/backup/util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/backup/util.py b/homeassistant/components/backup/util.py index 8112faf4459..1a32c938a54 100644 --- a/homeassistant/components/backup/util.py +++ b/homeassistant/components/backup/util.py @@ -332,6 +332,9 @@ def decrypt_backup( except (DecryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error decrypting backup: %s", err) error = err + except Exception as err: # noqa: BLE001 + LOGGER.exception("Unexpected error when decrypting backup: %s", err) + error = err else: # Pad the output stream to the requested minimum size padding = max(minimum_size - output_stream.tell(), 0) @@ -417,6 +420,9 @@ def encrypt_backup( except (EncryptError, SecureTarError, tarfile.TarError) as err: LOGGER.warning("Error encrypting backup: %s", err) error = err + except Exception as err: # noqa: BLE001 + LOGGER.exception("Unexpected error when decrypting backup: %s", err) + error = err else: # Pad the output stream to the requested minimum size padding = max(minimum_size - output_stream.tell(), 0) From 19b1dc8d652be857f471b7e802deb4aa2e2db9ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 9 May 2025 09:56:07 +0200 Subject: [PATCH 0267/1175] Add backup tests showing that unknown files are not ciphered (#144529) --- .../c0cb53bd.tar.decrypted_skip_core2 | Bin 0 -> 10240 bytes .../c0cb53bd.tar.encrypted_skip_core2 | Bin 0 -> 10240 bytes tests/components/backup/test_util.py | 74 ++++++++++++++---- 3 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted_skip_core2 create mode 100644 tests/components/backup/fixtures/test_backups/c0cb53bd.tar.encrypted_skip_core2 diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted_skip_core2 b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.decrypted_skip_core2 new file mode 100644 index 0000000000000000000000000000000000000000..ba53b103b03c80ef0741ffcba9c2796279d63155 GIT binary patch literal 10240 zcmdPXPfASAE-lc@D$dVipbanp0y7g61`rJd=(K^ksVPhhB5!D5WNOHuU_cw^pqg4* zT#{G>v>sJ-#PF(>lJj#5ic*Vn_8L~>KXv~21-D+sTCzfi9p5GN||}Uu*^$Njn4!brDT?5Vrpz|Y@A}0 zVs4URlxUflYGQ1ZmS&Wel$e-kW&+Y&o>`I+pIBOwpPN{cnH*o7T2hjkmtG9io|c$X zoLUQ1l98XAnpj+%2{O9`Y)@)Ma!zSVYCO=DiAjmYAeTbjU6xu@oSB~ovK{1fJtI8> zy_D2410|3_d5O7TPdNjlJU>?flI|5y)d7_i6y=wsCYOMN1MCQ(SaD8iI>^vugXAPr zlqxCO)vb%bLn&t$8QJW5S5HadBAeG{&Py%so_RVc%6$$v^Kz|A}N z&yKBLf~#(y`M>3ixc%Mocfa=k{r95&%m0J_g|0N*6lih$U*2&1;@b~(N4g>p|1lNFHyjCh&*1p!{MGD@XTp1@ZvXr5{H@~O_WPMk z!w>a0i#Q2u9XiZ-t^SMsFZsXyCQs`#cCi0A{qO$7|2rp2{jR_J(Z1%h{q`6C!~U$_ z|KMRDfQF;#QUDE3Er&rTl#)fJQ0Kho-6R5ELA zX1>g>I{)8aFC6~jAi&}{q5BE%tH4VY3>w(tjGAJj_5a9j|6{HHjV&zA%q>Rie`;p5 zL26bqs6#@ddLQOx((8W%Lo;CepDum!5#IhEt^Y^s|AFuS8zaU7K;s0!@jpZ0_}^&% zA6A0Wjy5vK(*HLxG%_|Gt^R2j>VwB*Q2lQ-!1cemi2+*wf3*ImR|*MEO)f1;4M{9w zLbj1{5scSe74f8hz20x-mblGg&P@-zH4>vD_ge^^{kNvI(A?64pVzgp_{klv8La-o zSKNQ395}bDYxeu?TdPz9;zUsgmdzVAk4AdWTWPFUR>`eUo>-U-P#rj!S zb6k&><;-*9GC#DPy-aSS__Uj>JJ(FQ#BUk2I<#3ts6ZzUTA}t`f8Kf!w}d_YGns9#2)By!FjgXA76T*G(^XsP%`6|5du4uum{%VcmwPsvR5e zuN1o8ziGDM#bEV^Nx56TRq%@BeK#r#HhNH#{q9@3u#V);`P!lXdDT?q=O0ru&U?&2o|x*k`D~_wOETkbS>xaOrFNNStBqg&nq>H~QugYFJCQFh zTd+0n*V-E=UG_=~so%)#J=IlXceB%Oh1b*lJk!*&*sj;;W#l~-QCpr~ztK$EQ~$ft uUES;$ZKJS|7rd}|9HmD?U^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLjVBYMb8NU literal 0 HcmV?d00001 diff --git a/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.encrypted_skip_core2 b/tests/components/backup/fixtures/test_backups/c0cb53bd.tar.encrypted_skip_core2 new file mode 100644 index 0000000000000000000000000000000000000000..40216194671268d8120c8b3c7fe9f6c66019ae12 GIT binary patch literal 10240 zcmeH~c~BEq9LEE7L@6o)iX3VPQjjC;m0J`kMjeWvfYnlsn`D6iImm7}Lh%5^L4|r$ ztqv{7VH_`-3Zi&Zr6PEYA}TN<;z=tVsUv7byNgAoUemF3+OqGTH}AK<_kF+jdGEKs zUADIrmBr{Z>}XuAV)RV|$i_tw2Gx`8&pp87Jb(7=IS4=;fWZX%CY;`?I-EeYWJtZO z{brQRlBty%Er#QmoF!xuS{>%eWXVwiBS$QVJP0oU^a7DkkRt?OA;f2MV2*$bdI97Z zV3EF9BB4b|=VX>bMY2@MFo}YaVsWJi2j;=B9Fp@8IfM#O41uAD2q;2|q9_-kCMPI} zC<&?~)Jl|4$Rs#M5DHZ!PELR;NN|Lu`O8$KnANC8R=?-JANDJaV;?`Rv{^d;5`Y8m*eZWW>uj@K{|W zr6~hsQVuMYQ;`!%nr<}>swEU?EMNHb`qYWVj$)EJQmWx?6$m^ z%mUcmU~z?>T7evq(f8JXUeV~byj5df%IhvQ+nep}8;mAS!%#VAoF*_qdorX1bV$) z*<>kXN9zCXJyH4p*7}bC3=rWV5QuQN)VBZ#0Wg{W|42>l=lLIk$#hyQ6xEu(eCj6o zJ)?t2tGY@w(B`zhy(Az=@M*-b@#>*FTon)Rgl=hwS-n3s%&Bm>SBBq8C+iX1y&TWh zmiu)FhF#2@1xqsRT4WzoUQ^C#_%>_4u<*C0{1T6x^r?~C59Mx(igPQfI6UjjLOX1N zcCEO!C7$zubE^xF;FgyTfq>ikV`bJJ387caj_zacQ7&(=lz6R+o|dwrz}l<5(KirV z;4-leFq&Ew7uGKhYrV)smN_2Hy3lgh)Bcbf z*@{ za)PR}VBuA(l42()m`Imv@{A`(3o{PZZfJ9M$TZYcKYG%<`Y^cMoCU+R7sjr#LjPNZ(sj0X-ctbNirC_Pow}nd2%?^qk|HdbW`$4>V6NVzh3_VkPFiFzlUF6_V23yR_p6J!e)5x8DG%o zn_Auy>p^@ayvkd02H)#)sUthNZG(5KXr67ByJvDrV*txRN9(NwRl@%X+#aH})b zt=TNCG{)c(A>3e-lM4*#D9)OZQF~_gw0&8-Om3VC8Cz->qnL9s#&1z#h5ND9U18H< zTq`nWe^;)EcOA^cnMaZuk7O=d*3o%4et7*IzOZzBd{TiG<8-(HnZM%bAb85aXpUlx zzxDOANwG=JyCcDoqO6pgW!VmM1&l*2ra6v*+rAu_CoG{Lhfg2KqU6|5u{_Ltp(t)Nj#jjDeD-VGC zY=mixEZWY=&3A^+OJazHv+^hC@sah%7xm@cp|xoOGy$3bO@Jmq6QBvu1ZV;@0h$0! UfF?i_pb5|fXaY2We>Z{O0lIRksQ>@~ literal 0 HcmV?d00001 diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index a999672e7f6..229e25c312d 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -167,17 +167,37 @@ def test_validate_password_no_homeassistant() -> None: assert validate_password(mock_path, "hunter2") is False -async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("addons", "padding_size", "decrypted_backup"), + [ + ( + [ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], + 40960, # 4 x 10240 byte of padding + "test_backups/c0cb53bd.tar.decrypted", + ), + ( + [ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + ], + 30720, # 3 x 10240 byte of padding + "test_backups/c0cb53bd.tar.decrypted_skip_core2", + ), + ], +) +async def test_decrypted_backup_streamer( + hass: HomeAssistant, + addons: list[AddonInfo], + padding_size: int, + decrypted_backup: str, +) -> None: """Test the decrypted backup streamer.""" - decrypted_backup_path = get_fixture_path( - "test_backups/c0cb53bd.tar.decrypted", DOMAIN - ) + decrypted_backup_path = get_fixture_path(decrypted_backup, DOMAIN) encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) backup = AgentBackup( - addons=[ - AddonInfo(name="Core 1", slug="core1", version="1.0.0"), - AddonInfo(name="Core 2", slug="core2", version="1.0.0"), - ], + addons=addons, backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -189,7 +209,7 @@ async def test_decrypted_backup_streamer(hass: HomeAssistant) -> None: protected=True, size=encrypted_backup_path.stat().st_size, ) - expected_padding = b"\0" * 40960 # 4 x 10240 byte of padding + expected_padding = b"\0" * padding_size async def send_backup() -> AsyncIterator[bytes]: f = encrypted_backup_path.open("rb") @@ -325,17 +345,39 @@ async def test_decrypted_backup_streamer_wrong_password(hass: HomeAssistant) -> assert isinstance(decryptor._workers[0].error, securetar.SecureTarReadError) -async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("addons", "padding_size", "encrypted_backup"), + [ + ( + [ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + AddonInfo(name="Core 2", slug="core2", version="1.0.0"), + ], + 40960, # 4 x 10240 byte of padding + "test_backups/c0cb53bd.tar", + ), + ( + [ + AddonInfo(name="Core 1", slug="core1", version="1.0.0"), + ], + 30720, # 3 x 10240 byte of padding + "test_backups/c0cb53bd.tar.encrypted_skip_core2", + ), + ], +) +async def test_encrypted_backup_streamer( + hass: HomeAssistant, + addons: list[AddonInfo], + padding_size: int, + encrypted_backup: str, +) -> None: """Test the encrypted backup streamer.""" decrypted_backup_path = get_fixture_path( "test_backups/c0cb53bd.tar.decrypted", DOMAIN ) - encrypted_backup_path = get_fixture_path("test_backups/c0cb53bd.tar", DOMAIN) + encrypted_backup_path = get_fixture_path(encrypted_backup, DOMAIN) backup = AgentBackup( - addons=[ - AddonInfo(name="Core 1", slug="core1", version="1.0.0"), - AddonInfo(name="Core 2", slug="core2", version="1.0.0"), - ], + addons=addons, backup_id="1234", date="2024-12-02T07:23:58.261875-05:00", database_included=False, @@ -347,7 +389,7 @@ async def test_encrypted_backup_streamer(hass: HomeAssistant) -> None: protected=False, size=decrypted_backup_path.stat().st_size, ) - expected_padding = b"\0" * 40960 # 4 x 10240 byte of padding + expected_padding = b"\0" * padding_size async def send_backup() -> AsyncIterator[bytes]: f = decrypted_backup_path.open("rb") From eab1d5717f18767f3a12cb4ac1be7506db22dec7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 May 2025 10:04:11 +0200 Subject: [PATCH 0268/1175] Use HassKey in hardware (#144337) --- homeassistant/components/hardware/__init__.py | 5 +++-- homeassistant/components/hardware/const.py | 11 +++++++++++ homeassistant/components/hardware/hardware.py | 10 ++++++---- homeassistant/components/hardware/models.py | 13 ++++++++++++- homeassistant/components/hardware/websocket_api.py | 13 +++++-------- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hardware/__init__.py b/homeassistant/components/hardware/__init__.py index 9de281b1e50..8576b29ef19 100644 --- a/homeassistant/components/hardware/__init__.py +++ b/homeassistant/components/hardware/__init__.py @@ -7,14 +7,15 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import websocket_api -from .const import DOMAIN +from .const import DATA_HARDWARE, DOMAIN +from .models import HardwareData CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Hardware.""" - hass.data[DOMAIN] = {} + hass.data[DATA_HARDWARE] = HardwareData() await websocket_api.async_setup(hass) diff --git a/homeassistant/components/hardware/const.py b/homeassistant/components/hardware/const.py index 7fd64d5d968..2bde218c19d 100644 --- a/homeassistant/components/hardware/const.py +++ b/homeassistant/components/hardware/const.py @@ -1,3 +1,14 @@ """Constants for the Hardware integration.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from .models import HardwareData + DOMAIN = "hardware" + +DATA_HARDWARE: HassKey[HardwareData] = HassKey(DOMAIN) diff --git a/homeassistant/components/hardware/hardware.py b/homeassistant/components/hardware/hardware.py index f2de9182b57..e07d970cd1f 100644 --- a/homeassistant/components/hardware/hardware.py +++ b/homeassistant/components/hardware/hardware.py @@ -8,13 +8,15 @@ from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from .const import DOMAIN +from .const import DATA_HARDWARE, DOMAIN from .models import HardwareProtocol -async def async_process_hardware_platforms(hass: HomeAssistant) -> None: +async def async_process_hardware_platforms( + hass: HomeAssistant, +) -> None: """Start processing hardware platforms.""" - hass.data[DOMAIN]["hardware_platform"] = {} + hass.data[DATA_HARDWARE].hardware_platform = {} await async_process_integration_platforms( hass, DOMAIN, _register_hardware_platform, wait_for_platforms=True @@ -30,4 +32,4 @@ def _register_hardware_platform( return if not hasattr(platform, "async_info"): raise HomeAssistantError(f"Invalid hardware platform {platform}") - hass.data[DOMAIN]["hardware_platform"][integration_domain] = platform + hass.data[DATA_HARDWARE].hardware_platform[integration_domain] = platform diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 6f25d6669cf..4b3c234f4c1 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -3,10 +3,21 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Protocol +from typing import TYPE_CHECKING, Protocol from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from .websocket_api import SystemStatus + + +@dataclass +class HardwareData: + """Hardware data.""" + + hardware_platform: dict[str, HardwareProtocol] = None # type: ignore[assignment] + system_status: SystemStatus = None # type: ignore[assignment] + @dataclass(slots=True) class BoardInfo: diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index 7224c0f8f7e..fa441beeae5 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -16,9 +16,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util -from .const import DOMAIN +from .const import DATA_HARDWARE from .hardware import async_process_hardware_platforms -from .models import HardwareProtocol @dataclass(slots=True) @@ -34,7 +33,7 @@ async def async_setup(hass: HomeAssistant) -> None: """Set up the hardware websocket API.""" websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_subscribe_system_status) - hass.data[DOMAIN]["system_status"] = SystemStatus( + hass.data[DATA_HARDWARE].system_status = SystemStatus( ha_psutil=await hass.async_add_executor_job(ha_psutil.PsutilWrapper), remove_periodic_timer=None, subscribers=set(), @@ -53,12 +52,10 @@ async def ws_info( """Return hardware info.""" hardware_info = [] - if "hardware_platform" not in hass.data[DOMAIN]: + if hass.data[DATA_HARDWARE].hardware_platform is None: await async_process_hardware_platforms(hass) - hardware_platform: dict[str, HardwareProtocol] = hass.data[DOMAIN][ - "hardware_platform" - ] + hardware_platform = hass.data[DATA_HARDWARE].hardware_platform for platform in hardware_platform.values(): if hasattr(platform, "async_info"): with contextlib.suppress(HomeAssistantError): @@ -78,7 +75,7 @@ def ws_subscribe_system_status( ) -> None: """Subscribe to system status updates.""" - system_status: SystemStatus = hass.data[DOMAIN]["system_status"] + system_status = hass.data[DATA_HARDWARE].system_status @callback def async_update_status(now: datetime) -> None: From a1e6f596d7d0d3987664316c505cfce7f86a0a06 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 9 May 2025 18:04:43 +1000 Subject: [PATCH 0269/1175] Add common translation section to Teslemetry (#144361) --- .../components/teslemetry/strings.json | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index 456850fde3e..b3837bb874d 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -1,4 +1,9 @@ { + "common": { + "unavailable": "Unavailable", + "abort": "Abort", + "vehicle": "Vehicle" + }, "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", @@ -568,7 +573,7 @@ "name": "Version" }, "vin": { - "name": "Vehicle", + "name": "[%key:component::teslemetry::common::vehicle%]", "state": { "disconnected": "[%key:common::state::disconnected%]" } @@ -753,40 +758,40 @@ "di_state_f": { "name": "Front drive inverter", "state": { - "unavailable": "Unavailable", + "unavailable": "[%key:component::teslemetry::common::unavailable%]", "standby": "[%key:common::state::standby%]", "fault": "[%key:common::state::fault%]", - "abort": "Abort", + "abort": "[%key:component::teslemetry::common::abort%]", "enabled": "[%key:common::state::enabled%]" } }, "di_state_r": { "name": "Rear drive inverter", "state": { - "unavailable": "[%key:component::teslemetry::entity::sensor::di_state_f::state::unavailable%]", + "unavailable": "[%key:component::teslemetry::common::unavailable%]", "standby": "[%key:common::state::standby%]", "fault": "[%key:common::state::fault%]", - "abort": "[%key:component::teslemetry::entity::sensor::di_state_f::state::abort%]", + "abort": "[%key:component::teslemetry::common::abort%]", "enabled": "[%key:common::state::enabled%]" } }, "di_state_rel": { "name": "Rear left drive inverter", "state": { - "unavailable": "[%key:component::teslemetry::entity::sensor::di_state_f::state::unavailable%]", + "unavailable": "[%key:component::teslemetry::common::unavailable%]", "standby": "[%key:common::state::standby%]", "fault": "[%key:common::state::fault%]", - "abort": "[%key:component::teslemetry::entity::sensor::di_state_f::state::abort%]", + "abort": "[%key:component::teslemetry::common::abort%]", "enabled": "[%key:common::state::enabled%]" } }, "di_state_rer": { "name": "Rear right drive inverter", "state": { - "unavailable": "[%key:component::teslemetry::entity::sensor::di_state_f::state::unavailable%]", + "unavailable": "[%key:component::teslemetry::common::unavailable%]", "standby": "[%key:common::state::standby%]", "fault": "[%key:common::state::fault%]", - "abort": "[%key:component::teslemetry::entity::sensor::di_state_f::state::abort%]", + "abort": "[%key:component::teslemetry::common::abort%]", "enabled": "[%key:common::state::enabled%]" } }, @@ -1112,7 +1117,7 @@ "fields": { "device_id": { "description": "Vehicle to share to.", - "name": "Vehicle" + "name": "[%key:component::teslemetry::common::vehicle%]" }, "gps": { "description": "Location to navigate to.", @@ -1130,7 +1135,7 @@ "fields": { "device_id": { "description": "Vehicle to schedule.", - "name": "Vehicle" + "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { "description": "Enable or disable scheduled charging.", @@ -1152,7 +1157,7 @@ }, "device_id": { "description": "Vehicle to schedule.", - "name": "Vehicle" + "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { "description": "Enable or disable scheduled departure.", @@ -1186,7 +1191,7 @@ "fields": { "device_id": { "description": "Vehicle to limit.", - "name": "Vehicle" + "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { "description": "Enable or disable speed limit.", @@ -1218,7 +1223,7 @@ "fields": { "device_id": { "description": "Vehicle to limit.", - "name": "Vehicle" + "name": "[%key:component::teslemetry::common::vehicle%]" }, "enable": { "description": "Enable or disable valet mode.", From 0d85cec770d8f677c2381b633c54561fcc6ce733 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 9 May 2025 10:37:48 +0200 Subject: [PATCH 0270/1175] Fix statistics coordinator subscription for lamarzocco (#144541) --- homeassistant/components/lamarzocco/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index 087605315e5..aecb2ff7f04 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -145,17 +145,18 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensor entities.""" - coordinator = entry.runtime_data.config_coordinator + config_coordinator = entry.runtime_data.config_coordinator + statistic_coordinators = entry.runtime_data.statistics_coordinator entities = [ - LaMarzoccoSensorEntity(coordinator, description) + LaMarzoccoSensorEntity(config_coordinator, description) for description in ENTITIES - if description.supported_fn(coordinator) + if description.supported_fn(config_coordinator) ] entities.extend( - LaMarzoccoStatisticSensorEntity(coordinator, description) + LaMarzoccoStatisticSensorEntity(statistic_coordinators, description) for description in STATISTIC_ENTITIES - if description.supported_fn(coordinator) + if description.supported_fn(statistic_coordinators) ) async_add_entities(entities) From 21e2bbd0660fa97b295cde1d0f8bdf9d91d4831f Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 9 May 2025 10:39:00 +0200 Subject: [PATCH 0271/1175] Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue (#144463) --- homeassistant/components/fronius/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 4ba893df85c..8a3d1ebf04c 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -45,7 +45,15 @@ type FroniusConfigEntry = ConfigEntry[FroniusSolarNet] async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool: """Set up fronius from a config entry.""" host = entry.data[CONF_HOST] - fronius = Fronius(async_get_clientsession(hass), host) + fronius = Fronius( + async_get_clientsession( + hass, + # Fronius Gen24 firmware 1.35.4-1 redirects to HTTPS with self-signed + # certificate. See https://github.com/home-assistant/core/issues/138881 + verify_ssl=False, + ), + host, + ) solar_net = FroniusSolarNet(hass, entry, fronius) await solar_net.init_devices() From b4ae08f83dfd967b503840402a8187c7afe0c8ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 9 May 2025 10:55:41 +0200 Subject: [PATCH 0272/1175] Move hardware initialisation to package module (#144540) --- homeassistant/components/hardware/__init__.py | 15 ++++++++++-- homeassistant/components/hardware/hardware.py | 2 -- homeassistant/components/hardware/models.py | 21 ++++++++++++----- .../components/hardware/websocket_api.py | 23 ++----------------- .../components/hardware/test_websocket_api.py | 2 +- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hardware/__init__.py b/homeassistant/components/hardware/__init__.py index 8576b29ef19..5db9671a4ed 100644 --- a/homeassistant/components/hardware/__init__.py +++ b/homeassistant/components/hardware/__init__.py @@ -2,20 +2,31 @@ from __future__ import annotations +import psutil_home_assistant as ha_psutil + from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType from . import websocket_api from .const import DATA_HARDWARE, DOMAIN -from .models import HardwareData +from .hardware import async_process_hardware_platforms +from .models import HardwareData, SystemStatus CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Hardware.""" - hass.data[DATA_HARDWARE] = HardwareData() + hass.data[DATA_HARDWARE] = HardwareData( + hardware_platform={}, + system_status=SystemStatus( + ha_psutil=await hass.async_add_executor_job(ha_psutil.PsutilWrapper), + remove_periodic_timer=None, + subscribers=set(), + ), + ) + await async_process_hardware_platforms(hass) await websocket_api.async_setup(hass) diff --git a/homeassistant/components/hardware/hardware.py b/homeassistant/components/hardware/hardware.py index e07d970cd1f..9fd257a14a7 100644 --- a/homeassistant/components/hardware/hardware.py +++ b/homeassistant/components/hardware/hardware.py @@ -16,8 +16,6 @@ async def async_process_hardware_platforms( hass: HomeAssistant, ) -> None: """Start processing hardware platforms.""" - hass.data[DATA_HARDWARE].hardware_platform = {} - await async_process_integration_platforms( hass, DOMAIN, _register_hardware_platform, wait_for_platforms=True ) diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 4b3c234f4c1..a972b567db2 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -3,20 +3,29 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Protocol +from typing import Protocol -from homeassistant.core import HomeAssistant, callback +import psutil_home_assistant as ha_psutil -if TYPE_CHECKING: - from .websocket_api import SystemStatus +from homeassistant.components import websocket_api +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @dataclass class HardwareData: """Hardware data.""" - hardware_platform: dict[str, HardwareProtocol] = None # type: ignore[assignment] - system_status: SystemStatus = None # type: ignore[assignment] + hardware_platform: dict[str, HardwareProtocol] + system_status: SystemStatus + + +@dataclass(slots=True) +class SystemStatus: + """System status.""" + + ha_psutil: ha_psutil + remove_periodic_timer: CALLBACK_TYPE | None + subscribers: set[tuple[websocket_api.ActiveConnection, int]] @dataclass(slots=True) diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py index fa441beeae5..599eab34135 100644 --- a/homeassistant/components/hardware/websocket_api.py +++ b/homeassistant/components/hardware/websocket_api.py @@ -3,41 +3,25 @@ from __future__ import annotations import contextlib -from dataclasses import asdict, dataclass +from dataclasses import asdict from datetime import datetime, timedelta from typing import Any -import psutil_home_assistant as ha_psutil import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt as dt_util from .const import DATA_HARDWARE -from .hardware import async_process_hardware_platforms - - -@dataclass(slots=True) -class SystemStatus: - """System status.""" - - ha_psutil: ha_psutil - remove_periodic_timer: CALLBACK_TYPE | None - subscribers: set[tuple[websocket_api.ActiveConnection, int]] async def async_setup(hass: HomeAssistant) -> None: """Set up the hardware websocket API.""" websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_subscribe_system_status) - hass.data[DATA_HARDWARE].system_status = SystemStatus( - ha_psutil=await hass.async_add_executor_job(ha_psutil.PsutilWrapper), - remove_periodic_timer=None, - subscribers=set(), - ) @websocket_api.websocket_command( @@ -52,9 +36,6 @@ async def ws_info( """Return hardware info.""" hardware_info = [] - if hass.data[DATA_HARDWARE].hardware_platform is None: - await async_process_hardware_platforms(hass) - hardware_platform = hass.data[DATA_HARDWARE].hardware_platform for platform in hardware_platform.values(): if hasattr(platform, "async_info"): diff --git a/tests/components/hardware/test_websocket_api.py b/tests/components/hardware/test_websocket_api.py index 64fcda02df4..f5591ff8480 100644 --- a/tests/components/hardware/test_websocket_api.py +++ b/tests/components/hardware/test_websocket_api.py @@ -50,7 +50,7 @@ async def test_system_status_subscription( return mock_psutil with patch( - "homeassistant.components.hardware.websocket_api.ha_psutil.PsutilWrapper", + "homeassistant.components.hardware.ha_psutil.PsutilWrapper", wraps=create_mock_psutil, ): assert await async_setup_component(hass, DOMAIN, {}) From 307bb056534f932db4076c7a792a581c471ec4ec Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 9 May 2025 11:28:25 +0200 Subject: [PATCH 0273/1175] Add support to create KNX Cover entities from UI (#141944) * Add UI to create KNX Cover entities * Use common constants source for UI and YAML config keys --- homeassistant/components/knx/const.py | 11 + homeassistant/components/knx/cover.py | 197 +++++++++++++----- homeassistant/components/knx/schema.py | 16 +- homeassistant/components/knx/storage/const.py | 6 + .../knx/storage/entity_store_schema.py | 92 +++++++- .../components/knx/storage/knx_selector.py | 15 +- .../knx/fixtures/config_store_cover.json | 82 ++++++++ tests/components/knx/test_cover.py | 109 +++++++++- tests/components/knx/test_knx_selectors.py | 68 ++++-- 9 files changed, 506 insertions(+), 90 deletions(-) create mode 100644 tests/components/knx/fixtures/config_store_cover.json diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index b403018dae3..c0c3b9ec2e6 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -160,6 +160,7 @@ SUPPORTED_PLATFORMS_YAML: Final = { SUPPORTED_PLATFORMS_UI: Final = { Platform.BINARY_SENSOR, + Platform.COVER, Platform.LIGHT, Platform.SWITCH, } @@ -182,3 +183,13 @@ CURRENT_HVAC_ACTIONS: Final = { HVACMode.FAN_ONLY: HVACAction.FAN, HVACMode.DRY: HVACAction.DRYING, } + + +class CoverConf: + """Common config keys for cover.""" + + TRAVELLING_TIME_DOWN: Final = "travelling_time_down" + TRAVELLING_TIME_UP: Final = "travelling_time_up" + INVERT_UPDOWN: Final = "invert_updown" + INVERT_POSITION: Final = "invert_position" + INVERT_ANGLE: Final = "invert_angle" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 3c5752b990c..3068e5d7ef1 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -2,9 +2,9 @@ from __future__ import annotations -from collections.abc import Callable -from typing import Any +from typing import Any, Literal +from xknx import XKNX from xknx.devices import Cover as XknxCover from homeassistant import config_entries @@ -22,13 +22,28 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.typing import ConfigType from . import KNXModule -from .const import KNX_MODULE_KEY -from .entity import KnxYamlEntity +from .const import CONF_SYNC_STATE, DOMAIN, KNX_MODULE_KEY, CoverConf +from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity from .schema import CoverSchema +from .storage.const import ( + CONF_ENTITY, + CONF_GA_ANGLE, + CONF_GA_PASSIVE, + CONF_GA_POSITION_SET, + CONF_GA_POSITION_STATE, + CONF_GA_STATE, + CONF_GA_STEP, + CONF_GA_STOP, + CONF_GA_UP_DOWN, + CONF_GA_WRITE, +) async def async_setup_entry( @@ -36,52 +51,47 @@ async def async_setup_entry( config_entry: config_entries.ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up cover(s) for KNX platform.""" + """Set up the KNX cover platform.""" knx_module = hass.data[KNX_MODULE_KEY] - config: list[ConfigType] = knx_module.config_yaml[Platform.COVER] + platform = async_get_current_platform() + knx_module.config_store.add_platform( + platform=Platform.COVER, + controller=KnxUiEntityPlatformController( + knx_module=knx_module, + entity_platform=platform, + entity_class=KnxUiCover, + ), + ) - async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config) + entities: list[KnxYamlEntity | KnxUiEntity] = [] + if yaml_platform_config := knx_module.config_yaml.get(Platform.COVER): + entities.extend( + KnxYamlCover(knx_module, entity_config) + for entity_config in yaml_platform_config + ) + if ui_config := knx_module.config_store.data["entities"].get(Platform.COVER): + entities.extend( + KnxUiCover(knx_module, unique_id, config) + for unique_id, config in ui_config.items() + ) + if entities: + async_add_entities(entities) -class KNXCover(KnxYamlEntity, CoverEntity): +class _KnxCover(CoverEntity): """Representation of a KNX cover.""" _device: XknxCover - def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: - """Initialize the cover.""" - super().__init__( - knx_module=knx_module, - device=XknxCover( - xknx=knx_module.xknx, - name=config[CONF_NAME], - group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), - group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), - group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS), - group_address_position_state=config.get( - CoverSchema.CONF_POSITION_STATE_ADDRESS - ), - group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS), - group_address_angle_state=config.get( - CoverSchema.CONF_ANGLE_STATE_ADDRESS - ), - group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), - travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN], - travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], - invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], - invert_position=config[CoverSchema.CONF_INVERT_POSITION], - invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], - ), - ) - self._unsubscribe_auto_updater: Callable[[], None] | None = None - - self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + def init_base(self) -> None: + """Initialize common attributes - may be based on xknx device instance.""" _supports_tilt = False self._attr_supported_features = ( - CoverEntityFeature.CLOSE - | CoverEntityFeature.OPEN - | CoverEntityFeature.SET_POSITION + CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN ) + if self._device.supports_position or self._device.supports_stop: + # when stop is supported, xknx travelcalculator can set position + self._attr_supported_features |= CoverEntityFeature.SET_POSITION if self._device.step.writable: _supports_tilt = True self._attr_supported_features |= ( @@ -97,13 +107,7 @@ class KNXCover(KnxYamlEntity, CoverEntity): if _supports_tilt: self._attr_supported_features |= CoverEntityFeature.STOP_TILT - self._attr_device_class = config.get(CONF_DEVICE_CLASS) or ( - CoverDeviceClass.BLIND if _supports_tilt else None - ) - self._attr_unique_id = ( - f"{self._device.updown.group_address}_" - f"{self._device.position_target.group_address}" - ) + self._attr_device_class = CoverDeviceClass.BLIND if _supports_tilt else None @property def current_cover_position(self) -> int | None: @@ -180,3 +184,102 @@ class KNXCover(KnxYamlEntity, CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._device.stop() + + +class KnxYamlCover(_KnxCover, KnxYamlEntity): + """Representation of a KNX cover configured from YAML.""" + + _device: XknxCover + + def __init__(self, knx_module: KNXModule, config: ConfigType) -> None: + """Initialize the cover.""" + super().__init__( + knx_module=knx_module, + device=XknxCover( + xknx=knx_module.xknx, + name=config[CONF_NAME], + group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), + group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), + group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS), + group_address_position_state=config.get( + CoverSchema.CONF_POSITION_STATE_ADDRESS + ), + group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS), + group_address_angle_state=config.get( + CoverSchema.CONF_ANGLE_STATE_ADDRESS + ), + group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), + travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN], + travel_time_up=config[CoverConf.TRAVELLING_TIME_UP], + invert_updown=config[CoverConf.INVERT_UPDOWN], + invert_position=config[CoverConf.INVERT_POSITION], + invert_angle=config[CoverConf.INVERT_ANGLE], + ), + ) + self.init_base() + + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) + self._attr_unique_id = ( + f"{self._device.updown.group_address}_" + f"{self._device.position_target.group_address}" + ) + if custom_device_class := config.get(CONF_DEVICE_CLASS): + self._attr_device_class = custom_device_class + + +def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover: + """Return a KNX Light device to be used within XKNX.""" + + def get_address( + key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE + ) -> str | None: + """Get a single group address for given key.""" + return knx_config[key][address_type] if key in knx_config else None + + def get_addresses( + key: str, address_type: Literal["write", "state"] = CONF_GA_STATE + ) -> list[Any] | None: + """Get group address including passive addresses as list.""" + return ( + [knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]] + if key in knx_config + else None + ) + + return XknxCover( + xknx=xknx, + name=name, + group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE), + group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE), + group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE), + group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE), + group_address_position_state=get_addresses(CONF_GA_POSITION_STATE), + group_address_angle=get_address(CONF_GA_ANGLE), + group_address_angle_state=get_addresses(CONF_GA_ANGLE), + travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN], + travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP], + invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False), + invert_position=knx_config.get(CoverConf.INVERT_POSITION, False), + invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False), + sync_state=knx_config[CONF_SYNC_STATE], + ) + + +class KnxUiCover(_KnxCover, KnxUiEntity): + """Representation of a KNX cover configured from the UI.""" + + _device: XknxCover + + def __init__( + self, knx_module: KNXModule, unique_id: str, config: dict[str, Any] + ) -> None: + """Initialize KNX cover.""" + super().__init__( + knx_module=knx_module, + unique_id=unique_id, + entity_config=config[CONF_ENTITY], + ) + self._device = _create_ui_cover( + knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] + ) + self.init_base() diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c9fe0bfc34e..e6dc0c1bb3e 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -56,6 +56,7 @@ from .const import ( CONF_SYNC_STATE, KNX_ADDRESS, ColorTempModes, + CoverConf, FanZeroMode, ) from .validation import ( @@ -453,11 +454,6 @@ class CoverSchema(KNXPlatformSchema): CONF_POSITION_STATE_ADDRESS = "position_state_address" CONF_ANGLE_ADDRESS = "angle_address" CONF_ANGLE_STATE_ADDRESS = "angle_state_address" - CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" - CONF_TRAVELLING_TIME_UP = "travelling_time_up" - CONF_INVERT_UPDOWN = "invert_updown" - CONF_INVERT_POSITION = "invert_position" - CONF_INVERT_ANGLE = "invert_angle" DEFAULT_TRAVEL_TIME = 25 DEFAULT_NAME = "KNX Cover" @@ -474,14 +470,14 @@ class CoverSchema(KNXPlatformSchema): vol.Optional(CONF_ANGLE_ADDRESS): ga_list_validator, vol.Optional(CONF_ANGLE_STATE_ADDRESS): ga_list_validator, vol.Optional( - CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME + CoverConf.TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME ): cv.positive_float, vol.Optional( - CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME + CoverConf.TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME ): cv.positive_float, - vol.Optional(CONF_INVERT_UPDOWN, default=False): cv.boolean, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + vol.Optional(CoverConf.INVERT_UPDOWN, default=False): cv.boolean, + vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } diff --git a/homeassistant/components/knx/storage/const.py b/homeassistant/components/knx/storage/const.py index cf3f2bb9f95..7cae0e9bbf6 100644 --- a/homeassistant/components/knx/storage/const.py +++ b/homeassistant/components/knx/storage/const.py @@ -27,3 +27,9 @@ CONF_GA_WHITE_BRIGHTNESS: Final = "ga_white_brightness" CONF_GA_WHITE_SWITCH: Final = "ga_white_switch" CONF_GA_HUE: Final = "ga_hue" CONF_GA_SATURATION: Final = "ga_saturation" +CONF_GA_UP_DOWN: Final = "ga_up_down" +CONF_GA_STOP: Final = "ga_stop" +CONF_GA_STEP: Final = "ga_step" +CONF_GA_POSITION_SET: Final = "ga_position_set" +CONF_GA_POSITION_STATE: Final = "ga_position_state" +CONF_GA_ANGLE: Final = "ga_angle" diff --git a/homeassistant/components/knx/storage/entity_store_schema.py b/homeassistant/components/knx/storage/entity_store_schema.py index cde18a181ec..85bcbd1809f 100644 --- a/homeassistant/components/knx/storage/entity_store_schema.py +++ b/homeassistant/components/knx/storage/entity_store_schema.py @@ -25,6 +25,7 @@ from ..const import ( DOMAIN, SUPPORTED_PLATFORMS_UI, ColorTempModes, + CoverConf, ) from ..validation import sync_state_validator from .const import ( @@ -33,6 +34,7 @@ from .const import ( CONF_DATA, CONF_DEVICE_INFO, CONF_ENTITY, + CONF_GA_ANGLE, CONF_GA_BLUE_BRIGHTNESS, CONF_GA_BLUE_SWITCH, CONF_GA_BRIGHTNESS, @@ -42,12 +44,17 @@ from .const import ( CONF_GA_GREEN_SWITCH, CONF_GA_HUE, CONF_GA_PASSIVE, + CONF_GA_POSITION_SET, + CONF_GA_POSITION_STATE, CONF_GA_RED_BRIGHTNESS, CONF_GA_RED_SWITCH, CONF_GA_SATURATION, CONF_GA_SENSOR, CONF_GA_STATE, + CONF_GA_STEP, + CONF_GA_STOP, CONF_GA_SWITCH, + CONF_GA_UP_DOWN, CONF_GA_WHITE_BRIGHTNESS, CONF_GA_WHITE_SWITCH, CONF_GA_WRITE, @@ -121,15 +128,64 @@ BINARY_SENSOR_SCHEMA = vol.Schema( } ) -SWITCH_SCHEMA = vol.Schema( +COVER_SCHEMA = vol.Schema( { vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, - vol.Required(DOMAIN): { - vol.Optional(CONF_INVERT, default=False): bool, - vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), - vol.Optional(CONF_RESPOND_TO_READ, default=False): bool, - vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, - }, + vol.Required(DOMAIN): vol.All( + vol.Schema( + { + **optional_ga_schema(CONF_GA_UP_DOWN, GASelector(state=False)), + vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(), + **optional_ga_schema(CONF_GA_STOP, GASelector(state=False)), + **optional_ga_schema(CONF_GA_STEP, GASelector(state=False)), + **optional_ga_schema(CONF_GA_POSITION_SET, GASelector(state=False)), + **optional_ga_schema( + CONF_GA_POSITION_STATE, GASelector(write=False) + ), + vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(), + **optional_ga_schema(CONF_GA_ANGLE, GASelector()), + vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(), + vol.Optional( + CoverConf.TRAVELLING_TIME_DOWN, default=25 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=1000, step=0.1, unit_of_measurement="s" + ) + ), + vol.Optional( + CoverConf.TRAVELLING_TIME_UP, default=25 + ): selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, max=1000, step=0.1, unit_of_measurement="s" + ) + ), + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + }, + extra=vol.REMOVE_EXTRA, + ), + vol.Any( + vol.Schema( + { + vol.Required(CONF_GA_UP_DOWN): GASelector( + state=False, write_required=True + ) + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Required(CONF_GA_POSITION_SET): GASelector( + state=False, write_required=True + ) + }, + extra=vol.ALLOW_EXTRA, + ), + msg=( + "At least one of 'Up/Down control' or" + " 'Position - Set position' is required." + ), + ), + ), } ) @@ -226,6 +282,19 @@ LIGHT_SCHEMA = vol.Schema( } ) + +SWITCH_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY): BASE_ENTITY_SCHEMA, + vol.Required(DOMAIN): { + vol.Optional(CONF_INVERT, default=False): bool, + vol.Required(CONF_GA_SWITCH): GASelector(write_required=True), + vol.Optional(CONF_RESPOND_TO_READ, default=False): bool, + vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator, + }, + } +) + ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( vol.Schema( { @@ -243,11 +312,14 @@ ENTITY_STORE_DATA_SCHEMA: VolSchemaType = vol.All( Platform.BINARY_SENSOR: vol.Schema( {vol.Required(CONF_DATA): BINARY_SENSOR_SCHEMA}, extra=vol.ALLOW_EXTRA ), - Platform.SWITCH: vol.Schema( - {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA + Platform.COVER: vol.Schema( + {vol.Required(CONF_DATA): COVER_SCHEMA}, extra=vol.ALLOW_EXTRA ), Platform.LIGHT: vol.Schema( - {vol.Required("data"): LIGHT_SCHEMA}, extra=vol.ALLOW_EXTRA + {vol.Required(CONF_DATA): LIGHT_SCHEMA}, extra=vol.ALLOW_EXTRA + ), + Platform.SWITCH: vol.Schema( + {vol.Required(CONF_DATA): SWITCH_SCHEMA}, extra=vol.ALLOW_EXTRA ), }, ), diff --git a/homeassistant/components/knx/storage/knx_selector.py b/homeassistant/components/knx/storage/knx_selector.py index 1ac99d192b8..a1510dbb384 100644 --- a/homeassistant/components/knx/storage/knx_selector.py +++ b/homeassistant/components/knx/storage/knx_selector.py @@ -43,7 +43,20 @@ class GASelector: self._add_group_addresses(schema) self._add_passive(schema) self._add_dpt(schema) - return vol.Schema(schema) + return vol.Schema( + vol.All( + schema, + vol.Schema( # one group address shall be included + vol.Any( + {vol.Required(CONF_GA_WRITE): vol.IsTrue()}, + {vol.Required(CONF_GA_STATE): vol.IsTrue()}, + {vol.Required(CONF_GA_PASSIVE): vol.IsTrue()}, + msg="At least one group address must be set", + ), + extra=vol.ALLOW_EXTRA, + ), + ) + ) def _add_group_addresses(self, schema: dict[vol.Marker, Any]) -> None: """Add basic group address items to the schema.""" diff --git a/tests/components/knx/fixtures/config_store_cover.json b/tests/components/knx/fixtures/config_store_cover.json new file mode 100644 index 00000000000..6ec8dcc90fa --- /dev/null +++ b/tests/components/knx/fixtures/config_store_cover.json @@ -0,0 +1,82 @@ +{ + "version": 1, + "minor_version": 1, + "key": "knx/config_store.json", + "data": { + "entities": { + "cover": { + "knx_es_01JQNM9A9G03952ZH0GDF51HB6": { + "entity": { + "name": "minimal", + "entity_category": null, + "device_info": null + }, + "knx": { + "ga_up_down": { + "write": "1/0/1", + "passive": [] + }, + "travelling_time_down": 25.0, + "travelling_time_up": 25.0, + "sync_state": true + } + }, + "knx_es_01JQNQVEB7WT3MYCX61RK361F8": { + "entity": { + "name": "position_only", + "entity_category": null, + "device_info": null + }, + "knx": { + "ga_position_set": { + "write": "2/0/1", + "passive": [] + }, + "ga_position_state": { + "state": "2/0/0", + "passive": [] + }, + "invert_position": true, + "travelling_time_up": 25.0, + "travelling_time_down": 25.0, + "sync_state": true + } + }, + "knx_es_01JQNQSDS4ZW96TX27S2NT3FYQ": { + "entity": { + "name": "tiltable", + "entity_category": null, + "device_info": null + }, + "knx": { + "ga_up_down": { + "write": "3/0/1", + "passive": [] + }, + "ga_stop": { + "write": "3/0/2", + "passive": [] + }, + "ga_position_set": { + "write": "3/1/1", + "passive": [] + }, + "ga_position_state": { + "state": "3/1/0", + "passive": [] + }, + "ga_angle": { + "write": "3/2/1", + "state": "3/2/0", + "passive": [] + }, + "travelling_time_down": 16.0, + "travelling_time_up": 16.0, + "invert_angle": true, + "sync_state": true + } + } + } + } + } +} diff --git a/tests/components/knx/test_cover.py b/tests/components/knx/test_cover.py index 0604b575c5b..2bb568ceb13 100644 --- a/tests/components/knx/test_cover.py +++ b/tests/components/knx/test_cover.py @@ -1,10 +1,15 @@ """Test KNX cover.""" -from homeassistant.components.cover import CoverState +from typing import Any + +import pytest + +from homeassistant.components.cover import CoverEntityFeature, CoverState from homeassistant.components.knx.schema import CoverSchema -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant +from . import KnxEntityGenerator from .conftest import KNXTestKit from tests.common import async_capture_events @@ -160,3 +165,103 @@ async def test_cover_tilt_move_short(hass: HomeAssistant, knx: KNXTestKit) -> No "cover", "open_cover_tilt", target={"entity_id": "cover.test"}, blocking=True ) await knx.assert_write("1/0/1", 0) + + +@pytest.mark.parametrize( + ("knx_data", "read_responses", "initial_state", "supported_features"), + [ + ( + { + "ga_up_down": {"write": "1/0/1"}, + "sync_state": True, + }, + {}, + STATE_UNKNOWN, + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE, + ), + ( + { + "ga_position_set": {"write": "2/0/1"}, + "ga_position_state": {"state": "2/0/0"}, + "sync_state": True, + }, + {"2/0/0": (0x00,)}, + CoverState.OPEN, + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION, + ), + ( + { + "ga_up_down": {"write": "3/0/1", "passive": []}, + "ga_stop": {"write": "3/0/2", "passive": []}, + "ga_position_set": {"write": "3/1/1", "passive": []}, + "ga_position_state": {"state": "3/1/0", "passive": []}, + "ga_angle": {"write": "3/2/1", "state": "3/2/0", "passive": []}, + "travelling_time_down": 16.0, + "travelling_time_up": 16.0, + "invert_angle": True, + "sync_state": True, + }, + {"3/1/0": (0x00,), "3/2/0": (0x00,)}, + CoverState.OPEN, + CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.STOP + | CoverEntityFeature.STOP_TILT, + ), + ], +) +async def test_cover_ui_create( + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, + knx_data: dict[str, Any], + read_responses: dict[str, int | tuple[int]], + initial_state: str, + supported_features: int, +) -> None: + """Test creating a cover.""" + await knx.setup_integration() + await create_ui_entity( + platform=Platform.COVER, + entity_data={"name": "test"}, + knx_data=knx_data, + ) + # created entity sends read-request to KNX bus + for ga, value in read_responses.items(): + await knx.assert_read(ga, response=value, ignore_order=True) + knx.assert_state("cover.test", initial_state, supported_features=supported_features) + + +async def test_cover_ui_load(knx: KNXTestKit) -> None: + """Test loading a cover from storage.""" + await knx.setup_integration(config_store_fixture="config_store_cover.json") + + await knx.assert_read("2/0/0", response=(0xFF,), ignore_order=True) + await knx.assert_read("3/1/0", response=(0xFF,), ignore_order=True) + await knx.assert_read("3/2/0", response=(0xFF,), ignore_order=True) + + knx.assert_state( + "cover.minimal", + STATE_UNKNOWN, + supported_features=CoverEntityFeature.CLOSE | CoverEntityFeature.OPEN, + ) + knx.assert_state( + "cover.position_only", + CoverState.OPEN, + supported_features=CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION, + ) + knx.assert_state( + "cover.tiltable", + CoverState.CLOSED, + supported_features=CoverEntityFeature.CLOSE + | CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.SET_TILT_POSITION + | CoverEntityFeature.STOP + | CoverEntityFeature.STOP_TILT, + ) diff --git a/tests/components/knx/test_knx_selectors.py b/tests/components/knx/test_knx_selectors.py index 7b2f09af84b..12acf691c08 100644 --- a/tests/components/knx/test_knx_selectors.py +++ b/tests/components/knx/test_knx_selectors.py @@ -14,11 +14,49 @@ INVALID = "invalid" @pytest.mark.parametrize( ("selector_config", "data", "expected"), [ + # empty data is invalid ( {}, {}, - {"write": None, "state": None, "passive": []}, + {INVALID: "At least one group address must be set"}, ), + ( + {"write": False}, + {}, + {INVALID: "At least one group address must be set"}, + ), + ( + {"passive": False}, + {}, + {INVALID: "At least one group address must be set"}, + ), + ( + {"write": False, "state": False, "passive": False}, + {}, + {INVALID: "At least one group address must be set"}, + ), + # stale data is invalid + ( + {"write": False}, + {"write": "1/2/3"}, + {INVALID: "At least one group address must be set"}, + ), + ( + {"write": False}, + {"passive": []}, + {INVALID: "At least one group address must be set"}, + ), + ( + {"state": False}, + {"write": None}, + {INVALID: "At least one group address must be set"}, + ), + ( + {"passive": False}, + {"passive": ["1/2/3"]}, + {INVALID: "At least one group address must be set"}, + ), + # valid data ( {}, {"write": "1/2/3"}, @@ -39,11 +77,6 @@ INVALID = "invalid" {"write": "1", "state": 2, "passive": ["1/2/3"]}, {"write": "1", "state": 2, "passive": ["1/2/3"]}, ), - ( - {"write": False}, - {"write": "1/2/3"}, - {"state": None, "passive": []}, - ), ( {"write": False}, {"state": "1/2/3"}, @@ -54,11 +87,6 @@ INVALID = "invalid" {"passive": ["1/2/3"]}, {"state": None, "passive": ["1/2/3"]}, ), - ( - {"passive": False}, - {"passive": ["1/2/3"]}, - {"write": None, "state": None}, - ), ( {"passive": False}, {"write": "1/2/3"}, @@ -68,12 +96,12 @@ INVALID = "invalid" ( {"write_required": True}, {}, - INVALID, + {INVALID: r"required key not provided*"}, ), ( {"state_required": True}, {}, - INVALID, + {INVALID: r"required key not provided*"}, ), ( {"write_required": True}, @@ -88,18 +116,18 @@ INVALID = "invalid" ( {"write_required": True}, {"state": "1/2/3"}, - INVALID, + {INVALID: r"required key not provided*"}, ), ( {"state_required": True}, {"write": "1/2/3"}, - INVALID, + {INVALID: r"required key not provided*"}, ), # dpt key ( {"dpt": ColorTempModes}, {"write": "1/2/3"}, - INVALID, + {INVALID: r"required key not provided*"}, ), ( {"dpt": ColorTempModes}, @@ -109,19 +137,19 @@ INVALID = "invalid" ( {"dpt": ColorTempModes}, {"write": "1/2/3", "state": None, "passive": [], "dpt": "invalid"}, - INVALID, + {INVALID: r"value must be one of ['5.001', '7.600', '9']*"}, ), ], ) def test_ga_selector( selector_config: dict[str, Any], data: dict[str, Any], - expected: str | dict[str, Any], + expected: dict[str, Any], ) -> None: """Test GASelector.""" selector = GASelector(**selector_config) - if expected == INVALID: - with pytest.raises(vol.Invalid): + if INVALID in expected: + with pytest.raises(vol.Invalid, match=expected[INVALID]): selector(data) else: result = selector(data) From e4b686bc43ab242da378555a02c3ab6c6113ebe8 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Fri, 9 May 2025 17:40:56 +0800 Subject: [PATCH 0274/1175] Bump PySwitchbot to 0.62.0 (#144527) bump pyswitchbot to 0.62.0 --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 176f85ab389..986a68a9e3e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.60.1"] + "requirements": ["PySwitchbot==0.62.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 96635f94b65..ffb30b80daf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.60.1 +PySwitchbot==0.62.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df125ecadb2..404e1de350e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.60.1 +PySwitchbot==0.62.0 # homeassistant.components.syncthru PySyncThru==0.8.0 From ff6f213664844106a30450041f20cdf5bb6ecc0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 9 May 2025 11:41:52 +0200 Subject: [PATCH 0275/1175] Matter refrigerator fixture (#144491) * Add files via upload * Add snapshots --- tests/components/matter/conftest.py | 1 + .../fixtures/nodes/silabs_refrigerator.json | 534 ++++++++++++++++++ .../matter/snapshots/test_button.ambr | 48 ++ .../matter/snapshots/test_select.ambr | 58 ++ .../matter/snapshots/test_switch.ambr | 48 ++ 5 files changed, 689 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/silabs_refrigerator.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index cbc01d132a1..734369a3bb2 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -110,6 +110,7 @@ async def integration_fixture( "silabs_dishwasher", "silabs_evse_charging", "silabs_laundrywasher", + "silabs_refrigerator", "silabs_water_heater", "smoke_detector", "solar_power", diff --git a/tests/components/matter/fixtures/nodes/silabs_refrigerator.json b/tests/components/matter/fixtures/nodes/silabs_refrigerator.json new file mode 100644 index 00000000000..e4e04ac6ca1 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/silabs_refrigerator.json @@ -0,0 +1,534 @@ +{ + "node_id": 58, + "date_commissioned": "2024-12-23T10:42:11.104085", + "last_interview": "2024-12-23T10:42:11.104098", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 48, 49, 51, 53, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1, 2, 3], + "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": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 3 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 18, + "0/40/1": "Silabs", + "0/40/2": 65521, + "0/40/3": "Refrigerator", + "0/40/4": 32782, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "", + "0/40/16": false, + "0/40/17": true, + "0/40/18": "3F67EB015C2A0D0E", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/21": 17039360, + "0/40/22": 1, + "0/40/65532": 0, + "0/40/65533": 3, + "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, 17, 18, 19, 21, + 22, 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/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/1": [ + { + "0": "p0jbsOzJRNw=", + "1": true + } + ], + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/6": "p0jbsOzJRNw=", + "0/49/7": null, + "0/49/9": 10, + "0/49/10": 5, + "0/49/65532": 2, + "0/49/65533": 2, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 3, 4, 6, 8], + "0/49/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "0/51/0": [ + { + "0": "MyHome36", + "1": true, + "2": null, + "3": null, + "4": "spIfNquw4AU=", + "5": [], + "6": [ + "/U8h7+VkAADWDI9VgtWoMw==", + "/QANuACgAAAAAAD//gBEbQ==", + "/QANuACgAACT8m5dNLdrXA==", + "/oAAAAAAAACwkh82q7DgBQ==" + ], + "7": 4 + } + ], + "0/51/1": 1, + "0/51/2": 141, + "0/51/3": 0, + "0/51/4": 6, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/53/0": 25, + "0/53/1": 4, + "0/53/2": "MyHome36", + "0/53/3": 4660, + "0/53/4": 12054125955590472924, + "0/53/5": "QP0ADbgAoAAA", + "0/53/7": [ + { + "0": 4222415899952472931, + "1": 8, + "2": 24576, + "3": 151026, + "4": 21588, + "5": 3, + "6": -71, + "7": -71, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 17459145101989614194, + "1": 3, + "2": 26624, + "3": 485082, + "4": 21597, + "5": 3, + "6": -38, + "7": -39, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + }, + { + "0": 8241705229565301122, + "1": 18, + "2": 57344, + "3": 276088, + "4": 22218, + "5": 3, + "6": -52, + "7": -47, + "8": 0, + "9": 0, + "10": true, + "11": true, + "12": true, + "13": false + } + ], + "0/53/8": [ + { + "0": 0, + "1": 3072, + "2": 3, + "3": 17, + "4": 2, + "5": 0, + "6": 0, + "7": 142, + "8": true, + "9": false + }, + { + "0": 0, + "1": 17408, + "2": 17, + "3": 63, + "4": 0, + "5": 0, + "6": 0, + "7": 142, + "8": true, + "9": false + }, + { + "0": 4222415899952472931, + "1": 24576, + "2": 24, + "3": 17, + "4": 1, + "5": 3, + "6": 2, + "7": 9, + "8": true, + "9": true + }, + { + "0": 17459145101989614194, + "1": 26624, + "2": 26, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 3, + "8": true, + "9": true + }, + { + "0": 0, + "1": 41984, + "2": 41, + "3": 17, + "4": 1, + "5": 0, + "6": 0, + "7": 25, + "8": true, + "9": false + }, + { + "0": 0, + "1": 43008, + "2": 42, + "3": 17, + "4": 2, + "5": 0, + "6": 0, + "7": 44, + "8": true, + "9": false + }, + { + "0": 0, + "1": 53248, + "2": 52, + "3": 17, + "4": 1, + "5": 0, + "6": 0, + "7": 142, + "8": true, + "9": false + }, + { + "0": 0, + "1": 55296, + "2": 54, + "3": 17, + "4": 1, + "5": 0, + "6": 0, + "7": 34, + "8": true, + "9": false + }, + { + "0": 8241705229565301122, + "1": 57344, + "2": 56, + "3": 17, + "4": 1, + "5": 3, + "6": 3, + "7": 18, + "8": true, + "9": true + } + ], + "0/53/9": 574987064, + "0/53/10": 68, + "0/53/11": 103, + "0/53/12": 223, + "0/53/13": 26, + "0/53/59": { + "0": 672, + "1": 143 + }, + "0/53/60": "AB//4A==", + "0/53/61": { + "0": true, + "1": false, + "2": true, + "3": true, + "4": true, + "5": true, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "0/53/62": [], + "0/53/65532": 0, + "0/53/65533": 2, + "0/53/65528": [], + "0/53/65529": [], + "0/53/65531": [ + 0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 59, 60, 61, 62, 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, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQROhgkBwEkCAEwCUEExxLSpAQ5YJUVxH4v83Guzd2imtKrSMm2ADzJvNu3KGxkTF64CkFtfnORTwJmEpVfWDHJCNXRVQz0hJzXCM54nzcKNQEoARgkAgE2AwQCBAEYMAQUTE8wRXsn1uG3FSVnXrmgueY73FYwBRRT9HTfU5Nds+HA8j+/MRP+0pVyIxgwC0Dl506KGNd+m9BX72z6nm68F8SRkuJEvza7BQyg23LqfODl5ZWm8SnVH6GeN2j5TzbBIt31YApS2aNomn6YJ2YGGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyT62Yt4qMI+MorlmQ/Hxh2CpLetznVknlAbhvYAwTexpSxp9GnhR09SrcUhz3mOb0eZa2TylqcnPBhHJ2Ih2RTcKNQEpARgkAmAwBBRT9HTfU5Nds+HA8j+/MRP+0pVyIzAFFOMCO8Jk7ZCknJquFGPtPzJiNqsDGDALQI/Kc38hQyK7AkT7/pN4hiYW3LoWKT3NA43+ssMJoVpDcaZ989GXBQKIbHKbBEXzUQ1J8wfL7l2pL0Z8Lso9JwgY", + "254": 3 + } + ], + "0/62/1": [ + { + "1": "BIrruNo7r0gX6j6lq1dDi5zeK3jxcTavjt2o4adCCSCYtbxOakfb7C3GXqgV4LzulFSinbewmYkdqFBHqm5pxvU=", + "2": 4939, + "3": 2, + "4": 58, + "5": "", + "254": 3 + } + ], + "0/62/2": 5, + "0/62/3": 3, + "0/62/4": [ + "FTABAQAkAgE3AyYUyakYCSYVj6gLsxgmBP2G+CskBQA3BiYUyakYCSYVj6gLsxgkBwEkCAEwCUEEgYwxrTB+tyiEGfrRwjlXTG34MiQtJXbg5Qqd0ohdRW7MfwYY7vZiX/0h9hI8MqUralFaVPcnghAP0MSJm1YrqTcKNQEpARgkAmAwBBS3BS9aJzt+p6i28Nj+trB2Uu+vdzAFFLcFL1onO36nqLbw2P62sHZS7693GDALQIrLt7Uq3S9HEe7apdzYSR+j3BLWNXSTLWD4YbrdyYLpm6xqHDV/NPARcIp4skZdtz91WwFBDfuS4jO5aVoER1sY", + "FTABAQAkAgE3AycUQhmZbaIbYjokFQIYJgRWZLcqJAUANwYnFEIZmW2iG2I6JBUCGCQHASQIATAJQQT2AlKGW/kOMjqayzeO0md523/fuhrhGEUU91uQpTiKo0I7wcPpKnmrwfQNPX6g0kEQl+VGaXa3e22lzfu5Tzp0Nwo1ASkBGCQCYDAEFOOMk13ScMKuT2hlaydi1yEJnhTqMAUU44yTXdJwwq5PaGVrJ2LXIQmeFOoYMAtAv2jJd1qd5miXbYesH1XrJ+vgyY0hzGuZ78N6Jw4Cb1oN1sLSpA+PNM0u7+hsEqcSvvn2eSV8EaRR+hg5YQjHDxg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEiuu42juvSBfqPqWrV0OLnN4rePFxNq+O3ajhp0IJIJi1vE5qR9vsLcZeqBXgvO6UVKKdt7CZiR2oUEeqbmnG9TcKNQEpARgkAmAwBBTjAjvCZO2QpJyarhRj7T8yYjarAzAFFOMCO8Jk7ZCknJquFGPtPzJiNqsDGDALQE7hTxTRg92QOxwA1hK3xv8DaxvxL71r6ZHcNRzug9wNnonJ+NC84SFKvKDxwcBxHYqFdIyDiDgwJNTQIBgasmIY" + ], + "0/62/5": 3, + "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": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/65532": 0, + "1/6/65533": 6, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 112, + "1": 1 + } + ], + "1/29/1": [3, 6, 29, 82, 87], + "1/29/2": [], + "1/29/3": [2, 3], + "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/82/0": [ + { + "0": "Normal", + "1": 0, + "2": [ + { + "1": 0 + } + ] + }, + { + "0": "Rapid Cool", + "1": 1, + "2": [ + { + "1": 16384 + } + ] + }, + { + "0": "Rapid Freeze", + "1": 2, + "2": [ + { + "1": 7 + }, + { + "1": 16385 + }, + { + "1": 0 + } + ] + } + ], + "1/82/1": 0, + "1/82/65532": 1, + "1/82/65533": 2, + "1/82/65528": [1], + "1/82/65529": [0], + "1/82/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/87/0": 1, + "1/87/2": 0, + "1/87/3": 1, + "1/87/65532": 0, + "1/87/65533": 1, + "1/87/65528": [], + "1/87/65529": [], + "1/87/65531": [0, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 113, + "1": 1 + } + ], + "2/29/1": [29, 86], + "2/29/2": [], + "2/29/3": [], + "2/29/4": [ + { + "0": null, + "1": 65, + "2": 1 + } + ], + "2/29/65532": 1, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/86/0": -1800, + "2/86/1": -1800, + "2/86/2": -1500, + "2/86/3": 100, + "2/86/65532": 1, + "2/86/65533": 1, + "2/86/65528": [], + "2/86/65529": [0], + "2/86/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "3/29/0": [ + { + "0": 113, + "1": 1 + } + ], + "3/29/1": [29, 86], + "3/29/2": [], + "3/29/3": [], + "3/29/4": [ + { + "0": null, + "1": 65, + "2": 0 + } + ], + "3/29/65532": 1, + "3/29/65533": 2, + "3/29/65528": [], + "3/29/65529": [], + "3/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "3/86/0": 400, + "3/86/1": 100, + "3/86/2": 400, + "3/86/3": 100, + "3/86/65532": 1, + "3/86/65533": 1, + "3/86/65528": [], + "3/86/65529": [0], + "3/86/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index 0563d1138b1..fe8ddb11aa9 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -1806,6 +1806,54 @@ 'state': 'unknown', }) # --- +# name: test_buttons[silabs_refrigerator][button.refrigerator_identify-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.refrigerator_identify', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Identify', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-IdentifyButton-3-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[silabs_refrigerator][button.refrigerator_identify-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'identify', + 'friendly_name': 'Refrigerator Identify', + }), + 'context': , + 'entity_id': 'button.refrigerator_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[smoke_detector][button.smoke_sensor_identify-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index e9faf39c9ab..d05ff71964b 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -2015,6 +2015,64 @@ 'state': 'Colors', }) # --- +# name: test_selects[silabs_refrigerator][select.refrigerator_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Normal', + 'Rapid Cool', + 'Rapid Freeze', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.refrigerator_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': 'Mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-MatterRefrigeratorAndTemperatureControlledCabinetMode-82-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[silabs_refrigerator][select.refrigerator_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Mode', + 'options': list([ + 'Normal', + 'Rapid Cool', + 'Rapid Freeze', + ]), + }), + 'context': , + 'entity_id': 'select.refrigerator_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Normal', + }) +# --- # name: test_selects[silabs_water_heater][select.water_heater_energy_management_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index 4bc56a04ba8..f80aaefbf91 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -573,6 +573,54 @@ 'state': 'on', }) # --- +# name: test_switches[silabs_refrigerator][switch.refrigerator_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.refrigerator_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[silabs_refrigerator][switch.refrigerator_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Refrigerator Power', + }), + 'context': , + 'entity_id': 'switch.refrigerator_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switches[switch_unit][switch.mock_switchunit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From a84b8b49f38d1a68c0bd402f590e0c60367f6127 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 9 May 2025 11:43:05 +0200 Subject: [PATCH 0276/1175] Update knx-frontend to 2025.4.1.91934 - Enable UI to create KNX Cover entities (#141993) Update knx-frontend to 2025.4.1.91934 --- 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 bde6dfa226f..9c3ac0c12d9 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "requirements": [ "xknx==3.6.0", "xknxproject==3.8.2", - "knx-frontend==2025.3.8.214559" + "knx-frontend==2025.4.1.91934" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index ffb30b80daf..33840e3b64c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1291,7 +1291,7 @@ kiwiki-client==0.1.1 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.3.8.214559 +knx-frontend==2025.4.1.91934 # homeassistant.components.konnected konnected==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 404e1de350e..d127d4efdd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1094,7 +1094,7 @@ kegtron-ble==0.4.0 knocki==0.4.2 # homeassistant.components.knx -knx-frontend==2025.3.8.214559 +knx-frontend==2025.4.1.91934 # homeassistant.components.konnected konnected==1.2.0 From 90a7ecdce37bd0898e2505b444b961e554e72704 Mon Sep 17 00:00:00 2001 From: DukeChocula <79062549+DukeChocula@users.noreply.github.com> Date: Fri, 9 May 2025 04:49:26 -0500 Subject: [PATCH 0277/1175] Add LAP-V102S-AUSR to VeSync (#144437) Update const.py Added LAP-V102S-AUSR to Vital 100S --- homeassistant/components/vesync/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index 4e39fe40f2d..ff55bcf2e37 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -97,6 +97,7 @@ SKU_TO_BASE_DEVICE = { "LAP-V102S-AASR": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-WEU": "Vital100S", # Alt ID Model Vital100S "LAP-V102S-WUK": "Vital100S", # Alt ID Model Vital100S + "LAP-V102S-AUSR": "Vital100S", # Alt ID Model Vital100S "EverestAir": "EverestAir", "LAP-EL551S-AUS": "EverestAir", # Alt ID Model EverestAir "LAP-EL551S-AEUR": "EverestAir", # Alt ID Model EverestAir From 9abb4ffc97ee418ecd78eab5e8b86e349bfc9436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 9 May 2025 11:52:30 +0200 Subject: [PATCH 0278/1175] Add drying step sensor for Miele tumble dryers (#144515) Add drying step sensor for tumple dryers --- homeassistant/components/miele/const.py | 14 ++++++++++++++ homeassistant/components/miele/icons.json | 3 +++ homeassistant/components/miele/sensor.py | 18 ++++++++++++++++++ homeassistant/components/miele/strings.json | 13 +++++++++++++ 4 files changed, 48 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 1802c6c9cd0..69e0ab1876e 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -339,6 +339,20 @@ class StateProgramType(MieleEnum): unknown = -9999 +class StateDryingStep(MieleEnum): + """Defines drying steps.""" + + extra_dry = 0 + normal_plus = 1 + normal = 2 + slightly_dry = 3 + hand_iron_1 = 4 + hand_iron_2 = 5 + machine_iron = 6 + smoothing = 7 + unknown = -9999 + + WASHING_MACHINE_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. 0: "no_program", # Returned by the API when no program is selected. diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index a0fb1daaedd..02374a10f90 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -32,6 +32,9 @@ "core_target_temperature": { "default": "mdi:thermometer-probe" }, + "drying_step": { + "default": "mdi:water-outline" + }, "program_id": { "default": "mdi:selection-ellipse-arrow-inside" }, diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 12f035485be..22a7916d892 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -32,6 +32,7 @@ from .const import ( STATE_PROGRAM_PHASE, STATE_STATUS_TAGS, MieleAppliance, + StateDryingStep, StateProgramType, StateStatus, ) @@ -416,6 +417,23 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( ), ), ), + MieleSensorDefinition( + types=( + MieleAppliance.WASHER_DRYER, + MieleAppliance.TUMBLE_DRYER, + MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL, + ), + description=MieleSensorDescription( + key="state_drying_step", + translation_key="drying_step", + value_fn=lambda value: StateDryingStep( + cast(int, value.state_drying_step) + ).name, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=sorted(StateDryingStep.keys()), + ), + ), ) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 7962f887e4f..764fc76a877 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -191,6 +191,19 @@ "energy_consumption": { "name": "Energy consumption" }, + "drying_step": { + "name": "Drying step", + "state": { + "extra_dry": "Extra dry", + "hand_iron_1": "Hand iron 1", + "hand_iron_2": "Hand iron 2", + "machine_iron": "Machine iron", + "normal_plus": "Normal plus", + "normal": "Normal", + "slightly_dry": "Slightly dry", + "smoothing": "Smoothing" + } + }, "program_phase": { "name": "Program phase", "state": { From 47455fee4188902168e2eabb733b8eebb6a55243 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 9 May 2025 12:16:49 +0200 Subject: [PATCH 0279/1175] SMA add re-authentication flow (#144538) * Add reauth flow * Small adjustment * Update homeassistant/components/sma/config_flow.py Co-authored-by: Joost Lekkerkerker * Review feedback * Update tests/components/sma/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Update tests/components/sma/test_config_flow.py Co-authored-by: Joost Lekkerkerker * Feedback --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/sma/__init__.py | 4 +- homeassistant/components/sma/config_flow.py | 37 ++++++++++ homeassistant/components/sma/strings.json | 10 ++- tests/components/sma/__init__.py | 4 ++ tests/components/sma/test_config_flow.py | 80 +++++++++++++++++++++ 5 files changed, 133 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 27fa54e46dd..0dc8fb83fac 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo @@ -63,6 +63,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: pysma.exceptions.SmaConnectionException, ) as exc: raise ConfigEntryNotReady from exc + except pysma.exceptions.SmaAuthenticationException as exc: + raise ConfigEntryAuthFailed from exc if TYPE_CHECKING: assert entry.unique_id diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 3210d904b6b..c920b4b0a3a 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -137,6 +138,42 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth on credential failure.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Prepare reauth.""" + errors: dict[str, str] = {} + if user_input is not None: + reauth_entry = self._get_reauth_entry() + errors, device_info = await self._handle_user_input( + user_input={ + **reauth_entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + ) + + if not errors: + return self.async_update_reload_and_abort( + reauth_entry, + data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]}, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + } + ), + errors=errors, + ) + async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index 16e5d7408c4..3a7c87acfcc 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -11,6 +12,13 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SMA integration needs to re-authenticate your connection details", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "group": "Group", diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 4a9e462501e..6b958650905 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -27,6 +27,10 @@ MOCK_USER_INPUT = { CONF_PASSWORD: "password", } +MOCK_USER_REAUTH = { + CONF_PASSWORD: "new_password", +} + MOCK_DHCP_DISCOVERY_INPUT = { # CONF_HOST: "1.1.1.2", CONF_SSL: True, diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 5033462d0a6..175dcc4f3a0 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -20,6 +20,7 @@ from . import ( MOCK_DHCP_DISCOVERY, MOCK_DHCP_DISCOVERY_INPUT, MOCK_USER_INPUT, + MOCK_USER_REAUTH, _patch_async_setup_entry, ) @@ -221,3 +222,82 @@ async def test_dhcp_exceptions( assert result["title"] == MOCK_DHCP_DISCOVERY["host"] assert result["data"] == MOCK_DHCP_DISCOVERY assert result["result"].unique_id == DHCP_DISCOVERY.hostname.replace("SMA", "") + + +async def test_full_flow_reauth( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the full flow of the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # There is no user input + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with ( + patch("pysma.SMA.new_session", return_value=True), + patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), + _patch_async_setup_entry() as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_REAUTH, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SmaConnectionException, "cannot_connect"), + (SmaAuthenticationException, "invalid_auth"), + (SmaReadException, "cannot_retrieve_device_info"), + (Exception, "unknown"), + ], +) +async def test_reauth_flow_exceptions( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test we handle cannot connect error.""" + result = await mock_config_entry.start_reauth_flow(hass) + + with ( + patch("pysma.SMA.new_session", side_effect=exception), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_REAUTH, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + assert result["step_id"] == "reauth_confirm" + + with ( + patch("pysma.SMA.new_session", return_value=True), + patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), + _patch_async_setup_entry() as mock_setup_entry, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_REAUTH, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 From 031b25cd1e38fee76f940ef7dc5d7b9e61a74297 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 9 May 2025 12:44:36 +0200 Subject: [PATCH 0280/1175] Remove redundant coordinator reference in OpenWeatherMap sensor (#144548) Remove redundant coordinator reference --- .../components/openweathermap/sensor.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index a595652d90b..47859b78812 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -50,7 +50,6 @@ from .const import ( MANUFACTURER, OWM_MODE_FREE_FORECAST, ) -from .coordinator import WeatherUpdateCoordinator WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -229,20 +228,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): """Implementation of an OpenWeatherMap sensor.""" - def __init__( - self, - name: str, - unique_id: str, - description: SensorEntityDescription, - weather_coordinator: WeatherUpdateCoordinator, - ) -> None: - """Initialize the sensor.""" - super().__init__(name, unique_id, description, weather_coordinator) - self._weather_coordinator = weather_coordinator - @property def native_value(self) -> StateType: """Return the state of the device.""" - return self._weather_coordinator.data[ATTR_API_CURRENT].get( - self.entity_description.key - ) + return self._coordinator.data[ATTR_API_CURRENT].get(self.entity_description.key) From 6350ed34155118ff9415ff9c08b14b3a908a0a82 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 9 May 2025 13:06:38 +0200 Subject: [PATCH 0281/1175] Add snapshot tests for OpenWeatherMap sensors (#139657) * Add snapshot tests for sensors * Code cleanup * Patch during async_setup only * Use snapshot_platform, split platforms for snapshot tests * Make mock_config_entry and mode fixtures * Update snapshots with latest device and state class changes * Move setup_platform to __init__.py and patch HA object instead of library * Remove if statements in tests * Add client mock fixture to patch get_weather instead of internal call * Code cleanup * Test explicit list of modes * Fix * Fix --------- Co-authored-by: Joostlek --- tests/components/openweathermap/__init__.py | 25 +- tests/components/openweathermap/conftest.py | 146 ++ .../openweathermap/snapshots/test_sensor.ambr | 1659 +++++++++++++++++ .../snapshots/test_weather.ambr | 187 +- .../openweathermap/test_config_flow.py | 170 +- .../components/openweathermap/test_sensor.py | 49 + .../components/openweathermap/test_weather.py | 106 +- 7 files changed, 2124 insertions(+), 218 deletions(-) create mode 100644 tests/components/openweathermap/conftest.py create mode 100644 tests/components/openweathermap/snapshots/test_sensor.ambr create mode 100644 tests/components/openweathermap/test_sensor.py diff --git a/tests/components/openweathermap/__init__.py b/tests/components/openweathermap/__init__.py index e718962766f..9552cdb4f70 100644 --- a/tests/components/openweathermap/__init__.py +++ b/tests/components/openweathermap/__init__.py @@ -1 +1,24 @@ -"""Tests for the OpenWeatherMap integration.""" +"""Shared utilities for OpenWeatherMap tests.""" + +from unittest.mock import patch + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + platforms: list[Platform], +): + """Set up the OpenWeatherMap platform.""" + config_entry.add_to_hass(hass) + with ( + patch("homeassistant.components.openweathermap.PLATFORMS", platforms), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/openweathermap/conftest.py b/tests/components/openweathermap/conftest.py new file mode 100644 index 00000000000..44f4b971bd8 --- /dev/null +++ b/tests/components/openweathermap/conftest.py @@ -0,0 +1,146 @@ +"""Configure tests for the OpenWeatherMap integration.""" + +from collections.abc import Generator +from datetime import UTC, datetime +from unittest.mock import AsyncMock + +from pyopenweathermap import ( + CurrentWeather, + DailyTemperature, + DailyWeatherForecast, + MinutelyWeatherForecast, + WeatherCondition, + WeatherReport, +) +from pyopenweathermap.client.owm_abstract_client import OWMClient +import pytest + +from homeassistant.components.openweathermap.const import DEFAULT_LANGUAGE, DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_LANGUAGE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) + +from tests.common import MockConfigEntry, patch + +API_KEY = "test_api_key" +LATITUDE = 12.34 +LONGITUDE = 56.78 +NAME = "openweathermap" + + +@pytest.fixture +def mode(request: pytest.FixtureRequest) -> str: + """Return mode passed in parameter.""" + return request.param + + +@pytest.fixture +def mock_config_entry(mode: str) -> MockConfigEntry: + """Fixture for creating a mock OpenWeatherMap config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: API_KEY, + CONF_LATITUDE: LATITUDE, + CONF_LONGITUDE: LONGITUDE, + CONF_NAME: NAME, + }, + options={ + CONF_MODE: mode, + CONF_LANGUAGE: DEFAULT_LANGUAGE, + }, + entry_id="test", + version=5, + unique_id=f"{LATITUDE}-{LONGITUDE}", + ) + + +@pytest.fixture +def owm_client_mock() -> Generator[AsyncMock]: + """Mock OWMClient.""" + client = AsyncMock(spec=OWMClient, autospec=True) + current_weather = CurrentWeather( + date_time=datetime.fromtimestamp(1714063536, tz=UTC), + temperature=6.84, + feels_like=2.07, + pressure=1000, + humidity=82, + dew_point=3.99, + uv_index=0.13, + cloud_coverage=75, + visibility=10000, + wind_speed=9.83, + wind_bearing=199, + wind_gust=None, + rain={"1h": 1.21}, + snow=None, + condition=WeatherCondition( + id=803, + main="Clouds", + description="broken clouds", + icon="04d", + ), + ) + daily_weather_forecast = DailyWeatherForecast( + date_time=datetime.fromtimestamp(1714063536, tz=UTC), + summary="There will be clear sky until morning, then partly cloudy", + temperature=DailyTemperature( + day=18.76, + min=8.11, + max=21.26, + night=13.06, + evening=20.51, + morning=8.47, + ), + feels_like=DailyTemperature( + day=18.76, + min=8.11, + max=21.26, + night=13.06, + evening=20.51, + morning=8.47, + ), + pressure=1015, + humidity=62, + dew_point=11.34, + wind_speed=8.14, + wind_bearing=168, + wind_gust=11.81, + condition=WeatherCondition( + id=803, + main="Clouds", + description="broken clouds", + icon="04d", + ), + cloud_coverage=84, + precipitation_probability=0, + uv_index=4.06, + rain=0, + snow=0, + ) + minutely_weather_forecast = [ + MinutelyWeatherForecast(date_time=1728672360, precipitation=0), + MinutelyWeatherForecast(date_time=1728672420, precipitation=1.23), + MinutelyWeatherForecast(date_time=1728672480, precipitation=4.5), + MinutelyWeatherForecast(date_time=1728672540, precipitation=0), + ] + client.get_weather.return_value = WeatherReport( + current_weather, minutely_weather_forecast, [], [daily_weather_forecast] + ) + client.validate_key.return_value = True + with ( + patch( + "homeassistant.components.openweathermap.create_owm_client", + return_value=client, + ), + patch( + "homeassistant.components.openweathermap.utils.create_owm_client", + return_value=client, + ), + ): + yield client diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..1f416f76578 --- /dev/null +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -0,0 +1,1659 @@ +# serializer version: 1 +# name: test_sensor_states[current][sensor.openweathermap_cloud_coverage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_cloud_coverage', + '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': 'openweathermap Cloud coverage', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-clouds', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_cloud_coverage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Cloud coverage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_cloud_coverage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_condition', + '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': 'openweathermap Condition', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-condition', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Condition', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_dew_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_dew_point', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Dew Point', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-dew_point', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Dew Point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.99', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_feels_like_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_feels_like_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': 'openweathermap Feels like temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-feels_like_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_feels_like_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Feels like temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_feels_like_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.07', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_humidity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Humidity', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'humidity', + 'friendly_name': 'openweathermap Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '82', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_precipitation_kind-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_precipitation_kind', + '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': 'openweathermap Precipitation kind', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-precipitation_kind', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_precipitation_kind-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Precipitation kind', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_precipitation_kind', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Rain', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_pressure', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Pressure', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'pressure', + 'friendly_name': 'openweathermap Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_rain', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Rain', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-rain', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'openweathermap Rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.21', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_snow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_snow', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Snow', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-snow', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_snow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'openweathermap Snow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_snow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_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': 'openweathermap Temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.84', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_uv_index', + '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': 'openweathermap UV Index', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-uv_index', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap UV Index', + 'state_class': , + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.13', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_visibility-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_visibility', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Visibility', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-visibility_distance', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_visibility-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'distance', + 'friendly_name': 'openweathermap Visibility', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_visibility', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_weather-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather', + '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': 'openweathermap Weather', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-weather', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_weather-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Weather', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'broken clouds', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_weather_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather_code', + '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': 'openweathermap Weather Code', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-weather_code', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_weather_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Weather Code', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '803', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_wind_bearing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_bearing', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Wind bearing', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_bearing', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_wind_bearing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_direction', + 'friendly_name': 'openweathermap Wind bearing', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_bearing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '199', + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_speed', + 'has_entity_name': False, + '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': 'openweathermap Wind speed', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[current][sensor.openweathermap_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_speed', + 'friendly_name': 'openweathermap Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.39', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_cloud_coverage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_cloud_coverage', + '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': 'openweathermap Cloud coverage', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-clouds', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_cloud_coverage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Cloud coverage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_cloud_coverage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_condition', + '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': 'openweathermap Condition', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-condition', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Condition', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_dew_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_dew_point', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Dew Point', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-dew_point', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_dew_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Dew Point', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_dew_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.99', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_feels_like_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_feels_like_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': 'openweathermap Feels like temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-feels_like_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_feels_like_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Feels like temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_feels_like_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.07', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_humidity', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Humidity', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'humidity', + 'friendly_name': 'openweathermap Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '82', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_precipitation_kind-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_precipitation_kind', + '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': 'openweathermap Precipitation kind', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-precipitation_kind', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_precipitation_kind-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Precipitation kind', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_precipitation_kind', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Rain', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_pressure', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Pressure', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'pressure', + 'friendly_name': 'openweathermap Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_rain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_rain', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Rain', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-rain', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_rain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'openweathermap Rain', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_rain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.21', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_snow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_snow', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Snow', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-snow', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_snow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'precipitation_intensity', + 'friendly_name': 'openweathermap Snow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_snow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_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': 'openweathermap Temperature', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'temperature', + 'friendly_name': 'openweathermap Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.84', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_uv_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_uv_index', + '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': 'openweathermap UV Index', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-uv_index', + 'unit_of_measurement': 'UV index', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_uv_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap UV Index', + 'state_class': , + 'unit_of_measurement': 'UV index', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_uv_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.13', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_visibility-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_visibility', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Visibility', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-visibility_distance', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_visibility-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'distance', + 'friendly_name': 'openweathermap Visibility', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_visibility', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_weather-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather', + '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': 'openweathermap Weather', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-weather', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_weather-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Weather', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'broken clouds', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_weather_code-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_weather_code', + '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': 'openweathermap Weather Code', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-weather_code', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_weather_code-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'friendly_name': 'openweathermap Weather Code', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_weather_code', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '803', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_bearing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_bearing', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'openweathermap Wind bearing', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_bearing', + 'unit_of_measurement': '°', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_bearing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_direction', + 'friendly_name': 'openweathermap Wind bearing', + 'state_class': , + 'unit_of_measurement': '°', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_bearing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '199', + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_wind_speed', + 'has_entity_name': False, + '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': 'openweathermap Wind speed', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-wind_speed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states[v3.0][sensor.openweathermap_wind_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'wind_speed', + 'friendly_name': 'openweathermap Wind speed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_wind_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35.39', + }) +# --- diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index c89dcb96a9c..90f583d9db1 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_get_minute_forecast[mock_service_response] +# name: test_get_minute_forecast[v3.0][mock_service_response] dict({ 'weather.openweathermap': dict({ 'forecast': list([ @@ -23,3 +23,188 @@ }), }) # --- +# name: test_weather_states[current][weather.openweathermap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.openweathermap', + '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': 'openweathermap', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather_states[current][weather.openweathermap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'apparent_temperature': 2.1, + 'attribution': 'Data provided by OpenWeatherMap', + 'cloud_coverage': 75, + 'dew_point': 4.0, + 'friendly_name': 'openweathermap', + 'humidity': 82, + 'precipitation_unit': , + 'pressure': 1000.0, + 'pressure_unit': , + 'temperature': 6.8, + 'temperature_unit': , + 'visibility_unit': , + 'wind_bearing': 199, + 'wind_speed': 35.39, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.openweathermap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- +# name: test_weather_states[forecast][weather.openweathermap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.openweathermap', + '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': 'openweathermap', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '12.34-56.78', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather_states[forecast][weather.openweathermap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'apparent_temperature': 2.1, + 'attribution': 'Data provided by OpenWeatherMap', + 'cloud_coverage': 75, + 'dew_point': 4.0, + 'friendly_name': 'openweathermap', + 'humidity': 82, + 'precipitation_unit': , + 'pressure': 1000.0, + 'pressure_unit': , + 'supported_features': , + 'temperature': 6.8, + 'temperature_unit': , + 'visibility_unit': , + 'wind_bearing': 199, + 'wind_speed': 35.39, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.openweathermap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- +# name: test_weather_states[v3.0][weather.openweathermap-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'weather', + 'entity_category': None, + 'entity_id': 'weather.openweathermap', + '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': 'openweathermap', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '12.34-56.78', + 'unit_of_measurement': None, + }) +# --- +# name: test_weather_states[v3.0][weather.openweathermap-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'apparent_temperature': 2.1, + 'attribution': 'Data provided by OpenWeatherMap', + 'cloud_coverage': 75, + 'dew_point': 4.0, + 'friendly_name': 'openweathermap', + 'humidity': 82, + 'precipitation_unit': , + 'pressure': 1000.0, + 'pressure_unit': , + 'supported_features': , + 'temperature': 6.8, + 'temperature_unit': , + 'visibility_unit': , + 'wind_bearing': 199, + 'wind_speed': 35.39, + 'wind_speed_unit': , + }), + 'context': , + 'entity_id': 'weather.openweathermap', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cloudy', + }) +# --- diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index d5e01677dd8..0315ca91010 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -1,17 +1,8 @@ """Define tests for the OpenWeatherMap config flow.""" -from datetime import UTC, datetime -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock -from pyopenweathermap import ( - CurrentWeather, - DailyTemperature, - DailyWeatherForecast, - MinutelyWeatherForecast, - RequestError, - WeatherCondition, - WeatherReport, -) +from pyopenweathermap import RequestError import pytest from homeassistant.components.openweathermap.const import ( @@ -32,13 +23,15 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import LATITUDE, LONGITUDE + from tests.common import MockConfigEntry CONFIG = { CONF_NAME: "openweathermap", CONF_API_KEY: "foo", - CONF_LATITUDE: 50, - CONF_LONGITUDE: 40, + CONF_LATITUDE: LATITUDE, + CONF_LONGITUDE: LONGITUDE, CONF_LANGUAGE: DEFAULT_LANGUAGE, CONF_MODE: OWM_MODE_V30, } @@ -46,118 +39,11 @@ CONFIG = { VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} -def _create_static_weather_report() -> WeatherReport: - """Create a static WeatherReport.""" - - current_weather = CurrentWeather( - date_time=datetime.fromtimestamp(1714063536, tz=UTC), - temperature=6.84, - feels_like=2.07, - pressure=1000, - humidity=82, - dew_point=3.99, - uv_index=0.13, - cloud_coverage=75, - visibility=10000, - wind_speed=9.83, - wind_bearing=199, - wind_gust=None, - rain={"1h": 1.21}, - snow=None, - condition=WeatherCondition( - id=803, - main="Clouds", - description="broken clouds", - icon="04d", - ), - ) - daily_weather_forecast = DailyWeatherForecast( - date_time=datetime.fromtimestamp(1714063536, tz=UTC), - summary="There will be clear sky until morning, then partly cloudy", - temperature=DailyTemperature( - day=18.76, - min=8.11, - max=21.26, - night=13.06, - evening=20.51, - morning=8.47, - ), - feels_like=DailyTemperature( - day=18.76, - min=8.11, - max=21.26, - night=13.06, - evening=20.51, - morning=8.47, - ), - pressure=1015, - humidity=62, - dew_point=11.34, - wind_speed=8.14, - wind_bearing=168, - wind_gust=11.81, - condition=WeatherCondition( - id=803, - main="Clouds", - description="broken clouds", - icon="04d", - ), - cloud_coverage=84, - precipitation_probability=0, - uv_index=4.06, - rain=0, - snow=0, - ) - minutely_weather_forecast = [ - MinutelyWeatherForecast(date_time=1728672360, precipitation=0), - MinutelyWeatherForecast(date_time=1728672420, precipitation=1.23), - MinutelyWeatherForecast(date_time=1728672480, precipitation=4.5), - MinutelyWeatherForecast(date_time=1728672540, precipitation=0), - ] - return WeatherReport( - current_weather, minutely_weather_forecast, [], [daily_weather_forecast] - ) - - -def _create_mocked_owm_factory(is_valid: bool): - """Create a mocked OWM client.""" - - weather_report = _create_static_weather_report() - mocked_owm_client = MagicMock() - mocked_owm_client.validate_key = AsyncMock(return_value=is_valid) - mocked_owm_client.get_weather = AsyncMock(return_value=weather_report) - - return mocked_owm_client - - -@pytest.fixture(name="owm_client_mock") -def mock_owm_client(): - """Mock config_flow OWMClient.""" - with patch( - "homeassistant.components.openweathermap.create_owm_client", - ) as mock: - yield mock - - -@pytest.fixture(name="config_flow_owm_client_mock") -def mock_config_flow_owm_client(): - """Mock config_flow OWMClient.""" - with patch( - "homeassistant.components.openweathermap.utils.create_owm_client", - ) as mock: - yield mock - - async def test_successful_config_flow( hass: HomeAssistant, - owm_client_mock, - config_flow_owm_client_mock, + owm_client_mock: AsyncMock, ) -> None: """Test that the form is served with valid input.""" - mock = _create_mocked_owm_factory(True) - owm_client_mock.return_value = mock - config_flow_owm_client_mock.return_value = mock - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -187,39 +73,32 @@ async def test_successful_config_flow( assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] +@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True) async def test_abort_config_flow( hass: HomeAssistant, - owm_client_mock, - config_flow_owm_client_mock, + owm_client_mock: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test that the form is served with same data.""" - mock = _create_mocked_owm_factory(True) - owm_client_mock.return_value = mock - config_flow_owm_client_mock.return_value = mock - + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER} ) - await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) - await hass.async_block_till_done() + 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"], CONFIG) assert result["type"] is FlowResultType.ABORT async def test_config_flow_options_change( hass: HomeAssistant, - owm_client_mock, - config_flow_owm_client_mock, + owm_client_mock: AsyncMock, ) -> None: """Test that the options form.""" - mock = _create_mocked_owm_factory(True) - owm_client_mock.return_value = mock - config_flow_owm_client_mock.return_value = mock - config_entry = MockConfigEntry( domain=DOMAIN, unique_id="openweathermap_unique_id", data=CONFIG ) @@ -274,10 +153,10 @@ async def test_config_flow_options_change( async def test_form_invalid_api_key( hass: HomeAssistant, - config_flow_owm_client_mock, + owm_client_mock: AsyncMock, ) -> None: """Test that the form is served with no input.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(False) + owm_client_mock.validate_key.return_value = False result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -285,7 +164,7 @@ async def test_form_invalid_api_key( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_api_key"} - config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) + owm_client_mock.validate_key.return_value = True result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) @@ -295,11 +174,10 @@ async def test_form_invalid_api_key( async def test_form_api_call_error( hass: HomeAssistant, - config_flow_owm_client_mock, + owm_client_mock: AsyncMock, ) -> None: """Test setting up with api call error.""" - config_flow_owm_client_mock.return_value = _create_mocked_owm_factory(True) - config_flow_owm_client_mock.side_effect = RequestError("oops") + owm_client_mock.validate_key.side_effect = RequestError("oops") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -307,7 +185,7 @@ async def test_form_api_call_error( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - config_flow_owm_client_mock.side_effect = None + owm_client_mock.validate_key.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG ) diff --git a/tests/components/openweathermap/test_sensor.py b/tests/components/openweathermap/test_sensor.py new file mode 100644 index 00000000000..8cb8bd11c26 --- /dev/null +++ b/tests/components/openweathermap/test_sensor.py @@ -0,0 +1,49 @@ +"""Tests for OpenWeatherMap sensors.""" + +from unittest.mock import MagicMock + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.openweathermap.const import ( + OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V30, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_platform + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT], indirect=True) +async def test_sensor_states( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + owm_client_mock: MagicMock, + mode: str, +) -> None: + """Test sensor states are correctly collected from library with different modes and mocked function responses.""" + + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("mode", [OWM_MODE_FREE_FORECAST], indirect=True) +async def test_mode_no_sensor( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + owm_client_mock: MagicMock, + mode: str, +) -> None: + """Test modes that do not provide any sensor.""" + + await setup_platform(hass, mock_config_entry, [Platform.SENSOR]) + assert len(entity_registry.entities) == 0 diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py index e9817e739ac..9ac51afd6b3 100644 --- a/tests/components/openweathermap/test_weather.py +++ b/tests/components/openweathermap/test_weather.py @@ -1,91 +1,40 @@ """Test the OpenWeatherMap weather entity.""" +from unittest.mock import MagicMock + import pytest from syrupy import SnapshotAssertion from homeassistant.components.openweathermap.const import ( - DEFAULT_LANGUAGE, DOMAIN, OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, OWM_MODE_V30, ) from homeassistant.components.openweathermap.weather import SERVICE_GET_MINUTE_FORECAST -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - CONF_API_KEY, - CONF_LANGUAGE, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MODE, - CONF_NAME, -) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er -from .test_config_flow import _create_static_weather_report +from . import setup_platform -from tests.common import AsyncMock, MockConfigEntry, patch +from tests.common import MockConfigEntry, snapshot_platform ENTITY_ID = "weather.openweathermap" -API_KEY = "test_api_key" -LATITUDE = 12.34 -LONGITUDE = 56.78 -NAME = "openweathermap" - -# Define test data for mocked weather report -static_weather_report = _create_static_weather_report() -def mock_config_entry(mode: str) -> MockConfigEntry: - """Create a mock OpenWeatherMap config entry.""" - return MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: API_KEY, - CONF_LATITUDE: LATITUDE, - CONF_LONGITUDE: LONGITUDE, - CONF_NAME: NAME, - }, - options={CONF_MODE: mode, CONF_LANGUAGE: DEFAULT_LANGUAGE}, - version=5, - ) - - -@pytest.fixture -def mock_config_entry_free_current() -> MockConfigEntry: - """Create a mock OpenWeatherMap FREE_CURRENT config entry.""" - return mock_config_entry(OWM_MODE_FREE_CURRENT) - - -@pytest.fixture -def mock_config_entry_v30() -> MockConfigEntry: - """Create a mock OpenWeatherMap v3.0 config entry.""" - return mock_config_entry(OWM_MODE_V30) - - -async def setup_mock_config_entry( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -): - """Set up the MockConfigEntry and assert it is loaded correctly.""" - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert hass.states.get(ENTITY_ID) - assert mock_config_entry.state is ConfigEntryState.LOADED - - -@patch( - "pyopenweathermap.client.onecall_client.OWMOneCallClient.get_weather", - AsyncMock(return_value=static_weather_report), -) +@pytest.mark.parametrize("mode", [OWM_MODE_V30], indirect=True) async def test_get_minute_forecast( hass: HomeAssistant, - mock_config_entry_v30: MockConfigEntry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + owm_client_mock: MagicMock, + mode: str, ) -> None: """Test the get_minute_forecast Service call.""" - await setup_mock_config_entry(hass, mock_config_entry_v30) + await setup_platform(hass, mock_config_entry, [Platform.WEATHER]) result = await hass.services.async_call( DOMAIN, SERVICE_GET_MINUTE_FORECAST, @@ -96,18 +45,19 @@ async def test_get_minute_forecast( assert result == snapshot(name="mock_service_response") -@patch( - "pyopenweathermap.client.free_client.OWMFreeClient.get_weather", - AsyncMock(return_value=static_weather_report), +@pytest.mark.parametrize( + "mode", [OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST], indirect=True ) -async def test_mode_fail( +async def test_get_minute_forecast_unavailable( hass: HomeAssistant, - mock_config_entry_free_current: MockConfigEntry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + owm_client_mock: MagicMock, + mode: str, ) -> None: """Test that Minute forecasting fails when mode is not v3.0.""" - await setup_mock_config_entry(hass, mock_config_entry_free_current) - # Expect a ServiceValidationError when mode is not OWM_MODE_V30 + await setup_platform(hass, mock_config_entry, [Platform.WEATHER]) with pytest.raises( ServiceValidationError, match="Minute forecast is available only when OpenWeatherMap mode is set to v3.0", @@ -119,3 +69,19 @@ async def test_mode_fail( blocking=True, return_response=True, ) + + +@pytest.mark.parametrize( + "mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST], indirect=True +) +async def test_weather_states( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + owm_client_mock: MagicMock, +) -> None: + """Test weather states are correctly collected from library with different modes and mocked function responses.""" + + await setup_platform(hass, mock_config_entry, [Platform.WEATHER]) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From c4ceb4759a1c45f80a96c47c988b5e8ddec1d3de Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 9 May 2025 13:08:34 +0200 Subject: [PATCH 0282/1175] Remove deprecated camera frontend_stream_type (#144539) --- homeassistant/components/camera/__init__.py | 51 ------------------- .../axis/snapshots/test_camera.ambr | 2 - tests/components/camera/test_init.py | 25 --------- tests/components/nest/test_camera.py | 2 - .../netatmo/snapshots/test_camera.ambr | 2 - .../ring/snapshots/test_camera.ambr | 3 -- .../tplink/snapshots/test_camera.ambr | 1 - tests/components/unifiprotect/test_camera.py | 10 ++-- .../custom_components/test/camera.py | 41 --------------- 9 files changed, 7 insertions(+), 130 deletions(-) delete mode 100644 tests/testing_config/custom_components/test/camera.py diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index aa5d766c874..ba80864b769 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -61,7 +61,6 @@ from homeassistant.helpers.deprecation import ( from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.frame import ReportBehavior, report_usage from homeassistant.helpers.network import get_url from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, VolDictType @@ -436,7 +435,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CACHED_PROPERTIES_WITH_ATTR_ = { "brand", "frame_interval", - "frontend_stream_type", "is_on", "is_recording", "is_streaming", @@ -456,8 +454,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Entity Properties _attr_brand: str | None = None _attr_frame_interval: float = MIN_STREAM_INTERVAL - # Deprecated in 2024.12. Remove in 2025.6 - _attr_frontend_stream_type: StreamType | None _attr_is_on: bool = True _attr_is_recording: bool = False _attr_is_streaming: bool = False @@ -488,16 +484,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): type(self).async_handle_async_webrtc_offer != Camera.async_handle_async_webrtc_offer ) - self._deprecate_attr_frontend_stream_type_logged = False - if type(self).frontend_stream_type != Camera.frontend_stream_type: - report_usage( - ( - f"is overwriting the 'frontend_stream_type' property in the {type(self).__name__} class," - " which is deprecated and will be removed in Home Assistant 2025.6, " - ), - core_integration_behavior=ReportBehavior.ERROR, - exclude_integrations={DOMAIN}, - ) @cached_property def entity_picture(self) -> str: @@ -559,40 +545,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the interval between frames of the mjpeg stream.""" return self._attr_frame_interval - @property - def frontend_stream_type(self) -> StreamType | None: - """Return the type of stream supported by this camera. - - A camera may have a single stream type which is used to inform the - frontend which camera attributes and player to use. The default type - is to use HLS, and components can override to change the type. - """ - # Deprecated in 2024.12. Remove in 2025.6 - # Use the camera_capabilities instead - if hasattr(self, "_attr_frontend_stream_type"): - if not self._deprecate_attr_frontend_stream_type_logged: - report_usage( - ( - f"is setting the '_attr_frontend_stream_type' attribute in the {type(self).__name__} class," - " which is deprecated and will be removed in Home Assistant 2025.6, " - ), - core_integration_behavior=ReportBehavior.ERROR, - exclude_integrations={DOMAIN}, - ) - - self._deprecate_attr_frontend_stream_type_logged = True - return self._attr_frontend_stream_type - if CameraEntityFeature.STREAM not in self.supported_features_compat: - return None - if ( - self._webrtc_provider - or self._legacy_webrtc_provider - or self._supports_native_sync_webrtc - or self._supports_native_async_webrtc - ): - return StreamType.WEB_RTC - return StreamType.HLS - @property def available(self) -> bool: """Return True if entity is available.""" @@ -797,9 +749,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if motion_detection_enabled := self.motion_detection_enabled: attrs["motion_detection"] = motion_detection_enabled - if frontend_stream_type := self.frontend_stream_type: - attrs["frontend_stream_type"] = frontend_stream_type - return attrs @callback diff --git a/tests/components/axis/snapshots/test_camera.ambr b/tests/components/axis/snapshots/test_camera.ambr index 1e70e2a799f..d323a209dc8 100644 --- a/tests/components/axis/snapshots/test_camera.ambr +++ b/tests/components/axis/snapshots/test_camera.ambr @@ -39,7 +39,6 @@ 'access_token': '1', 'entity_picture': '/api/camera_proxy/camera.home?token=1', 'friendly_name': 'home', - 'frontend_stream_type': , 'supported_features': , }), 'context': , @@ -90,7 +89,6 @@ 'access_token': '1', 'entity_picture': '/api/camera_proxy/camera.home?token=1', 'friendly_name': 'home', - 'frontend_stream_type': , 'supported_features': , }), 'context': , diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7fd469fa51a..7fd5cd0855f 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -27,7 +27,6 @@ from homeassistant.components.camera.helper import get_camera_from_entity_id from homeassistant.components.websocket_api import TYPE_RESULT from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_PLATFORM, EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE, ) @@ -1036,27 +1035,3 @@ async def test_camera_capabilities_changing_native_support( await hass.async_block_till_done() await _test_capabilities(hass, hass_ws_client, cam.entity_id, set(), set()) - - -@pytest.mark.usefixtures("enable_custom_integrations") -async def test_deprecated_frontend_stream_type_logs( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test using (_attr_)frontend_stream_type will log.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - await hass.async_block_till_done() - - for entity_id in ( - "camera.property_frontend_stream_type", - "camera.attr_frontend_stream_type", - ): - camera_obj = get_camera_from_entity_id(hass, entity_id) - assert camera_obj.frontend_stream_type == StreamType.WEB_RTC - - assert ( - "Detected that custom integration 'test' is overwriting the 'frontend_stream_type' property in the PropertyFrontendStreamTypeCamera class, which is deprecated and will be removed in Home Assistant 2025.6," - ) in caplog.text - assert ( - "Detected that custom integration 'test' is setting the '_attr_frontend_stream_type' attribute in the AttrFrontendStreamTypeCamera class, which is deprecated and will be removed in Home Assistant 2025.6," - ) in caplog.text diff --git a/tests/components/nest/test_camera.py b/tests/components/nest/test_camera.py index 3e7dbd3f223..c0579c99a62 100644 --- a/tests/components/nest/test_camera.py +++ b/tests/components/nest/test_camera.py @@ -826,7 +826,6 @@ async def test_camera_multiple_streams( assert cam is not None assert cam.state == CameraState.STREAMING # Prefer WebRTC over RTSP/HLS - assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC client = await hass_ws_client(hass) assert await async_frontend_stream_types(client, "camera.my_camera") == [ StreamType.WEB_RTC @@ -905,7 +904,6 @@ async def test_webrtc_refresh_expired_stream( cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == CameraState.STREAMING - assert cam.attributes["frontend_stream_type"] == StreamType.WEB_RTC client = await hass_ws_client(hass) assert await async_frontend_stream_types(client, "camera.my_camera") == [ StreamType.WEB_RTC diff --git a/tests/components/netatmo/snapshots/test_camera.ambr b/tests/components/netatmo/snapshots/test_camera.ambr index 9bd10ed9b5f..7f38e261768 100644 --- a/tests/components/netatmo/snapshots/test_camera.ambr +++ b/tests/components/netatmo/snapshots/test_camera.ambr @@ -42,7 +42,6 @@ 'brand': 'Netatmo', 'entity_picture': '/api/camera_proxy/camera.front?token=1caab5c3b3', 'friendly_name': 'Front', - 'frontend_stream_type': , 'id': '12:34:56:10:b9:0e', 'is_local': False, 'light_state': None, @@ -104,7 +103,6 @@ 'brand': 'Netatmo', 'entity_picture': '/api/camera_proxy/camera.hall?token=1caab5c3b3', 'friendly_name': 'Hall', - 'frontend_stream_type': , 'id': '12:34:56:00:f1:62', 'is_local': True, 'light_state': None, diff --git a/tests/components/ring/snapshots/test_camera.ambr b/tests/components/ring/snapshots/test_camera.ambr index 8c3b8a083b0..0e5efd68753 100644 --- a/tests/components/ring/snapshots/test_camera.ambr +++ b/tests/components/ring/snapshots/test_camera.ambr @@ -94,7 +94,6 @@ 'attribution': 'Data provided by Ring.com', 'entity_picture': '/api/camera_proxy/camera.front_door_live_view?token=1caab5c3b3', 'friendly_name': 'Front Door Live view', - 'frontend_stream_type': , 'last_video_id': None, 'supported_features': , 'video_url': None, @@ -201,7 +200,6 @@ 'attribution': 'Data provided by Ring.com', 'entity_picture': '/api/camera_proxy/camera.front_live_view?token=1caab5c3b3', 'friendly_name': 'Front Live view', - 'frontend_stream_type': , 'last_video_id': None, 'supported_features': , 'video_url': None, @@ -309,7 +307,6 @@ 'attribution': 'Data provided by Ring.com', 'entity_picture': '/api/camera_proxy/camera.internal_live_view?token=1caab5c3b3', 'friendly_name': 'Internal Live view', - 'frontend_stream_type': , 'last_video_id': None, 'supported_features': , 'video_url': None, diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index e037c2c9e40..67749b30d1a 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -39,7 +39,6 @@ 'access_token': '1caab5c3b3', 'entity_picture': '/api/camera_proxy/camera.my_camera_live_view?token=1caab5c3b3', 'friendly_name': 'my_camera Live view', - 'frontend_stream_type': , 'supported_features': , }), 'context': , diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 975e93edf09..34a1d064547 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -12,6 +12,7 @@ from uiprotect.websocket import WebsocketState from webrtc_models import RTCIceCandidateInit from homeassistant.components.camera import ( + CameraCapabilities, CameraEntityFeature, CameraState, CameraWebRTCProvider, @@ -21,6 +22,7 @@ from homeassistant.components.camera import ( async_get_stream_source, async_register_webrtc_provider, ) +from homeassistant.components.camera.helper import get_camera_from_entity_id from homeassistant.components.unifiprotect.const import ( ATTR_BITRATE, ATTR_CHANNEL_ID, @@ -345,9 +347,11 @@ async def test_webrtc_support( camera_high_only.channels[2].is_rtsp_enabled = False await init_entry(hass, ufp, [camera_high_only]) entity_id = validate_default_camera_entity(hass, camera_high_only, 0) - state = hass.states.get(entity_id) - assert state - assert StreamType.WEB_RTC in state.attributes["frontend_stream_type"] + assert hass.states.get(entity_id) + camera_obj = get_camera_from_entity_id(hass, entity_id) + assert camera_obj.camera_capabilities == CameraCapabilities( + {StreamType.HLS, StreamType.WEB_RTC} + ) async def test_adopt( diff --git a/tests/testing_config/custom_components/test/camera.py b/tests/testing_config/custom_components/test/camera.py deleted file mode 100644 index b2aa1bbc53b..00000000000 --- a/tests/testing_config/custom_components/test/camera.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Provide a mock remote platform. - -Call init before using it in your tests to ensure clean test data. -""" - -from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities_callback: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Return mock entities.""" - async_add_entities_callback( - [AttrFrontendStreamTypeCamera(), PropertyFrontendStreamTypeCamera()] - ) - - -class AttrFrontendStreamTypeCamera(Camera): - """attr frontend stream type Camera.""" - - _attr_name = "attr frontend stream type" - _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM - _attr_frontend_stream_type: StreamType = StreamType.WEB_RTC - - -class PropertyFrontendStreamTypeCamera(Camera): - """property frontend stream type Camera.""" - - _attr_name = "property frontend stream type" - _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM - - @property - def frontend_stream_type(self) -> StreamType | None: - """Return the stream type of the camera.""" - return StreamType.WEB_RTC From d6e5fdceb7fe9485648783c36887a357b363491e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 9 May 2025 13:09:05 +0200 Subject: [PATCH 0283/1175] Update frontend to 20250509.0 (#144549) --- 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 84062384bf5..9471f863a72 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==20250507.0"] + "requirements": ["home-assistant-frontend==20250509.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0e69000fa9d..f8fcde7e9fe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.100.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 33840e3b64c..5d660fd5bf2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1158,7 +1158,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d127d4efdd3..f0b10e68c7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.70 # homeassistant.components.frontend -home-assistant-frontend==20250507.0 +home-assistant-frontend==20250509.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From 4271d3f32fa5468be72c6aeb818b4399541404ca Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Fri, 9 May 2025 19:09:18 +0800 Subject: [PATCH 0284/1175] Add exception-translations for switchbot integration (#143444) * add exception handler * add unit test * test exception per platform * optimize unit tests * update quality scale --- homeassistant/components/switchbot/cover.py | 14 +- homeassistant/components/switchbot/entity.py | 31 +++- .../components/switchbot/humidifier.py | 4 +- homeassistant/components/switchbot/light.py | 9 +- homeassistant/components/switchbot/lock.py | 5 +- .../components/switchbot/quality_scale.yaml | 2 +- .../components/switchbot/strings.json | 5 + tests/components/switchbot/test_cover.py | 158 ++++++++++++++++++ tests/components/switchbot/test_humidifier.py | 52 ++++++ tests/components/switchbot/test_light.py | 130 ++++++++++---- tests/components/switchbot/test_lock.py | 51 ++++++ tests/components/switchbot/test_switch.py | 62 ++++++- 12 files changed, 478 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index bb73339aa05..9124dc7f846 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator -from .entity import SwitchbotEntity +from .entity import SwitchbotEntity, exception_handler # Initialize the logger _LOGGER = logging.getLogger(__name__) @@ -76,6 +76,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if self._attr_current_cover_position is not None: self._attr_is_closed = self._attr_current_cover_position <= 20 + @exception_handler async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" @@ -85,6 +86,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" @@ -94,6 +96,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" @@ -103,6 +106,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) @@ -161,6 +165,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): _tilt > self.CLOSED_UP_THRESHOLD ) + @exception_handler async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the tilt.""" @@ -168,6 +173,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._last_run_success = bool(await self._device.open()) self.async_write_ha_state() + @exception_handler async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the tilt.""" @@ -175,6 +181,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._last_run_success = bool(await self._device.close()) self.async_write_ha_state() + @exception_handler async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the moving of this device.""" @@ -182,6 +189,7 @@ class SwitchBotBlindTiltEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._last_run_success = bool(await self._device.stop()) self.async_write_ha_state() + @exception_handler async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" position = kwargs.get(ATTR_TILT_POSITION) @@ -237,6 +245,7 @@ class SwitchBotRollerShadeEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if self._attr_current_cover_position is not None: self._attr_is_closed = self._attr_current_cover_position <= 20 + @exception_handler async def async_open_cover(self, **kwargs: Any) -> None: """Open the roller shade.""" @@ -246,6 +255,7 @@ class SwitchBotRollerShadeEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_close_cover(self, **kwargs: Any) -> None: """Close the roller shade.""" @@ -255,6 +265,7 @@ class SwitchBotRollerShadeEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of roller shade.""" @@ -264,6 +275,7 @@ class SwitchBotRollerShadeEntity(SwitchbotEntity, CoverEntity, RestoreEntity): self._attr_is_closing = self._device.is_closing() self.async_write_ha_state() + @exception_handler async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 282d23bfd1a..b7ee36fc1ae 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -2,22 +2,24 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Coroutine, Mapping import logging -from typing import Any +from typing import Any, Concatenate from switchbot import Switchbot, SwitchbotDevice +from switchbot.devices.device import SwitchbotOperationError from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, ) from homeassistant.const import ATTR_CONNECTIONS from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import ToggleEntity -from .const import MANUFACTURER +from .const import DOMAIN, MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -88,11 +90,33 @@ class SwitchbotEntity( await self._device.update() +def exception_handler[_EntityT: SwitchbotEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate Switchbot calls to handle exceptions.. + + A decorator that wraps the passed in function, catches Switchbot errors. + """ + + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except SwitchbotOperationError as error: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="operation_error", + translation_placeholders={"error": str(error)}, + ) from error + + return handler + + class SwitchbotSwitchedEntity(SwitchbotEntity, ToggleEntity): """Base class for Switchbot entities that can be turned on and off.""" _device: Switchbot + @exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" _LOGGER.debug("Turn Switchbot device on %s", self._address) @@ -102,6 +126,7 @@ class SwitchbotSwitchedEntity(SwitchbotEntity, ToggleEntity): self._attr_is_on = True self.async_write_ha_state() + @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" _LOGGER.debug("Turn Switchbot device off %s", self._address) diff --git a/homeassistant/components/switchbot/humidifier.py b/homeassistant/components/switchbot/humidifier.py index 34a24948df1..c15cf7ac9c6 100644 --- a/homeassistant/components/switchbot/humidifier.py +++ b/homeassistant/components/switchbot/humidifier.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import SwitchbotConfigEntry -from .entity import SwitchbotSwitchedEntity +from .entity import SwitchbotSwitchedEntity, exception_handler PARALLEL_UPDATES = 0 @@ -55,11 +55,13 @@ class SwitchBotHumidifier(SwitchbotSwitchedEntity, HumidifierEntity): """Return the humidity we try to reach.""" return self._device.get_target_humidity() + @exception_handler async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" self._last_run_success = bool(await self._device.set_level(humidity)) self.async_write_ha_state() + @exception_handler async def async_set_mode(self, mode: str) -> None: """Set new target humidity.""" if mode == MODE_AUTO: diff --git a/homeassistant/components/switchbot/light.py b/homeassistant/components/switchbot/light.py index 4b9a7e1b988..ad37f3ebec0 100644 --- a/homeassistant/components/switchbot/light.py +++ b/homeassistant/components/switchbot/light.py @@ -4,7 +4,8 @@ from __future__ import annotations from typing import Any, cast -from switchbot import ColorMode as SwitchBotColorMode, SwitchbotBaseLight +import switchbot +from switchbot import ColorMode as SwitchBotColorMode from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator -from .entity import SwitchbotEntity +from .entity import SwitchbotEntity, exception_handler SWITCHBOT_COLOR_MODE_TO_HASS = { SwitchBotColorMode.RGB: ColorMode.RGB, @@ -39,7 +40,7 @@ async def async_setup_entry( class SwitchbotLightEntity(SwitchbotEntity, LightEntity): """Representation of switchbot light bulb.""" - _device: SwitchbotBaseLight + _device: switchbot.SwitchbotBaseLight _attr_name = None def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: @@ -66,6 +67,7 @@ class SwitchbotLightEntity(SwitchbotEntity, LightEntity): self._attr_rgb_color = device.rgb self._attr_color_mode = ColorMode.RGB + @exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" brightness = round( @@ -89,6 +91,7 @@ class SwitchbotLightEntity(SwitchbotEntity, LightEntity): return await self._device.turn_on() + @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self._device.turn_off() diff --git a/homeassistant/components/switchbot/lock.py b/homeassistant/components/switchbot/lock.py index d9ff2433cf8..069b01521c4 100644 --- a/homeassistant/components/switchbot/lock.py +++ b/homeassistant/components/switchbot/lock.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import CONF_LOCK_NIGHTLATCH, DEFAULT_LOCK_NIGHTLATCH from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator -from .entity import SwitchbotEntity +from .entity import SwitchbotEntity, exception_handler PARALLEL_UPDATES = 0 @@ -54,11 +54,13 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): LockStatus.UNLOCKING_STOP, } + @exception_handler async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" self._last_run_success = await self._device.lock() self.async_write_ha_state() + @exception_handler async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" if self._attr_supported_features & (LockEntityFeature.OPEN): @@ -67,6 +69,7 @@ class SwitchBotLock(SwitchbotEntity, LockEntity): self._last_run_success = await self._device.unlock() self.async_write_ha_state() + @exception_handler async def async_open(self, **kwargs: Any) -> None: """Open the lock.""" self._last_run_success = await self._device.unlock() diff --git a/homeassistant/components/switchbot/quality_scale.yaml b/homeassistant/components/switchbot/quality_scale.yaml index e9d8a9626ac..b8db573f405 100644 --- a/homeassistant/components/switchbot/quality_scale.yaml +++ b/homeassistant/components/switchbot/quality_scale.yaml @@ -70,7 +70,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: status: exempt comment: | diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index f0d075eafc9..bd41502d8b7 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -181,5 +181,10 @@ } } } + }, + "exceptions": { + "operation_error": { + "message": "An error occurred while performing the action: {error}" + } } } diff --git a/tests/components/switchbot/test_cover.py b/tests/components/switchbot/test_cover.py index b52436f1932..9430a45d106 100644 --- a/tests/components/switchbot/test_cover.py +++ b/tests/components/switchbot/test_cover.py @@ -3,6 +3,10 @@ from collections.abc import Callable from unittest.mock import AsyncMock, patch +import pytest +from switchbot.devices.device import SwitchbotOperationError + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -23,6 +27,7 @@ from homeassistant.const import ( SERVICE_STOP_COVER_TILT, ) from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from . import ( ROLLER_SHADE_SERVICE_INFO, @@ -490,3 +495,156 @@ async def test_roller_shade_controlling( state = hass.states.get(entity_id) assert state.state == CoverState.OPEN assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +@pytest.mark.parametrize( + ( + "sensor_type", + "service_info", + "class_name", + "service", + "service_data", + "mock_method", + ), + [ + ( + "curtain", + WOCURTAIN3_SERVICE_INFO, + "SwitchbotCurtain", + SERVICE_CLOSE_COVER, + {}, + "close", + ), + ( + "curtain", + WOCURTAIN3_SERVICE_INFO, + "SwitchbotCurtain", + SERVICE_OPEN_COVER, + {}, + "open", + ), + ( + "curtain", + WOCURTAIN3_SERVICE_INFO, + "SwitchbotCurtain", + SERVICE_STOP_COVER, + {}, + "stop", + ), + ( + "curtain", + WOCURTAIN3_SERVICE_INFO, + "SwitchbotCurtain", + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50}, + "set_position", + ), + ( + "roller_shade", + ROLLER_SHADE_SERVICE_INFO, + "SwitchbotRollerShade", + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50}, + "set_position", + ), + ( + "roller_shade", + ROLLER_SHADE_SERVICE_INFO, + "SwitchbotRollerShade", + SERVICE_OPEN_COVER, + {}, + "open", + ), + ( + "roller_shade", + ROLLER_SHADE_SERVICE_INFO, + "SwitchbotRollerShade", + SERVICE_CLOSE_COVER, + {}, + "close", + ), + ( + "roller_shade", + ROLLER_SHADE_SERVICE_INFO, + "SwitchbotRollerShade", + SERVICE_STOP_COVER, + {}, + "stop", + ), + ( + "blind_tilt", + WOBLINDTILT_SERVICE_INFO, + "SwitchbotBlindTilt", + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_TILT_POSITION: 50}, + "set_position", + ), + ( + "blind_tilt", + WOBLINDTILT_SERVICE_INFO, + "SwitchbotBlindTilt", + SERVICE_OPEN_COVER_TILT, + {}, + "open", + ), + ( + "blind_tilt", + WOBLINDTILT_SERVICE_INFO, + "SwitchbotBlindTilt", + SERVICE_CLOSE_COVER_TILT, + {}, + "close", + ), + ( + "blind_tilt", + WOBLINDTILT_SERVICE_INFO, + "SwitchbotBlindTilt", + SERVICE_STOP_COVER_TILT, + {}, + "stop", + ), + ], +) +async def test_exception_handling_cover_service( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + sensor_type: str, + service_info: BluetoothServiceInfoBleak, + class_name: str, + service: str, + service_data: dict, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for cover service with exception.""" + inject_bluetooth_service_info(hass, service_info) + + entry = mock_entry_factory(sensor_type=sensor_type) + entry.add_to_hass(hass) + entity_id = "cover.test_name" + + with patch.multiple( + f"homeassistant.components.switchbot.cover.switchbot.{class_name}", + update=AsyncMock(return_value=None), + **{mock_method: AsyncMock(side_effect=exception)}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + COVER_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/switchbot/test_humidifier.py b/tests/components/switchbot/test_humidifier.py index cb2882a7475..fa9efac0bfd 100644 --- a/tests/components/switchbot/test_humidifier.py +++ b/tests/components/switchbot/test_humidifier.py @@ -4,6 +4,7 @@ from collections.abc import Callable from unittest.mock import AsyncMock, patch import pytest +from switchbot.devices.device import SwitchbotOperationError from homeassistant.components.humidifier import ( ATTR_HUMIDITY, @@ -18,6 +19,7 @@ from homeassistant.components.humidifier import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import HUMIDIFIER_SERVICE_INFO @@ -121,3 +123,53 @@ async def test_humidifier_services( } mock_instance = mock_map[mock_method] mock_instance.assert_awaited_once_with(*expected_args) + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +@pytest.mark.parametrize( + ("service", "service_data", "mock_method"), + [ + (SERVICE_TURN_ON, {}, "turn_on"), + (SERVICE_TURN_OFF, {}, "turn_off"), + (SERVICE_SET_HUMIDITY, {ATTR_HUMIDITY: 60}, "set_level"), + (SERVICE_SET_MODE, {ATTR_MODE: MODE_AUTO}, "async_set_auto"), + (SERVICE_SET_MODE, {ATTR_MODE: MODE_NORMAL}, "async_set_manual"), + ], +) +async def test_exception_handling_humidifier_service( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + service: str, + service_data: dict, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for humidifier service with exception.""" + inject_bluetooth_service_info(hass, HUMIDIFIER_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="humidifier") + entry.add_to_hass(hass) + entity_id = "humidifier.test_name" + + patch_target = f"homeassistant.components.switchbot.humidifier.switchbot.SwitchbotHumidifier.{mock_method}" + + with patch(patch_target, new=AsyncMock(side_effect=exception)): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + HUMIDIFIER_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/switchbot/test_light.py b/tests/components/switchbot/test_light.py index ef46017e9ae..957d56411da 100644 --- a/tests/components/switchbot/test_light.py +++ b/tests/components/switchbot/test_light.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch import pytest from switchbot import ColorMode as switchbotColorMode +from switchbot.devices.device import SwitchbotOperationError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,6 +18,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import WOSTRIP_SERVICE_INFO @@ -93,30 +95,14 @@ async def test_light_strip_services( entry.add_to_hass(hass) entity_id = "light.test_name" - with ( - patch("switchbot.SwitchbotLightStrip.color_modes", new=color_modes), - patch("switchbot.SwitchbotLightStrip.color_mode", new=color_mode), - patch( - "switchbot.SwitchbotLightStrip.turn_on", - new=AsyncMock(return_value=True), - ) as mock_turn_on, - patch( - "switchbot.SwitchbotLightStrip.turn_off", - new=AsyncMock(return_value=True), - ) as mock_turn_off, - patch( - "switchbot.SwitchbotLightStrip.set_brightness", - new=AsyncMock(return_value=True), - ) as mock_set_brightness, - patch( - "switchbot.SwitchbotLightStrip.set_rgb", - new=AsyncMock(return_value=True), - ) as mock_set_rgb, - patch( - "switchbot.SwitchbotLightStrip.set_color_temp", - new=AsyncMock(return_value=True), - ) as mock_set_color_temp, - patch("switchbot.SwitchbotLightStrip.update", new=AsyncMock(return_value=None)), + mocked_instance = AsyncMock(return_value=True) + + with patch.multiple( + "homeassistant.components.switchbot.light.switchbot.SwitchbotLightStrip", + color_modes=color_modes, + color_mode=color_mode, + update=AsyncMock(return_value=None), + **{mock_method: mocked_instance}, ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -128,12 +114,90 @@ async def test_light_strip_services( blocking=True, ) - mock_map = { - "turn_off": mock_turn_off, - "turn_on": mock_turn_on, - "set_brightness": mock_set_brightness, - "set_rgb": mock_set_rgb, - "set_color_temp": mock_set_color_temp, - } - mock_instance = mock_map[mock_method] - mock_instance.assert_awaited_once_with(*expected_args) + mocked_instance.assert_awaited_once_with(*expected_args) + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +@pytest.mark.parametrize( + ("service", "service_data", "mock_method", "color_modes", "color_mode"), + [ + ( + SERVICE_TURN_ON, + {}, + "turn_on", + {switchbotColorMode.RGB}, + switchbotColorMode.RGB, + ), + ( + SERVICE_TURN_OFF, + {}, + "turn_off", + {switchbotColorMode.RGB}, + switchbotColorMode.RGB, + ), + ( + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 128}, + "set_brightness", + {switchbotColorMode.RGB}, + switchbotColorMode.RGB, + ), + ( + SERVICE_TURN_ON, + {ATTR_RGB_COLOR: (255, 0, 0)}, + "set_rgb", + {switchbotColorMode.RGB}, + switchbotColorMode.RGB, + ), + ( + SERVICE_TURN_ON, + {ATTR_COLOR_TEMP_KELVIN: 4000}, + "set_color_temp", + {switchbotColorMode.COLOR_TEMP}, + switchbotColorMode.COLOR_TEMP, + ), + ], +) +async def test_exception_handling_light_service( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + service: str, + service_data: dict, + mock_method: str, + color_modes: set | None, + color_mode: switchbotColorMode | None, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for light service with exception.""" + inject_bluetooth_service_info(hass, WOSTRIP_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="light_strip") + entry.add_to_hass(hass) + entity_id = "light.test_name" + + with patch.multiple( + "homeassistant.components.switchbot.light.switchbot.SwitchbotLightStrip", + color_modes=color_modes, + color_mode=color_mode, + update=AsyncMock(return_value=None), + **{mock_method: AsyncMock(side_effect=exception)}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + LIGHT_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/switchbot/test_lock.py b/tests/components/switchbot/test_lock.py index b7153a041d0..ea8939c8e41 100644 --- a/tests/components/switchbot/test_lock.py +++ b/tests/components/switchbot/test_lock.py @@ -4,6 +4,7 @@ from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest +from switchbot.devices.device import SwitchbotOperationError from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -14,6 +15,7 @@ from homeassistant.const import ( SERVICE_UNLOCK, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from . import LOCK_SERVICE_INFO, WOLOCKPRO_SERVICE_INFO @@ -103,3 +105,52 @@ async def test_lock_services_with_night_latch_enabled( ) mocked_instance.assert_awaited_once() + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +@pytest.mark.parametrize( + ("service", "mock_method"), + [ + (SERVICE_LOCK, "lock"), + (SERVICE_OPEN, "unlock"), + (SERVICE_UNLOCK, "unlock_without_unlatch"), + ], +) +async def test_exception_handling_lock_service( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service: str, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for lock service with exception.""" + inject_bluetooth_service_info(hass, LOCK_SERVICE_INFO) + + entry = mock_entry_encrypted_factory(sensor_type="lock") + entry.add_to_hass(hass) + entity_id = "lock.test_name" + + with patch.multiple( + "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", + is_night_latch_enabled=MagicMock(return_value=True), + **{mock_method: AsyncMock(side_effect=exception)}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + LOCK_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) diff --git a/tests/components/switchbot/test_switch.py b/tests/components/switchbot/test_switch.py index 2d572fd9996..be28b2a02a8 100644 --- a/tests/components/switchbot/test_switch.py +++ b/tests/components/switchbot/test_switch.py @@ -1,10 +1,20 @@ """Test the switchbot switches.""" from collections.abc import Callable -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from homeassistant.components.switch import STATE_ON +import pytest +from switchbot.devices.device import SwitchbotOperationError + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from . import WOHAND_SERVICE_INFO @@ -45,3 +55,51 @@ async def test_switchbot_switch_with_restore_state( state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes["last_run_success"] is True + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +@pytest.mark.parametrize( + ("service", "mock_method"), + [ + (SERVICE_TURN_ON, "turn_on"), + (SERVICE_TURN_OFF, "turn_off"), + ], +) +async def test_exception_handling_switch( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + service: str, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for switch service with exception.""" + inject_bluetooth_service_info(hass, WOHAND_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="bot") + entry.add_to_hass(hass) + entity_id = "switch.test_name" + + patch_target = ( + f"homeassistant.components.switchbot.switch.switchbot.Switchbot.{mock_method}" + ) + + with patch(patch_target, new=AsyncMock(side_effect=exception)): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) From e69b3ebf1e7540f78056b27448fea10f06a91a2f Mon Sep 17 00:00:00 2001 From: markhannon Date: Fri, 9 May 2025 21:10:28 +1000 Subject: [PATCH 0285/1175] Add fan entity to Zimi integration (#144327) * Import fan.py * Align to light design * Consistency for debug message * ruff (post merge) * Fixed stale docstring * refactor init * Combine aysnc_add_entities Use _attr_speed_range instead of property * Remove unused self._speed (and useless init) * Refactor self._device to device in Entity init --- homeassistant/components/zimi/__init__.py | 2 +- homeassistant/components/zimi/entity.py | 14 ++-- homeassistant/components/zimi/fan.py | 94 +++++++++++++++++++++++ 3 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/zimi/fan.py diff --git a/homeassistant/components/zimi/__init__.py b/homeassistant/components/zimi/__init__.py index 5827684cc3d..ab52c1491e1 100644 --- a/homeassistant/components/zimi/__init__.py +++ b/homeassistant/components/zimi/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN from .helpers import async_connect_to_controller -PLATFORMS = [Platform.LIGHT, Platform.SWITCH] +PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zimi/entity.py b/homeassistant/components/zimi/entity.py index 64781454b2c..68911992014 100644 --- a/homeassistant/components/zimi/entity.py +++ b/homeassistant/components/zimi/entity.py @@ -25,19 +25,19 @@ class ZimiEntity(Entity): """Initialize an HA Entity which is a ZimiDevice.""" self._device = device - self._attr_unique_id = self._device.identifier + self._attr_unique_id = device.identifier self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._device.manufacture_info.identifier)}, - manufacturer=self._device.manufacture_info.manufacturer, - model=self._device.manufacture_info.model, - name=self._device.manufacture_info.name, + identifiers={(DOMAIN, device.manufacture_info.identifier)}, + manufacturer=device.manufacture_info.manufacturer, + model=device.manufacture_info.model, + name=device.manufacture_info.name, hw_version=device.manufacture_info.hwVersion, sw_version=device.manufacture_info.firmwareVersion, suggested_area=device.room, via_device=(DOMAIN, api.mac), ) - self._attr_name = self._device.name.strip() - self._attr_suggested_area = self._device.room + self._attr_name = device.name.strip() + self._attr_suggested_area = device.room @property def available(self) -> bool: diff --git a/homeassistant/components/zimi/fan.py b/homeassistant/components/zimi/fan.py new file mode 100644 index 00000000000..19c51371d1a --- /dev/null +++ b/homeassistant/components/zimi/fan.py @@ -0,0 +1,94 @@ +"""Platform for fan integration.""" + +from __future__ import annotations + +import logging +import math +from typing import Any + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) +from homeassistant.util.scaling import int_states_in_range + +from . import ZimiConfigEntry +from .entity import ZimiEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ZimiConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Zimi Fan platform.""" + + api = config_entry.runtime_data + + async_add_entities([ZimiFan(device, api) for device in api.fans]) + + +class ZimiFan(ZimiEntity, FanEntity): + """Representation of a Zimi fan.""" + + _attr_speed_range = (0, 7) + + _attr_supported_features = ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the desired speed for the fan.""" + + if percentage == 0: + await self.async_turn_off() + return + + target_speed = math.ceil( + percentage_to_ranged_value(self._attr_speed_range, percentage) + ) + + _LOGGER.debug( + "Sending async_set_percentage() for %s with percentage %s", + self.name, + percentage, + ) + + await self._device.set_fanspeed(target_speed) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Instruct the fan to turn on.""" + + _LOGGER.debug("Sending turn_on() for %s", self.name) + await self._device.turn_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the fan to turn off.""" + + _LOGGER.debug("Sending turn_off() for %s", self.name) + + await self._device.turn_off() + + @property + def percentage(self) -> int: + """Return the current speed percentage for the fan.""" + if not self._device.fanspeed: + return 0 + return ranged_value_to_percentage(self._attr_speed_range, self._device.fanspeed) + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(self._attr_speed_range) From af019144e518e2e1ec97b5aa4a91e1b70b611d25 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 9 May 2025 14:15:35 +0300 Subject: [PATCH 0286/1175] Exempt entity categories for Comelit (#142858) * Add entity categories to Comelit * update snapshots * revert EntityCategory changes * update quality scale * update tests --- homeassistant/components/comelit/quality_scale.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/comelit/quality_scale.yaml b/homeassistant/components/comelit/quality_scale.yaml index b6d6cbc1046..09871838914 100644 --- a/homeassistant/components/comelit/quality_scale.yaml +++ b/homeassistant/components/comelit/quality_scale.yaml @@ -65,8 +65,8 @@ rules: status: todo comment: missing implementation entity-category: - status: todo - comment: PR in progress + status: exempt + comment: no config or diagnostic entities entity-device-class: done entity-disabled-by-default: done entity-translations: done From 7bad07ac1094ad2455d85f42e38ed2cc4d25b3dd Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 9 May 2025 21:16:54 +1000 Subject: [PATCH 0287/1175] Add left & right temp request entities to Teslemetry (#144364) Add left right --- .../components/teslemetry/icons.json | 6 +++++ homeassistant/components/teslemetry/sensor.py | 22 +++++++++++++++++++ .../components/teslemetry/strings.json | 6 +++++ 3 files changed, 34 insertions(+) diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 5bc3f52b9b7..242dccf90ec 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -583,6 +583,12 @@ "hvac_fan_status": { "default": "mdi:fan" }, + "hvac_left_temperature_request": { + "default": "mdi:thermometer" + }, + "hvac_right_temperature_request": { + "default": "mdi:thermometer" + }, "isolation_resistance": { "default": "mdi:resistor" }, diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 20e2abfe9e6..52b978dfd21 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -477,6 +477,28 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + TeslemetryVehicleSensorEntityDescription( + key="hvac_left_temperature_request", + streaming_listener=lambda vehicle, + callback: vehicle.listen_HvacLeftTemperatureRequest(callback), + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + TeslemetryVehicleSensorEntityDescription( + key="hvac_right_temperature_request", + streaming_listener=lambda vehicle, + callback: vehicle.listen_HvacRightTemperatureRequest(callback), + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + suggested_display_precision=1, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), TeslemetryVehicleSensorEntityDescription( key="drive_state_active_route_traffic_minutes_delay", polling=True, diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index b3837bb874d..cd20cde6293 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -1016,6 +1016,12 @@ "charge_rate_mile_per_hour": { "name": "Charge rate" }, + "hvac_left_temperature_request": { + "name": "Left temperature request" + }, + "hvac_right_temperature_request": { + "name": "Right temperature request" + }, "hvac_power_state": { "name": "HVAC power state", "state": { From a93bf3c150befa2f0994d01ea8136c20337450c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 9 May 2025 13:17:37 +0200 Subject: [PATCH 0288/1175] Add vacuum platform to miele (#143757) * Add vacuum platform * Add comments * Update snapshot * Use class defined in pymiele * Use device class transation * Fix typo * Cleanup consts * Clean up activity property * Address review comments * Address review comments --- homeassistant/components/miele/__init__.py | 1 + homeassistant/components/miele/const.py | 1 + homeassistant/components/miele/vacuum.py | 224 ++++++++++++++++++ .../miele/fixtures/vacuum_device.json | 81 +++++++ .../miele/snapshots/test_vacuum.ambr | 63 +++++ tests/components/miele/test_vacuum.py | 119 ++++++++++ 6 files changed, 489 insertions(+) create mode 100644 homeassistant/components/miele/vacuum.py create mode 100644 tests/components/miele/fixtures/vacuum_device.json create mode 100644 tests/components/miele/snapshots/test_vacuum.ambr create mode 100644 tests/components/miele/test_vacuum.py diff --git a/homeassistant/components/miele/__init__.py b/homeassistant/components/miele/__init__.py index 98a6919980a..9b9ec81bea9 100644 --- a/homeassistant/components/miele/__init__.py +++ b/homeassistant/components/miele/__init__.py @@ -26,6 +26,7 @@ PLATFORMS: list[Platform] = [ Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, + Platform.VACUUM, ] diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 69e0ab1876e..237302937e2 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -11,6 +11,7 @@ ACTIONS = "actions" POWER_ON = "powerOn" POWER_OFF = "powerOff" PROCESS_ACTION = "processAction" +PROGRAM_ID = "programId" VENTILATION_STEP = "ventilationStep" TARGET_TEMPERATURE = "targetTemperature" AMBIENT_LIGHT = "ambientLight" diff --git a/homeassistant/components/miele/vacuum.py b/homeassistant/components/miele/vacuum.py new file mode 100644 index 00000000000..02d85cabdef --- /dev/null +++ b/homeassistant/components/miele/vacuum.py @@ -0,0 +1,224 @@ +"""Platform for Miele vacuum integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum +import logging +from typing import Any, Final + +from aiohttp import ClientResponseError +from pymiele import MieleEnum + +from homeassistant.components.vacuum import ( + StateVacuumEntity, + StateVacuumEntityDescription, + VacuumActivity, + VacuumEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN, PROCESS_ACTION, PROGRAM_ID, MieleActions, MieleAppliance +from .coordinator import MieleConfigEntry +from .entity import MieleEntity + +_LOGGER = logging.getLogger(__name__) + +# The following const classes define program speeds and programs for the vacuum cleaner. +# Miele have used the same and overlapping names for fan_speeds and programs even +# if the contexts are different. This is an attempt to make it clearer in the integration. + + +class FanSpeed(IntEnum): + """Define fan speeds.""" + + normal = 0 + turbo = 1 + silent = 2 + + +class FanProgram(IntEnum): + """Define fan programs.""" + + auto = 1 + spot = 2 + turbo = 3 + silent = 4 + + +PROGRAM_MAP = { + "normal": FanProgram.auto, + "turbo": FanProgram.turbo, + "silent": FanProgram.silent, +} + +PROGRAM_TO_SPEED: dict[int, str] = { + FanProgram.auto: "normal", + FanProgram.turbo: "turbo", + FanProgram.silent: "silent", + FanProgram.spot: "normal", +} + + +class MieleVacuumStateCode(MieleEnum): + """Define vacuum state codes.""" + + idle = 0 + cleaning = 5889 + returning = 5890 + paused = 5891 + going_to_target_area = 5892 + wheel_lifted = 5893 + dirty_sensors = 5894 + dust_box_missing = 5895 + blocked_drive_wheels = 5896 + blocked_brushes = 5897 + check_dust_box_and_filter = 5898 + internal_fault_reboot = 5899 + blocked_front_wheel = 5900 + docked = 5903, 5904 + remote_controlled = 5910 + unknown = -9999 + + +SUPPORTED_FEATURES = ( + VacuumEntityFeature.STATE + | VacuumEntityFeature.BATTERY + | VacuumEntityFeature.FAN_SPEED + | VacuumEntityFeature.START + | VacuumEntityFeature.STOP + | VacuumEntityFeature.PAUSE + | VacuumEntityFeature.CLEAN_SPOT +) + + +@dataclass(frozen=True, kw_only=True) +class MieleVacuumDescription(StateVacuumEntityDescription): + """Class describing Miele vacuum entities.""" + + on_value: int + + +@dataclass +class MieleVacuumDefinition: + """Class for defining vacuum entities.""" + + types: tuple[MieleAppliance, ...] + description: MieleVacuumDescription + + +VACUUM_TYPES: Final[tuple[MieleVacuumDefinition, ...]] = ( + MieleVacuumDefinition( + types=(MieleAppliance.ROBOT_VACUUM_CLEANER,), + description=MieleVacuumDescription( + key="vacuum", + on_value=14, + name=None, + translation_key="vacuum", + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: MieleConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the vacuum platform.""" + coordinator = config_entry.runtime_data + + async_add_entities( + MieleVacuum(coordinator, device_id, definition.description) + for device_id, device in coordinator.data.devices.items() + for definition in VACUUM_TYPES + if device.device_type in definition.types + ) + + +VACUUM_PHASE_TO_ACTIVITY = { + MieleVacuumStateCode.idle: VacuumActivity.IDLE, + MieleVacuumStateCode.docked: VacuumActivity.DOCKED, + MieleVacuumStateCode.cleaning: VacuumActivity.CLEANING, + MieleVacuumStateCode.going_to_target_area: VacuumActivity.CLEANING, + MieleVacuumStateCode.returning: VacuumActivity.RETURNING, + MieleVacuumStateCode.wheel_lifted: VacuumActivity.ERROR, + MieleVacuumStateCode.dirty_sensors: VacuumActivity.ERROR, + MieleVacuumStateCode.dust_box_missing: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_drive_wheels: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_brushes: VacuumActivity.ERROR, + MieleVacuumStateCode.check_dust_box_and_filter: VacuumActivity.ERROR, + MieleVacuumStateCode.internal_fault_reboot: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_front_wheel: VacuumActivity.ERROR, + MieleVacuumStateCode.paused: VacuumActivity.PAUSED, + MieleVacuumStateCode.remote_controlled: VacuumActivity.PAUSED, +} + + +class MieleVacuum(MieleEntity, StateVacuumEntity): + """Representation of a Vacuum entity.""" + + entity_description: MieleVacuumDescription + _attr_supported_features = SUPPORTED_FEATURES + _attr_fan_speed_list = [fan_speed.name for fan_speed in FanSpeed] + _attr_name = None + + @property + def activity(self) -> VacuumActivity | None: + """Return activity.""" + return VACUUM_PHASE_TO_ACTIVITY.get( + MieleVacuumStateCode(self.device.state_program_phase) + ) + + @property + def battery_level(self) -> int | None: + """Return the battery level.""" + return self.device.state_battery_level + + @property + def fan_speed(self) -> str | None: + """Return the fan speed.""" + return PROGRAM_TO_SPEED.get(self.device.state_program_id) + + @property + def available(self) -> bool: + """Return the availability of the entity.""" + + return ( + self.action.power_off_enabled or self.action.power_on_enabled + ) and super().available + + async def send(self, device_id: str, action: dict[str, Any]) -> None: + """Send action to the device.""" + try: + await self.api.send_action(device_id, action) + except ClientResponseError as ex: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_state_error", + translation_placeholders={ + "entity": self.entity_id, + }, + ) from ex + + async def async_clean_spot(self, **kwargs: Any) -> None: + """Clean spot.""" + await self.send(self._device_id, {PROGRAM_ID: FanProgram.spot}) + + async def async_start(self, **kwargs: Any) -> None: + """Start cleaning.""" + await self.send(self._device_id, {PROCESS_ACTION: MieleActions.START}) + + async def async_stop(self, **kwargs: Any) -> None: + """Stop cleaning.""" + await self.send(self._device_id, {PROCESS_ACTION: MieleActions.STOP}) + + async def async_pause(self, **kwargs: Any) -> None: + """Pause cleaning.""" + await self.send(self._device_id, {PROCESS_ACTION: MieleActions.PAUSE}) + + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + await self.send(self._device_id, {PROGRAM_ID: PROGRAM_MAP[fan_speed]}) diff --git a/tests/components/miele/fixtures/vacuum_device.json b/tests/components/miele/fixtures/vacuum_device.json new file mode 100644 index 00000000000..6f2d486a8bc --- /dev/null +++ b/tests/components/miele/fixtures/vacuum_device.json @@ -0,0 +1,81 @@ +{ + "Dummy_Vacuum_1": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 23, + "value_localized": "Robot vacuum cleaner" + }, + "deviceName": "", + "protocolVersion": 0, + "deviceIdentLabel": { + "fabNumber": "161173909", + "fabIndex": "32", + "techType": "RX3", + "matNumber": "11686510", + "swids": ["", "", "", "<...>"] + }, + "xkmIdentLabel": { "techType": "", "releaseVersion": "" } + }, + "state": { + "ProgramID": { + "value_raw": 1, + "value_localized": "Auto", + "key_localized": "Program name" + }, + "status": { + "value_raw": 2, + "value_localized": "On", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "Program", + "key_localized": "Program type" + }, + "programPhase": { + "xvalue_raw": 5889, + "zvalue_raw": 5904, + "value_raw": 5893, + "value_localized": "in the base station", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [], + "temperature": [], + "coreTargetTemperature": [], + "coreTemperature": [], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": 65 + } + } +} diff --git a/tests/components/miele/snapshots/test_vacuum.ambr b/tests/components/miele/snapshots/test_vacuum.ambr new file mode 100644 index 00000000000..c3029e83fd8 --- /dev/null +++ b/tests/components/miele/snapshots/test_vacuum.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_sensor_states[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_speed_list': list([ + 'normal', + 'turbo', + 'silent', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'vacuum', + 'entity_category': None, + 'entity_id': 'vacuum.robot_vacuum_cleaner', + 'has_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': 'miele', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'vacuum', + 'unique_id': 'Dummy_Vacuum_1-vacuum', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'battery_icon': 'mdi:battery-60', + 'battery_level': 65, + 'fan_speed': 'normal', + 'fan_speed_list': list([ + 'normal', + 'turbo', + 'silent', + ]), + 'friendly_name': 'robot_vacuum_cleaner', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.robot_vacuum_cleaner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'paused', + }) +# --- diff --git a/tests/components/miele/test_vacuum.py b/tests/components/miele/test_vacuum.py new file mode 100644 index 00000000000..81e29bb30b6 --- /dev/null +++ b/tests/components/miele/test_vacuum.py @@ -0,0 +1,119 @@ +"""Tests for miele vacuum module.""" + +from unittest.mock import MagicMock + +from aiohttp import ClientResponseError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.miele.const import PROCESS_ACTION, PROGRAM_ID +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + DOMAIN as VACUUM_DOMAIN, + SERVICE_CLEAN_SPOT, + SERVICE_PAUSE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + +TEST_PLATFORM = VACUUM_DOMAIN +ENTITY_ID = "vacuum.robot_vacuum_cleaner" + +pytestmark = [ + pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]), + pytest.mark.parametrize("load_device_file", ["vacuum_device.json"]), +] + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test vacuum entity setup.""" + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("service", "action_command", "vacuum_power"), + [ + (SERVICE_START, PROCESS_ACTION, 1), + (SERVICE_STOP, PROCESS_ACTION, 2), + (SERVICE_PAUSE, PROCESS_ACTION, 3), + (SERVICE_CLEAN_SPOT, PROGRAM_ID, 2), + ], +) +async def test_vacuum_program( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + service: str, + vacuum_power: int | str, + action_command: str, +) -> None: + """Test the vacuum can be controlled.""" + + await hass.services.async_call( + TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + mock_miele_client.send_action.assert_called_once_with( + "Dummy_Vacuum_1", {action_command: vacuum_power} + ) + + +@pytest.mark.parametrize( + ("fan_speed", "expected"), [("normal", 1), ("turbo", 3), ("silent", 4)] +) +async def test_vacuum_fan_speed( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + fan_speed: str, + expected: int, +) -> None: + """Test the vacuum can be controlled.""" + + await hass.services.async_call( + TEST_PLATFORM, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: fan_speed}, + blocking=True, + ) + mock_miele_client.send_action.assert_called_once_with( + "Dummy_Vacuum_1", {"programId": expected} + ) + + +@pytest.mark.parametrize( + ("service"), + [ + (SERVICE_START), + (SERVICE_STOP), + ], +) +async def test_api_failure( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + service: str, +) -> None: + """Test handling of exception from API.""" + mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + mock_miele_client.send_action.assert_called_once() From 1f84c5e1f15ae373d0bb426e9ba91bc128edeb48 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 9 May 2025 13:50:38 +0200 Subject: [PATCH 0289/1175] Remove deprecated legacy WebRTC provider (#144547) --- homeassistant/components/camera/__init__.py | 31 +-- homeassistant/components/camera/strings.json | 4 - homeassistant/components/camera/webrtc.py | 128 +--------- tests/components/camera/test_webrtc.py | 246 +------------------ 4 files changed, 7 insertions(+), 402 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ba80864b769..194f316c13a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -85,7 +85,6 @@ from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 from .webrtc import ( DATA_ICE_SERVERS, - CameraWebRTCLegacyProvider, CameraWebRTCProvider, WebRTCAnswer, WebRTCCandidate, # noqa: F401 @@ -93,10 +92,8 @@ from .webrtc import ( WebRTCError, WebRTCMessage, # noqa: F401 WebRTCSendMessage, - async_get_supported_legacy_provider, async_get_supported_provider, async_register_ice_servers, - async_register_rtsp_to_web_rtc_provider, # noqa: F401 async_register_webrtc_provider, # noqa: F401 async_register_ws, ) @@ -476,7 +473,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self.async_update_token() self._create_stream_lock: asyncio.Lock | None = None self._webrtc_provider: CameraWebRTCProvider | None = None - self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None self._supports_native_sync_webrtc = ( type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer ) @@ -646,14 +642,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ) return - if self._legacy_webrtc_provider and ( - answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer( - self, offer_sdp - ) - ): - send_message(WebRTCAnswer(answer)) - else: - raise HomeAssistantError("Camera does not support WebRTC") + raise HomeAssistantError("Camera does not support WebRTC") def camera_image( self, width: int | None = None, height: int | None = None @@ -772,9 +761,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): providers or inputs to the state attributes change. """ old_provider = self._webrtc_provider - old_legacy_provider = self._legacy_webrtc_provider new_provider = None - new_legacy_provider = None # Skip all providers if the camera has a native WebRTC implementation if not ( @@ -785,15 +772,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): async_get_supported_provider ) - if new_provider is None: - # Only add the legacy provider if the new provider is not available - new_legacy_provider = await self._async_get_supported_webrtc_provider( - async_get_supported_legacy_provider - ) - - if old_provider != new_provider or old_legacy_provider != new_legacy_provider: + if old_provider != new_provider: self._webrtc_provider = new_provider - self._legacy_webrtc_provider = new_legacy_provider self._invalidate_camera_capabilities_cache() if write_state: self.async_write_ha_state() @@ -828,10 +808,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ] config.configuration.ice_servers.extend(ice_servers) - config.get_candidates_upfront = ( - self._supports_native_sync_webrtc - or self._legacy_webrtc_provider is not None - ) + config.get_candidates_upfront = self._supports_native_sync_webrtc return config @@ -867,7 +844,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): else: frontend_stream_types.add(StreamType.HLS) - if self._webrtc_provider or self._legacy_webrtc_provider: + if self._webrtc_provider: frontend_stream_types.add(StreamType.WEB_RTC) return CameraCapabilities(frontend_stream_types) diff --git a/homeassistant/components/camera/strings.json b/homeassistant/components/camera/strings.json index 4a7e9aafc6e..9176c5ad84a 100644 --- a/homeassistant/components/camera/strings.json +++ b/homeassistant/components/camera/strings.json @@ -46,10 +46,6 @@ } } } - }, - "legacy_webrtc_provider": { - "title": "Detected use of legacy WebRTC provider registered by {legacy_integration}", - "description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant." } }, "services": { diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 3630acf1cfe..723d44409fd 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable from dataclasses import asdict, dataclass, field from functools import cache, partial, wraps import logging -from typing import TYPE_CHECKING, Any, Protocol +from typing import TYPE_CHECKING, Any from mashumaro import MissingField import voluptuous as vol @@ -22,8 +22,7 @@ from webrtc_models import ( from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, issue_registry as ir -from homeassistant.helpers.deprecation import deprecated_function +from homeassistant.helpers import config_validation as cv from homeassistant.util.hass_dict import HassKey from homeassistant.util.ulid import ulid @@ -39,9 +38,6 @@ _LOGGER = logging.getLogger(__name__) DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey( "camera_webrtc_providers" ) -DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey( - "camera_webrtc_legacy_providers" -) DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey( "camera_webrtc_ice_servers" ) @@ -163,18 +159,6 @@ class CameraWebRTCProvider(ABC): return ## This is an optional method so we need a default here. -class CameraWebRTCLegacyProvider(Protocol): - """WebRTC provider.""" - - async def async_is_supported(self, stream_source: str) -> bool: - """Determine if the provider supports the stream source.""" - - async def async_handle_web_rtc_offer( - self, camera: Camera, offer_sdp: str - ) -> str | None: - """Handle the WebRTC offer and return an answer.""" - - @callback def async_register_webrtc_provider( hass: HomeAssistant, @@ -204,8 +188,6 @@ def async_register_webrtc_provider( async def _async_refresh_providers(hass: HomeAssistant) -> None: """Check all cameras for any state changes for registered providers.""" - _async_check_conflicting_legacy_provider(hass) - component = hass.data[DATA_COMPONENT] await asyncio.gather( *(camera.async_refresh_providers() for camera in component.entities) @@ -380,21 +362,6 @@ async def async_get_supported_provider( return None -async def async_get_supported_legacy_provider( - hass: HomeAssistant, camera: Camera -) -> CameraWebRTCLegacyProvider | None: - """Return the first supported provider for the camera.""" - providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS) - if not providers or not (stream_source := await camera.stream_source()): - return None - - for provider in providers.values(): - if await provider.async_is_supported(stream_source): - return provider - - return None - - @callback def async_register_ice_servers( hass: HomeAssistant, @@ -411,94 +378,3 @@ def async_register_ice_servers( servers.append(get_ice_server_fn) return remove - - -# The following code is legacy code that was introduced with rtsp_to_webrtc and will be deprecated/removed in the future. -# Left it so custom integrations can still use it. - -_RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"} - -# An RtspToWebRtcProvider accepts these inputs: -# stream_source: The RTSP url -# offer_sdp: The WebRTC SDP offer -# stream_id: A unique id for the stream, used to update an existing source -# The output is the SDP answer, or None if the source or offer is not eligible. -# The Callable may throw HomeAssistantError on failure. -type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]] - - -class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider): - def __init__(self, fn: RtspToWebRtcProviderType) -> None: - """Initialize the RTSP to WebRTC provider.""" - self._fn = fn - - async def async_is_supported(self, stream_source: str) -> bool: - """Return if this provider is supports the Camera as source.""" - return any(stream_source.startswith(prefix) for prefix in _RTSP_PREFIXES) - - async def async_handle_web_rtc_offer( - self, camera: Camera, offer_sdp: str - ) -> str | None: - """Handle the WebRTC offer and return an answer.""" - if not (stream_source := await camera.stream_source()): - return None - - return await self._fn(stream_source, offer_sdp, camera.entity_id) - - -@deprecated_function("async_register_webrtc_provider", breaks_in_ha_version="2025.6") -def async_register_rtsp_to_web_rtc_provider( - hass: HomeAssistant, - domain: str, - provider: RtspToWebRtcProviderType, -) -> Callable[[], None]: - """Register an RTSP to WebRTC provider. - - The first provider to satisfy the offer will be used. - """ - if DOMAIN not in hass.data: - raise ValueError("Unexpected state, camera not loaded") - - legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {}) - - if domain in legacy_providers: - raise ValueError("Provider already registered") - - provider_instance = _CameraRtspToWebRTCProvider(provider) - - @callback - def remove_provider() -> None: - legacy_providers.pop(domain) - hass.async_create_task(_async_refresh_providers(hass)) - - legacy_providers[domain] = provider_instance - hass.async_create_task(_async_refresh_providers(hass)) - - return remove_provider - - -@callback -def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None: - """Check if a legacy provider is registered together with the builtin provider.""" - builtin_provider_domain = "go2rtc" - if ( - (legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)) - and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS)) - and any(provider.domain == builtin_provider_domain for provider in providers) - ): - for domain in legacy_providers: - ir.async_create_issue( - hass, - DOMAIN, - f"legacy_webrtc_provider_{domain}", - is_fixable=False, - is_persistent=False, - issue_domain=domain, - learn_more_url="https://www.home-assistant.io/integrations/go2rtc/", - severity=ir.IssueSeverity.WARNING, - translation_key="legacy_webrtc_provider", - translation_placeholders={ - "legacy_integration": domain, - "builtin_integration": builtin_provider_domain, - }, - ) diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index a7c6d889409..c7ea82f7b9d 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -1,6 +1,6 @@ """Test camera WebRTC.""" -from collections.abc import AsyncGenerator, Generator +from collections.abc import AsyncGenerator import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -20,9 +20,7 @@ from homeassistant.components.camera import ( WebRTCError, WebRTCMessage, WebRTCSendMessage, - async_get_supported_legacy_provider, async_register_ice_servers, - async_register_rtsp_to_web_rtc_provider, async_register_webrtc_provider, get_camera_from_entity_id, ) @@ -31,7 +29,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant, callback from homeassistant.core_config import async_process_ha_core_config from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider @@ -427,21 +424,6 @@ async def provide_webrtc_answer(stream_source: str, offer: str, stream_id: str) return WEBRTC_ANSWER -@pytest.fixture(name="mock_rtsp_to_webrtc") -def mock_rtsp_to_webrtc_fixture( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> Generator[Mock]: - """Fixture that registers a mock rtsp to webrtc provider.""" - mock_provider = Mock(side_effect=provide_webrtc_answer) - unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider) - assert ( - "async_register_rtsp_to_web_rtc_provider is a deprecated function which will" - " be removed in HA Core 2025.6. Use async_register_webrtc_provider instead" - ) in caplog.text - yield mock_provider - unsub() - - @pytest.mark.usefixtures("mock_test_webrtc_cameras") async def test_websocket_webrtc_offer( hass: HomeAssistant, hass_ws_client: WebSocketGenerator @@ -804,45 +786,6 @@ async def test_websocket_webrtc_offer_invalid_stream_type( } -@pytest.mark.usefixtures("mock_camera", "mock_stream_source") -async def test_rtsp_to_webrtc_offer( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - mock_rtsp_to_webrtc: Mock, -) -> None: - """Test creating a webrtc offer from an rstp provider.""" - client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "answer", - "answer": WEBRTC_ANSWER, - } - - assert mock_rtsp_to_webrtc.called - - @pytest.fixture(name="mock_hls_stream_source") async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]: """Fixture to create an HLS stream source.""" @@ -853,117 +796,6 @@ async def mock_hls_stream_source_fixture() -> AsyncGenerator[AsyncMock]: yield mock_hls_stream_source -@pytest.mark.usefixtures("mock_camera", "mock_stream_source") -async def test_rtsp_to_webrtc_provider_unregistered( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test creating a webrtc offer from an rstp provider.""" - mock_provider = Mock(side_effect=provide_webrtc_answer) - unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider) - - client = await hass_ws_client(hass) - - # Registered provider can handle the WebRTC offer - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "answer", - "answer": WEBRTC_ANSWER, - } - - assert mock_provider.called - mock_provider.reset_mock() - - # Unregister provider, then verify the WebRTC offer cannot be handled - unsub() - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - assert response.get("type") == TYPE_RESULT - assert not response["success"] - assert response["error"] == { - "code": "webrtc_offer_failed", - "message": "Camera does not support WebRTC, frontend_stream_types={}", - } - - assert not mock_provider.called - - -@pytest.mark.usefixtures("mock_camera", "mock_stream_source") -async def test_rtsp_to_webrtc_offer_not_accepted( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test a provider that can't satisfy the rtsp to webrtc offer.""" - - async def provide_none( - stream_source: str, offer: str, stream_id: str - ) -> str | None: - """Simulate a provider that can't accept the offer.""" - return None - - mock_provider = Mock(side_effect=provide_none) - unsub = async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", mock_provider) - client = await hass_ws_client(hass) - - # Registered provider can handle the WebRTC offer - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.demo_camera", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "error", - "code": "webrtc_offer_failed", - "message": "Camera does not support WebRTC", - } - - assert mock_provider.called - - unsub() - - @pytest.mark.parametrize( ("frontend_candidate", "expected_candidate"), [ @@ -1224,79 +1056,3 @@ async def test_webrtc_provider_optional_interface(hass: HomeAssistant) -> None: "session_id", RTCIceCandidateInit("candidate") ) provider.async_close_session("session_id") - - -@pytest.mark.usefixtures("mock_camera") -async def test_repair_issue_legacy_provider( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test repair issue created for legacy provider.""" - # Ensure no issue if no provider is registered - assert not issue_registry.async_get_issue( - "camera", "legacy_webrtc_provider_mock_domain" - ) - - # Register a legacy provider - legacy_provider = Mock(side_effect=provide_webrtc_answer) - unsub_legacy_provider = async_register_rtsp_to_web_rtc_provider( - hass, "mock_domain", legacy_provider - ) - await hass.async_block_till_done() - - # Ensure no issue if only legacy provider is registered - assert not issue_registry.async_get_issue( - "camera", "legacy_webrtc_provider_mock_domain" - ) - - provider = Go2RTCProvider() - unsub_go2rtc_provider = async_register_webrtc_provider(hass, provider) - await hass.async_block_till_done() - - # Ensure issue when legacy and builtin provider are registered - issue = issue_registry.async_get_issue( - "camera", "legacy_webrtc_provider_mock_domain" - ) - assert issue - assert issue.is_fixable is False - assert issue.is_persistent is False - assert issue.issue_domain == "mock_domain" - assert issue.learn_more_url == "https://www.home-assistant.io/integrations/go2rtc/" - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.issue_id == "legacy_webrtc_provider_mock_domain" - assert issue.translation_key == "legacy_webrtc_provider" - assert issue.translation_placeholders == { - "legacy_integration": "mock_domain", - "builtin_integration": "go2rtc", - } - - unsub_legacy_provider() - unsub_go2rtc_provider() - - -@pytest.mark.usefixtures("mock_camera", "register_test_provider", "mock_rtsp_to_webrtc") -async def test_no_repair_issue_without_new_provider( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, -) -> None: - """Test repair issue not created if no go2rtc provider exists.""" - assert not issue_registry.async_get_issue( - "camera", "legacy_webrtc_provider_mock_domain" - ) - - -@pytest.mark.usefixtures("mock_camera", "mock_rtsp_to_webrtc") -async def test_registering_same_legacy_provider( - hass: HomeAssistant, -) -> None: - """Test registering the same legacy provider twice.""" - legacy_provider = Mock(side_effect=provide_webrtc_answer) - with pytest.raises(ValueError, match="Provider already registered"): - async_register_rtsp_to_web_rtc_provider(hass, "mock_domain", legacy_provider) - - -@pytest.mark.usefixtures("mock_hls_stream_source", "mock_camera", "mock_rtsp_to_webrtc") -async def test_get_not_supported_legacy_provider(hass: HomeAssistant) -> None: - """Test getting a not supported legacy provider.""" - camera = get_camera_from_entity_id(hass, "camera.demo_camera") - assert await async_get_supported_legacy_provider(hass, camera) is None From d2bdc85a7b8d224abc1736db61fe0e23889ebeab Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 9 May 2025 14:34:55 +0200 Subject: [PATCH 0290/1175] Remove deprecated async_forward_entry_setup function (#144560) --- homeassistant/config_entries.py | 40 ----------- tests/test_config_entries.py | 114 +------------------------------- 2 files changed, 3 insertions(+), 151 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c58a33ad68d..4f07c9cf574 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2603,46 +2603,6 @@ class ConfigEntries: ) ) - async def async_forward_entry_setup( - self, entry: ConfigEntry, domain: Platform | str - ) -> bool: - """Forward the setup of an entry to a different component. - - By default an entry is setup with the component it belongs to. If that - component also has related platforms, the component will have to - forward the entry to be setup by that component. - - This method is deprecated and will stop working in Home Assistant 2025.6. - - Instead, await async_forward_entry_setups as it can load - multiple platforms at once and is more efficient since it - does not require a separate import executor job for each platform. - """ - report_usage( - "calls async_forward_entry_setup for " - f"integration, {entry.domain} with title: {entry.title} " - f"and entry_id: {entry.entry_id}, which is deprecated, " - "await async_forward_entry_setups instead", - core_behavior=ReportBehavior.LOG, - breaks_in_ha_version="2025.6", - ) - if not entry.setup_lock.locked(): - async with entry.setup_lock: - if entry.state is not ConfigEntryState.LOADED: - raise OperationNotAllowed( - f"The config entry '{entry.title}' ({entry.domain}) with " - f"entry_id '{entry.entry_id}' cannot forward setup for " - f"{domain} because it is in state {entry.state}, but needs " - f"to be in the {ConfigEntryState.LOADED} state" - ) - return await self._async_forward_entry_setup(entry, domain, True) - result = await self._async_forward_entry_setup(entry, domain, True) - # If the lock was held when we stated, and it was released during - # the platform setup, it means they did not await the setup call. - if not entry.setup_lock.locked(): - _report_non_awaited_platform_forwards(entry, "async_forward_entry_setup") - return result - async def _async_forward_entry_setup( self, entry: ConfigEntry, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ba599c88518..75610c7d076 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1365,42 +1365,6 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( assert len(mock_setup_entry.mock_calls) == 0 -async def test_async_forward_entry_setup_deprecated( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test async_forward_entry_setup is deprecated.""" - entry = MockConfigEntry( - domain="original", state=config_entries.ConfigEntryState.LOADED - ) - - mock_original_setup_entry = AsyncMock(return_value=True) - integration = mock_integration( - hass, MockModule("original", async_setup_entry=mock_original_setup_entry) - ) - - mock_setup = AsyncMock(return_value=False) - mock_setup_entry = AsyncMock() - mock_integration( - hass, - MockModule( - "forwarded", async_setup=mock_setup, async_setup_entry=mock_setup_entry - ), - ) - - entry_id = entry.entry_id - caplog.clear() - with patch.object(integration, "async_get_platforms"): - async with entry.setup_lock: - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") - - assert ( - "Detected code that calls async_forward_entry_setup for integration, " - f"original with title: Mock Title and entry_id: {entry_id}, " - "which is deprecated, await async_forward_entry_setups instead. " - "This will stop working in Home Assistant 2025.6, please report this issue" - ) in caplog.text - - async def test_reauth_issue_flow_returns_abort( hass: HomeAssistant, manager: config_entries.ConfigEntries, @@ -7386,78 +7350,6 @@ async def test_non_awaited_async_forward_entry_setups( ) in caplog.text -async def test_non_awaited_async_forward_entry_setup( - hass: HomeAssistant, - manager: config_entries.ConfigEntries, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test async_forward_entry_setup not being awaited.""" - forward_event = asyncio.Event() - task: asyncio.Task | None = None - - async def mock_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock setting up entry.""" - # Call async_forward_entry_setup without awaiting it - # This is not allowed and will raise a warning - nonlocal task - task = create_eager_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) - return True - - async def mock_unload_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock unloading an entry.""" - result = await hass.config_entries.async_unload_platforms(entry, ["light"]) - assert result - return result - - mock_remove_entry = AsyncMock(return_value=None) - - async def mock_setup_entry_platform( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, - ) -> None: - """Mock setting up platform.""" - await forward_event.wait() - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=mock_setup_entry, - async_unload_entry=mock_unload_entry, - async_remove_entry=mock_remove_entry, - ), - ) - mock_platform( - hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) - ) - mock_platform(hass, "test.config_flow", None) - - entry = MockConfigEntry(domain="test", entry_id="test2") - entry.add_to_manager(manager) - - # Setup entry - await manager.async_setup(entry.entry_id) - await hass.async_block_till_done() - forward_event.set() - await hass.async_block_till_done() - await task - - assert ( - "Detected code that calls async_forward_entry_setup for integration " - "test with title: Mock Title and entry_id: test2, during setup without " - "awaiting async_forward_entry_setup, which can cause the setup lock " - "to be released before the setup is done. This will stop working in " - "Home Assistant 2025.1, please report this issue" - ) in caplog.text - - async def test_config_entry_unloaded_during_platform_setup( hass: HomeAssistant, manager: config_entries.ConfigEntries, @@ -7476,7 +7368,7 @@ async def test_config_entry_unloaded_during_platform_setup( def _late_setup(): nonlocal task task = asyncio.create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") + hass.config_entries.async_forward_entry_setups(entry, ["light"]) ) hass.loop.call_soon(_late_setup) @@ -7527,7 +7419,7 @@ async def test_config_entry_unloaded_during_platform_setup( assert ( "OperationNotAllowed: The config entry 'Mock Title' (test) with " - "entry_id 'test2' cannot forward setup for light because it is " + "entry_id 'test2' cannot forward setup for ['light'] because it is " "in state ConfigEntryState.NOT_LOADED, but needs to be in the " "ConfigEntryState.LOADED state" ) in caplog.text @@ -7551,7 +7443,7 @@ async def test_config_entry_late_platform_setup( def _late_setup(): nonlocal task task = asyncio.create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") + hass.config_entries.async_forward_entry_setups(entry, ["light"]) ) hass.loop.call_soon(_late_setup) From 920d281d45776cc3a65daa3b2b386cc299dd30f4 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 9 May 2025 14:40:23 +0200 Subject: [PATCH 0291/1175] Remove deprecated core set_time_zone function (#144559) --- homeassistant/core_config.py | 21 --------------------- tests/test_core_config.py | 13 ------------- 2 files changed, 34 deletions(-) diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py index 9cd232097a7..ccae24a907b 100644 --- a/homeassistant/core_config.py +++ b/homeassistant/core_config.py @@ -60,7 +60,6 @@ from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from .generated.currencies import HISTORIC_CURRENCIES from .helpers import config_validation as cv, issue_registry as ir from .helpers.entity_values import EntityValues -from .helpers.frame import ReportBehavior, report_usage from .helpers.storage import Store from .helpers.typing import UNDEFINED, UndefinedType from .util import dt as dt_util, location @@ -712,26 +711,6 @@ class Config: else: raise ValueError(f"Received invalid time zone {time_zone_str}") - def set_time_zone(self, time_zone_str: str) -> None: - """Set the time zone. - - This is a legacy method that should not be used in new code. - Use async_set_time_zone instead. - - It will be removed in Home Assistant 2025.6. - """ - report_usage( - "sets the time zone using set_time_zone instead of async_set_time_zone", - core_integration_behavior=ReportBehavior.ERROR, - custom_integration_behavior=ReportBehavior.ERROR, - breaks_in_ha_version="2025.6", - ) - if time_zone := dt_util.get_time_zone(time_zone_str): - self.time_zone = time_zone_str - dt_util.set_default_time_zone(time_zone) - else: - raise ValueError(f"Received invalid time zone {time_zone_str}") - async def _async_update( self, *, diff --git a/tests/test_core_config.py b/tests/test_core_config.py index 2723c8e7196..7fbd10db206 100644 --- a/tests/test_core_config.py +++ b/tests/test_core_config.py @@ -5,7 +5,6 @@ from collections import OrderedDict import copy import os from pathlib import Path -import re from tempfile import TemporaryDirectory from typing import Any from unittest.mock import Mock, PropertyMock, patch @@ -1072,18 +1071,6 @@ async def test_debug_mode_defaults_to_off(hass: HomeAssistant) -> None: assert not hass.config.debug -async def test_set_time_zone_deprecated(hass: HomeAssistant) -> None: - """Test set_time_zone is deprecated.""" - with pytest.raises( - RuntimeError, - match=re.escape( - "Detected code that sets the time zone using set_time_zone instead of " - "async_set_time_zone. Please report this issue" - ), - ): - await hass.config.set_time_zone("America/New_York") - - async def test_core_config_schema_imperial_unit( hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: From 9757009d8f31afb3efd2362117a105f97958e49d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 14:47:38 +0200 Subject: [PATCH 0292/1175] Reolink clean device registry mac (#144554) --- homeassistant/components/reolink/__init__.py | 10 +++- tests/components/reolink/test_init.py | 52 +++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index f7d13c1d90f..433af396d63 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -380,6 +380,14 @@ def migrate_entity_ids( if ch is None or is_chime: continue # Do not consider the NVR itself or chimes + # Check for wrongfully added MAC of the NVR/Hub to the camera + # Can be removed in HA 2025.12 + host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) + if host_connnection in device.connections: + new_connections = device.connections.copy() + new_connections.remove(host_connnection) + device_reg.async_update_device(device.id, new_connections=new_connections) + ch_device_ids[device.id] = ch if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): if host.api.supported(None, "UID"): diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 5915bd06608..6b57c1c253f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -39,7 +39,7 @@ from homeassistant.helpers import ( entity_registry as er, issue_registry as ir, ) -from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.setup import async_setup_component from .conftest import ( @@ -51,6 +51,7 @@ from .conftest import ( TEST_HOST, TEST_HOST_MODEL, TEST_MAC, + TEST_MAC_CAM, TEST_NVR_NAME, TEST_PORT, TEST_PRIVACY, @@ -614,6 +615,55 @@ async def test_migrate_with_already_existing_entity( assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) +async def test_cleanup_mac_connection( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the MAC of a IPC which was set to the MAC of the host.""" + reolink_connect.channels = [0] + reolink_connect.baichuan.mac_address.return_value = None + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + dev_entry = device_registry.async_get_or_create( + identifiers={(DOMAIN, dev_id)}, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=entity_id, + config_entry=config_entry, + suggested_object_id=entity_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.connections == {(CONNECTION_NETWORK_MAC, TEST_MAC)} + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.connections == set() + + reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From 93fd82d1fa95214e342066d8031ced35f26e76b9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 14:50:00 +0200 Subject: [PATCH 0293/1175] Prevent errors during cleaning of connections/identifiers in device registry (#144558) --- homeassistant/helpers/device_registry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 79d6774c407..a80e74e7eb2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -575,9 +575,11 @@ class DeviceRegistryItems[_EntryTypeT: (DeviceEntry, DeletedDeviceEntry)]( """Unindex an entry.""" old_entry = self.data[key] for connection in old_entry.connections: - del self._connections[connection] + if connection in self._connections: + del self._connections[connection] for identifier in old_entry.identifiers: - del self._identifiers[identifier] + if identifier in self._identifiers: + del self._identifiers[identifier] def get_entry( self, From 9e3684b00126c1c1093eb535bfb450684352334a Mon Sep 17 00:00:00 2001 From: agorecki Date: Fri, 9 May 2025 09:11:22 -0400 Subject: [PATCH 0294/1175] Add Lux sensor to Airthings Cloud (#141035) * change light to lux for airthings cloud * Add back light sensor for airthings * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/airthings/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index a0d9c97c8c8..f2bf8e071f7 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory, @@ -78,6 +79,12 @@ SENSORS: dict[str, SensorEntityDescription] = { translation_key="light", state_class=SensorStateClass.MEASUREMENT, ), + "lux": SensorEntityDescription( + key="lux", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), "virusRisk": SensorEntityDescription( key="virusRisk", translation_key="virus_risk", From 763f2bcfcc697a59802c873078cd3750141013e1 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 9 May 2025 15:15:09 +0200 Subject: [PATCH 0295/1175] Remove deprecated address argument in all lcn services (#144557) --- homeassistant/components/lcn/helpers.py | 20 ---- homeassistant/components/lcn/services.py | 57 ++--------- homeassistant/components/lcn/strings.json | 7 -- tests/components/lcn/test_services.py | 119 +++++----------------- 4 files changed, 36 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index a2796f88368..1bc4c6caa41 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -283,26 +283,6 @@ def get_device_config( return None -def is_address(value: str) -> tuple[AddressType, str]: - """Validate the given address string. - - Examples for S000M005 at myhome: - myhome.s000.m005 - myhome.s0.m5 - myhome.0.5 ("m" is implicit if missing) - - Examples for s000g011 - myhome.0.g11 - myhome.s0.g11 - """ - if matcher := PATTERN_ADDRESS.match(value): - is_group = matcher.group("type") == "g" - addr = (int(matcher.group("seg_id")), int(matcher.group("id")), is_group) - conn_id = matcher.group("conn_id") - return addr, conn_id - raise ValueError(f"{value} is not a valid address string") - - def is_states_string(states_string: str) -> list[str]: """Validate the given states string and return states list.""" if len(states_string) != 8: diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 2694bed31d2..fdc5359d300 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -6,10 +6,8 @@ import pypck import voluptuous as vol from homeassistant.const import ( - CONF_ADDRESS, CONF_BRIGHTNESS, CONF_DEVICE_ID, - CONF_HOST, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) @@ -21,7 +19,6 @@ from homeassistant.core import ( ) from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( CONF_KEYS, @@ -51,12 +48,7 @@ from .const import ( VAR_UNITS, VARIABLES, ) -from .helpers import ( - DeviceConnectionType, - get_device_connection, - is_address, - is_states_string, -) +from .helpers import DeviceConnectionType, is_states_string class LcnServiceCall: @@ -64,8 +56,7 @@ class LcnServiceCall: schema = vol.Schema( { - vol.Optional(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_ADDRESS): is_address, + vol.Required(CONF_DEVICE_ID): cv.string, } ) supports_response = SupportsResponse.NONE @@ -76,46 +67,18 @@ class LcnServiceCall: def get_device_connection(self, service: ServiceCall) -> DeviceConnectionType: """Get address connection object.""" - if CONF_DEVICE_ID not in service.data and CONF_ADDRESS not in service.data: + device_id = service.data[CONF_DEVICE_ID] + device_registry = dr.async_get(self.hass) + if not (device := device_registry.async_get(device_id)): raise ServiceValidationError( translation_domain=DOMAIN, - translation_key="no_device_identifier", + translation_key="invalid_device_id", + translation_placeholders={"device_id": device_id}, ) - if CONF_DEVICE_ID in service.data: - device_id = service.data[CONF_DEVICE_ID] - device_registry = dr.async_get(self.hass) - if not (device := device_registry.async_get(device_id)): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="invalid_device_id", - translation_placeholders={"device_id": device_id}, - ) - - return self.hass.data[DOMAIN][device.primary_config_entry][ - DEVICE_CONNECTIONS - ][device_id] - - async_create_issue( - self.hass, - DOMAIN, - "deprecated_address_parameter", - breaks_in_ha_version="2025.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_address_parameter", - ) - - address, host_name = service.data[CONF_ADDRESS] - for config_entry in self.hass.config_entries.async_entries(DOMAIN): - if config_entry.data[CONF_HOST] == host_name: - device_connection = get_device_connection( - self.hass, address, config_entry - ) - if device_connection is None: - raise ValueError("Wrong address.") - return device_connection - raise ValueError("Invalid host name.") + return self.hass.data[DOMAIN][device.primary_config_entry][DEVICE_CONNECTIONS][ + device_id + ] async def async_call_service(self, service: ServiceCall) -> ServiceResponse: """Execute service call.""" diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 0a8112d997a..4295ceb384d 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -81,10 +81,6 @@ "deprecated_keylock_sensor": { "title": "Deprecated LCN key lock binary sensor", "description": "Your LCN key lock binary sensor entity `{entity}` is being used in automations or scripts. A key lock switch entity is available and should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue." - }, - "deprecated_address_parameter": { - "title": "Deprecated 'address' parameter", - "description": "The 'address' parameter in the LCN action calls is deprecated. The 'device ID' parameter should be used going forward.\n\nPlease adjust your automations or scripts to fix this issue." } }, "services": { @@ -418,9 +414,6 @@ } }, "exceptions": { - "no_device_identifier": { - "message": "No device identifier provided. Please provide the device ID." - }, "invalid_address": { "message": "LCN device for given address has not been configured." }, diff --git a/tests/components/lcn/test_services.py b/tests/components/lcn/test_services.py index c9eda40fdba..cdc8e9671c0 100644 --- a/tests/components/lcn/test_services.py +++ b/tests/components/lcn/test_services.py @@ -24,14 +24,12 @@ from homeassistant.components.lcn.const import ( ) from homeassistant.components.lcn.services import LcnService from homeassistant.const import ( - CONF_ADDRESS, CONF_BRIGHTNESS, CONF_DEVICE_ID, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from .conftest import ( @@ -42,20 +40,9 @@ from .conftest import ( ) -def device_config( - hass: HomeAssistant, entry: MockConfigEntry, config_type: str -) -> dict[str, str]: - """Return test device config depending on type.""" - if config_type == CONF_ADDRESS: - return {CONF_ADDRESS: "pchk.s0.m7"} - return {CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id} - - -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_output_abs( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test output_abs service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -66,7 +53,7 @@ async def test_service_output_abs( DOMAIN, LcnService.OUTPUT_ABS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 100, CONF_TRANSITION: 5, @@ -77,11 +64,9 @@ async def test_service_output_abs( dim_output.assert_awaited_with(0, 100, 9) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_output_rel( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test output_rel service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -92,7 +77,7 @@ async def test_service_output_rel( DOMAIN, LcnService.OUTPUT_REL, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_OUTPUT: "output1", CONF_BRIGHTNESS: 25, }, @@ -102,11 +87,9 @@ async def test_service_output_rel( rel_output.assert_awaited_with(0, 25) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_output_toggle( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test output_toggle service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -117,7 +100,7 @@ async def test_service_output_toggle( DOMAIN, LcnService.OUTPUT_TOGGLE, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_OUTPUT: "output1", CONF_TRANSITION: 5, }, @@ -127,11 +110,9 @@ async def test_service_output_toggle( toggle_output.assert_awaited_with(0, 9) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_relays( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test relays service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -141,7 +122,10 @@ async def test_service_relays( await hass.services.async_call( DOMAIN, LcnService.RELAYS, - {**device_config(hass, entry, config_type), CONF_STATE: "0011TT--"}, + { + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, + CONF_STATE: "0011TT--", + }, blocking=True, ) @@ -151,11 +135,9 @@ async def test_service_relays( control_relays.assert_awaited_with(relay_states) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_led( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test led service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -166,7 +148,7 @@ async def test_service_led( DOMAIN, LcnService.LED, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_LED: "led6", CONF_STATE: "blink", }, @@ -179,11 +161,9 @@ async def test_service_led( control_led.assert_awaited_with(led, led_state) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_var_abs( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test var_abs service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -194,7 +174,7 @@ async def test_service_var_abs( DOMAIN, LcnService.VAR_ABS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_VARIABLE: "var1", CONF_VALUE: 75, CONF_UNIT_OF_MEASUREMENT: "%", @@ -207,11 +187,9 @@ async def test_service_var_abs( ) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_var_rel( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test var_rel service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -222,7 +200,7 @@ async def test_service_var_rel( DOMAIN, LcnService.VAR_REL, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_VARIABLE: "var1", CONF_VALUE: 10, CONF_UNIT_OF_MEASUREMENT: "%", @@ -239,11 +217,9 @@ async def test_service_var_rel( ) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_var_reset( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test var_reset service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -253,18 +229,19 @@ async def test_service_var_reset( await hass.services.async_call( DOMAIN, LcnService.VAR_RESET, - {**device_config(hass, entry, config_type), CONF_VARIABLE: "var1"}, + { + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, + CONF_VARIABLE: "var1", + }, blocking=True, ) var_reset.assert_awaited_with(pypck.lcn_defs.Var["VAR1"]) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_regulator( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test lock_regulator service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -275,7 +252,7 @@ async def test_service_lock_regulator( DOMAIN, LcnService.LOCK_REGULATOR, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_SETPOINT: "r1varsetpoint", CONF_STATE: True, }, @@ -285,11 +262,9 @@ async def test_service_lock_regulator( lock_regulator.assert_awaited_with(0, True) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_send_keys( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test send_keys service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -300,7 +275,7 @@ async def test_service_send_keys( DOMAIN, LcnService.SEND_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_KEYS: "a1a5d8", CONF_STATE: "hit", }, @@ -315,11 +290,9 @@ async def test_service_send_keys( send_keys.assert_awaited_with(keys, pypck.lcn_defs.SendKeyCommand["HIT"]) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_send_keys_hit_deferred( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test send_keys (hit_deferred) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -338,7 +311,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_KEYS: "a1a5d8", CONF_TIME: 5, CONF_TIME_UNIT: "s", @@ -361,7 +334,7 @@ async def test_service_send_keys_hit_deferred( DOMAIN, LcnService.SEND_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_KEYS: "a1a5d8", CONF_STATE: "make", CONF_TIME: 5, @@ -371,11 +344,9 @@ async def test_service_send_keys_hit_deferred( ) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_keys( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test lock_keys service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -386,7 +357,7 @@ async def test_service_lock_keys( DOMAIN, LcnService.LOCK_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_TABLE: "a", CONF_STATE: "0011TT--", }, @@ -399,11 +370,9 @@ async def test_service_lock_keys( lock_keys.assert_awaited_with(0, lock_states) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_lock_keys_tab_a_temporary( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test lock_keys (tab_a_temporary) service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -417,7 +386,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_STATE: "0011TT--", CONF_TIME: 10, CONF_TIME_UNIT: "s", @@ -443,7 +412,7 @@ async def test_service_lock_keys_tab_a_temporary( DOMAIN, LcnService.LOCK_KEYS, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_TABLE: "b", CONF_STATE: "0011TT--", CONF_TIME: 10, @@ -453,11 +422,9 @@ async def test_service_lock_keys_tab_a_temporary( ) -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_dyn_text( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test dyn_text service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -468,7 +435,7 @@ async def test_service_dyn_text( DOMAIN, LcnService.DYN_TEXT, { - **device_config(hass, entry, config_type), + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, CONF_ROW: 1, CONF_TEXT: "text in row 1", }, @@ -478,11 +445,9 @@ async def test_service_dyn_text( dyn_text.assert_awaited_with(0, "text in row 1") -@pytest.mark.parametrize("config_type", [CONF_ADDRESS, CONF_DEVICE_ID]) async def test_service_pck( hass: HomeAssistant, entry: MockConfigEntry, - config_type: str, ) -> None: """Test pck service.""" await async_setup_component(hass, "persistent_notification", {}) @@ -492,43 +457,11 @@ async def test_service_pck( await hass.services.async_call( DOMAIN, LcnService.PCK, - {**device_config(hass, entry, config_type), CONF_PCK: "PIN4"}, + { + CONF_DEVICE_ID: get_device(hass, entry, (0, 7, False)).id, + CONF_PCK: "PIN4", + }, blocking=True, ) pck.assert_awaited_with("PIN4") - - -async def test_service_called_with_invalid_host_id( - hass: HomeAssistant, entry: MockConfigEntry -) -> None: - """Test service was called with non existing host id.""" - await async_setup_component(hass, "persistent_notification", {}) - await init_integration(hass, entry) - - with patch.object(MockModuleConnection, "pck") as pck, pytest.raises(ValueError): - await hass.services.async_call( - DOMAIN, - LcnService.PCK, - {CONF_ADDRESS: "foobar.s0.m7", CONF_PCK: "PIN4"}, - blocking=True, - ) - - pck.assert_not_awaited() - - -async def test_service_with_deprecated_address_parameter( - hass: HomeAssistant, entry: MockConfigEntry, issue_registry: ir.IssueRegistry -) -> None: - """Test service puts issue in registry if called with address parameter.""" - await async_setup_component(hass, "persistent_notification", {}) - await init_integration(hass, entry) - - await hass.services.async_call( - DOMAIN, - LcnService.PCK, - {CONF_ADDRESS: "pchk.s0.m7", CONF_PCK: "PIN4"}, - blocking=True, - ) - - assert issue_registry.async_get_issue(DOMAIN, "deprecated_address_parameter") From ed6cfa42f041dbe1bff4804215968527b4cbf2ad Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 9 May 2025 15:33:13 +0200 Subject: [PATCH 0296/1175] Make all devolo Home Network conflig flow tests end correctly (#144378) --- .../devolo_home_network/test_config_flow.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 923b7298893..16d3e6a8b9e 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -77,14 +77,30 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error) - ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_IP_ADDRESS: IP, - }, + {CONF_IP_ADDRESS: IP, CONF_PASSWORD: ""}, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} + with ( + patch( + "homeassistant.components.devolo_home_network.async_setup_entry", + return_value=True, + ), + patch( + "homeassistant.components.devolo_home_network.config_flow.Device", + new=MockDevice, + ), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_IP_ADDRESS: IP, CONF_PASSWORD: ""}, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + async def test_zeroconf(hass: HomeAssistant) -> None: """Test that the zeroconf form is served.""" From bd284528072ff22a0751014f4ffa7527128188b1 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Fri, 9 May 2025 14:35:10 +0100 Subject: [PATCH 0297/1175] Add Squeezebox service update entities (#125764) * first cut at update entties * remove sensors for now * make update vserion less wordy * fix re escape * Use name * use Caps * fix translation * move all data manipulation to data prepare fn, refine regexes and provide as much info as possible * fix formatting * update return type * fix class inherit * Fix ruff * update tests * fix spelling * ruff * Update homeassistant/components/squeezebox/update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * Update tests/components/squeezebox/test_update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * Update tests/components/squeezebox/test_update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * Update tests/components/squeezebox/test_update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * Update tests/components/squeezebox/test_update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * Update tests/components/squeezebox/test_update.py Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> * fix tests * ruff * update text based on feedback from docs * make the plugin update entity smarter * update plugin updater tests * define attr * Callable type * callable guard * ruff * add local release info page * fix typing * refactor use release notes for LMS update * Make update simple and produce a release summary instead * Update tests * Fix tests * Tighten english * test for restart fail * be more explicit with coordinator error * remove unused regex * revert error msg unrealted * Fix newline * Fix socket usage during tests * Simplify based on new lib version * CI Fixes * fix typing * fix enitiy call back * fix enitiy call back types * remove some unrelated titdying --------- Co-authored-by: Raj Laud <50647620+rajlaud@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .../components/squeezebox/__init__.py | 1 + homeassistant/components/squeezebox/const.py | 6 +- .../components/squeezebox/coordinator.py | 38 +-- .../components/squeezebox/strings.json | 8 + homeassistant/components/squeezebox/update.py | 170 +++++++++++++ tests/components/squeezebox/conftest.py | 9 +- tests/components/squeezebox/test_update.py | 232 ++++++++++++++++++ 7 files changed, 434 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/squeezebox/update.py create mode 100644 tests/components/squeezebox/test_update.py diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 78a97e38833..d29e7287340 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -56,6 +56,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, + Platform.UPDATE, ] diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 5ce95d25632..3f355951acf 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -13,8 +13,6 @@ SERVER_MODEL = "Lyrion Music Server" STATUS_API_TIMEOUT = 10 STATUS_SENSOR_LASTSCAN = "lastscan" STATUS_SENSOR_NEEDSRESTART = "needsrestart" -STATUS_SENSOR_NEWVERSION = "newversion" -STATUS_SENSOR_NEWPLUGINS = "newplugins" STATUS_SENSOR_RESCAN = "rescan" STATUS_SENSOR_INFO_TOTAL_ALBUMS = "info total albums" STATUS_SENSOR_INFO_TOTAL_ARTISTS = "info total artists" @@ -27,6 +25,8 @@ STATUS_QUERY_LIBRARYNAME = "libraryname" STATUS_QUERY_MAC = "mac" STATUS_QUERY_UUID = "uuid" STATUS_QUERY_VERSION = "version" +STATUS_UPDATE_NEWVERSION = "newversion" +STATUS_UPDATE_NEWPLUGINS = "newplugins" SQUEEZEBOX_SOURCE_STRINGS = ( "source:", "wavin:", @@ -44,3 +44,5 @@ DEFAULT_VOLUME_STEP = 5 ATTR_ANNOUNCE_VOLUME = "announce_volume" ATTR_ANNOUNCE_TIMEOUT = "announce_timeout" UNPLAYABLE_TYPES = ("text", "actions") +UPDATE_PLUGINS_RELEASE_SUMMARY = "update_plugins_release_summary" +UPDATE_RELEASE_SUMMARY = "update_release_summary" diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py index 955e2896947..e5d78024ef0 100644 --- a/homeassistant/components/squeezebox/coordinator.py +++ b/homeassistant/components/squeezebox/coordinator.py @@ -6,7 +6,6 @@ from asyncio import timeout from collections.abc import Callable from datetime import timedelta import logging -import re from typing import TYPE_CHECKING, Any from pysqueezebox import Player, Server @@ -14,7 +13,6 @@ from pysqueezebox import Player, Server from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util if TYPE_CHECKING: from . import SqueezeboxConfigEntry @@ -24,9 +22,6 @@ from .const import ( SENSOR_UPDATE_INTERVAL, SIGNAL_PLAYER_REDISCOVERED, STATUS_API_TIMEOUT, - STATUS_SENSOR_LASTSCAN, - STATUS_SENSOR_NEEDSRESTART, - STATUS_SENSOR_RESCAN, ) _LOGGER = logging.getLogger(__name__) @@ -50,7 +45,16 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator): always_update=False, ) self.lms = lms - self.newversion_regex = re.compile("<.*$") + self.can_server_restart = False + + async def _async_setup(self) -> None: + """Query LMS capabilities.""" + result = await self.lms.async_query("can", "restartserver", "?") + if result and "_can" in result and result["_can"] == 1: + _LOGGER.debug("Can restart %s", self.lms.name) + self.can_server_restart = True + else: + _LOGGER.warning("Can't query server capabilities %s", self.lms.name) async def _async_update_data(self) -> dict: """Fetch data from LMS status call. @@ -58,32 +62,12 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator): Then we process only a subset to make then nice for HA """ async with timeout(STATUS_API_TIMEOUT): - data = await self.lms.async_status() + data: dict | None = await self.lms.async_prepared_status() if not data: raise UpdateFailed("No data from status poll") _LOGGER.debug("Raw serverstatus %s=%s", self.lms.name, data) - return self._prepare_status_data(data) - - def _prepare_status_data(self, data: dict) -> dict: - """Sensors that need the data changing for HA presentation.""" - - # Binary sensors - # rescan bool are we rescanning alter poll not present if false - data[STATUS_SENSOR_RESCAN] = STATUS_SENSOR_RESCAN in data - # needsrestart bool pending lms plugin updates not present if false - data[STATUS_SENSOR_NEEDSRESTART] = STATUS_SENSOR_NEEDSRESTART in data - - # Sensors that need special handling - # 'lastscan': '1718431678', epoc -> ISO 8601 not always present - data[STATUS_SENSOR_LASTSCAN] = ( - dt_util.utc_from_timestamp(int(data[STATUS_SENSOR_LASTSCAN])) - if STATUS_SENSOR_LASTSCAN in data - else None - ) - - _LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data) return data diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 83c5d7dd5d0..9109a378ea8 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -125,6 +125,14 @@ "name": "Player count off service", "unit_of_measurement": "[%key:component::squeezebox::entity::sensor::player_count::unit_of_measurement%]" } + }, + "update": { + "newversion": { + "name": "Lyrion Music Server" + }, + "newplugins": { + "name": "Updated plugins" + } } }, "options": { diff --git a/homeassistant/components/squeezebox/update.py b/homeassistant/components/squeezebox/update.py new file mode 100644 index 00000000000..c37594d346d --- /dev/null +++ b/homeassistant/components/squeezebox/update.py @@ -0,0 +1,170 @@ +"""Platform for update integration for squeezebox.""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +import logging +from typing import Any + +from homeassistant.components.update import ( + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from . import SqueezeboxConfigEntry +from .const import ( + SERVER_MODEL, + STATUS_QUERY_VERSION, + STATUS_UPDATE_NEWPLUGINS, + STATUS_UPDATE_NEWVERSION, + UPDATE_PLUGINS_RELEASE_SUMMARY, + UPDATE_RELEASE_SUMMARY, +) +from .entity import LMSStatusEntity + +newserver = UpdateEntityDescription( + key=STATUS_UPDATE_NEWVERSION, +) + +newplugins = UpdateEntityDescription( + key=STATUS_UPDATE_NEWPLUGINS, +) + +POLL_AFTER_INSTALL = 120 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SqueezeboxConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Platform setup using common elements.""" + + async_add_entities( + [ + ServerStatusUpdateLMS(entry.runtime_data.coordinator, newserver), + ServerStatusUpdatePlugins(entry.runtime_data.coordinator, newplugins), + ] + ) + + +class ServerStatusUpdate(LMSStatusEntity, UpdateEntity): + """LMS Status update sensors via cooridnatior.""" + + @property + def latest_version(self) -> str: + """LMS Status directly from coordinator data.""" + return str(self.coordinator.data[self.entity_description.key]) + + +class ServerStatusUpdateLMS(ServerStatusUpdate): + """LMS Status update sensor from LMS via cooridnatior.""" + + title: str = SERVER_MODEL + + @property + def installed_version(self) -> str: + """LMS Status directly from coordinator data.""" + return str(self.coordinator.data[STATUS_QUERY_VERSION]) + + @property + def release_url(self) -> str: + """LMS Update info page.""" + return str(self.coordinator.lms.generate_image_url("updateinfo.html")) + + @property + def release_summary(self) -> None | str: + """If install is supported give some info.""" + return ( + str(self.coordinator.data[UPDATE_RELEASE_SUMMARY]) + if self.coordinator.data[UPDATE_RELEASE_SUMMARY] + else None + ) + + +class ServerStatusUpdatePlugins(ServerStatusUpdate): + """LMS Plugings update sensor from LMS via cooridnatior.""" + + auto_update = True + title: str = SERVER_MODEL + " Plugins" + installed_version = "Current" + restart_triggered = False + _cancel_update: Callable | None = None + + @property + def supported_features(self) -> UpdateEntityFeature: + """Support install if we can.""" + return ( + (UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS) + if self.coordinator.can_server_restart + else UpdateEntityFeature(0) + ) + + @property + def release_summary(self) -> None | str: + """If install is supported give some info.""" + rs = self.coordinator.data[UPDATE_PLUGINS_RELEASE_SUMMARY] + return ( + (rs or "") + + "The Plugins will be updated on the next restart triggred by selecting the Install button. Allow enough time for the service to restart. It will become briefly unavailable." + if self.coordinator.can_server_restart + else rs + ) + + @property + def release_url(self) -> str: + """LMS Plugins info page.""" + return str( + self.coordinator.lms.generate_image_url( + "/settings/index.html?activePage=SETUP_PLUGINS" + ) + ) + + @property + def in_progress(self) -> bool: + """Are we restarting.""" + if self.latest_version == self.installed_version and self.restart_triggered: + _LOGGER.debug("plugin progress reset %s", self.coordinator.lms.name) + if callable(self._cancel_update): + self._cancel_update() + self.restart_triggered = False + return self.restart_triggered + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install all plugin updates.""" + _LOGGER.debug( + "server restart for plugin install on %s", self.coordinator.lms.name + ) + self.restart_triggered = True + self.async_write_ha_state() + + result = await self.coordinator.lms.async_query("restartserver") + _LOGGER.debug("restart server result %s", result) + if not result: + self._cancel_update = async_call_later( + self.hass, POLL_AFTER_INSTALL, self._async_update_catchall + ) + else: + self.restart_triggered = False + self.async_write_ha_state() + raise HomeAssistantError( + "Error trying to update LMS Plugins: Restart failed" + ) + + async def _async_update_catchall(self, now: datetime | None = None) -> None: + """Request update. clear restart catchall.""" + if self.restart_triggered: + _LOGGER.debug("server restart catchall for %s", self.coordinator.lms.name) + self.restart_triggered = False + self.async_write_ha_state() + await self.async_update() diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 769e611bf28..fb2428ba758 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -25,6 +25,8 @@ from homeassistant.components.squeezebox.const import ( STATUS_SENSOR_OTHER_PLAYER_COUNT, STATUS_SENSOR_PLAYER_COUNT, STATUS_SENSOR_RESCAN, + STATUS_UPDATE_NEWPLUGINS, + STATUS_UPDATE_NEWVERSION, ) from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant @@ -69,6 +71,9 @@ FAKE_QUERY_RESPONSE = { STATUS_SENSOR_INFO_TOTAL_SONGS: 42, STATUS_SENSOR_PLAYER_COUNT: 10, STATUS_SENSOR_OTHER_PLAYER_COUNT: 0, + STATUS_UPDATE_NEWVERSION: 'A new version of Logitech Media Server is available (8.5.2 - 0). Click here for further information.', + STATUS_UPDATE_NEWPLUGINS: "Plugins have been updated - Restart Required (Big Sounds)", + "_can": 1, "players_loop": [ { "isplaying": 0, @@ -299,7 +304,9 @@ def mock_pysqueezebox_server( mock_lms.uuid = uuid mock_lms.name = TEST_SERVER_NAME mock_lms.async_query = AsyncMock(return_value={"uuid": format_mac(uuid)}) - mock_lms.async_status = AsyncMock(return_value={"uuid": format_mac(uuid)}) + mock_lms.async_status = AsyncMock( + return_value={"uuid": format_mac(uuid), "version": FAKE_VERSION} + ) return mock_lms diff --git a/tests/components/squeezebox/test_update.py b/tests/components/squeezebox/test_update.py new file mode 100644 index 00000000000..b233afbcde1 --- /dev/null +++ b/tests/components/squeezebox/test_update.py @@ -0,0 +1,232 @@ +"""Test squeezebox update platform.""" + +import copy +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.squeezebox.const import ( + SENSOR_UPDATE_INTERVAL, + STATUS_UPDATE_NEWPLUGINS, +) +from homeassistant.components.update import ( + ATTR_IN_PROGRESS, + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt as dt_util + +from .conftest import FAKE_QUERY_RESPONSE + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_update_lms( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test binary sensor states and attributes.""" + + # Setup component + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.UPDATE], + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("update.fakelib_lyrion_music_server") + + assert state is not None + assert state.state == STATE_ON + + +async def test_update_plugins_install_fallback( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test binary sensor states and attributes.""" + + entity_id = "update.fakelib_updated_plugins" + # Setup component + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.UPDATE], + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + polltime = 30 + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=False, + ), + patch( + "homeassistant.components.squeezebox.update.POLL_AFTER_INSTALL", + polltime, + ), + ): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] + + with ( + patch( + "homeassistant.components.squeezebox.Server.async_status", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=polltime + 1), + ) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + attrs = state.attributes + assert not attrs[ATTR_IN_PROGRESS] + + +async def test_update_plugins_install_restart_fail( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test binary sensor states and attributes.""" + + entity_id = "update.fakelib_updated_plugins" + # Setup component + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.UPDATE], + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=True, + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + attrs = state.attributes + assert not attrs[ATTR_IN_PROGRESS] + + +async def test_update_plugins_install_ok( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test binary sensor states and attributes.""" + + entity_id = "update.fakelib_updated_plugins" + # Setup component + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.UPDATE], + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=False, + ), + ): + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + attrs = state.attributes + assert attrs[ATTR_IN_PROGRESS] + + resp = copy.deepcopy(FAKE_QUERY_RESPONSE) + del resp[STATUS_UPDATE_NEWPLUGINS] + + with ( + patch( + "homeassistant.components.squeezebox.Server.async_status", + return_value=resp, + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), + ), + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=SENSOR_UPDATE_INTERVAL + 1), + ) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + attrs = state.attributes + assert not attrs[ATTR_IN_PROGRESS] From 75b8cb19cf8deb397d2b6cc5cfbd534080ff85c9 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 9 May 2025 15:37:23 +0200 Subject: [PATCH 0298/1175] Deprecate Homee valve sensor (#139578) * remove valve sensor * deprecate valve sensor * fix * Add deprecation issue test * Add test for deleting disabled deprecated entities * parametrize issue test * eliminate one if iteration * review change 1 * review change 2 * add info where to find valve * Update homeassistant/components/homee/sensor.py --------- Co-authored-by: Robert Resch Co-authored-by: Joost Lekkerkerker --- homeassistant/components/homee/sensor.py | 67 +++++++++++-- homeassistant/components/homee/strings.json | 6 ++ .../homee/snapshots/test_sensor.ambr | 51 ---------- tests/components/homee/test_sensor.py | 95 +++++++++++++++++-- 4 files changed, 155 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/homee/sensor.py b/homeassistant/components/homee/sensor.py index e65b73b4a67..ab1d5bd4f49 100644 --- a/homeassistant/components/homee/sensor.py +++ b/homeassistant/components/homee/sensor.py @@ -6,7 +6,10 @@ from dataclasses import dataclass from pyHomee.const import AttributeType, NodeState from pyHomee.model import HomeeAttribute, HomeeNode +from homeassistant.components.automation import automations_with_entity +from homeassistant.components.script import scripts_with_entity from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -14,10 +17,17 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from . import HomeeConfigEntry from .const import ( + DOMAIN, HOMEE_UNIT_TO_HA_UNIT, OPEN_CLOSE_MAP, OPEN_CLOSE_MAP_REVERSED, @@ -274,14 +284,55 @@ NODE_SENSOR_DESCRIPTIONS: tuple[HomeeNodeSensorEntityDescription, ...] = ( ) +def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]: + """Get list of related automations and scripts.""" + used_in = automations_with_entity(hass, entity_id) + used_in += scripts_with_entity(hass, entity_id) + return used_in + + async def async_setup_entry( hass: HomeAssistant, config_entry: HomeeConfigEntry, async_add_devices: AddConfigEntryEntitiesCallback, ) -> None: """Add the homee platform for the sensor components.""" - + ent_reg = er.async_get(hass) devices: list[HomeeSensor | HomeeNodeSensor] = [] + + def add_deprecated_entity( + attribute: HomeeAttribute, description: HomeeSensorEntityDescription + ) -> None: + """Add deprecated entities.""" + entity_uid = f"{config_entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}" + if entity_id := ent_reg.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, entity_uid): + entity_entry = ent_reg.async_get(entity_id) + if entity_entry and entity_entry.disabled: + ent_reg.async_remove(entity_id) + async_delete_issue( + hass, + DOMAIN, + f"deprecated_entity_{entity_uid}", + ) + elif entity_entry: + devices.append(HomeeSensor(attribute, config_entry, description)) + if entity_used_in(hass, entity_id): + async_create_issue( + hass, + DOMAIN, + f"deprecated_entity_{entity_uid}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders={ + "name": str( + entity_entry.name or entity_entry.original_name + ), + "entity": entity_id, + }, + ) + for node in config_entry.runtime_data.nodes: # Node properties that are sensors. devices.extend( @@ -290,11 +341,15 @@ async def async_setup_entry( ) # Node attributes that are sensors. - devices.extend( - HomeeSensor(attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type]) - for attribute in node.attributes - if attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable - ) + for attribute in node.attributes: + if attribute.type == AttributeType.CURRENT_VALVE_POSITION: + add_deprecated_entity(attribute, SENSOR_DESCRIPTIONS[attribute.type]) + elif attribute.type in SENSOR_DESCRIPTIONS and not attribute.editable: + devices.append( + HomeeSensor( + attribute, config_entry, SENSOR_DESCRIPTIONS[attribute.type] + ) + ) if devices: async_add_devices(devices) diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index f8d83a3073e..d0ea91b4225 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -357,5 +357,11 @@ "connection_closed": { "message": "Could not connect to homee while setting attribute." } + }, + "issues": { + "deprecated_entity": { + "title": "The Homee {name} entity is deprecated", + "description": "The Homee entity `{entity}` is deprecated and will be removed in release 2025.12.\nThe valve is available directly in the respective climate entity.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue." + } } } diff --git a/tests/components/homee/snapshots/test_sensor.ambr b/tests/components/homee/snapshots/test_sensor.ambr index b35943630d5..ff04f245504 100644 --- a/tests/components/homee/snapshots/test_sensor.ambr +++ b/tests/components/homee/snapshots/test_sensor.ambr @@ -1591,57 +1591,6 @@ 'state': '6.0', }) # --- -# name: test_sensor_snapshot[sensor.test_multisensor_valve_position-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.test_multisensor_valve_position', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Valve position', - 'platform': 'homee', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'valve_position', - 'unique_id': '00055511EECC-1-9', - 'unit_of_measurement': '%', - }) -# --- -# name: test_sensor_snapshot[sensor.test_multisensor_valve_position-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test MultiSensor Valve position', - 'state_class': , - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.test_multisensor_valve_position', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '70.0', - }) -# --- # name: test_sensor_snapshot[sensor.test_multisensor_voltage_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/homee/test_sensor.py b/tests/components/homee/test_sensor.py index bbdad4c4469..14a9320ffa1 100644 --- a/tests/components/homee/test_sensor.py +++ b/tests/components/homee/test_sensor.py @@ -6,16 +6,19 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.homee.const import ( + DOMAIN, OPEN_CLOSE_MAP, OPEN_CLOSE_MAP_REVERSED, WINDOW_MAP, WINDOW_MAP_REVERSED, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import entity_registry as er, issue_registry as ir from . import async_update_attribute_value, build_mock_node, setup_integration +from .conftest import HOMEE_ID from tests.common import MockConfigEntry, snapshot_platform @@ -25,15 +28,22 @@ def enable_all_entities(entity_registry_enabled_by_default: None) -> None: """Make sure all entities are enabled.""" +async def setup_sensor( + hass: HomeAssistant, mock_homee: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Setups the integration for sensor tests.""" + mock_homee.nodes = [build_mock_node("sensors.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + await setup_integration(hass, mock_config_entry) + + async def test_up_down_values( hass: HomeAssistant, mock_homee: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test values for up/down sensor.""" - mock_homee.nodes = [build_mock_node("sensors.json")] - mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] - await setup_integration(hass, mock_config_entry) + await setup_sensor(hass, mock_homee, mock_config_entry) assert hass.states.get("sensor.test_multisensor_state").state == OPEN_CLOSE_MAP[0] @@ -60,9 +70,7 @@ async def test_window_position( mock_config_entry: MockConfigEntry, ) -> None: """Test values for window handle position.""" - mock_homee.nodes = [build_mock_node("sensors.json")] - mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] - await setup_integration(hass, mock_config_entry) + await setup_sensor(hass, mock_homee, mock_config_entry) assert ( hass.states.get("sensor.test_multisensor_window_position").state @@ -87,6 +95,79 @@ async def test_window_position( ) +@pytest.mark.parametrize( + ("disabler", "expected_entity", "expected_issue"), + [ + (None, False, False), + (er.RegistryEntryDisabler.USER, True, True), + ], +) +async def test_sensor_deprecation( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, + entity_registry: er.EntityRegistry, + disabler: er.RegistryEntryDisabler, + expected_entity: bool, + expected_issue: bool, +) -> None: + """Test sensor deprecation issue.""" + entity_uid = f"{HOMEE_ID}-1-9" + entity_id = "test_multisensor_valve_position" + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + entity_uid, + suggested_object_id=entity_id, + disabled_by=disabler, + ) + + with patch( + "homeassistant.components.homee.sensor.entity_used_in", return_value=True + ): + await setup_sensor(hass, mock_homee, mock_config_entry) + + assert (entity_registry.async_get(f"sensor.{entity_id}") is None) is expected_entity + assert ( + issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=f"deprecated_entity_{entity_uid}", + ) + is None + ) is expected_issue + + +async def test_sensor_deprecation_unused_entity( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + issue_registry: ir.IssueRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor deprecation issue.""" + entity_uid = f"{HOMEE_ID}-1-9" + entity_id = "test_multisensor_valve_position" + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + entity_uid, + suggested_object_id=entity_id, + disabled_by=None, + ) + + await setup_sensor(hass, mock_homee, mock_config_entry) + + assert entity_registry.async_get(f"sensor.{entity_id}") is not None + assert ( + issue_registry.async_get_issue( + domain=DOMAIN, + issue_id=f"deprecated_entity_{entity_uid}", + ) + is None + ) + + async def test_sensor_snapshot( hass: HomeAssistant, mock_homee: MagicMock, From 7dad6ebe676e7e199a02344711a0f7640ad7a172 Mon Sep 17 00:00:00 2001 From: Renier Moorcroft <66512715+RenierM26@users.noreply.github.com> Date: Fri, 9 May 2025 15:45:18 +0200 Subject: [PATCH 0299/1175] Switch to PyEzvizApi (#135926) * Update library * update library * Bump api to pin mqtt to compatable version * fix after rebase * Update code owners * codeowners --- CODEOWNERS | 4 ++-- homeassistant/components/ezviz/__init__.py | 4 ++-- homeassistant/components/ezviz/alarm_control_panel.py | 4 ++-- homeassistant/components/ezviz/button.py | 6 +++--- homeassistant/components/ezviz/camera.py | 2 +- homeassistant/components/ezviz/config_flow.py | 6 +++--- homeassistant/components/ezviz/coordinator.py | 4 ++-- homeassistant/components/ezviz/image.py | 4 ++-- homeassistant/components/ezviz/light.py | 4 ++-- homeassistant/components/ezviz/manifest.json | 6 +++--- homeassistant/components/ezviz/number.py | 4 ++-- homeassistant/components/ezviz/select.py | 4 ++-- homeassistant/components/ezviz/siren.py | 2 +- homeassistant/components/ezviz/switch.py | 4 ++-- homeassistant/components/ezviz/update.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ezviz/test_config_flow.py | 2 +- 18 files changed, 33 insertions(+), 33 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cffe9416374..6bb09b0238c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -455,8 +455,8 @@ build.json @home-assistant/supervisor /tests/components/evil_genius_labs/ @balloob /homeassistant/components/evohome/ @zxdavb /tests/components/evohome/ @zxdavb -/homeassistant/components/ezviz/ @RenierM26 @baqs -/tests/components/ezviz/ @RenierM26 @baqs +/homeassistant/components/ezviz/ @RenierM26 +/tests/components/ezviz/ @RenierM26 /homeassistant/components/faa_delays/ @ntilley905 /tests/components/faa_delays/ @ntilley905 /homeassistant/components/fan/ @home-assistant/core diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 43a71458fb2..a93954b8a9b 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -2,8 +2,8 @@ import logging -from pyezviz.client import EzvizClient -from pyezviz.exceptions import ( +from pyezvizapi.client import EzvizClient +from pyezvizapi.exceptions import ( EzvizAuthTokenExpired, EzvizAuthVerificationCode, HTTPError, diff --git a/homeassistant/components/ezviz/alarm_control_panel.py b/homeassistant/components/ezviz/alarm_control_panel.py index 08fa0a68ee8..f945fcf3667 100644 --- a/homeassistant/components/ezviz/alarm_control_panel.py +++ b/homeassistant/components/ezviz/alarm_control_panel.py @@ -6,8 +6,8 @@ from dataclasses import dataclass from datetime import timedelta import logging -from pyezviz import PyEzvizError -from pyezviz.constants import DefenseModeType +from pyezvizapi import PyEzvizError +from pyezvizapi.constants import DefenseModeType from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, diff --git a/homeassistant/components/ezviz/button.py b/homeassistant/components/ezviz/button.py index 6dbb419c903..52e029dca98 100644 --- a/homeassistant/components/ezviz/button.py +++ b/homeassistant/components/ezviz/button.py @@ -6,9 +6,9 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from pyezviz import EzvizClient -from pyezviz.constants import SupportExt -from pyezviz.exceptions import HTTPError, PyEzvizError +from pyezvizapi import EzvizClient +from pyezvizapi.constants import SupportExt +from pyezvizapi.exceptions import HTTPError, PyEzvizError from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index e3d01bef83e..a968543e5b7 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError +from pyezvizapi.exceptions import HTTPError, InvalidHost, PyEzvizError from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 845656c1d1d..622f767443d 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -6,15 +6,15 @@ from collections.abc import Mapping import logging from typing import TYPE_CHECKING, Any -from pyezviz.client import EzvizClient -from pyezviz.exceptions import ( +from pyezvizapi.client import EzvizClient +from pyezvizapi.exceptions import ( AuthTestResultFailed, EzvizAuthVerificationCode, InvalidHost, InvalidURL, PyEzvizError, ) -from pyezviz.test_cam_rtsp import TestRTSPAuth +from pyezvizapi.test_cam_rtsp import TestRTSPAuth import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index 0830784a501..c43e006ff96 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -4,8 +4,8 @@ import asyncio from datetime import timedelta import logging -from pyezviz.client import EzvizClient -from pyezviz.exceptions import ( +from pyezvizapi.client import EzvizClient +from pyezvizapi.exceptions import ( EzvizAuthTokenExpired, EzvizAuthVerificationCode, HTTPError, diff --git a/homeassistant/components/ezviz/image.py b/homeassistant/components/ezviz/image.py index 28ebc7279e6..6ba1eec462c 100644 --- a/homeassistant/components/ezviz/image.py +++ b/homeassistant/components/ezviz/image.py @@ -5,8 +5,8 @@ from __future__ import annotations import logging from propcache.api import cached_property -from pyezviz.exceptions import PyEzvizError -from pyezviz.utils import decrypt_image +from pyezvizapi.exceptions import PyEzvizError +from pyezvizapi.utils import decrypt_image from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription from homeassistant.config_entries import SOURCE_IGNORE diff --git a/homeassistant/components/ezviz/light.py b/homeassistant/components/ezviz/light.py index ba398dd3ed4..9c9382a4f3e 100644 --- a/homeassistant/components/ezviz/light.py +++ b/homeassistant/components/ezviz/light.py @@ -4,8 +4,8 @@ from __future__ import annotations from typing import Any -from pyezviz.constants import DeviceCatagories, DeviceSwitchType, SupportExt -from pyezviz.exceptions import HTTPError, PyEzvizError +from pyezvizapi.constants import DeviceCatagories, DeviceSwitchType, SupportExt +from pyezvizapi.exceptions import HTTPError, PyEzvizError from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 53976bf3002..bef054eac27 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,11 +1,11 @@ { "domain": "ezviz", "name": "EZVIZ", - "codeowners": ["@RenierM26", "@baqs"], + "codeowners": ["@RenierM26"], "config_flow": true, "dependencies": ["ffmpeg"], "documentation": "https://www.home-assistant.io/integrations/ezviz", "iot_class": "cloud_polling", - "loggers": ["paho_mqtt", "pyezviz"], - "requirements": ["pyezviz==0.2.1.2"] + "loggers": ["paho_mqtt", "pyezvizapi"], + "requirements": ["pyezvizapi==1.0.0.7"] } diff --git a/homeassistant/components/ezviz/number.py b/homeassistant/components/ezviz/number.py index 9bdd1feb81d..68a184d4972 100644 --- a/homeassistant/components/ezviz/number.py +++ b/homeassistant/components/ezviz/number.py @@ -6,8 +6,8 @@ from dataclasses import dataclass from datetime import timedelta import logging -from pyezviz.constants import SupportExt -from pyezviz.exceptions import ( +from pyezvizapi.constants import SupportExt +from pyezvizapi.exceptions import ( EzvizAuthTokenExpired, EzvizAuthVerificationCode, HTTPError, diff --git a/homeassistant/components/ezviz/select.py b/homeassistant/components/ezviz/select.py index 486564bff6e..44f80ad6cd1 100644 --- a/homeassistant/components/ezviz/select.py +++ b/homeassistant/components/ezviz/select.py @@ -4,8 +4,8 @@ from __future__ import annotations from dataclasses import dataclass -from pyezviz.constants import DeviceSwitchType, SoundMode -from pyezviz.exceptions import HTTPError, PyEzvizError +from pyezvizapi.constants import DeviceSwitchType, SoundMode +from pyezvizapi.exceptions import HTTPError, PyEzvizError from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory diff --git a/homeassistant/components/ezviz/siren.py b/homeassistant/components/ezviz/siren.py index a2c88f58972..1cbc17ba464 100644 --- a/homeassistant/components/ezviz/siren.py +++ b/homeassistant/components/ezviz/siren.py @@ -6,7 +6,7 @@ from collections.abc import Callable from datetime import datetime, timedelta from typing import Any -from pyezviz import HTTPError, PyEzvizError, SupportExt +from pyezvizapi import HTTPError, PyEzvizError, SupportExt from homeassistant.components.siren import ( SirenEntity, diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 01f7cac1a55..ae8419367c4 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -5,8 +5,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from pyezviz.constants import DeviceSwitchType, SupportExt -from pyezviz.exceptions import HTTPError, PyEzvizError +from pyezvizapi.constants import DeviceSwitchType, SupportExt +from pyezvizapi.exceptions import HTTPError, PyEzvizError from homeassistant.components.switch import ( SwitchDeviceClass, diff --git a/homeassistant/components/ezviz/update.py b/homeassistant/components/ezviz/update.py index c9f8038b336..ffd9a260ce9 100644 --- a/homeassistant/components/ezviz/update.py +++ b/homeassistant/components/ezviz/update.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any -from pyezviz import HTTPError, PyEzvizError +from pyezvizapi import HTTPError, PyEzvizError from homeassistant.components.update import ( UpdateDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index 5d660fd5bf2..fcb6bf9bf7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1967,7 +1967,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.1.2 +pyezvizapi==1.0.0.7 # homeassistant.components.fibaro pyfibaro==0.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b10e68c7c..6d23fd6ba7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1606,7 +1606,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.1.2 +pyezvizapi==1.0.0.7 # homeassistant.components.fibaro pyfibaro==0.8.2 diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index ff538b31edb..20d70902e83 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from pyezviz.exceptions import ( +from pyezvizapi.exceptions import ( EzvizAuthVerificationCode, InvalidHost, InvalidURL, From 3e0e807c967b9564cc892d36b7487d23359788b1 Mon Sep 17 00:00:00 2001 From: ichbinsteffen Date: Fri, 9 May 2025 15:53:37 +0200 Subject: [PATCH 0300/1175] Add control bus mode selector to Cambridge Audio (#139131) * [CambridgeAudio Integration] Add switch to enable Control Bus Mode * remove load_fn * Add import for ControlBusMode * Add strings for control_bus_mode * Add icons for control_bus_mode * Add test case for the select ControlBusMode.Amplifier * Change the set of icons * Fix the usage of the wrong property name * Fix test * Fix test 2 * add new snapshot * fix test name * Fix --------- Co-authored-by: Joost Lekkerkerker --- .../components/cambridge_audio/icons.json | 7 +++ .../cambridge_audio/media_player.py | 3 + .../components/cambridge_audio/select.py | 16 ++++- .../components/cambridge_audio/strings.json | 8 +++ .../snapshots/test_select.ambr | 58 +++++++++++++++++++ .../cambridge_audio/test_media_player.py | 24 ++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cambridge_audio/icons.json b/homeassistant/components/cambridge_audio/icons.json index b4346a7fe8e..dda7d71e506 100644 --- a/homeassistant/components/cambridge_audio/icons.json +++ b/homeassistant/components/cambridge_audio/icons.json @@ -11,6 +11,13 @@ }, "audio_output": { "default": "mdi:audio-input-stereo-minijack" + }, + "control_bus_mode": { + "default": "mdi:audio-video-off", + "state": { + "amplifier": "mdi:speaker", + "receiver": "mdi:audio-video" + } } }, "switch": { diff --git a/homeassistant/components/cambridge_audio/media_player.py b/homeassistant/components/cambridge_audio/media_player.py index 5322ae7d9a2..e8f92c0b25c 100644 --- a/homeassistant/components/cambridge_audio/media_player.py +++ b/homeassistant/components/cambridge_audio/media_player.py @@ -11,6 +11,7 @@ from aiostreammagic import ( StreamMagicClient, TransportControl, ) +from aiostreammagic.models import ControlBusMode from homeassistant.components.media_player import ( BrowseMedia, @@ -91,6 +92,8 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): features = BASE_FEATURES if self.client.state.pre_amp_mode: features |= PREAMP_FEATURES + if self.client.state.control_bus == ControlBusMode.AMPLIFIER: + features |= MediaPlayerEntityFeature.VOLUME_STEP if TransportControl.PLAY_PAUSE in controls: features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE for control in controls: diff --git a/homeassistant/components/cambridge_audio/select.py b/homeassistant/components/cambridge_audio/select.py index e7d9136711f..cdc163f555d 100644 --- a/homeassistant/components/cambridge_audio/select.py +++ b/homeassistant/components/cambridge_audio/select.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from aiostreammagic import StreamMagicClient -from aiostreammagic.models import DisplayBrightness +from aiostreammagic.models import ControlBusMode, DisplayBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.const import EntityCategory @@ -76,6 +76,20 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = ( value_fn=_audio_output_value_fn, set_value_fn=_audio_output_set_value_fn, ), + CambridgeAudioSelectEntityDescription( + key="control_bus_mode", + translation_key="control_bus_mode", + options=[ + ControlBusMode.AMPLIFIER.value, + ControlBusMode.RECEIVER.value, + ControlBusMode.OFF.value, + ], + entity_category=EntityCategory.CONFIG, + value_fn=lambda client: client.state.control_bus, + set_value_fn=lambda client, value: client.set_control_bus_mode( + ControlBusMode(value) + ), + ), ) diff --git a/homeassistant/components/cambridge_audio/strings.json b/homeassistant/components/cambridge_audio/strings.json index 6041232fe65..e2c89bcbbb0 100644 --- a/homeassistant/components/cambridge_audio/strings.json +++ b/homeassistant/components/cambridge_audio/strings.json @@ -46,6 +46,14 @@ }, "audio_output": { "name": "Audio output" + }, + "control_bus_mode": { + "name": "Control Bus mode", + "state": { + "amplifier": "Amplifier", + "receiver": "Receiver", + "off": "[%key:common::state::off%]" + } } }, "switch": { diff --git a/tests/components/cambridge_audio/snapshots/test_select.ambr b/tests/components/cambridge_audio/snapshots/test_select.ambr index 8c9801b101b..c83e101f363 100644 --- a/tests/components/cambridge_audio/snapshots/test_select.ambr +++ b/tests/components/cambridge_audio/snapshots/test_select.ambr @@ -57,6 +57,64 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[select.cambridge_audio_cxnv2_control_bus_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'amplifier', + 'receiver', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.cambridge_audio_cxnv2_control_bus_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': 'Control Bus mode', + 'platform': 'cambridge_audio', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'control_bus_mode', + 'unique_id': '0020c2d8-control_bus_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[select.cambridge_audio_cxnv2_control_bus_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Cambridge Audio CXNv2 Control Bus mode', + 'options': list([ + 'amplifier', + 'receiver', + 'off', + ]), + }), + 'context': , + 'entity_id': 'select.cambridge_audio_cxnv2_control_bus_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[select.cambridge_audio_cxnv2_display_brightness-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/cambridge_audio/test_media_player.py b/tests/components/cambridge_audio/test_media_player.py index ef7e911fbba..10e9311c4b0 100644 --- a/tests/components/cambridge_audio/test_media_player.py +++ b/tests/components/cambridge_audio/test_media_player.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock from aiostreammagic import ( + ControlBusMode, RepeatMode as CambridgeRepeatMode, ShuffleMode, TransportControl, @@ -129,6 +130,29 @@ async def test_entity_supported_features( ) +async def test_entity_supported_features_with_control_bus( + hass: HomeAssistant, + mock_stream_magic_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test entity attributes with control bus state.""" + await setup_integration(hass, mock_config_entry) + + mock_stream_magic_client.state.pre_amp_mode = False + mock_stream_magic_client.state.control_bus = ControlBusMode.AMPLIFIER + + await mock_state_update(mock_stream_magic_client) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + attrs = state.attributes + assert MediaPlayerEntityFeature.VOLUME_STEP in attrs[ATTR_SUPPORTED_FEATURES] + assert ( + MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE + not in attrs[ATTR_SUPPORTED_FEATURES] + ) + + @pytest.mark.parametrize( ("power_state", "play_state", "media_player_state"), [ From b6c4b06fc789855642e53504d3a541cc6c4ca215 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 9 May 2025 16:15:17 +0200 Subject: [PATCH 0301/1175] Skip check for entry updated by current flow in _async_abort_entries_match (#141003) * Ignore entries with source reconfigure in _async_abort_entries_match * Exclude reconfigure and reauth entry from match check * Add tests * Fix tests for other components * Revert unrelated changes * Update docstring * Make test more realistic * Change name and docstring for sabnzbd test --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/config_entries.py | 8 +++- tests/components/pushover/test_config_flow.py | 2 +- tests/components/sabnzbd/test_config_flow.py | 6 +-- tests/test_config_entries.py | 46 +++++++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4f07c9cf574..c2481ae3fa3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2839,10 +2839,16 @@ class ConfigFlow(ConfigEntryBaseFlow): ) -> None: """Abort if current entries match all data. + Do not abort for the entry that is being updated by the current flow. Requires `already_configured` in strings.json in user visible flows. """ _async_abort_entries_match( - self._async_current_entries(include_ignore=False), match_dict + [ + entry + for entry in self._async_current_entries(include_ignore=False) + if entry.entry_id != self.context.get("entry_id") + ], + match_dict, ) @callback diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py index 58485bfb427..a3c9ac3ccbb 100644 --- a/tests/components/pushover/test_config_flow.py +++ b/tests/components/pushover/test_config_flow.py @@ -217,7 +217,7 @@ async def test_reauth_with_existing_config(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_API_KEY: "MYAPIKEY2", + CONF_API_KEY: MOCK_CONFIG[CONF_API_KEY], }, ) diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 98422e931ec..ec9044f4223 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -153,10 +153,10 @@ async def test_abort_already_configured( assert result["reason"] == "already_configured" -async def test_abort_reconfigure_already_configured( +async def test_abort_reconfigure_successful( hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: - """Test that the reconfigure flow aborts if SABnzbd instance is already configured.""" + """Test that the reconfigure flow aborts successfully if SABnzbd instance is already configured.""" result = await config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -166,4 +166,4 @@ async def test_abort_reconfigure_already_configured( VALID_CONFIG, ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "reconfigure_successful" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 75610c7d076..ffff19f2c46 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -5268,6 +5268,52 @@ async def test_async_abort_entries_match( assert result["reason"] == reason +@pytest.mark.parametrize( + ("matchers", "reason"), + [ + ({"host": "3.4.5.6", "ip": "1.2.3.4", "port": 23}, "no_match"), + ], +) +async def test_async_abort_entries_match_context( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + matchers: dict[str, str], + reason: str, +) -> None: + """Test aborting if matching config entries exist.""" + entry = MockConfigEntry( + domain="comp", data={"ip": "1.2.3.4", "host": "3.4.5.6", "port": 23} + ) + entry.add_to_hass(hass) + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_platform(hass, "comp.config_flow", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_reconfigure(self, user_input=None): + """Test user step.""" + self._async_abort_entries_match(matchers) + return self.async_abort(reason="no_match") + + with mock_config_flow("comp", TestFlow), mock_config_flow("invalid_flow", 5): + result = await manager.flow.async_init( + "comp", + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason + + @pytest.mark.parametrize( ("matchers", "reason"), [ From 4cecb6c8518a4a38539a86624285616007dc7bd8 Mon Sep 17 00:00:00 2001 From: Louis Christ Date: Fri, 9 May 2025 16:15:52 +0200 Subject: [PATCH 0302/1175] Replace custom actions for sleep timer with buttons in bluesound integration (#133604) * Use entity services * Add buttons for sleep timer * Fix merge * Replace hass.data with runtime_data from config_entries * Disable button by default * Remove duplicate dispatchers * Add tests for buttons * Fix merge commit * Fix merge commit * Update deprecation version * Remove update_before_add * Use entity_registry_enabled_by_default * Use EnitiyDescriptions for buttons * Update version for deprecate * Use tranlation_key; Move default disable to EntityDescription * Fix merge commit * Fix callback type; fix breaks version * Use normal issue * Apply suggestions from code review --------- Co-authored-by: Franck Nijhof Co-authored-by: Joost Lekkerkerker --- .../components/bluesound/__init__.py | 1 + homeassistant/components/bluesound/button.py | 128 ++++++++++++++++++ .../components/bluesound/media_player.py | 34 ++++- .../components/bluesound/strings.json | 20 +++ tests/components/bluesound/test_button.py | 47 +++++++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluesound/button.py create mode 100644 tests/components/bluesound/test_button.py diff --git a/homeassistant/components/bluesound/__init__.py b/homeassistant/components/bluesound/__init__.py index 37e83ce2c47..d5dfbb4b582 100644 --- a/homeassistant/components/bluesound/__init__.py +++ b/homeassistant/components/bluesound/__init__.py @@ -21,6 +21,7 @@ from .coordinator import ( CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS = [ + Platform.BUTTON, Platform.MEDIA_PLAYER, ] diff --git a/homeassistant/components/bluesound/button.py b/homeassistant/components/bluesound/button.py new file mode 100644 index 00000000000..4c9d363fa5f --- /dev/null +++ b/homeassistant/components/bluesound/button.py @@ -0,0 +1,128 @@ +"""Button entities for Bluesound.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from pyblu import Player + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.const import CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + DeviceInfo, + format_mac, +) +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import BluesoundCoordinator +from .media_player import DEFAULT_PORT +from .utils import format_unique_id + +if TYPE_CHECKING: + from . import BluesoundConfigEntry + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BluesoundConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Bluesound entry.""" + + async_add_entities( + BluesoundButton( + config_entry.runtime_data.coordinator, + config_entry.runtime_data.player, + config_entry.data[CONF_PORT], + description, + ) + for description in BUTTON_DESCRIPTIONS + ) + + +@dataclass(kw_only=True, frozen=True) +class BluesoundButtonEntityDescription(ButtonEntityDescription): + """Description for Bluesound button entities.""" + + press_fn: Callable[[Player], Awaitable[None]] + + +async def clear_sleep_timer(player: Player) -> None: + """Clear the sleep timer.""" + sleep = -1 + while sleep != 0: + sleep = await player.sleep_timer() + + +async def set_sleep_timer(player: Player) -> None: + """Set the sleep timer.""" + await player.sleep_timer() + + +BUTTON_DESCRIPTIONS = [ + BluesoundButtonEntityDescription( + key="set_sleep_timer", + translation_key="set_sleep_timer", + entity_registry_enabled_default=False, + press_fn=set_sleep_timer, + ), + BluesoundButtonEntityDescription( + key="clear_sleep_timer", + translation_key="clear_sleep_timer", + entity_registry_enabled_default=False, + press_fn=clear_sleep_timer, + ), +] + + +class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity): + """Base class for Bluesound buttons.""" + + _attr_has_entity_name = True + entity_description: BluesoundButtonEntityDescription + + def __init__( + self, + coordinator: BluesoundCoordinator, + player: Player, + port: int, + description: BluesoundButtonEntityDescription, + ) -> None: + """Initialize the Bluesound button.""" + super().__init__(coordinator) + sync_status = coordinator.data.sync_status + + self.entity_description = description + self._player = player + self._attr_unique_id = ( + f"{description.key}-{format_unique_id(sync_status.mac, port)}" + ) + + if port == DEFAULT_PORT: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, format_mac(sync_status.mac))}, + connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))}, + name=sync_status.name, + manufacturer=sync_status.brand, + model=sync_status.model_name, + model_id=sync_status.model, + ) + else: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))}, + name=sync_status.name, + manufacturer=sync_status.brand, + model=sync_status.model_name, + model_id=sync_status.model, + via_device=(DOMAIN, format_mac(sync_status.mac)), + ) + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.press_fn(self._player) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 337dc3d3a33..2662562f575 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -22,7 +22,11 @@ from homeassistant.components.media_player import ( from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + entity_platform, + issue_registry as ir, +) from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceInfo, @@ -34,7 +38,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util import dt as dt_util +from homeassistant.util import dt as dt_util, slugify from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN from .coordinator import BluesoundCoordinator @@ -488,10 +492,36 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity async def async_increase_timer(self) -> int: """Increase sleep time on player.""" + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_service_{SERVICE_SET_TIMER}", + is_fixable=False, + breaks_in_ha_version="2025.12.0", + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_service_set_sleep_timer", + translation_placeholders={ + "name": slugify(self.sync_status.name), + }, + ) return await self._player.sleep_timer() async def async_clear_timer(self) -> None: """Clear sleep timer on player.""" + ir.async_create_issue( + self.hass, + DOMAIN, + f"deprecated_service_{SERVICE_CLEAR_TIMER}", + is_fixable=False, + breaks_in_ha_version="2025.12.0", + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_service_clear_sleep_timer", + translation_placeholders={ + "name": slugify(self.sync_status.name), + }, + ) sleep = 1 while sleep > 0: sleep = await self._player.sleep_timer() diff --git a/homeassistant/components/bluesound/strings.json b/homeassistant/components/bluesound/strings.json index 1170e0b92e0..236113a835b 100644 --- a/homeassistant/components/bluesound/strings.json +++ b/homeassistant/components/bluesound/strings.json @@ -26,6 +26,16 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, + "issues": { + "deprecated_service_set_sleep_timer": { + "title": "Detected use of deprecated action bluesound.set_sleep_timer", + "description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts." + }, + "deprecated_service_clear_sleep_timer": { + "title": "Detected use of deprecated action bluesound.clear_sleep_timer", + "description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts." + } + }, "services": { "join": { "name": "Join", @@ -71,5 +81,15 @@ } } } + }, + "entity": { + "button": { + "set_sleep_timer": { + "name": "Set sleep timer" + }, + "clear_sleep_timer": { + "name": "Clear sleep timer" + } + } } } diff --git a/tests/components/bluesound/test_button.py b/tests/components/bluesound/test_button.py new file mode 100644 index 00000000000..0cb40f53d27 --- /dev/null +++ b/tests/components/bluesound/test_button.py @@ -0,0 +1,47 @@ +"""Test for bluesound buttons.""" + +from unittest.mock import call + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from .conftest import PlayerMocks + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_set_sleep_timer( + hass: HomeAssistant, + player_mocks: PlayerMocks, + setup_config_entry: None, +) -> None: + """Test the media player volume set.""" + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.player_name1111_set_sleep_timer"}, + blocking=True, + ) + + player_mocks.player_data.player.sleep_timer.assert_called_once() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_clear_sleep_timer( + hass: HomeAssistant, + player_mocks: PlayerMocks, + setup_config_entry: None, +) -> None: + """Test the media player volume set.""" + player_mocks.player_data.player.sleep_timer.side_effect = [15, 30, 45, 60, 90, 0] + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.player_name1111_clear_sleep_timer"}, + blocking=True, + ) + + player_mocks.player_data.player.sleep_timer.assert_has_calls([call()] * 6) From 9a2f17c2b27b17d5a59e937fcea238982ed75600 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Fri, 9 May 2025 16:42:22 +0200 Subject: [PATCH 0303/1175] Refactor Bring! integration to poll activity data at a slower interval (#142621) * Refactor Bring integration to poll activity with slower interval * add test --- homeassistant/components/bring/__init__.py | 12 +- homeassistant/components/bring/coordinator.py | 95 ++++++++- homeassistant/components/bring/diagnostics.py | 11 +- homeassistant/components/bring/entity.py | 10 +- homeassistant/components/bring/event.py | 13 +- homeassistant/components/bring/sensor.py | 3 +- homeassistant/components/bring/todo.py | 3 +- .../bring/snapshots/test_diagnostics.ambr | 188 +++++++++--------- tests/components/bring/test_init.py | 66 ++++++ tests/components/bring/test_util.py | 12 +- 10 files changed, 286 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/bring/__init__.py b/homeassistant/components/bring/__init__.py index 6dd2d36351c..6c0b34c66f0 100644 --- a/homeassistant/components/bring/__init__.py +++ b/homeassistant/components/bring/__init__.py @@ -10,7 +10,12 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import BringConfigEntry, BringDataUpdateCoordinator +from .coordinator import ( + BringActivityCoordinator, + BringConfigEntry, + BringCoordinators, + BringDataUpdateCoordinator, +) PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO] @@ -26,7 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo coordinator = BringDataUpdateCoordinator(hass, entry, bring) await coordinator.async_config_entry_first_refresh() - entry.runtime_data = coordinator + activity_coordinator = BringActivityCoordinator(hass, entry, coordinator) + await activity_coordinator.async_config_entry_first_refresh() + + entry.runtime_data = BringCoordinators(coordinator, activity_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/bring/coordinator.py b/homeassistant/components/bring/coordinator.py index e1f9fa45ac8..0a8d980a6aa 100644 --- a/homeassistant/components/bring/coordinator.py +++ b/homeassistant/components/bring/coordinator.py @@ -30,7 +30,15 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator] +type BringConfigEntry = ConfigEntry[BringCoordinators] + + +@dataclass +class BringCoordinators: + """Data class holding coordinators.""" + + data: BringDataUpdateCoordinator + activity: BringActivityCoordinator @dataclass(frozen=True) @@ -39,17 +47,28 @@ class BringData(DataClassORJSONMixin): lst: BringList content: BringItemsResponse + + +@dataclass(frozen=True) +class BringActivityData(DataClassORJSONMixin): + """Coordinator data class.""" + activity: BringActivityResponse users: BringUsersResponse -class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): - """A Bring Data Update Coordinator.""" +class BringBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): + """Bring base coordinator.""" config_entry: BringConfigEntry - user_settings: BringUserSettingsResponse lists: list[BringList] + +class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]): + """A Bring Data Update Coordinator.""" + + user_settings: BringUserSettingsResponse + def __init__( self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring ) -> None: @@ -90,16 +109,19 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): current_lists := {lst.listUuid for lst in self.lists} ): self._purge_deleted_lists() + new_lists = current_lists - self.previous_lists self.previous_lists = current_lists list_dict: dict[str, BringData] = {} for lst in self.lists: - if (ctx := set(self.async_contexts())) and lst.listUuid not in ctx: + if ( + (ctx := set(self.async_contexts())) + and lst.listUuid not in ctx + and lst.listUuid not in new_lists + ): continue try: items = await self.bring.get_list(lst.listUuid) - activity = await self.bring.get_activity(lst.listUuid) - users = await self.bring.get_list_users(lst.listUuid) except BringRequestException as e: raise UpdateFailed( translation_domain=DOMAIN, @@ -111,7 +133,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): translation_key="setup_parse_exception", ) from e else: - list_dict[lst.listUuid] = BringData(lst, items, activity, users) + list_dict[lst.listUuid] = BringData(lst, items) return list_dict @@ -156,3 +178,60 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]): device_reg.async_update_device( device.id, remove_config_entry_id=self.config_entry.entry_id ) + + +class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]]): + """A Bring Activity Data Update Coordinator.""" + + user_settings: BringUserSettingsResponse + + def __init__( + self, + hass: HomeAssistant, + config_entry: BringConfigEntry, + coordinator: BringDataUpdateCoordinator, + ) -> None: + """Initialize the Bring Activity data coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=timedelta(minutes=10), + ) + + self.coordinator = coordinator + self.lists = coordinator.lists + + async def _async_update_data(self) -> dict[str, BringActivityData]: + """Fetch activity data from bring.""" + + list_dict: dict[str, BringActivityData] = {} + for lst in self.lists: + if ( + ctx := set(self.coordinator.async_contexts()) + ) and lst.listUuid not in ctx: + continue + try: + activity = await self.coordinator.bring.get_activity(lst.listUuid) + users = await self.coordinator.bring.get_list_users(lst.listUuid) + except BringAuthException as e: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="setup_authentication_exception", + translation_placeholders={CONF_EMAIL: self.coordinator.bring.mail}, + ) from e + except BringRequestException as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="setup_request_exception", + ) from e + except BringParseException as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="setup_parse_exception", + ) from e + else: + list_dict[lst.listUuid] = BringActivityData(activity, users) + + return list_dict diff --git a/homeassistant/components/bring/diagnostics.py b/homeassistant/components/bring/diagnostics.py index e5cafd30ab5..2f5a0cae504 100644 --- a/homeassistant/components/bring/diagnostics.py +++ b/homeassistant/components/bring/diagnostics.py @@ -20,9 +20,12 @@ async def async_get_config_entry_diagnostics( return { "data": { - k: async_redact_data(v.to_dict(), TO_REDACT) - for k, v in config_entry.runtime_data.data.items() + k: v.to_dict() for k, v in config_entry.runtime_data.data.data.items() }, - "lists": [lst.to_dict() for lst in config_entry.runtime_data.lists], - "user_settings": config_entry.runtime_data.user_settings.to_dict(), + "activity": { + k: async_redact_data(v.to_dict(), TO_REDACT) + for k, v in config_entry.runtime_data.activity.data.items() + }, + "lists": [lst.to_dict() for lst in config_entry.runtime_data.data.lists], + "user_settings": config_entry.runtime_data.data.user_settings.to_dict(), } diff --git a/homeassistant/components/bring/entity.py b/homeassistant/components/bring/entity.py index ee90f22beef..1bb49afeb5d 100644 --- a/homeassistant/components/bring/entity.py +++ b/homeassistant/components/bring/entity.py @@ -8,17 +8,17 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import BringDataUpdateCoordinator +from .coordinator import BringBaseCoordinator -class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]): +class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]): """Bring base entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: BringDataUpdateCoordinator, + coordinator: BringBaseCoordinator, bring_list: BringList, ) -> None: """Initialize the entity.""" @@ -34,5 +34,7 @@ class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]): }, manufacturer="Bring! Labs AG", model="Bring! Grocery Shopping List", - configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}", + configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}" + if bring_list in self.coordinator.lists + else None, ) diff --git a/homeassistant/components/bring/event.py b/homeassistant/components/bring/event.py index 403856405ce..e9e286dccf0 100644 --- a/homeassistant/components/bring/event.py +++ b/homeassistant/components/bring/event.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import BringConfigEntry -from .coordinator import BringDataUpdateCoordinator +from .coordinator import BringActivityCoordinator from .entity import BringBaseEntity PARALLEL_UPDATES = 0 @@ -32,18 +32,18 @@ async def async_setup_entry( """Add event entities.""" nonlocal lists_added - if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added: + if new_lists := {lst.listUuid for lst in coordinator.data.lists} - lists_added: async_add_entities( BringEventEntity( - coordinator, + coordinator.activity, bring_list, ) - for bring_list in coordinator.lists + for bring_list in coordinator.data.lists if bring_list.listUuid in new_lists ) lists_added |= new_lists - coordinator.async_add_listener(add_entities) + coordinator.activity.async_add_listener(add_entities) add_entities() @@ -51,10 +51,11 @@ class BringEventEntity(BringBaseEntity, EventEntity): """An event entity.""" _attr_translation_key = "activities" + coordinator: BringActivityCoordinator def __init__( self, - coordinator: BringDataUpdateCoordinator, + coordinator: BringActivityCoordinator, bring_list: BringList, ) -> None: """Initialize the entity.""" diff --git a/homeassistant/components/bring/sensor.py b/homeassistant/components/bring/sensor.py index 2a09d574607..88399ea26f7 100644 --- a/homeassistant/components/bring/sensor.py +++ b/homeassistant/components/bring/sensor.py @@ -88,7 +88,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator = config_entry.runtime_data + coordinator = config_entry.runtime_data.data lists_added: set[str] = set() @callback @@ -117,6 +117,7 @@ class BringSensorEntity(BringBaseEntity, SensorEntity): """A sensor entity.""" entity_description: BringSensorEntityDescription + coordinator: BringDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index d1eb9e78341..c72b8c7ca0e 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -44,7 +44,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor from a config entry created in the integrations UI.""" - coordinator = config_entry.runtime_data + coordinator = config_entry.runtime_data.data lists_added: set[str] = set() @callback @@ -88,6 +88,7 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity): | TodoListEntityFeature.DELETE_TODO_ITEM | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM ) + coordinator: BringDataUpdateCoordinator def __init__( self, coordinator: BringDataUpdateCoordinator, bring_list: BringList diff --git a/tests/components/bring/snapshots/test_diagnostics.ambr b/tests/components/bring/snapshots/test_diagnostics.ambr index 3f4c8f5f339..4c8475428e9 100644 --- a/tests/components/bring/snapshots/test_diagnostics.ambr +++ b/tests/components/bring/snapshots/test_diagnostics.ambr @@ -1,7 +1,7 @@ # serializer version: 1 # name: test_diagnostics dict({ - 'data': dict({ + 'activity': dict({ 'b4776778-7f6c-496e-951b-92a35d3db0dd': dict({ 'activity': dict({ 'timeline': list([ @@ -79,58 +79,6 @@ 'timestamp': '2025-01-01T03:09:33.036000+00:00', 'totalEvents': 3, }), - 'content': dict({ - 'items': dict({ - 'purchase': list([ - dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, - }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Paprika', - 'specification': 'Rot', - 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', - }), - dict({ - 'attributes': list([ - dict({ - 'content': dict({ - 'convenient': True, - 'discounted': True, - 'urgent': True, - }), - 'type': 'PURCHASE_CONDITIONS', - }), - ]), - 'itemId': 'Pouletbrüstli', - 'specification': 'Bio', - 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', - }), - ]), - 'recently': list([ - dict({ - 'attributes': list([ - ]), - 'itemId': 'Ananas', - 'specification': '', - 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', - }), - ]), - }), - 'status': 'REGISTERED', - 'uuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', - }), - 'lst': dict({ - 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', - 'name': '**REDACTED**', - 'theme': 'ch.publisheria.bring.theme.home', - }), 'users': dict({ 'users': list([ dict({ @@ -246,6 +194,101 @@ 'timestamp': '2025-01-01T03:09:33.036000+00:00', 'totalEvents': 3, }), + 'users': dict({ + 'users': list([ + dict({ + 'country': 'DE', + 'email': '**REDACTED**', + 'language': 'de', + 'name': '**REDACTED**', + 'photoPath': '', + 'plusExpiry': None, + 'plusTryOut': False, + 'publicUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': '**REDACTED**', + 'language': 'en', + 'name': '**REDACTED**', + 'photoPath': '', + 'plusExpiry': None, + 'plusTryOut': False, + 'publicUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', + 'pushEnabled': True, + }), + dict({ + 'country': 'US', + 'email': None, + 'language': 'en', + 'name': None, + 'photoPath': None, + 'plusExpiry': None, + 'plusTryOut': False, + 'publicUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', + 'pushEnabled': True, + }), + ]), + }), + }), + }), + 'data': dict({ + 'b4776778-7f6c-496e-951b-92a35d3db0dd': dict({ + 'content': dict({ + 'items': dict({ + 'purchase': list([ + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Paprika', + 'specification': 'Rot', + 'uuid': 'b5d0790b-5f32-4d5c-91da-e29066f167de', + }), + dict({ + 'attributes': list([ + dict({ + 'content': dict({ + 'convenient': True, + 'discounted': True, + 'urgent': True, + }), + 'type': 'PURCHASE_CONDITIONS', + }), + ]), + 'itemId': 'Pouletbrüstli', + 'specification': 'Bio', + 'uuid': '72d370ab-d8ca-4e41-b956-91df94795b4e', + }), + ]), + 'recently': list([ + dict({ + 'attributes': list([ + ]), + 'itemId': 'Ananas', + 'specification': '', + 'uuid': 'fc8db30a-647e-4e6c-9d71-3b85d6a2d954', + }), + ]), + }), + 'status': 'REGISTERED', + 'uuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + }), + 'lst': dict({ + 'listUuid': 'b4776778-7f6c-496e-951b-92a35d3db0dd', + 'name': 'Baumarkt', + 'theme': 'ch.publisheria.bring.theme.home', + }), + }), + 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5': dict({ 'content': dict({ 'items': dict({ 'purchase': list([ @@ -295,46 +338,9 @@ }), 'lst': dict({ 'listUuid': 'e542eef6-dba7-4c31-a52c-29e6ab9d83a5', - 'name': '**REDACTED**', + 'name': 'Einkauf', 'theme': 'ch.publisheria.bring.theme.home', }), - 'users': dict({ - 'users': list([ - dict({ - 'country': 'DE', - 'email': '**REDACTED**', - 'language': 'de', - 'name': '**REDACTED**', - 'photoPath': '', - 'plusExpiry': None, - 'plusTryOut': False, - 'publicUuid': '9a21fdfc-63a4-441a-afc1-ef3030605a9d', - 'pushEnabled': True, - }), - dict({ - 'country': 'US', - 'email': '**REDACTED**', - 'language': 'en', - 'name': '**REDACTED**', - 'photoPath': '', - 'plusExpiry': None, - 'plusTryOut': False, - 'publicUuid': '73af455f-c158-4004-a5e0-79f4f8a6d4bd', - 'pushEnabled': True, - }), - dict({ - 'country': 'US', - 'email': None, - 'language': 'en', - 'name': None, - 'photoPath': None, - 'plusExpiry': None, - 'plusTryOut': False, - 'publicUuid': '7d5e9d08-877a-4c36-8740-a9bf74ec690a', - 'pushEnabled': True, - }), - ]), - }), }), }), 'lists': list([ diff --git a/tests/components/bring/test_init.py b/tests/components/bring/test_init.py index f053f294ef1..7f235ea505c 100644 --- a/tests/components/bring/test_init.py +++ b/tests/components/bring/test_init.py @@ -139,6 +139,31 @@ async def test_config_entry_not_ready_udpdate_failed( assert bring_config_entry.state is ConfigEntryState.SETUP_RETRY +@pytest.mark.parametrize( + ("exception", "state"), + [ + (BringRequestException, ConfigEntryState.SETUP_RETRY), + (BringParseException, ConfigEntryState.SETUP_RETRY), + (BringAuthException, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_activity_coordinator_errors( + hass: HomeAssistant, + bring_config_entry: MockConfigEntry, + mock_bring_client: AsyncMock, + exception: Exception, + state: ConfigEntryState, +) -> None: + """Test config entry not ready from update failed in _async_update_data.""" + mock_bring_client.get_activity.side_effect = exception + + bring_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(bring_config_entry.entry_id) + await hass.async_block_till_done() + + assert bring_config_entry.state is state + + @pytest.mark.parametrize( ("exception", "state"), [ @@ -263,3 +288,44 @@ async def test_create_devices( assert device_registry.async_get_device( {(DOMAIN, f"{bring_config_entry.unique_id}_{list_uuid}")} ) + + +@pytest.mark.usefixtures("mock_bring_client") +async def test_coordinator_update_intervals( + hass: HomeAssistant, + bring_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + mock_bring_client: AsyncMock, +) -> None: + """Test the coordinator updates at the specified intervals.""" + await setup_integration(hass, bring_config_entry) + + assert bring_config_entry.state is ConfigEntryState.LOADED + + # fetch 2 lists on first refresh + assert mock_bring_client.load_lists.await_count == 2 + assert mock_bring_client.get_activity.await_count == 2 + + mock_bring_client.load_lists.reset_mock() + mock_bring_client.get_activity.reset_mock() + + mock_bring_client.load_lists.return_value = BringListResponse.from_json( + load_fixture("lists2.json", DOMAIN) + ) + freezer.tick(timedelta(seconds=90)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # main coordinator refreshes, activity does not + assert mock_bring_client.load_lists.await_count == 1 + assert mock_bring_client.get_activity.await_count == 0 + + mock_bring_client.load_lists.reset_mock() + mock_bring_client.get_activity.reset_mock() + + freezer.tick(timedelta(seconds=510)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # assert activity refreshes after 10min and has up-to-date lists data + assert mock_bring_client.get_activity.await_count == 1 diff --git a/tests/components/bring/test_util.py b/tests/components/bring/test_util.py index 673c4e68a4d..a1d7de2b553 100644 --- a/tests/components/bring/test_util.py +++ b/tests/components/bring/test_util.py @@ -1,12 +1,6 @@ """Test for utility functions of the Bring! integration.""" -from bring_api import ( - BringActivityResponse, - BringItemsResponse, - BringListResponse, - BringUserSettingsResponse, -) -from bring_api.types import BringUsersResponse +from bring_api import BringItemsResponse, BringListResponse, BringUserSettingsResponse import pytest from homeassistant.components.bring.const import DOMAIN @@ -47,10 +41,8 @@ def test_sum_attributes(attribute: str, expected: int) -> None: """Test function sum_attributes.""" items = BringItemsResponse.from_json(load_fixture("items.json", DOMAIN)) lst = BringListResponse.from_json(load_fixture("lists.json", DOMAIN)) - activity = BringActivityResponse.from_json(load_fixture("activity.json", DOMAIN)) - users = BringUsersResponse.from_json(load_fixture("users.json", DOMAIN)) result = sum_attributes( - BringData(lst.lists[0], items, activity, users), + BringData(lst.lists[0], items), attribute, ) From a7afeb078cd307b601651115da8f2ec242590998 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 9 May 2025 17:14:02 +0200 Subject: [PATCH 0304/1175] Avoid split of unique id to build OpenWeatherMap sensors (#144546) * Avoid split of unique id * Assert that unique_id is not None --- homeassistant/components/openweathermap/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 47859b78812..15935556ef3 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -161,6 +161,8 @@ async def async_setup_entry( """Set up OpenWeatherMap sensor entities based on a config entry.""" domain_data = config_entry.runtime_data name = domain_data.name + unique_id = config_entry.unique_id + assert unique_id is not None weather_coordinator = domain_data.coordinator if domain_data.mode == OWM_MODE_FREE_FORECAST: @@ -174,7 +176,7 @@ async def async_setup_entry( async_add_entities( OpenWeatherMapSensor( name, - f"{config_entry.unique_id}-{description.key}", + unique_id, description, weather_coordinator, ) @@ -200,11 +202,10 @@ class AbstractOpenWeatherMapSensor(SensorEntity): self._coordinator = coordinator self._attr_name = f"{name} {description.name}" - self._attr_unique_id = unique_id - split_unique_id = unique_id.split("-") + self._attr_unique_id = f"{unique_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, name=DEFAULT_NAME, ) From c18b6d736a67eb6831d474b4be7a9850f5748181 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Sat, 10 May 2025 04:17:26 +1200 Subject: [PATCH 0305/1175] Add switch platform to bosch alarm (#142157) * add switch platform to bosch alarm * fix tests * one device per output * add switch for door * add switch entities for door * fix switch devices * apply changes from review * update identifiers * add missing entity * use base entity for switch * rename var * fix icons * give user a nice error if they try to lock or secure a door that is in the process of being cycled * fix test * Update homeassistant/components/bosch_alarm/switch.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/bosch_alarm/switch.py Co-authored-by: Joost Lekkerkerker * use service constants --------- Co-authored-by: Joost Lekkerkerker --- .../components/bosch_alarm/__init__.py | 6 +- .../components/bosch_alarm/entity.py | 54 ++ .../components/bosch_alarm/icons.json | 22 +- .../components/bosch_alarm/strings.json | 14 + .../components/bosch_alarm/switch.py | 150 +++++ tests/components/bosch_alarm/conftest.py | 2 + .../bosch_alarm/snapshots/test_switch.ambr | 565 ++++++++++++++++++ tests/components/bosch_alarm/test_switch.py | 147 +++++ 8 files changed, 958 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bosch_alarm/switch.py create mode 100644 tests/components/bosch_alarm/snapshots/test_switch.ambr create mode 100644 tests/components/bosch_alarm/test_switch.py diff --git a/homeassistant/components/bosch_alarm/__init__.py b/homeassistant/components/bosch_alarm/__init__.py index 602c801701d..19debe10549 100644 --- a/homeassistant/components/bosch_alarm/__init__.py +++ b/homeassistant/components/bosch_alarm/__init__.py @@ -14,7 +14,11 @@ from homeassistant.helpers import device_registry as dr from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN -PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.ALARM_CONTROL_PANEL, + Platform.SENSOR, + Platform.SWITCH, +] type BoschAlarmConfigEntry = ConfigEntry[Panel] diff --git a/homeassistant/components/bosch_alarm/entity.py b/homeassistant/components/bosch_alarm/entity.py index f74634125c4..e9223b729c4 100644 --- a/homeassistant/components/bosch_alarm/entity.py +++ b/homeassistant/components/bosch_alarm/entity.py @@ -86,3 +86,57 @@ class BoschAlarmAreaEntity(BoschAlarmEntity): self._area.ready_observer.detach(self.schedule_update_ha_state) if self._observe_status: self._area.status_observer.detach(self.schedule_update_ha_state) + + +class BoschAlarmDoorEntity(BoschAlarmEntity): + """A base entity for area related entities within a bosch alarm panel.""" + + def __init__(self, panel: Panel, door_id: int, unique_id: str) -> None: + """Set up a area related entity for a bosch alarm panel.""" + super().__init__(panel, unique_id) + self._door_id = door_id + self._door = panel.doors[door_id] + self._door_unique_id = f"{unique_id}_door_{door_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._door_unique_id)}, + name=self._door.name, + manufacturer="Bosch Security Systems", + via_device=(DOMAIN, unique_id), + ) + + async def async_added_to_hass(self) -> None: + """Observe state changes.""" + await super().async_added_to_hass() + self._door.status_observer.attach(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Stop observing state changes.""" + await super().async_added_to_hass() + self._door.status_observer.detach(self.schedule_update_ha_state) + + +class BoschAlarmOutputEntity(BoschAlarmEntity): + """A base entity for area related entities within a bosch alarm panel.""" + + def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None: + """Set up a output related entity for a bosch alarm panel.""" + super().__init__(panel, unique_id) + self._output_id = output_id + self._output = panel.outputs[output_id] + self._output_unique_id = f"{unique_id}_output_{output_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._output_unique_id)}, + name=self._output.name, + manufacturer="Bosch Security Systems", + via_device=(DOMAIN, unique_id), + ) + + async def async_added_to_hass(self) -> None: + """Observe state changes.""" + await super().async_added_to_hass() + self._output.status_observer.attach(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Stop observing state changes.""" + await super().async_added_to_hass() + self._output.status_observer.detach(self.schedule_update_ha_state) diff --git a/homeassistant/components/bosch_alarm/icons.json b/homeassistant/components/bosch_alarm/icons.json index 1e207310713..44a94fdc570 100644 --- a/homeassistant/components/bosch_alarm/icons.json +++ b/homeassistant/components/bosch_alarm/icons.json @@ -2,7 +2,27 @@ "entity": { "sensor": { "faulting_points": { - "default": "mdi:alert-circle-outline" + "default": "mdi:alert-circle" + } + }, + "switch": { + "locked": { + "default": "mdi:lock", + "state": { + "off": "mdi:lock-open" + } + }, + "secured": { + "default": "mdi:lock", + "state": { + "off": "mdi:lock-open" + } + }, + "cycling": { + "default": "mdi:lock", + "state": { + "on": "mdi:lock-open" + } } } } diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 6b916dad4fa..4e71d14fe4a 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -54,9 +54,23 @@ }, "authentication_failed": { "message": "Incorrect credentials for panel." + }, + "incorrect_door_state": { + "message": "Door cannot be manipulated while it is being cycled." } }, "entity": { + "switch": { + "secured": { + "name": "Secured" + }, + "cycling": { + "name": "Cycling" + }, + "locked": { + "name": "Locked" + } + }, "sensor": { "faulting_points": { "name": "Faulting points", diff --git a/homeassistant/components/bosch_alarm/switch.py b/homeassistant/components/bosch_alarm/switch.py new file mode 100644 index 00000000000..9d6e48d591d --- /dev/null +++ b/homeassistant/components/bosch_alarm/switch.py @@ -0,0 +1,150 @@ +"""Support for Bosch Alarm Panel outputs and doors as switches.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from bosch_alarm_mode2 import Panel +from bosch_alarm_mode2.panel import Door + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import BoschAlarmConfigEntry +from .const import DOMAIN +from .entity import BoschAlarmDoorEntity, BoschAlarmOutputEntity + + +@dataclass(kw_only=True, frozen=True) +class BoschAlarmSwitchEntityDescription(SwitchEntityDescription): + """Describes Bosch Alarm door entity.""" + + value_fn: Callable[[Door], bool] + on_fn: Callable[[Panel, int], Coroutine[Any, Any, None]] + off_fn: Callable[[Panel, int], Coroutine[Any, Any, None]] + + +DOOR_SWITCH_TYPES: list[BoschAlarmSwitchEntityDescription] = [ + BoschAlarmSwitchEntityDescription( + key="locked", + translation_key="locked", + value_fn=lambda door: door.is_locked(), + on_fn=lambda panel, door_id: panel.door_relock(door_id), + off_fn=lambda panel, door_id: panel.door_unlock(door_id), + ), + BoschAlarmSwitchEntityDescription( + key="secured", + translation_key="secured", + value_fn=lambda door: door.is_secured(), + on_fn=lambda panel, door_id: panel.door_secure(door_id), + off_fn=lambda panel, door_id: panel.door_unsecure(door_id), + ), + BoschAlarmSwitchEntityDescription( + key="cycling", + translation_key="cycling", + value_fn=lambda door: door.is_cycling(), + on_fn=lambda panel, door_id: panel.door_cycle(door_id), + off_fn=lambda panel, door_id: panel.door_relock(door_id), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BoschAlarmConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up switch entities for outputs.""" + + panel = config_entry.runtime_data + entities: list[SwitchEntity] = [ + PanelOutputEntity( + panel, output_id, config_entry.unique_id or config_entry.entry_id + ) + for output_id in panel.outputs + ] + + entities.extend( + PanelDoorEntity( + panel, + door_id, + config_entry.unique_id or config_entry.entry_id, + entity_description, + ) + for door_id in panel.doors + for entity_description in DOOR_SWITCH_TYPES + ) + + async_add_entities(entities) + + +PARALLEL_UPDATES = 0 + + +class PanelDoorEntity(BoschAlarmDoorEntity, SwitchEntity): + """A switch entity for a door on a bosch alarm panel.""" + + entity_description: BoschAlarmSwitchEntityDescription + + def __init__( + self, + panel: Panel, + door_id: int, + unique_id: str, + entity_description: BoschAlarmSwitchEntityDescription, + ) -> None: + """Set up a switch entity for a door on a bosch alarm panel.""" + super().__init__(panel, door_id, unique_id) + self.entity_description = entity_description + self._attr_unique_id = f"{self._door_unique_id}_{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return the value function.""" + return self.entity_description.value_fn(self._door) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Run the on function.""" + # If the door is currently cycling, we can't send it any other commands until it is done + if self._door.is_cycling(): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="incorrect_door_state" + ) + await self.entity_description.on_fn(self.panel, self._door_id) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Run the off function.""" + # If the door is currently cycling, we can't send it any other commands until it is done + if self._door.is_cycling(): + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="incorrect_door_state" + ) + await self.entity_description.off_fn(self.panel, self._door_id) + + +class PanelOutputEntity(BoschAlarmOutputEntity, SwitchEntity): + """An output entity for a bosch alarm panel.""" + + _attr_name = None + + def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None: + """Set up an output entity for a bosch alarm panel.""" + super().__init__(panel, output_id, unique_id) + self._attr_unique_id = self._output_unique_id + + @property + def is_on(self) -> bool: + """Check if this entity is on.""" + return self._output.is_active() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on this output.""" + await self.panel.set_output_active(self._output_id) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off this output.""" + await self.panel.set_output_inactive(self._output_id) diff --git a/tests/components/bosch_alarm/conftest.py b/tests/components/bosch_alarm/conftest.py index 02ec592d061..76bb896daf5 100644 --- a/tests/components/bosch_alarm/conftest.py +++ b/tests/components/bosch_alarm/conftest.py @@ -118,6 +118,8 @@ def door() -> Generator[Door]: mock.name = "Main Door" mock.status_observer = AsyncMock(spec=Observable) mock.is_open.return_value = False + mock.is_cycling.return_value = False + mock.is_secured.return_value = False mock.is_locked.return_value = True return mock diff --git a/tests/components/bosch_alarm/snapshots/test_switch.ambr b/tests/components/bosch_alarm/snapshots/test_switch.ambr new file mode 100644 index 00000000000..079e765c35c --- /dev/null +++ b/tests/components/bosch_alarm/snapshots/test_switch.ambr @@ -0,0 +1,565 @@ +# serializer version: 1 +# name: test_switch[amax_3000][switch.main_door_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cycling', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[amax_3000][switch.main_door_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Cycling', + }), + 'context': , + 'entity_id': 'switch.main_door_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[amax_3000][switch.main_door_locked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_locked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Locked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'locked', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[amax_3000][switch.main_door_locked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Locked', + }), + 'context': , + 'entity_id': 'switch.main_door_locked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[amax_3000][switch.main_door_secured-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_secured', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Secured', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'secured', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[amax_3000][switch.main_door_secured-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Secured', + }), + 'context': , + 'entity_id': 'switch.main_door_secured', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[amax_3000][switch.output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.output_a', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[amax_3000][switch.output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Output A', + }), + 'context': , + 'entity_id': 'switch.output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[b5512][switch.main_door_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cycling', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[b5512][switch.main_door_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Cycling', + }), + 'context': , + 'entity_id': 'switch.main_door_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[b5512][switch.main_door_locked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_locked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Locked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'locked', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[b5512][switch.main_door_locked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Locked', + }), + 'context': , + 'entity_id': 'switch.main_door_locked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[b5512][switch.main_door_secured-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_secured', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Secured', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'secured', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[b5512][switch.main_door_secured-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Secured', + }), + 'context': , + 'entity_id': 'switch.main_door_secured', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[b5512][switch.output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.output_a', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[b5512][switch.output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Output A', + }), + 'context': , + 'entity_id': 'switch.output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[solution_3000][switch.main_door_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cycling', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '1234567890_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[solution_3000][switch.main_door_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Cycling', + }), + 'context': , + 'entity_id': 'switch.main_door_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[solution_3000][switch.main_door_locked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_locked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Locked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'locked', + 'unique_id': '1234567890_door_1_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[solution_3000][switch.main_door_locked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Locked', + }), + 'context': , + 'entity_id': 'switch.main_door_locked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[solution_3000][switch.main_door_secured-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_secured', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Secured', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'secured', + 'unique_id': '1234567890_door_1_secured', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[solution_3000][switch.main_door_secured-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Secured', + }), + 'context': , + 'entity_id': 'switch.main_door_secured', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[solution_3000][switch.output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.output_a', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_output_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[solution_3000][switch.output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Output A', + }), + 'context': , + 'entity_id': 'switch.output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/bosch_alarm/test_switch.py b/tests/components/bosch_alarm/test_switch.py new file mode 100644 index 00000000000..6f25624dcbb --- /dev/null +++ b/tests/components/bosch_alarm/test_switch.py @@ -0,0 +1,147 @@ +"""Tests for Bosch Alarm component.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import call_observable, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.bosch_alarm.PLATFORMS", [Platform.SWITCH]): + yield + + +async def test_update_switch_device( + hass: HomeAssistant, + mock_panel: AsyncMock, + output: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that output state changes after turning on the output.""" + await setup_integration(hass, mock_config_entry) + entity_id = "switch.output_a" + assert hass.states.get(entity_id).state == STATE_OFF + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + output.is_active.return_value = True + await call_observable(hass, output.status_observer) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_unlock_door( + hass: HomeAssistant, + mock_panel: AsyncMock, + door: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that door state changes after unlocking the door.""" + await setup_integration(hass, mock_config_entry) + entity_id = "switch.main_door_locked" + assert hass.states.get(entity_id).state == STATE_ON + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + door.is_locked.return_value = False + door.is_open.return_value = True + await call_observable(hass, door.status_observer) + assert hass.states.get(entity_id).state == STATE_OFF + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + door.is_locked.return_value = True + door.is_open.return_value = False + await call_observable(hass, door.status_observer) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_secure_door( + hass: HomeAssistant, + mock_panel: AsyncMock, + door: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that door state changes after unlocking the door.""" + await setup_integration(hass, mock_config_entry) + entity_id = "switch.main_door_secured" + assert hass.states.get(entity_id).state == STATE_OFF + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + door.is_secured.return_value = True + await call_observable(hass, door.status_observer) + assert hass.states.get(entity_id).state == STATE_ON + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + door.is_secured.return_value = False + await call_observable(hass, door.status_observer) + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_cycle_door( + hass: HomeAssistant, + mock_panel: AsyncMock, + door: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that door state changes after unlocking the door.""" + await setup_integration(hass, mock_config_entry) + entity_id = "switch.main_door_cycling" + assert hass.states.get(entity_id).state == STATE_OFF + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + door.is_cycling.return_value = True + await call_observable(hass, door.status_observer) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_switch( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_panel: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the switch state.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 9537229c92ca3c72351555aaf3e355304cbd9f90 Mon Sep 17 00:00:00 2001 From: Ted van den Brink Date: Fri, 9 May 2025 18:23:50 +0200 Subject: [PATCH 0306/1175] Add status to whois (#141051) * Add status to whois component * Fix tests * Added translations for statuses * Convert status to enum * Fix tests, add test for status sensor --- homeassistant/components/whois/const.py | 28 ++++ homeassistant/components/whois/icons.json | 3 + homeassistant/components/whois/sensor.py | 36 ++++- homeassistant/components/whois/strings.json | 28 ++++ tests/components/whois/conftest.py | 4 +- .../whois/snapshots/test_diagnostics.ambr | 2 +- .../whois/snapshots/test_sensor.ambr | 132 ++++++++++++++++++ tests/components/whois/test_sensor.py | 3 + 8 files changed, 232 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/whois/const.py b/homeassistant/components/whois/const.py index f196053f48d..0b1d1717474 100644 --- a/homeassistant/components/whois/const.py +++ b/homeassistant/components/whois/const.py @@ -19,3 +19,31 @@ ATTR_EXPIRES = "expires" ATTR_NAME_SERVERS = "name_servers" ATTR_REGISTRAR = "registrar" ATTR_UPDATED = "updated" + +# Mapping of ICANN status codes to Home Assistant status types. +# From https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en +STATUS_TYPES = { + "addPeriod": "add_period", + "autoRenewPeriod": "auto_renew_period", + "inactive": "inactive", + "active": "active", + "pendingCreate": "pending_create", + "pendingRenew": "pending_renew", + "pendingRestore": "pending_restore", + "pendingTransfer": "pending_transfer", + "pendingUpdate": "pending_update", + "redemptionPeriod": "redemption_period", + "renewPeriod": "renew_period", + "serverDeleteProhibited": "server_delete_prohibited", + "serverHold": "server_hold", + "serverRenewProhibited": "server_renew_prohibited", + "serverTransferProhibited": "server_transfer_prohibited", + "serverUpdateProhibited": "server_update_prohibited", + "transferPeriod": "transfer_period", + "clientDeleteProhibited": "client_delete_prohibited", + "clientHold": "client_hold", + "clientRenewProhibited": "client_renew_prohibited", + "clientTransferProhibited": "client_transfer_prohibited", + "clientUpdateProhibited": "client_update_prohibited", + "ok": "ok", +} diff --git a/homeassistant/components/whois/icons.json b/homeassistant/components/whois/icons.json index 459ae252138..5ce1fb9717b 100644 --- a/homeassistant/components/whois/icons.json +++ b/homeassistant/components/whois/icons.json @@ -18,6 +18,9 @@ }, "reseller": { "default": "mdi:store" + }, + "status": { + "default": "mdi:check-circle" } } } diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 8098e052575..474ac366be2 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -25,7 +25,14 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util import dt as dt_util -from .const import ATTR_EXPIRES, ATTR_NAME_SERVERS, ATTR_REGISTRAR, ATTR_UPDATED, DOMAIN +from .const import ( + ATTR_EXPIRES, + ATTR_NAME_SERVERS, + ATTR_REGISTRAR, + ATTR_UPDATED, + DOMAIN, + STATUS_TYPES, +) @dataclass(frozen=True, kw_only=True) @@ -58,6 +65,24 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: return timestamp +def _get_status_type(status: str | None) -> str | None: + """Get the status type from the status string. + + Returns the status type in snake_case, so it can be used as a key for the translations. + E.g: "clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited" -> "client_delete_prohibited". + """ + if status is None: + return None + + # If the status is not in the STATUS_TYPES, return the status as is. + for icann_status, hass_status in STATUS_TYPES.items(): + if icann_status in status: + return hass_status + + # If the status is not in the STATUS_TYPES, return None. + return None + + SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( WhoisSensorEntityDescription( key="admin", @@ -121,6 +146,15 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, value_fn=lambda domain: getattr(domain, "reseller", None), ), + WhoisSensorEntityDescription( + key="status", + translation_key="status", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.ENUM, + options=list(STATUS_TYPES.values()), + entity_registry_enabled_default=False, + value_fn=lambda domain: _get_status_type(domain.status), + ), ) diff --git a/homeassistant/components/whois/strings.json b/homeassistant/components/whois/strings.json index 3b0f9dfd4d1..b236bb06208 100644 --- a/homeassistant/components/whois/strings.json +++ b/homeassistant/components/whois/strings.json @@ -47,6 +47,34 @@ }, "reseller": { "name": "Reseller" + }, + "status": { + "name": "Status", + "state": { + "add_period": "Add period", + "auto_renew_period": "Auto renew period", + "inactive": "Inactive", + "ok": "Active", + "active": "Active", + "pending_create": "Pending create", + "pending_renew": "Pending renew", + "pending_restore": "Pending restore", + "pending_transfer": "Pending transfer", + "pending_update": "Pending update", + "redemption_period": "Redemption period", + "renew_period": "Renew period", + "server_delete_prohibited": "Server delete prohibited", + "server_hold": "Server hold", + "server_renew_prohibited": "Server renew prohibited", + "server_transfer_prohibited": "Server transfer prohibited", + "server_update_prohibited": "Server update prohibited", + "transfer_period": "Transfer period", + "client_delete_prohibited": "Client delete prohibited", + "client_hold": "Client hold", + "client_renew_prohibited": "Client renew prohibited", + "client_transfer_prohibited": "Client transfer prohibited", + "client_update_prohibited": "Client update prohibited" + } } } } diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 4bb18581c1a..c4138a5d1d2 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -63,7 +63,7 @@ def mock_whois() -> Generator[MagicMock]: domain.registrant = "registrant@example.com" domain.registrar = "My Registrar" domain.reseller = "Top Domains, Low Prices" - domain.status = "OK" + domain.status = "ok" domain.statuses = ["OK"] yield whois_mock @@ -86,7 +86,7 @@ def mock_whois_missing_some_attrs() -> Generator[Mock]: self.name = "home-assistant.io" self.name_servers = ["ns1.example.com", "ns2.example.com"] self.registrar = "My Registrar" - self.status = "OK" + self.status = "ok" self.statuses = ["OK"] with patch( diff --git a/tests/components/whois/snapshots/test_diagnostics.ambr b/tests/components/whois/snapshots/test_diagnostics.ambr index f373a20700e..a498d0f88e9 100644 --- a/tests/components/whois/snapshots/test_diagnostics.ambr +++ b/tests/components/whois/snapshots/test_diagnostics.ambr @@ -5,7 +5,7 @@ 'dnssec': True, 'expiration_date': '2023-01-01T00:00:00', 'last_updated': '2022-01-01T00:00:00+01:00', - 'status': 'OK', + 'status': 'ok', 'statuses': list([ 'OK', ]), diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index b5b1dde1c3d..61499ba0f9d 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -727,6 +727,138 @@ 'via_device_id': None, }) # --- +# name: test_whois_sensors[sensor.home_assistant_io_status] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'home-assistant.io Status', + 'options': list([ + 'add_period', + 'auto_renew_period', + 'inactive', + 'active', + 'pending_create', + 'pending_renew', + 'pending_restore', + 'pending_transfer', + 'pending_update', + 'redemption_period', + 'renew_period', + 'server_delete_prohibited', + 'server_hold', + 'server_renew_prohibited', + 'server_transfer_prohibited', + 'server_update_prohibited', + 'transfer_period', + 'client_delete_prohibited', + 'client_hold', + 'client_renew_prohibited', + 'client_transfer_prohibited', + 'client_update_prohibited', + 'ok', + ]), + }), + 'context': , + 'entity_id': 'sensor.home_assistant_io_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_whois_sensors[sensor.home_assistant_io_status].1 + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'add_period', + 'auto_renew_period', + 'inactive', + 'active', + 'pending_create', + 'pending_renew', + 'pending_restore', + 'pending_transfer', + 'pending_update', + 'redemption_period', + 'renew_period', + 'server_delete_prohibited', + 'server_hold', + 'server_renew_prohibited', + 'server_transfer_prohibited', + 'server_update_prohibited', + 'transfer_period', + 'client_delete_prohibited', + 'client_hold', + 'client_renew_prohibited', + 'client_transfer_prohibited', + 'client_update_prohibited', + 'ok', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.home_assistant_io_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': 'Status', + 'platform': 'whois', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'home-assistant.io_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_whois_sensors[sensor.home_assistant_io_status].2 + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'whois', + 'home-assistant.io', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'home-assistant.io', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_whois_sensors_missing_some_attrs StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index d290bc347a9..69e32d923c4 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -32,6 +32,7 @@ pytestmark = [ "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", + "sensor.home_assistant_io_status", ], ) async def test_whois_sensors( @@ -73,6 +74,7 @@ async def test_whois_sensors_missing_some_attrs( "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", + "sensor.home_assistant_io_status", ], ) async def test_disabled_by_default_sensors( @@ -98,6 +100,7 @@ async def test_disabled_by_default_sensors( "sensor.home_assistant_io_registrant", "sensor.home_assistant_io_registrar", "sensor.home_assistant_io_reseller", + "sensor.home_assistant_io_status", ], ) async def test_no_data( From ad6f66c9458a308ba167a50df00b2d118f8e8526 Mon Sep 17 00:00:00 2001 From: TimL Date: Sat, 10 May 2025 02:31:00 +1000 Subject: [PATCH 0307/1175] Allow dns hostnames to be retained for SMLIGHT user flow. (#142514) * Dont overwrite host with local IP * adjust test for user flow change --- homeassistant/components/smlight/config_flow.py | 2 -- tests/components/smlight/conftest.py | 1 + tests/components/smlight/test_config_flow.py | 16 +++++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smlight/config_flow.py b/homeassistant/components/smlight/config_flow.py index ce4f8f43233..39750bdc422 100644 --- a/homeassistant/components/smlight/config_flow.py +++ b/homeassistant/components/smlight/config_flow.py @@ -53,7 +53,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): try: if not await self._async_check_auth_required(user_input): info = await self.client.get_info() - self._host = str(info.device_ip) self._device_name = str(info.hostname) if info.model not in Devices: @@ -79,7 +78,6 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN): try: if not await self._async_check_auth_required(user_input): info = await self.client.get_info() - self._host = str(info.device_ip) self._device_name = str(info.hostname) if info.model not in Devices: diff --git a/tests/components/smlight/conftest.py b/tests/components/smlight/conftest.py index 7a1b16f1d6b..6c056c95fd9 100644 --- a/tests/components/smlight/conftest.py +++ b/tests/components/smlight/conftest.py @@ -21,6 +21,7 @@ from tests.common import ( MOCK_DEVICE_NAME = "slzb-06" MOCK_HOST = "192.168.1.161" +MOCK_HOSTNAME = "slzb-06p7.lan" MOCK_USERNAME = "test-user" MOCK_PASSWORD = "test-pass" diff --git a/tests/components/smlight/test_config_flow.py b/tests/components/smlight/test_config_flow.py index 4ecfe9366e3..497cb8d9484 100644 --- a/tests/components/smlight/test_config_flow.py +++ b/tests/components/smlight/test_config_flow.py @@ -15,7 +15,13 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from .conftest import MOCK_DEVICE_NAME, MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME +from .conftest import ( + MOCK_DEVICE_NAME, + MOCK_HOST, + MOCK_HOSTNAME, + MOCK_PASSWORD, + MOCK_USERNAME, +) from tests.common import MockConfigEntry @@ -53,14 +59,14 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: "slzb-06p7.local", + CONF_HOST: MOCK_HOSTNAME, }, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SLZB-06p7" assert result2["data"] == { - CONF_HOST: MOCK_HOST, + CONF_HOST: MOCK_HOSTNAME, } assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 @@ -82,7 +88,7 @@ async def test_user_flow_auth( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: "slzb-06p7.local", + CONF_HOST: MOCK_HOSTNAME, }, ) assert result2["type"] is FlowResultType.FORM @@ -100,7 +106,7 @@ async def test_user_flow_auth( assert result3["data"] == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, - CONF_HOST: MOCK_HOST, + CONF_HOST: MOCK_HOSTNAME, } assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 From 87bd6e3ca0e844492432f33551975e6d04d10954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 9 May 2025 18:40:56 +0200 Subject: [PATCH 0308/1175] Matter pump fixture (#144572) * Create pump.json * Add pump fixture * Add snapshots --- tests/components/matter/conftest.py | 1 + .../matter/fixtures/nodes/pump.json | 271 ++++++++++++++++++ .../matter/snapshots/test_number.ambr | 56 ++++ .../matter/snapshots/test_sensor.ambr | 155 ++++++++++ .../matter/snapshots/test_switch.ambr | 48 ++++ 5 files changed, 531 insertions(+) create mode 100644 tests/components/matter/fixtures/nodes/pump.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 734369a3bb2..6cd5d703e44 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -105,6 +105,7 @@ async def integration_fixture( "onoff_light_alt_name", "onoff_light_no_name", "onoff_light_with_levelcontrol_present", + "pump", "pressure_sensor", "room_airconditioner", "silabs_dishwasher", diff --git a/tests/components/matter/fixtures/nodes/pump.json b/tests/components/matter/fixtures/nodes/pump.json new file mode 100644 index 00000000000..39579f4448c --- /dev/null +++ b/tests/components/matter/fixtures/nodes/pump.json @@ -0,0 +1,271 @@ +{ + "node_id": 3, + "date_commissioned": "2025-05-09T15:45:16.457511", + "last_interview": "2025-05-09T15:49:41.414681", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 45, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Mock Pump", + "0/40/4": 32768, + "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/18": "C7C87250EABB7BC8", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 5, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 18, 19, 21, 22, 24, 65532, 65533, 65528, + 65529, 65531 + ], + "0/45/65532": 0, + "0/45/65533": 2, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkLWHXRl", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZARgk66TFlR1w==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 282, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [0, 1, 2, 8, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRAxgkBwEkCAEwCUEE3Z+JMyIjVAtmzqwEaVxp1V6SNzKfmJT0691W905Zr2Sv2fSCu0OMmvZAt1ih58GZj9MTRYM4Up3sJF481rks+zcKNQEoARgkAgE2AwQCBAEYMAQUjivV8lU5bIctgqrN/Mb2xBPB6XwwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0CrPeCxivaBtn7q7Pcj7JvVWdN2JAZ+lVlL08Uix9hjOCShJntfL6j+LFRKPQ1elgp2E3DO/jvkSAEFmAzXp8zOGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BBmX+KwLR5HGlVNbvlC+dO8Jv9fPthHiTfGpUzi2JJADX5az6GxBAFn02QKHwLcZHyh+lh9faf6rf38/nPYF7/M=", + "2": 4939, + "3": 2, + "4": 3, + "5": "ha-freebox", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEGZf4rAtHkcaVU1u+UL507wm/18+2EeJN8alTOLYkkANflrPobEEAWfTZAofAtxkfKH6WH19p/qt/fz+c9gXv8zcKNQEpARgkAmAwBBT0+qfdyShnG+4Pq01pwOnrxdhHRjAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQPVrsFnfFplsQGV5m5EUua+rmo9hAr+OP1bvaifdLqiEIn3uXLTLoKmVUkPImRL2Fb+xcMEAqR2p7RM6ZlFCR20Y" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "0/62/65528": [1, 3, 5, 8, 14], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11, 12, 13], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 6, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "1/6/0": true, + "1/6/65532": 0, + "1/6/65533": 5, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "1/8/0": 254, + "1/8/15": 0, + "1/8/17": 0, + "1/8/65532": 0, + "1/8/65533": 6, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65531": [0, 15, 17, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 771, + "1": 1 + } + ], + "1/29/1": [3, 6, 8, 29, 512, 1026, 1027, 1028], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "1/512/0": 32767, + "1/512/1": 65534, + "1/512/2": 65534, + "1/512/16": 5, + "1/512/17": 0, + "1/512/18": 5, + "1/512/19": null, + "1/512/20": 1000, + "1/512/32": 0, + "1/512/33": 5, + "1/512/65532": 0, + "1/512/65533": 6, + "1/512/65528": [], + "1/512/65529": [], + "1/512/65531": [ + 0, 1, 2, 16, 17, 18, 19, 20, 32, 33, 65532, 65533, 65528, 65529, 65531 + ], + "1/1026/0": 6000, + "1/1026/1": -27315, + "1/1026/2": 32767, + "1/1026/65532": 0, + "1/1026/65533": 6, + "1/1026/65528": [], + "1/1026/65529": [], + "1/1026/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "1/1027/0": 100, + "1/1027/1": -32767, + "1/1027/2": 32767, + "1/1027/65532": 0, + "1/1027/65533": 6, + "1/1027/65528": [], + "1/1027/65529": [], + "1/1027/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "1/1028/0": 50, + "1/1028/1": 0, + "1/1028/2": 65534, + "1/1028/65532": 0, + "1/1028/65533": 6, + "1/1028/65528": [], + "1/1028/65529": [], + "1/1028/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index 03006b2210c..eb0a12bfc4d 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -1700,3 +1700,59 @@ 'state': '0.0', }) # --- +# name: test_numbers[pump][number.mock_pump_on_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 255, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_pump_on_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'On level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'on_level', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-on_level-8-17', + 'unit_of_measurement': None, + }) +# --- +# name: test_numbers[pump][number.mock_pump_on_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Pump On level', + 'max': 255, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.mock_pump_on_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 6c00dc5cede..9c44be97bc9 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2691,6 +2691,161 @@ 'state': '0.0', }) # --- +# name: test_sensors[pump][sensor.mock_pump_flow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_pump_flow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Flow', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'flow', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-FlowSensor-1028-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_flow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Pump Flow', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_pump_flow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_pump_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pressure', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PressureSensor-1027-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Mock Pump Pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_pump_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_pump_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': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-TemperatureSensor-1026-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Pump Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_pump_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- # name: test_sensors[room_airconditioner][sensor.room_airconditioner_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index f80aaefbf91..e37b3d9f2b4 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -478,6 +478,54 @@ 'state': 'off', }) # --- +# name: test_switches[pump][switch.mock_pump_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_pump_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[pump][switch.mock_pump_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Pump Power', + }), + 'context': , + 'entity_id': 'switch.mock_pump_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_switches[room_airconditioner][switch.room_airconditioner_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 356775c19b1e8523345b7c765dcc7b798cab3cf2 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sat, 10 May 2025 00:44:43 +0800 Subject: [PATCH 0309/1175] Add water flowing status for YoLink water meter(YS5018). (#144535) * Add water flowing status for YoLink water meter(YS5018). * Fixes --- .../components/yolink/binary_sensor.py | 28 ++++++++++++++++--- homeassistant/components/yolink/const.py | 2 ++ .../components/yolink/coordinator.py | 5 ++++ homeassistant/components/yolink/entity.py | 2 +- homeassistant/components/yolink/icons.json | 5 ++++ homeassistant/components/yolink/strings.json | 3 ++ 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 30c04d3a424..e5200c66afd 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -25,7 +25,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from .const import ( + DEV_MODEL_WATER_METER_YS5018_EC, + DEV_MODEL_WATER_METER_YS5018_UC, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -37,6 +41,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True state_key: str = "state" value: Callable[[Any], bool | None] = lambda _: None + should_update_entity: Callable = lambda state: True SENSOR_DEVICE_TYPE = [ @@ -95,6 +100,17 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER ), ), + YoLinkBinarySensorEntityDescription( + key="water_running", + translation_key="water_running", + value=lambda state: state.get("waterFlowing") if state is not None else None, + should_update_entity=lambda value: value is not None, + exists_fn=lambda device: ( + device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER + and device.device_model_name + in [DEV_MODEL_WATER_METER_YS5018_EC, DEV_MODEL_WATER_METER_YS5018_UC] + ), + ), ) @@ -141,9 +157,13 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): @callback def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" - self._attr_is_on = self.entity_description.value( - state.get(self.entity_description.state_key) - ) + if ( + _attr_val := self.entity_description.value( + state.get(self.entity_description.state_key) + ) + ) is None or self.entity_description.should_update_entity(_attr_val) is False: + return + self._attr_is_on = _attr_val self.async_write_ha_state() @property diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 8879ef15125..960bf8568d4 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -37,3 +37,5 @@ DEV_MODEL_SWITCH_YS5708_UC = "YS5708-UC" DEV_MODEL_SWITCH_YS5708_EC = "YS5708-EC" DEV_MODEL_SWITCH_YS5709_UC = "YS5709-UC" DEV_MODEL_SWITCH_YS5709_EC = "YS5709-EC" +DEV_MODEL_WATER_METER_YS5018_EC = "YS5018-EC" +DEV_MODEL_WATER_METER_YS5018_UC = "YS5018-UC" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index d18a37bd276..8fd450df4a5 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -76,6 +76,11 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): except YoLinkAuthFailError as yl_auth_err: raise ConfigEntryAuthFailed from yl_auth_err except YoLinkClientError as yl_client_err: + _LOGGER.error( + "Failed to obtain device status, device: %s, error: %s ", + self.device.device_id, + yl_client_err, + ) raise UpdateFailed from yl_client_err if device_state is not None: return device_state diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 0f500b72404..7828bf91541 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -45,7 +45,7 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def _handle_coordinator_update(self) -> None: """Update state.""" data = self.coordinator.data - if data is not None: + if data is not None and len(data) > 0: self.update_entity_state(data) @property diff --git a/homeassistant/components/yolink/icons.json b/homeassistant/components/yolink/icons.json index c58d219a2e0..6d9062a92b8 100644 --- a/homeassistant/components/yolink/icons.json +++ b/homeassistant/components/yolink/icons.json @@ -1,5 +1,10 @@ { "entity": { + "binary_sensor": { + "water_running": { + "default": "mdi:waves-arrow-right" + } + }, "number": { "config_volume": { "default": "mdi:volume-high" diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 8867457342f..825f9e3e619 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -44,6 +44,9 @@ } }, "entity": { + "binary_sensor": { + "water_running": { "name": "Water is flowing" } + }, "switch": { "usb_ports": { "name": "USB ports" }, "plug_1": { "name": "Plug 1" }, From e892744328e4fde0f216a9383c72cc103b763a9c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 9 May 2025 18:46:32 +0200 Subject: [PATCH 0310/1175] Reolink fix privacy mode availability for NVR IPC cams (#144569) * Correct "available" for IPC cams * Check privacy mode when updating --- homeassistant/components/reolink/entity.py | 9 ++++++++- homeassistant/components/reolink/host.py | 7 ++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 3325eab6f42..f2a0b20994a 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -198,7 +198,14 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._host.api.camera_online(self._channel) + if self.entity_description.always_available: + return True + + return ( + super().available + and self._host.api.camera_online(self._channel) + and not self._host.api.baichuan.privacy_mode(self._channel) + ) def register_callback(self, callback_id: str, cmd_id: int) -> None: """Register callback for TCP push events.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a027177f1fc..378c167d469 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -465,10 +465,11 @@ class ReolinkHost: wake = True self.last_wake = time() + for channel in self._api.channels: + if self._api.baichuan.privacy_mode(channel): + await self._api.baichuan.get_privacy_mode(channel) if self._api.baichuan.privacy_mode(): - await self._api.baichuan.get_privacy_mode() - if self._api.baichuan.privacy_mode(): - return # API is shutdown, no need to check states + return # API is shutdown, no need to check states await self._api.get_states(cmd_list=self.update_cmd, wake=wake) From e29fc37bb1eb2238dce9cd5d7b66d4e09e355b25 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Fri, 9 May 2025 18:47:18 +0200 Subject: [PATCH 0311/1175] Use device and entity name for OpenWeather map entities (#144513) * Use entity name * Update snapshot with expected chnages --- .../components/openweathermap/sensor.py | 5 +- .../components/openweathermap/weather.py | 5 +- .../openweathermap/snapshots/test_sensor.ambr | 128 +++++++++--------- .../snapshots/test_weather.ambr | 12 +- 4 files changed, 75 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 15935556ef3..e37ff678708 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -45,7 +45,6 @@ from .const import ( ATTR_API_WIND_BEARING, ATTR_API_WIND_SPEED, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, OWM_MODE_FREE_FORECAST, @@ -189,6 +188,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): _attr_should_poll = False _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -201,13 +201,12 @@ class AbstractOpenWeatherMapSensor(SensorEntity): self.entity_description = description self._coordinator = coordinator - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{unique_id}-{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, ) @property diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 12d883c871a..d6cdee77ce9 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -79,6 +79,8 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina _attr_attribution = ATTRIBUTION _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = None _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS _attr_native_pressure_unit = UnitOfPressure.HPA @@ -95,13 +97,12 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina ) -> None: """Initialize the sensor.""" super().__init__(weather_coordinator) - self._attr_name = name self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, ) self.mode = mode diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index 1f416f76578..7b0cf4fbf99 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -15,7 +15,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_cloud_coverage', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -26,7 +26,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Cloud coverage', + 'original_name': 'Cloud coverage', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -65,7 +65,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_condition', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -76,7 +76,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Condition', + 'original_name': 'Condition', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -115,7 +115,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_dew_point', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -126,7 +126,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Dew Point', + 'original_name': 'Dew Point', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -168,7 +168,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_feels_like_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -179,7 +179,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Feels like temperature', + 'original_name': 'Feels like temperature', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -221,7 +221,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_humidity', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -232,7 +232,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Humidity', + 'original_name': 'Humidity', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -272,7 +272,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_precipitation_kind', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -283,7 +283,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Precipitation kind', + 'original_name': 'Precipitation kind', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -322,7 +322,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_pressure', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -333,7 +333,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Pressure', + 'original_name': 'Pressure', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -375,7 +375,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_rain', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -386,7 +386,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Rain', + 'original_name': 'Rain', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -428,7 +428,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_snow', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -439,7 +439,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Snow', + 'original_name': 'Snow', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -481,7 +481,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -492,7 +492,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Temperature', + 'original_name': 'Temperature', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -534,7 +534,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_uv_index', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -545,7 +545,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap UV Index', + 'original_name': 'UV Index', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -586,7 +586,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_visibility', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -597,7 +597,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Visibility', + 'original_name': 'Visibility', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -637,7 +637,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_weather', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -648,7 +648,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Weather', + 'original_name': 'Weather', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -685,7 +685,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_weather_code', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -696,7 +696,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Weather Code', + 'original_name': 'Weather Code', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -735,7 +735,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_wind_bearing', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -746,7 +746,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Wind bearing', + 'original_name': 'Wind bearing', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -788,7 +788,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_wind_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -802,7 +802,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Wind speed', + 'original_name': 'Wind speed', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -844,7 +844,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_cloud_coverage', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -855,7 +855,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Cloud coverage', + 'original_name': 'Cloud coverage', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -894,7 +894,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_condition', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -905,7 +905,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Condition', + 'original_name': 'Condition', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -944,7 +944,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_dew_point', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -955,7 +955,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Dew Point', + 'original_name': 'Dew Point', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -997,7 +997,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_feels_like_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1008,7 +1008,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Feels like temperature', + 'original_name': 'Feels like temperature', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1050,7 +1050,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_humidity', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1061,7 +1061,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Humidity', + 'original_name': 'Humidity', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1101,7 +1101,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_precipitation_kind', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1112,7 +1112,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Precipitation kind', + 'original_name': 'Precipitation kind', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1151,7 +1151,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_pressure', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1162,7 +1162,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Pressure', + 'original_name': 'Pressure', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1204,7 +1204,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_rain', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1215,7 +1215,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Rain', + 'original_name': 'Rain', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1257,7 +1257,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_snow', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1268,7 +1268,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Snow', + 'original_name': 'Snow', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1310,7 +1310,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1321,7 +1321,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Temperature', + 'original_name': 'Temperature', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1363,7 +1363,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_uv_index', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1374,7 +1374,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap UV Index', + 'original_name': 'UV Index', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1415,7 +1415,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_visibility', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1426,7 +1426,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Visibility', + 'original_name': 'Visibility', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1466,7 +1466,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_weather', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1477,7 +1477,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Weather', + 'original_name': 'Weather', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1514,7 +1514,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_weather_code', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1525,7 +1525,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap Weather Code', + 'original_name': 'Weather Code', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1564,7 +1564,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_wind_bearing', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1575,7 +1575,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Wind bearing', + 'original_name': 'Wind bearing', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -1617,7 +1617,7 @@ 'domain': 'sensor', 'entity_category': None, 'entity_id': 'sensor.openweathermap_wind_speed', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -1631,7 +1631,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'openweathermap Wind speed', + 'original_name': 'Wind speed', 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index 90f583d9db1..1d77d9179a5 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -37,7 +37,7 @@ 'domain': 'weather', 'entity_category': None, 'entity_id': 'weather.openweathermap', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -48,7 +48,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap', + 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': 0, @@ -98,7 +98,7 @@ 'domain': 'weather', 'entity_category': None, 'entity_id': 'weather.openweathermap', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -109,7 +109,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap', + 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': , @@ -160,7 +160,7 @@ 'domain': 'weather', 'entity_category': None, 'entity_id': 'weather.openweathermap', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -171,7 +171,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'openweathermap', + 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, 'supported_features': , From ad7cfe49c81bdd67d4ddbb3ff20192b0d41f051c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Fri, 9 May 2025 18:49:22 +0200 Subject: [PATCH 0312/1175] Airthings DHCP discovery (#144280) * Add DHCP to Airthings manifest * Update manifest * Update manifest * Add tests * Fix pr comments * fix naming for all tests * Fix pr comment --- .../components/airthings/manifest.json | 13 +++ homeassistant/generated/dhcp.py | 14 +++ .../components/airthings/test_config_flow.py | 102 +++++++++++++++--- 3 files changed, 116 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airthings/manifest.json b/homeassistant/components/airthings/manifest.json index 67057ff09f5..5204d7a4ba8 100644 --- a/homeassistant/components/airthings/manifest.json +++ b/homeassistant/components/airthings/manifest.json @@ -3,6 +3,19 @@ "name": "Airthings", "codeowners": ["@danielhiversen", "@LaStrada"], "config_flow": true, + "dhcp": [ + { + "hostname": "airthings-view" + }, + { + "hostname": "airthings-hub", + "macaddress": "D0141190*" + }, + { + "hostname": "airthings-hub", + "macaddress": "70B3D52A0*" + } + ], "documentation": "https://www.home-assistant.io/integrations/airthings", "iot_class": "cloud_polling", "loggers": ["airthings"], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 26302b0ac8b..94f5e06bf54 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -8,6 +8,20 @@ from __future__ import annotations from typing import Final DHCP: Final[list[dict[str, str | bool]]] = [ + { + "domain": "airthings", + "hostname": "airthings-view", + }, + { + "domain": "airthings", + "hostname": "airthings-hub", + "macaddress": "D0141190*", + }, + { + "domain": "airthings", + "hostname": "airthings-hub", + "macaddress": "70B3D52A0*", + }, { "domain": "airzone", "macaddress": "E84F25*", diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 081e1bfd86d..a96fe33c9d0 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -3,12 +3,14 @@ from unittest.mock import patch import airthings +import pytest from homeassistant import config_entries from homeassistant.components.airthings.const import CONF_SECRET, DOMAIN from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -17,6 +19,24 @@ TEST_DATA = { CONF_SECRET: "secret", } +DHCP_SERVICE_INFO = [ + DhcpServiceInfo( + hostname="airthings-view", + ip="192.168.1.100", + macaddress="00:00:00:00:00:00", + ), + DhcpServiceInfo( + hostname="airthings-hub", + ip="192.168.1.101", + macaddress="D0:14:11:90:00:00", + ), + DhcpServiceInfo( + hostname="airthings-hub", + ip="192.168.1.102", + macaddress="70:B3:D5:2A:00:00", + ), +] + async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -37,15 +57,15 @@ async def test_form(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Airthings" - assert result2["data"] == TEST_DATA + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Airthings" + assert result["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -59,13 +79,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: "airthings.get_token", side_effect=airthings.AirthingsAuthError, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -78,13 +98,13 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: "airthings.get_token", side_effect=airthings.AirthingsConnectionError, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} async def test_form_unknown_error(hass: HomeAssistant) -> None: @@ -97,13 +117,13 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: "airthings.get_token", side_effect=Exception, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: @@ -123,3 +143,59 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("dhcp_service_info", DHCP_SERVICE_INFO) +async def test_dhcp_flow( + hass: HomeAssistant, dhcp_service_info: DhcpServiceInfo +) -> None: + """Test the DHCP discovery flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=dhcp_service_info, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + with ( + patch( + "homeassistant.components.airthings.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + patch( + "airthings.get_token", + return_value="test_token", + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Airthings" + assert result["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_flow_hub_already_configured(hass: HomeAssistant) -> None: + """Test that DHCP discovery fails when already configured.""" + + first_entry = MockConfigEntry( + domain="airthings", + data=TEST_DATA, + unique_id=TEST_DATA[CONF_ID], + ) + first_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO[0], + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From cac0e0f6e8d3ce53febb88c1829eac31b2728577 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Fri, 9 May 2025 12:50:55 -0400 Subject: [PATCH 0313/1175] Don't scale Roborock mop Path (#144421) don't scale mop path --- homeassistant/components/roborock/coordinator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 2439a4f904a..dc0677b25d2 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -28,7 +28,7 @@ from roborock.version_a01_apis import RoborockClientA01 from roborock.web_api import RoborockApiClient from vacuum_map_parser_base.config.color import ColorsPalette from vacuum_map_parser_base.config.image_config import ImageConfig -from vacuum_map_parser_base.config.size import Sizes +from vacuum_map_parser_base.config.size import Size, Sizes from vacuum_map_parser_base.map_data import MapData from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser @@ -148,7 +148,13 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ] self.map_parser = RoborockMapDataParser( ColorsPalette(), - Sizes({k: v * MAP_SCALE for k, v in Sizes.SIZES.items()}), + Sizes( + { + k: v * MAP_SCALE + for k, v in Sizes.SIZES.items() + if k != Size.MOP_PATH_WIDTH + } + ), drawables, ImageConfig(scale=MAP_SCALE), [], From ba8d40f7d36509cd9c6d81d8bcb46092d57fee7e Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Fri, 9 May 2025 18:51:57 +0200 Subject: [PATCH 0314/1175] Add homee fan platform (#143524) * Initial fan * add more tests * add last fan tests and small fixes * fix tests after latest change * another small correction * use common strings * add snapshot test * fix review comments * fix typing * remove uneeded None * remove unwanted file * fix turn_on function * typo * Use constants for preset modes. * fix review notes. --- homeassistant/components/homee/__init__.py | 1 + homeassistant/components/homee/const.py | 4 +- homeassistant/components/homee/fan.py | 134 ++++++++++++ homeassistant/components/homee/icons.json | 13 ++ homeassistant/components/homee/strings.json | 16 ++ tests/components/homee/fixtures/fan.json | 73 +++++++ .../components/homee/snapshots/test_fan.ambr | 63 ++++++ tests/components/homee/test_fan.py | 192 ++++++++++++++++++ 8 files changed, 495 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homee/fan.py create mode 100644 tests/components/homee/fixtures/fan.json create mode 100644 tests/components/homee/snapshots/test_fan.ambr create mode 100644 tests/components/homee/test_fan.py diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index fbd34743496..579704aea44 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -19,6 +19,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CLIMATE, Platform.COVER, + Platform.FAN, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, diff --git a/homeassistant/components/homee/const.py b/homeassistant/components/homee/const.py index 468fb2d49ac..7bc3de189d6 100644 --- a/homeassistant/components/homee/const.py +++ b/homeassistant/components/homee/const.py @@ -96,5 +96,7 @@ LIGHT_PROFILES = [ NodeProfile.WIFI_ON_OFF_DIMMABLE_METERING_SWITCH, ] -# Climate Presets +# Preset modes +PRESET_AUTO = "auto" PRESET_MANUAL = "manual" +PRESET_SUMMER = "summer" diff --git a/homeassistant/components/homee/fan.py b/homeassistant/components/homee/fan.py new file mode 100644 index 00000000000..d4694ee8d66 --- /dev/null +++ b/homeassistant/components/homee/fan.py @@ -0,0 +1,134 @@ +"""The Homee fan platform.""" + +import math +from typing import Any, cast + +from pyHomee.const import AttributeType, NodeProfile +from pyHomee.model import HomeeAttribute, HomeeNode + +from homeassistant.components.fan import FanEntity, FanEntityFeature +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) +from homeassistant.util.scaling import int_states_in_range + +from . import HomeeConfigEntry +from .const import DOMAIN, PRESET_AUTO, PRESET_MANUAL, PRESET_SUMMER +from .entity import HomeeNodeEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HomeeConfigEntry, + async_add_devices: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Homee fan platform.""" + + async_add_devices( + HomeeFan(node, config_entry) + for node in config_entry.runtime_data.nodes + if node.profile == NodeProfile.VENTILATION_CONTROL + ) + + +class HomeeFan(HomeeNodeEntity, FanEntity): + """Representation of a Homee fan entity.""" + + _attr_translation_key = DOMAIN + _attr_name = None + _attr_preset_modes = [PRESET_MANUAL, PRESET_AUTO, PRESET_SUMMER] + speed_range = (1, 8) + _attr_speed_count = int_states_in_range(speed_range) + + def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None: + """Initialize a Homee fan entity.""" + super().__init__(node, entry) + self._speed_attribute: HomeeAttribute = cast( + HomeeAttribute, node.get_attribute_by_type(AttributeType.VENTILATION_LEVEL) + ) + self._mode_attribute: HomeeAttribute = cast( + HomeeAttribute, node.get_attribute_by_type(AttributeType.VENTILATION_MODE) + ) + + @property + def supported_features(self) -> FanEntityFeature: + """Return the supported features based on preset_mode.""" + features = FanEntityFeature.PRESET_MODE + + if self.preset_mode == PRESET_MANUAL: + features |= ( + FanEntityFeature.SET_SPEED + | FanEntityFeature.TURN_ON + | FanEntityFeature.TURN_OFF + ) + + return features + + @property + def is_on(self) -> bool: + """Return true if the entity is on.""" + return self.percentage > 0 + + @property + def percentage(self) -> int: + """Return the current speed percentage.""" + return ranged_value_to_percentage( + self.speed_range, self._speed_attribute.current_value + ) + + @property + def preset_mode(self) -> str: + """Return the mode from the float state.""" + return self._attr_preset_modes[int(self._mode_attribute.current_value)] + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + await self.async_set_homee_value( + self._speed_attribute, + math.ceil(percentage_to_ranged_value(self.speed_range, percentage)), + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + await self.async_set_homee_value( + self._mode_attribute, self._attr_preset_modes.index(preset_mode) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + await self.async_set_homee_value(self._speed_attribute, 0) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn the fan on.""" + if preset_mode is not None: + if preset_mode != "manual": + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_preset_mode", + translation_placeholders={"preset_mode": preset_mode}, + ) + + await self.async_set_preset_mode(preset_mode) + + # If no percentage is given, use the last known value. + if percentage is None: + percentage = ranged_value_to_percentage( + self.speed_range, + self._speed_attribute.last_value, + ) + # If the last known value is 0, set 100%. + if percentage == 0: + percentage = 100 + + await self.async_set_percentage(percentage) diff --git a/homeassistant/components/homee/icons.json b/homeassistant/components/homee/icons.json index d6d327a32c5..062b530ac7e 100644 --- a/homeassistant/components/homee/icons.json +++ b/homeassistant/components/homee/icons.json @@ -11,6 +11,19 @@ } } }, + "fan": { + "homee": { + "state_attributes": { + "preset_mode": { + "state": { + "manual": "mdi:hand-back-left", + "auto": "mdi:auto-mode", + "summer": "mdi:sun-thermometer-outline" + } + } + } + } + }, "sensor": { "brightness": { "default": "mdi:brightness-5" diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index d0ea91b4225..c53a1c2d3e2 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -142,6 +142,19 @@ } } }, + "fan": { + "homee": { + "state_attributes": { + "preset_mode": { + "state": { + "manual": "[%key:common::state::manual%]", + "auto": "[%key:common::state::auto%]", + "summer": "Summer" + } + } + } + } + }, "light": { "light_instance": { "name": "Light {instance}" @@ -356,6 +369,9 @@ "exceptions": { "connection_closed": { "message": "Could not connect to homee while setting attribute." + }, + "invalid_preset_mode": { + "message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'." } }, "issues": { diff --git a/tests/components/homee/fixtures/fan.json b/tests/components/homee/fixtures/fan.json new file mode 100644 index 00000000000..9a6cd028dc1 --- /dev/null +++ b/tests/components/homee/fixtures/fan.json @@ -0,0 +1,73 @@ +{ + "id": 77, + "name": "Test Fan", + "profile": 3019, + "image": "default", + "favorite": 0, + "order": 76, + "protocol": 3, + "routing": 0, + "state": 1, + "state_changed": 1736106044, + "added": 1723550156, + "history": 1, + "cube_type": 3, + "note": "", + "services": 1, + "phonetic_name": "", + "owner": 2, + "security": 0, + "attributes": [ + { + "id": 1, + "node_id": 77, + "instance": 0, + "minimum": 0, + "maximum": 8, + "current_value": 3.0, + "target_value": 3.0, + "last_value": 6.0, + "unit": "n/a", + "step_value": 1.0, + "editable": 1, + "type": 99, + "state": 5, + "last_changed": 1729920212, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "", + "options": { + "automations": ["step"], + "history": { + "day": 35, + "week": 5, + "month": 1, + "stepped": true + } + } + }, + { + "id": 2, + "node_id": 77, + "instance": 0, + "minimum": 0, + "maximum": 2, + "current_value": 0.0, + "target_value": 0.0, + "last_value": 1.0, + "unit": "n/a", + "step_value": 1.0, + "editable": 1, + "type": 100, + "state": 1, + "last_changed": 1736106312, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "" + } + ] +} diff --git a/tests/components/homee/snapshots/test_fan.ambr b/tests/components/homee/snapshots/test_fan.ambr new file mode 100644 index 00000000000..f680ec63e0f --- /dev/null +++ b/tests/components/homee/snapshots/test_fan.ambr @@ -0,0 +1,63 @@ +# serializer version: 1 +# name: test_fan_snapshot[fan.test_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'manual', + 'auto', + 'summer', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.test_fan', + 'has_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': 'homee', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'homee', + 'unique_id': '00055511EECC-77', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_snapshot[fan.test_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Fan', + 'percentage': 37, + 'percentage_step': 12.5, + 'preset_mode': 'manual', + 'preset_modes': list([ + 'manual', + 'auto', + 'summer', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.test_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/homee/test_fan.py b/tests/components/homee/test_fan.py new file mode 100644 index 00000000000..55d019af746 --- /dev/null +++ b/tests/components/homee/test_fan.py @@ -0,0 +1,192 @@ +"""Test Homee fans.""" + +from unittest.mock import MagicMock, call, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, + DOMAIN as FAN_DOMAIN, + SERVICE_DECREASE_SPEED, + SERVICE_INCREASE_SPEED, + SERVICE_SET_PERCENTAGE, + SERVICE_SET_PRESET_MODE, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.homee.const import ( + DOMAIN, + PRESET_AUTO, + PRESET_MANUAL, + PRESET_SUMMER, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import build_mock_node, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.parametrize( + ("speed", "expected"), + [ + (0, 0), + (1, 12), + (2, 25), + (3, 37), + (4, 50), + (5, 62), + (6, 75), + (7, 87), + (8, 100), + ], +) +async def test_percentage( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, + speed: int, + expected: int, +) -> None: + """Test percentage.""" + mock_homee.nodes = [build_mock_node("fan.json")] + mock_homee.nodes[0].attributes[0].current_value = speed + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("fan.test_fan").attributes["percentage"] == expected + + +@pytest.mark.parametrize( + ("mode_value", "expected"), + [ + (0, "manual"), + (1, "auto"), + (2, "summer"), + ], +) +async def test_preset_mode( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, + mode_value: int, + expected: str, +) -> None: + """Test preset mode.""" + mock_homee.nodes = [build_mock_node("fan.json")] + mock_homee.nodes[0].attributes[1].current_value = mode_value + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("fan.test_fan").attributes["preset_mode"] == expected + + +@pytest.mark.parametrize( + ("service", "options", "expected"), + [ + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 100}, (77, 1, 8)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 86}, (77, 1, 7)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 63}, (77, 1, 6)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 60}, (77, 1, 5)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 50}, (77, 1, 4)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 34}, (77, 1, 3)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 17}, (77, 1, 2)), + (SERVICE_TURN_ON, {ATTR_PERCENTAGE: 8}, (77, 1, 1)), + (SERVICE_TURN_ON, {}, (77, 1, 6)), + (SERVICE_TURN_OFF, {}, (77, 1, 0)), + (SERVICE_INCREASE_SPEED, {}, (77, 1, 4)), + (SERVICE_DECREASE_SPEED, {}, (77, 1, 2)), + (SERVICE_SET_PERCENTAGE, {ATTR_PERCENTAGE: 42}, (77, 1, 4)), + (SERVICE_SET_PRESET_MODE, {ATTR_PRESET_MODE: PRESET_MANUAL}, (77, 2, 0)), + (SERVICE_SET_PRESET_MODE, {ATTR_PRESET_MODE: PRESET_AUTO}, (77, 2, 1)), + (SERVICE_SET_PRESET_MODE, {ATTR_PRESET_MODE: PRESET_SUMMER}, (77, 2, 2)), + (SERVICE_TOGGLE, {}, (77, 1, 0)), + ], +) +async def test_fan_services( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, + service: str, + options: int | None, + expected: tuple[int, int, int], +) -> None: + """Test fan services.""" + mock_homee.nodes = [build_mock_node("fan.json")] + await setup_integration(hass, mock_config_entry) + + OPTIONS = {ATTR_ENTITY_ID: "fan.test_fan"} + OPTIONS.update(options) + + await hass.services.async_call( + FAN_DOMAIN, + service, + OPTIONS, + blocking=True, + ) + + mock_homee.set_value.assert_called_once_with(*expected) + + +async def test_turn_on_preset_last_value_zero( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, +) -> None: + """Test turn on with preset last value == 0.""" + mock_homee.nodes = [build_mock_node("fan.json")] + mock_homee.nodes[0].attributes[0].last_value = 0 + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.test_fan", ATTR_PRESET_MODE: PRESET_MANUAL}, + blocking=True, + ) + + assert mock_homee.set_value.call_args_list == [ + call(77, 2, 0), + call(77, 1, 8), + ] + + +async def test_turn_on_invalid_preset( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homee: MagicMock, +) -> None: + """Test turn on with invalid preset.""" + mock_homee.nodes = [build_mock_node("fan.json")] + await setup_integration(hass, mock_config_entry) + + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.test_fan", ATTR_PRESET_MODE: PRESET_AUTO}, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "invalid_preset_mode" + + +async def test_fan_snapshot( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the fan snapshot.""" + mock_homee.nodes = [build_mock_node("fan.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + with patch("homeassistant.components.homee.PLATFORMS", [Platform.FAN]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 2940cb0fa0c55dc63ff73a6800976e32cc9fc9f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 May 2025 11:59:38 -0500 Subject: [PATCH 0315/1175] Bump aiodiscover to 2.7.0 (#144571) --- 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 64fd2ff38c6..c425aafdb00 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "aiodhcpwatcher==1.1.1", - "aiodiscover==2.6.1", + "aiodiscover==2.7.0", "cached-ipaddress==0.10.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f8fcde7e9fe..aec8d3979f9 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.1.1 -aiodiscover==2.6.1 +aiodiscover==2.7.0 aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index fcb6bf9bf7e..cd7caf31cd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -217,7 +217,7 @@ aiocomelit==0.12.0 aiodhcpwatcher==1.1.1 # homeassistant.components.dhcp -aiodiscover==2.6.1 +aiodiscover==2.7.0 # homeassistant.components.dnsip aiodns==3.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d23fd6ba7e..221ab72dc2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ aiocomelit==0.12.0 aiodhcpwatcher==1.1.1 # homeassistant.components.dhcp -aiodiscover==2.6.1 +aiodiscover==2.7.0 # homeassistant.components.dnsip aiodns==3.4.0 From 85f1c8980800370add84b70774afef3142ff906a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 9 May 2025 20:07:23 +0200 Subject: [PATCH 0316/1175] Fix sensor setup during dynamic addition of Miele devices (#144551) Fix sensors when dynamic addition of devices --- homeassistant/components/miele/sensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 22a7916d892..b5b74db5bcc 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -455,7 +455,10 @@ async def async_setup_entry( for device_id, device in coordinator.data.devices.items(): for definition in SENSOR_TYPES: - if device.device_type in definition.types: + if ( + device_id in new_devices_set + and device.device_type in definition.types + ): match definition.description.key: case "state_status": entity_class = MieleStatusSensor @@ -466,8 +469,7 @@ async def async_setup_entry( case _: entity_class = MieleSensor if ( - device_id in new_devices_set - and definition.description.device_class + definition.description.device_class == SensorDeviceClass.TEMPERATURE and definition.description.value_fn(device) == DISABLED_TEMPERATURE / 100 From 131ba3cdef3d043eda66266c832717b92727040a Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 9 May 2025 20:12:32 +0200 Subject: [PATCH 0317/1175] Fix sentence-casing in config fields of `aurora_abb_powerone` (#144577) * Fix sentence-casing in data field names of `aurora_abb_powerone` * Add suggestion from review. Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/aurora_abb_powerone/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/strings.json b/homeassistant/components/aurora_abb_powerone/strings.json index 6b28d9d8c1c..96e7ac7bcd7 100644 --- a/homeassistant/components/aurora_abb_powerone/strings.json +++ b/homeassistant/components/aurora_abb_powerone/strings.json @@ -4,8 +4,8 @@ "user": { "description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel", "data": { - "port": "RS485 or USB-RS485 Adaptor Port", - "address": "Inverter Address" + "port": "RS485 or USB-RS485 adaptor port", + "address": "Inverter address" } } }, @@ -16,7 +16,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." + "no_serial_ports": "No com ports found. The integration needs a valid RS485 device to communicate." } }, "entity": { From 970edbed409e79976988cb37e9c874b0f49af952 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Fri, 9 May 2025 21:06:07 +0200 Subject: [PATCH 0318/1175] Sentence-case names and remove "True/False" in `emulated_roku` setup (#144579) Sentence-case names and remove "True/False" in `emulated_roku`setup As a binary field is shown as an on/off toggle in the UI there is no need to include "(True/False)" in the field label. --- homeassistant/components/emulated_roku/strings.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/emulated_roku/strings.json b/homeassistant/components/emulated_roku/strings.json index fe5f603b04a..e740f6d8f53 100644 --- a/homeassistant/components/emulated_roku/strings.json +++ b/homeassistant/components/emulated_roku/strings.json @@ -7,12 +7,12 @@ "step": { "user": { "data": { - "advertise_ip": "Advertise IP Address", - "advertise_port": "Advertise Port", - "host_ip": "Host IP Address", - "listen_port": "Listen Port", + "advertise_ip": "Advertise IP address", + "advertise_port": "Advertise port", + "host_ip": "Host IP address", + "listen_port": "Listen port", "name": "[%key:common::config_flow::data::name%]", - "upnp_bind_multicast": "Bind multicast (True/False)" + "upnp_bind_multicast": "Bind multicast" }, "title": "Define server configuration" } From 2bce697aa7fb81f7616af619859b9c8c3b4cf0b5 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 9 May 2025 22:55:08 +0200 Subject: [PATCH 0319/1175] SMA add snapshots & tests (#144555) * Refactor the sensor test to use snapshots * Review feedback * Remove leftover --- tests/components/sma/__init__.py | 20 +- tests/components/sma/conftest.py | 69 +- .../sma/snapshots/test_diagnostics.ambr | 2 +- .../components/sma/snapshots/test_sensor.ambr | 5554 +++++++++++++++++ tests/components/sma/test_config_flow.py | 168 +- tests/components/sma/test_init.py | 37 +- tests/components/sma/test_sensor.py | 55 +- 7 files changed, 5737 insertions(+), 168 deletions(-) create mode 100644 tests/components/sma/snapshots/test_sensor.ambr diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 6b958650905..61d3f81a9fc 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -1,8 +1,5 @@ """Tests for the sma integration.""" -import unittest -from unittest.mock import patch - from homeassistant.components.sma.const import CONF_GROUP from homeassistant.const import ( CONF_HOST, @@ -11,12 +8,16 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry MOCK_DEVICE = { "manufacturer": "SMA", "name": "SMA Device Name", "type": "Sunny Boy 3.6", "serial": 123456789, + "sw_version": "1.0.0", } MOCK_USER_INPUT = { @@ -32,7 +33,6 @@ MOCK_USER_REAUTH = { } MOCK_DHCP_DISCOVERY_INPUT = { - # CONF_HOST: "1.1.1.2", CONF_SSL: True, CONF_VERIFY_SSL: False, CONF_GROUP: "user", @@ -49,9 +49,9 @@ MOCK_DHCP_DISCOVERY = { } -def _patch_async_setup_entry(return_value=True) -> unittest.mock._patch: - """Patch async_setup_entry.""" - return patch( - "homeassistant.components.sma.async_setup_entry", - return_value=return_value, - ) +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index 2b4c157175b..5b4ab23213c 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -1,13 +1,17 @@ """Fixtures for sma tests.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch -from pysma.const import GENERIC_SENSORS +from pysma.const import ( + ENERGY_METER_VIA_INVERTER, + GENERIC_SENSORS, + OPTIMIZERS_VIA_INVERTER, +) from pysma.definitions import sensor_map from pysma.sensor import Sensors import pytest -from homeassistant import config_entries from homeassistant.components.sma.const import DOMAIN from homeassistant.core import HomeAssistant @@ -19,31 +23,54 @@ from tests.common import MockConfigEntry @pytest.fixture def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: """Return the default mocked config entry.""" - entry = MockConfigEntry( + + return MockConfigEntry( domain=DOMAIN, title=MOCK_DEVICE["name"], unique_id=str(MOCK_DEVICE["serial"]), data=MOCK_USER_INPUT, - source=config_entries.SOURCE_IMPORT, minor_version=2, + entry_id="sma_entry_123", ) - entry.add_to_hass(hass) - return entry @pytest.fixture -async def init_integration( - hass: HomeAssistant, mock_config_entry: MockConfigEntry -) -> MockConfigEntry: - """Create a fake SMA Config Entry.""" - mock_config_entry.add_to_hass(hass) +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock the setup entry.""" + with patch( + "homeassistant.components.sma.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry - with ( - patch("pysma.SMA.read"), - patch( - "pysma.SMA.get_sensors", return_value=Sensors(sensor_map[GENERIC_SENSORS]) - ), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - return mock_config_entry + +@pytest.fixture +def mock_sma_client() -> Generator[MagicMock]: + """Mock the SMA client.""" + with patch("homeassistant.components.sma.pysma.SMA", autospec=True) as client: + client.return_value.device_info.return_value = MOCK_DEVICE + client.new_session.return_value = True + client.return_value.get_sensors.return_value = Sensors( + sensor_map[GENERIC_SENSORS] + + sensor_map[OPTIMIZERS_VIA_INVERTER] + + sensor_map[ENERGY_METER_VIA_INVERTER] + ) + + default_sensor_values = { + "6100_00499100": 5000, + "6100_00499500": 230, + "6100_00499200": 20, + "6100_00499300": 50, + "6100_00499400": 100, + "6100_00499600": 10, + "6100_00499700": 1000, + } + + def mock_read(sensors): + for sensor in sensors: + if sensor.key in default_sensor_values: + sensor.value = default_sensor_values[sensor.key] + return True + + client.return_value.read.side_effect = mock_read + + yield client diff --git a/tests/components/sma/snapshots/test_diagnostics.ambr b/tests/components/sma/snapshots/test_diagnostics.ambr index 14b0d120190..e8a119291d4 100644 --- a/tests/components/sma/snapshots/test_diagnostics.ambr +++ b/tests/components/sma/snapshots/test_diagnostics.ambr @@ -20,7 +20,7 @@ }), 'pref_disable_new_entities': False, 'pref_disable_polling': False, - 'source': 'import', + 'source': 'user', 'subentries': list([ ]), 'title': 'SMA Device Name', diff --git a/tests/components/sma/snapshots/test_sensor.ambr b/tests/components/sma/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..8911df46169 --- /dev/null +++ b/tests/components/sma/snapshots/test_sensor.ambr @@ -0,0 +1,5554 @@ +# serializer version: 1 +# name: test_all_entities[sensor.sma_device_name_battery_capacity_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_capacity_a', + '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': 'SMA Device Name Battery Capacity A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499100_0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Battery Capacity A', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_capacity_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5000', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_capacity_b', + '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': 'SMA Device Name Battery Capacity B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499100_1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Battery Capacity B', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_capacity_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5000', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_capacity_c', + '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': 'SMA Device Name Battery Capacity C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499100_2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Battery Capacity C', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_capacity_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5000', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_capacity_total', + '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': 'SMA Device Name Battery Capacity Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00696E00_0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_capacity_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Battery Capacity Total', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_capacity_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charge_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charge A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Charge A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charge_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charge_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charge B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499500_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Charge B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charge_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charge_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charge C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499500_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Charge C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charge_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charge_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charge Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00496700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charge_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Charge Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charge_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charging Voltage A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6102_00493500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Charging Voltage A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charging Voltage B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6102_00493500_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Charging Voltage B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Charging Voltage C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6102_00493500_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_charging_voltage_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Charging Voltage C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_charging_voltage_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_current_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Current A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495D00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Battery Current A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_current_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_current_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Current B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495D00_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Battery Current B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_current_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_current_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Current C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495D00_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_current_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Battery Current C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_current_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_discharge_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Discharge A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499600_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Discharge A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_discharge_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_discharge_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Discharge B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499600_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Discharge B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_discharge_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_discharge_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Discharge C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00499600_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Discharge C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_discharge_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_discharge_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Discharge Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00496800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_discharge_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Battery Discharge Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_discharge_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_charge_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Charge A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499300_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Charge A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_charge_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_charge_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Charge B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499300_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Charge B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_charge_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_charge_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Charge C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499300_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Charge C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_charge_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_charge_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Charge Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00496900_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_charge_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Charge Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_charge_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Discharge A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499400_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Discharge A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Discharge B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499400_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Discharge B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Discharge C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00499400_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Discharge C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Power Discharge Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00496A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_power_discharge_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Battery Power Discharge Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_power_discharge_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_soc_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery SOC A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00498F00_0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SMA Device Name Battery SOC A', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_soc_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_soc_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery SOC B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00498F00_1', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SMA Device Name Battery SOC B', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_soc_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_soc_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery SOC C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00498F00_2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SMA Device Name Battery SOC C', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_soc_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_soc_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery SOC Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00295A00_0', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_soc_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SMA Device Name Battery SOC Total', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_soc_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_status_operating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_status_operating_mode', + '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': 'SMA Device Name Battery Status Operating Mode', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08495E00_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_status_operating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Battery Status Operating Mode', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_status_operating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_temp_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Temp A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495B00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'SMA Device Name Battery Temp A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_temp_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_temp_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Temp B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495B00_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'SMA Device Name Battery Temp B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_temp_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_temp_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Temp C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40495B00_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_temp_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'SMA Device Name Battery Temp C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_temp_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_voltage_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Voltage A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00495C00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Voltage A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_voltage_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_voltage_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Voltage B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00495C00_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Voltage B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_voltage_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_battery_voltage_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Battery Voltage C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00495C00_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_battery_voltage_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Battery Voltage C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_battery_voltage_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_current_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Current L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40465300_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Current L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_current_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_current_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Current L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40465400_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Current L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_current_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_current_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Current L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40465500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Current L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_current_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_current_total', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Current Total', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00664F00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_current_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Current Total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_current_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_daily_yield-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_daily_yield', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Daily Yield', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00262200_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_daily_yield-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Daily Yield', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_daily_yield', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_frequency', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Frequency', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00465700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'SMA Device Name Frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_apparent_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': 'SMA Device Name Grid Apparent Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'SMA Device Name Grid Apparent Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Apparent Power L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'SMA Device Name Grid Apparent Power L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Apparent Power L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666900_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'SMA Device Name Grid Apparent Power L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Apparent Power L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_apparent_power_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'SMA Device Name Grid Apparent Power L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_apparent_power_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_connection_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_grid_connection_status', + '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': 'SMA Device Name Grid Connection Status', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_0846A700_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_connection_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Grid Connection Status', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_connection_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_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': 'SMA Device Name Grid Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40263F00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Grid Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_power_factor', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Power Factor', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00665900_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'SMA Device Name Grid Power Factor', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power_factor_excitation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_grid_power_factor_excitation', + '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': 'SMA Device Name Grid Power Factor Excitation', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08465A00_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_power_factor_excitation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Grid Power Factor Excitation', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_power_factor_excitation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_reactive_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': 'SMA Device Name Grid Reactive Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40265F00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'SMA Device Name Grid Reactive Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Reactive Power L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666000_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'SMA Device Name Grid Reactive Power L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Reactive Power L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'SMA Device Name Grid Reactive Power L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Grid Reactive Power L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40666200_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_reactive_power_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'SMA Device Name Grid Reactive Power L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_reactive_power_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_relay_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_grid_relay_status', + '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': 'SMA Device Name Grid Relay Status', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08416400_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_grid_relay_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Grid Relay Status', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_grid_relay_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_insulation_residual_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_insulation_residual_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Insulation Residual Current', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6102_40254E00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_insulation_residual_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Insulation Residual Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_insulation_residual_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_condition-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_inverter_condition', + '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': 'SMA Device Name Inverter Condition', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08414C00_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_condition-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Inverter Condition', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_inverter_condition', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_power_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_inverter_power_limit', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Inverter Power Limit', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6800_00832A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_power_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Inverter Power Limit', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_inverter_power_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_system_init-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_inverter_system_init', + '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': 'SMA Device Name Inverter System Init', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6800_08811F00_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_inverter_system_init-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Inverter System Init', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_inverter_system_init', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Draw L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046EB00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Draw L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Draw L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046EC00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Draw L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Draw L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046ED00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_draw_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Draw L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_draw_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Feed L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046E800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Feed L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Feed L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046E900_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Feed L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Active Power Feed L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046EA00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_active_power_feed_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Active Power Feed L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_active_power_feed_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_current_consumption', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Current Consumption', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00543100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Current Consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_current_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_current_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Current L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40466500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Metering Current L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_current_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_current_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Current L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40466600_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Metering Current L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_current_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_current_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Current L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40466B00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_current_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Metering Current L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_current_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_frequency', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Frequency', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00468100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'SMA Device Name Metering Frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_power_absorbed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_power_absorbed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Power Absorbed', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40463700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_power_absorbed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Power Absorbed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_power_absorbed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_power_supplied-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_power_supplied', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Power Supplied', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40463600_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_power_supplied-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Metering Power Supplied', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_power_supplied', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_absorbed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_total_absorbed', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Total Absorbed', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00462500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_absorbed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Metering Total Absorbed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_total_absorbed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_total_consumption', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Total Consumption', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00543A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Metering Total Consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_total_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_yield-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_total_yield', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Total Yield', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00462400_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_total_yield-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Metering Total Yield', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_total_yield', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_voltage_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Voltage L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046E500_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Metering Voltage L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_voltage_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_voltage_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Voltage L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046E600_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Metering Voltage L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_voltage_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_metering_voltage_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Metering Voltage L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046E700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_metering_voltage_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Metering Voltage L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_metering_voltage_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_operating_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_operating_status', + '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': 'SMA Device Name Operating Status', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08412B00_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_operating_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Operating Status', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_operating_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_operating_status_general-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_operating_status_general', + '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': 'SMA Device Name Operating Status General', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08412800_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_operating_status_general-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Operating Status General', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_operating_status_general', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_optimizer_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Optimizer Current', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40652900_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Optimizer Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_optimizer_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_optimizer_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': 'SMA Device Name Optimizer Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40652A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Optimizer Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_optimizer_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_temp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_optimizer_temp', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Optimizer Temp', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40652B00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_temp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'SMA Device Name Optimizer Temp', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_optimizer_temp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_optimizer_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Optimizer Voltage', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40652800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_optimizer_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Optimizer Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_optimizer_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_power_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Power L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40464000_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Power L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_power_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_power_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Power L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40464100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Power L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_power_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_power_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Power L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_40464200_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_power_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Power L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_power_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_current_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Current A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40452100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name PV Current A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_current_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_current_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Current B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40452100_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name PV Current B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_current_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_current_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Current C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40452100_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_current_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name PV Current C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_current_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_gen_meter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_gen_meter', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Gen Meter', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_0046C300_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_gen_meter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name PV Gen Meter', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_gen_meter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_isolation_resistance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_isolation_resistance', + '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': 'SMA Device Name PV Isolation Resistance', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6102_00254F00_0', + 'unit_of_measurement': 'kOhms', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_isolation_resistance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name PV Isolation Resistance', + 'state_class': , + 'unit_of_measurement': 'kOhms', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_isolation_resistance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_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': 'SMA Device Name PV Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046C200_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name PV Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_power_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Power A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40251E00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name PV Power A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_power_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_power_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Power B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40251E00_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name PV Power B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_power_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_power_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Power C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40251E00_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_power_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name PV Power C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_power_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_voltage_a', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Voltage A', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40451F00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name PV Voltage A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_voltage_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_voltage_b', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Voltage B', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40451F00_1', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name PV Voltage B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_voltage_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_pv_voltage_c', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name PV Voltage C', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6380_40451F00_2', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_pv_voltage_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name PV Voltage C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_pv_voltage_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_secure_power_supply_current', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Secure Power Supply Current', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046C700_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'SMA Device Name Secure Power Supply Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_secure_power_supply_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_secure_power_supply_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': 'SMA Device Name Secure Power Supply Power', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046C800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SMA Device Name Secure Power Supply Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_secure_power_supply_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_secure_power_supply_voltage', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Secure Power Supply Voltage', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_0046C600_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_secure_power_supply_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Secure Power Supply Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_secure_power_supply_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sma_device_name_status', + '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': 'SMA Device Name Status', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6180_08214800_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.sma_device_name_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SMA Device Name Status', + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_total_yield-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_total_yield', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Total Yield', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6400_00260100_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_total_yield-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SMA Device Name Total Yield', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_total_yield', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_voltage_l1', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Voltage L1', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00464800_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Voltage L1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_voltage_l1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_voltage_l2', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Voltage L2', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00464900_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Voltage L2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_voltage_l2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sma_device_name_voltage_l3', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SMA Device Name Voltage L3', + 'platform': 'sma', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789-6100_00464A00_0', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.sma_device_name_voltage_l3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'SMA Device Name Voltage L3', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sma_device_name_voltage_l3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 175dcc4f3a0..c8939ef2d64 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -1,6 +1,6 @@ """Test the sma config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pysma.exceptions import ( SmaAuthenticationException, @@ -21,7 +21,6 @@ from . import ( MOCK_DHCP_DISCOVERY_INPUT, MOCK_USER_INPUT, MOCK_USER_REAUTH, - _patch_async_setup_entry, ) from tests.conftest import MockConfigEntry @@ -39,7 +38,9 @@ DHCP_DISCOVERY_DUPLICATE = DhcpServiceInfo( ) -async def test_form(hass: HomeAssistant) -> None: +async def test_form( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_sma_client: AsyncMock +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -48,16 +49,11 @@ async def test_form(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with ( - patch("pysma.SMA.new_session", return_value=True), - patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), - _patch_async_setup_entry() as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_USER_INPUT, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] @@ -76,18 +72,18 @@ async def test_form(hass: HomeAssistant) -> None: ], ) async def test_form_exceptions( - hass: HomeAssistant, exception: Exception, error: str + hass: HomeAssistant, + mock_setup_entry: MockConfigEntry, + exception: Exception, + error: str, ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - with ( - patch( - "homeassistant.components.sma.pysma.SMA.new_session", side_effect=exception - ), - _patch_async_setup_entry() as mock_setup_entry, + with patch( + "homeassistant.components.sma.pysma.SMA.new_session", side_effect=exception ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -96,39 +92,34 @@ async def test_form_exceptions( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - assert len(mock_setup_entry.mock_calls) == 0 async def test_form_already_configured( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_sma_client: AsyncMock ) -> None: """Test starting a flow by user when already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - with ( - patch("homeassistant.components.sma.pysma.SMA.new_session", return_value=True), - patch( - "homeassistant.components.sma.pysma.SMA.device_info", - return_value=MOCK_DEVICE, - ), - patch( - "homeassistant.components.sma.pysma.SMA.close_session", return_value=True - ), - _patch_async_setup_entry() as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_USER_INPUT, - ) + 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.ABORT assert result["reason"] == "already_configured" - assert len(mock_setup_entry.mock_calls) == 0 -async def test_dhcp_discovery(hass: HomeAssistant) -> None: +async def test_dhcp_discovery( + hass: HomeAssistant, mock_setup_entry: MockConfigEntry, mock_sma_client: AsyncMock +) -> None: """Test we can setup from dhcp discovery.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -139,31 +130,22 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" - with ( - patch("homeassistant.components.sma.pysma.SMA.new_session", return_value=True), - patch( - "homeassistant.components.sma.pysma.SMA.device_info", - return_value=MOCK_DEVICE, - ), - _patch_async_setup_entry() as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_DHCP_DISCOVERY_INPUT, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_DHCP_DISCOVERY_INPUT, + ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DHCP_DISCOVERY["host"] assert result["data"] == MOCK_DHCP_DISCOVERY assert result["result"].unique_id == DHCP_DISCOVERY.hostname.replace("SMA", "") - assert len(mock_setup_entry.mock_calls) == 1 - async def test_dhcp_already_configured( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test starting a flow by dhcp when already configured.""" + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY_DUPLICATE ) @@ -182,18 +164,23 @@ async def test_dhcp_already_configured( ], ) async def test_dhcp_exceptions( - hass: HomeAssistant, exception: Exception, error: str + hass: HomeAssistant, + mock_setup_entry: MockConfigEntry, + mock_sma_client: AsyncMock, + exception: Exception, + error: str, ) -> None: - """Test we handle cannot connect error.""" + """Test we handle cannot connect error in DHCP flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=DHCP_DISCOVERY, ) - with patch( - "homeassistant.components.sma.pysma.SMA.new_session", side_effect=exception - ): + with patch("homeassistant.components.sma.pysma.SMA") as mock_sma: + mock_sma_instance = mock_sma.return_value + mock_sma_instance.new_session = AsyncMock(side_effect=exception) + result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_DHCP_DISCOVERY_INPUT, @@ -202,17 +189,12 @@ async def test_dhcp_exceptions( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - with ( - patch("homeassistant.components.sma.pysma.SMA.new_session", return_value=True), - patch( - "homeassistant.components.sma.pysma.SMA.device_info", - return_value=MOCK_DEVICE, - ), - patch( - "homeassistant.components.sma.pysma.SMA.close_session", return_value=True - ), - _patch_async_setup_entry(), - ): + with patch("homeassistant.components.sma.pysma.SMA") as mock_sma: + mock_sma_instance = mock_sma.return_value + mock_sma_instance.new_session = AsyncMock(return_value=True) + mock_sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) + mock_sma_instance.close_session = AsyncMock(return_value=True) + result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_DHCP_DISCOVERY_INPUT, @@ -225,14 +207,16 @@ async def test_dhcp_exceptions( async def test_full_flow_reauth( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, mock_setup_entry: MockConfigEntry, mock_sma_client: AsyncMock ) -> None: """Test the full flow of the config flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - result = await mock_config_entry.start_reauth_flow(hass) + result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -241,16 +225,11 @@ async def test_full_flow_reauth( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with ( - patch("pysma.SMA.new_session", return_value=True), - patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), - _patch_async_setup_entry() as mock_setup_entry, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - MOCK_USER_REAUTH, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_REAUTH, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -272,26 +251,28 @@ async def test_reauth_flow_exceptions( exception: Exception, error: str, ) -> None: - """Test we handle cannot connect error.""" - result = await mock_config_entry.start_reauth_flow(hass) + """Test we handle errors during reauth flow properly.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) - with ( - patch("pysma.SMA.new_session", side_effect=exception), - ): + result = await entry.start_reauth_flow(hass) + + with patch("homeassistant.components.sma.pysma.SMA") as mock_sma: + mock_sma_instance = mock_sma.return_value + mock_sma_instance.new_session = AsyncMock(side_effect=exception) result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_REAUTH, ) - assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": error} - assert result["step_id"] == "reauth_confirm" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + assert result["step_id"] == "reauth_confirm" + + mock_sma_instance.new_session = AsyncMock(return_value=True) + mock_sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) + mock_sma_instance.close_session = AsyncMock(return_value=True) - with ( - patch("pysma.SMA.new_session", return_value=True), - patch("pysma.SMA.device_info", return_value=MOCK_DEVICE), - _patch_async_setup_entry() as mock_setup_entry, - ): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_REAUTH, @@ -300,4 +281,3 @@ async def test_reauth_flow_exceptions( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sma/test_init.py b/tests/components/sma/test_init.py index 0cc82f49a41..57c3cab33e7 100644 --- a/tests/components/sma/test_init.py +++ b/tests/components/sma/test_init.py @@ -1,27 +1,32 @@ """Test the sma init file.""" +from collections.abc import AsyncGenerator + from homeassistant.components.sma.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.core import HomeAssistant -from . import MOCK_DEVICE, MOCK_USER_INPUT, _patch_async_setup_entry +from . import MOCK_DEVICE, MOCK_USER_INPUT from tests.common import MockConfigEntry -async def test_migrate_entry_minor_version_1_2(hass: HomeAssistant) -> None: +async def test_migrate_entry_minor_version_1_2( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_sma_client: AsyncGenerator, +) -> None: """Test migrating a 1.1 config entry to 1.2.""" - with _patch_async_setup_entry(): - entry = MockConfigEntry( - domain=DOMAIN, - title=MOCK_DEVICE["name"], - unique_id=MOCK_DEVICE["serial"], # Not converted to str - data=MOCK_USER_INPUT, - source=SOURCE_IMPORT, - minor_version=1, - ) - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - assert entry.version == 1 - assert entry.minor_version == 2 - assert entry.unique_id == str(MOCK_DEVICE["serial"]) + entry = MockConfigEntry( + domain=DOMAIN, + title=MOCK_DEVICE["name"], + unique_id=MOCK_DEVICE["serial"], # Not converted to str + data=MOCK_USER_INPUT, + source=SOURCE_IMPORT, + minor_version=1, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.unique_id == str(MOCK_DEVICE["serial"]) diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index de7e1167f1f..92b8c12554c 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,31 +1,34 @@ -"""Test the sma sensor platform.""" +"""Test the SMA sensor platform.""" -from pysma.const import ( - ENERGY_METER_VIA_INVERTER, - GENERIC_SENSORS, - OPTIMIZERS_VIA_INVERTER, -) -from pysma.definitions import sensor_map +from collections.abc import Generator +from unittest.mock import patch -from homeassistant.components.sma.sensor import SENSOR_ENTITIES -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower +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, snapshot_platform -async def test_sensors(hass: HomeAssistant, init_integration) -> None: - """Test states of the sensors.""" - state = hass.states.get("sensor.sma_device_grid_power") - assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT - - -async def test_sensor_entities(hass: HomeAssistant, init_integration) -> None: - """Test SENSOR_ENTITIES contains a SensorEntityDescription for each pysma sensor.""" - pysma_sensor_definitions = ( - sensor_map[GENERIC_SENSORS] - + sensor_map[OPTIMIZERS_VIA_INVERTER] - + sensor_map[ENERGY_METER_VIA_INVERTER] - ) - - for sensor in pysma_sensor_definitions: - assert sensor.name in SENSOR_ENTITIES +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_sma_client: Generator, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.sma.PLATFORMS", + [Platform.SENSOR], + ): + await setup_integration(hass, mock_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) From 5fadc56475b4180d9123996d5774d36ae6ffdcda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 May 2025 17:19:00 -0500 Subject: [PATCH 0320/1175] Mark inkbird coordinator as not needing connectable (#144584) --- homeassistant/components/inkbird/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/inkbird/coordinator.py b/homeassistant/components/inkbird/coordinator.py index d52ebd83595..fbacedf7e0f 100644 --- a/homeassistant/components/inkbird/coordinator.py +++ b/homeassistant/components/inkbird/coordinator.py @@ -58,6 +58,7 @@ class INKBIRDActiveBluetoothProcessorCoordinator( update_method=self._async_on_update, needs_poll_method=self._async_needs_poll, poll_method=self._async_poll_data, + connectable=False, # Polling only happens if active scanning is disabled ) async def async_init(self) -> None: From 1654249dabc2dd4c3f55a13988e25e15060a3ed9 Mon Sep 17 00:00:00 2001 From: tdfountain <174762217+tdfountain@users.noreply.github.com> Date: Fri, 9 May 2025 15:20:03 -0700 Subject: [PATCH 0321/1175] Use strict typing for ConfigEntry on remove in NUT (#144588) --- homeassistant/components/nut/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index ae20ed39251..2f2c6badc4c 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -187,7 +187,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool async def async_remove_config_entry_device( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: NutConfigEntry, device_entry: dr.DeviceEntry, ) -> bool: """Remove NUT config entry from a device.""" From 626f8a9166a1173d2a13b303aa013122e80dd0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85slund?= Date: Sat, 10 May 2025 00:54:36 +0200 Subject: [PATCH 0322/1175] Add codeowner to Adax (#144587) * Add codeowner to Adax * Reformatted manifest file --- CODEOWNERS | 4 ++-- homeassistant/components/adax/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6bb09b0238c..bbc1b30efdc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,8 +46,8 @@ build.json @home-assistant/supervisor /tests/components/accuweather/ @bieniu /homeassistant/components/acmeda/ @atmurray /tests/components/acmeda/ @atmurray -/homeassistant/components/adax/ @danielhiversen -/tests/components/adax/ @danielhiversen +/homeassistant/components/adax/ @danielhiversen @lazytarget +/tests/components/adax/ @danielhiversen @lazytarget /homeassistant/components/adguard/ @frenck /tests/components/adguard/ @frenck /homeassistant/components/ads/ @mrpasztoradam diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 2742180333b..efbc611f9d3 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -1,7 +1,7 @@ { "domain": "adax", "name": "Adax", - "codeowners": ["@danielhiversen"], + "codeowners": ["@danielhiversen", "@lazytarget"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", "iot_class": "local_polling", From 977d2fe8b33e1583ea1ff43438d891c8339f3a2f Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Sat, 10 May 2025 15:34:51 +0800 Subject: [PATCH 0323/1175] Add switchbot vacuum support (#144550) * add support for vacuum * add vacuum unit test --- .../components/switchbot/__init__.py | 10 ++ homeassistant/components/switchbot/const.py | 10 ++ .../components/switchbot/strings.json | 12 ++ homeassistant/components/switchbot/vacuum.py | 126 ++++++++++++++++++ tests/components/switchbot/__init__.py | 125 +++++++++++++++++ tests/components/switchbot/test_vacuum.py | 77 +++++++++++ 6 files changed, 360 insertions(+) create mode 100644 homeassistant/components/switchbot/vacuum.py create mode 100644 tests/components/switchbot/test_vacuum.py diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 8f417bc641a..1f41f494764 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -73,6 +73,11 @@ PLATFORMS_BY_TYPE = { ], SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR], SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR], + SupportedModels.K20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, @@ -89,6 +94,11 @@ CLASS_BY_DEVICE = { SupportedModels.RELAY_SWITCH_1.value: switchbot.SwitchbotRelaySwitch, SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade, SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan, + SupportedModels.K20_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.S10_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.K10_PRO_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum, } diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 41bbb247929..327b6e704a0 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -38,6 +38,11 @@ class SupportedModels(StrEnum): ROLLER_SHADE = "roller_shade" HUBMINI_MATTER = "hubmini_matter" CIRCULATOR_FAN = "circulator_fan" + K20_VACUUM = "k20_vacuum" + S10_VACUUM = "s10_vacuum" + K10_VACUUM = "k10_vacuum" + K10_PRO_VACUUM = "k10_pro_vacuum" + K10_PRO_COMBO_VACUUM = "k10_pro_combo_vacumm" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -56,6 +61,11 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.RELAY_SWITCH_1: SupportedModels.RELAY_SWITCH_1, SwitchbotModel.ROLLER_SHADE: SupportedModels.ROLLER_SHADE, SwitchbotModel.CIRCULATOR_FAN: SupportedModels.CIRCULATOR_FAN, + SwitchbotModel.K20_VACUUM: SupportedModels.K20_VACUUM, + SwitchbotModel.S10_VACUUM: SupportedModels.S10_VACUUM, + SwitchbotModel.K10_VACUUM: SupportedModels.K10_VACUUM, + SwitchbotModel.K10_PRO_VACUUM: SupportedModels.K10_PRO_VACUUM, + SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index bd41502d8b7..41bc09dde1a 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -180,6 +180,18 @@ } } } + }, + "vacuum": { + "vacuum": { + "state_attributes": { + "last_run_success": { + "state": { + "true": "[%key:component::binary_sensor::entity_component::problem::state::off%]", + "false": "[%key:component::binary_sensor::entity_component::problem::state::on%]" + } + } + } + } } }, "exceptions": { diff --git a/homeassistant/components/switchbot/vacuum.py b/homeassistant/components/switchbot/vacuum.py new file mode 100644 index 00000000000..9dade6b7f46 --- /dev/null +++ b/homeassistant/components/switchbot/vacuum.py @@ -0,0 +1,126 @@ +"""Support for switchbot vacuums.""" + +from __future__ import annotations + +from typing import Any + +import switchbot +from switchbot import SwitchbotModel + +from homeassistant.components.vacuum import ( + StateVacuumEntity, + VacuumActivity, + VacuumEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator +from .entity import SwitchbotEntity + +PARALLEL_UPDATES = 0 + +DEVICE_SUPPORT_PROTOCOL_VERSION_1 = [ + SwitchbotModel.K10_VACUUM, + SwitchbotModel.K10_PRO_VACUUM, +] + +PROTOCOL_VERSION_1_STATE_TO_HA_STATE: dict[int, VacuumActivity] = { + 0: VacuumActivity.CLEANING, + 1: VacuumActivity.DOCKED, +} + +PROTOCOL_VERSION_2_STATE_TO_HA_STATE: dict[int, VacuumActivity] = { + 1: VacuumActivity.IDLE, # idle + 2: VacuumActivity.DOCKED, # charge + 3: VacuumActivity.DOCKED, # charge complete + 4: VacuumActivity.IDLE, # self-check + 5: VacuumActivity.IDLE, # the drum is moist + 6: VacuumActivity.CLEANING, # exploration + 7: VacuumActivity.CLEANING, # re-location + 8: VacuumActivity.CLEANING, # cleaning and sweeping + 9: VacuumActivity.CLEANING, # cleaning + 10: VacuumActivity.CLEANING, # sweeping + 11: VacuumActivity.PAUSED, # pause + 12: VacuumActivity.CLEANING, # getting out of trouble + 13: VacuumActivity.ERROR, # trouble + 14: VacuumActivity.CLEANING, # mpo cleaning + 15: VacuumActivity.RETURNING, # returning + 16: VacuumActivity.CLEANING, # deep cleaning + 17: VacuumActivity.CLEANING, # Sewage extraction + 18: VacuumActivity.CLEANING, # replenish water for mop + 19: VacuumActivity.CLEANING, # dust collection + 20: VacuumActivity.CLEANING, # dry + 21: VacuumActivity.IDLE, # dormant + 22: VacuumActivity.IDLE, # network configuration + 23: VacuumActivity.CLEANING, # remote control + 24: VacuumActivity.RETURNING, # return to base + 25: VacuumActivity.IDLE, # shut down + 26: VacuumActivity.IDLE, # mark water base station + 27: VacuumActivity.IDLE, # rinse the filter screen + 28: VacuumActivity.IDLE, # mark humidifier location + 29: VacuumActivity.IDLE, # on the way to the humidifier + 30: VacuumActivity.IDLE, # add water for humidifier + 31: VacuumActivity.IDLE, # upgrading + 32: VacuumActivity.PAUSED, # pause during recharging + 33: VacuumActivity.IDLE, # integrated with the platform + 34: VacuumActivity.CLEANING, # working for the platform +} + +SWITCHBOT_VACUUM_STATE_MAP: dict[int, dict[int, VacuumActivity]] = { + 1: PROTOCOL_VERSION_1_STATE_TO_HA_STATE, + 2: PROTOCOL_VERSION_2_STATE_TO_HA_STATE, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SwitchbotConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the switchbot vacuum.""" + async_add_entities([SwitchbotVacuumEntity(entry.runtime_data)]) + + +class SwitchbotVacuumEntity(SwitchbotEntity, StateVacuumEntity): + """Representation of a SwitchBot vacuum.""" + + _device: switchbot.SwitchbotVacuum + _attr_supported_features = ( + VacuumEntityFeature.BATTERY + | VacuumEntityFeature.RETURN_HOME + | VacuumEntityFeature.START + | VacuumEntityFeature.STATE + ) + _attr_translation_key = "vacuum" + _attr_name = None + + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: + """Initialize the Switchbot.""" + super().__init__(coordinator) + self.protocol_version = ( + 1 if coordinator.model in DEVICE_SUPPORT_PROTOCOL_VERSION_1 else 2 + ) + + @property + def activity(self) -> VacuumActivity | None: + """Return the status of the vacuum cleaner.""" + status_code = self._device.get_work_status() + return SWITCHBOT_VACUUM_STATE_MAP[self.protocol_version].get(status_code) + + @property + def battery_level(self) -> int: + """Return the vacuum battery.""" + return self._device.get_battery() + + async def async_start(self) -> None: + """Start or resume the cleaning task.""" + self._last_run_success = bool( + await self._device.clean_up(self.protocol_version) + ) + + async def async_return_to_base(self, **kwargs: Any) -> None: + """Return to dock.""" + self._last_run_success = bool( + await self._device.return_to_dock(self.protocol_version) + ) diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 941d58c8e3a..5ab9dc7df13 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -555,3 +555,128 @@ CIRCULATOR_FAN_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + + +K20_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="K20 Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="K20 Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "K20 Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) + + +K10_PRO_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="K10 Pro Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="K10 Pro Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "K10 Pro Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) + + +K10_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="K10 Vacuum", + manufacturer_data={ + 2409: b"\xca8\x06\xa9_\xf1\x02 d", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="K10 Vacuum", + manufacturer_data={ + 2409: b"\xca8\x06\xa9_\xf1\x02 d", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "K10 Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) + + +K10_POR_COMBO_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="K10 Pro Combo Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="K10 Pro Combo Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "K10 Pro Combo Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) + + +S10_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( + name="S10 Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="S10 Vacuum", + manufacturer_data={ + 2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "S10 Vacuum"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_vacuum.py b/tests/components/switchbot/test_vacuum.py new file mode 100644 index 00000000000..7822bda15db --- /dev/null +++ b/tests/components/switchbot/test_vacuum.py @@ -0,0 +1,77 @@ +"""Tests for switchbot vacuum.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.vacuum import ( + DOMAIN as VACUUM_DOMAIN, + SERVICE_RETURN_TO_BASE, + SERVICE_START, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import ( + K10_POR_COMBO_VACUUM_SERVICE_INFO, + K10_PRO_VACUUM_SERVICE_INFO, + K10_VACUUM_SERVICE_INFO, + K20_VACUUM_SERVICE_INFO, + S10_VACUUM_SERVICE_INFO, +) + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + ("sensor_type", "service_info"), + [ + ("k20_vacuum", K20_VACUUM_SERVICE_INFO), + ("s10_vacuum", S10_VACUUM_SERVICE_INFO), + ("k10_pro_combo_vacumm", K10_POR_COMBO_VACUUM_SERVICE_INFO), + ("k10_vacuum", K10_VACUUM_SERVICE_INFO), + ("k10_pro_vacuum", K10_PRO_VACUUM_SERVICE_INFO), + ], +) +@pytest.mark.parametrize( + ("service", "mock_method"), + [(SERVICE_START, "clean_up"), (SERVICE_RETURN_TO_BASE, "return_to_dock")], +) +async def test_vacuum_controlling( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + sensor_type: str, + service: str, + mock_method: str, + service_info: BluetoothServiceInfoBleak, +) -> None: + """Test switchbot vacuum controlling.""" + + inject_bluetooth_service_info(hass, service_info) + + entry = mock_entry_factory(sensor_type) + entry.add_to_hass(hass) + + mocked_instance = AsyncMock(return_value=True) + + with patch.multiple( + "homeassistant.components.switchbot.vacuum.switchbot.SwitchbotVacuum", + update=MagicMock(return_value=None), + **{mock_method: mocked_instance}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_id = "vacuum.test_name" + + await hass.services.async_call( + VACUUM_DOMAIN, + service, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + mocked_instance.assert_awaited_once() From 35ab2a21d641147c8dfcc7b5c04b514bc9394e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Sat, 10 May 2025 09:43:40 +0200 Subject: [PATCH 0324/1175] Matter Oven fixture (#144603) * Create cooktop.json * Update conftest.py * Fix format * Add snapshots * Add snapshots * Oven fixture * Oven fixture * Add snapshot --- tests/components/matter/conftest.py | 3 +- .../matter/fixtures/nodes/oven.json | 484 ++++++++++++++++++ .../matter/snapshots/test_select.ambr | 128 +++++ .../matter/snapshots/test_sensor.ambr | 222 ++++++++ .../matter/snapshots/test_switch.ambr | 96 ++++ 5 files changed, 932 insertions(+), 1 deletion(-) create mode 100644 tests/components/matter/fixtures/nodes/oven.json diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 6cd5d703e44..7da9a28484e 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -105,8 +105,9 @@ async def integration_fixture( "onoff_light_alt_name", "onoff_light_no_name", "onoff_light_with_levelcontrol_present", - "pump", + "oven", "pressure_sensor", + "pump", "room_airconditioner", "silabs_dishwasher", "silabs_evse_charging", diff --git a/tests/components/matter/fixtures/nodes/oven.json b/tests/components/matter/fixtures/nodes/oven.json new file mode 100644 index 00000000000..6e325146f83 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/oven.json @@ -0,0 +1,484 @@ +{ + "node_id": 2, + "date_commissioned": "2025-04-29T15:37:55.171819", + "last_interview": "2025-04-29T15:37:55.171832", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 3 + } + ], + "0/29/1": [29, 31, 40, 43, 45, 48, 49, 51, 54, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1, 2, 3, 4], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "0/31/0": [ + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 1 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 1, + "0/31/65533": 2, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/40/0": 19, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Mock Oven", + "0/40/4": 32768, + "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/17": true, + "0/40/18": "EB38EF759DAA4DB8", + "0/40/19": { + "0": 3, + "1": 65535 + }, + "0/40/21": 17104896, + "0/40/22": 1, + "0/40/24": 1, + "0/40/65532": 0, + "0/40/65533": 5, + "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, 17, 18, 19, 21, + 22, 24, 65532, 65533, 65528, 65529, 65531 + ], + "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, 65532, 65533, 65528, 65529, 65531], + "0/45/0": 0, + "0/45/65532": 1, + "0/45/65533": 2, + "0/45/65528": [], + "0/45/65529": [], + "0/45/65531": [0, 65532, 65533, 65528, 65529, 65531], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 2, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 2, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "0/49/0": 1, + "0/49/1": [ + { + "0": "ZW5zMzM=", + "1": true + } + ], + "0/49/4": true, + "0/49/5": null, + "0/49/6": null, + "0/49/7": null, + "0/49/65532": 4, + "0/49/65533": 2, + "0/49/65528": [], + "0/49/65529": [], + "0/49/65531": [0, 1, 4, 5, 6, 7, 65532, 65533, 65528, 65529, 65531], + "0/51/0": [ + { + "0": "docker0", + "1": false, + "2": null, + "3": null, + "4": "AkIN/v6b", + "5": ["rBEAAQ=="], + "6": [""], + "7": 0 + }, + { + "0": "ens33", + "1": true, + "2": null, + "3": null, + "4": "AAwp/F0T", + "5": ["wKgBqA=="], + "6": [ + "KgEOCgKzOZAP/YMcX0yMLQ==", + "KgEOCgKzOZC/O1Ew1WvS4A==", + "/oAAAAAAAADml3Ozl7GZug==" + ], + "7": 2 + }, + { + "0": "lo", + "1": true, + "2": null, + "3": null, + "4": "AAAAAAAA", + "5": ["fwAAAQ=="], + "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], + "7": 0 + } + ], + "0/51/1": 1, + "0/51/2": 26, + "0/51/3": 0, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 2, + "0/51/65528": [2], + "0/51/65529": [0, 1], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65532, 65533, 65528, 65529, 65531 + ], + "0/54/0": null, + "0/54/1": null, + "0/54/2": 3, + "0/54/3": null, + "0/54/4": null, + "0/54/5": null, + "0/54/6": null, + "0/54/7": null, + "0/54/8": null, + "0/54/9": null, + "0/54/10": null, + "0/54/11": null, + "0/54/12": null, + "0/54/65532": 3, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [0], + "0/54/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65532, 65533, 65528, 65529, + 65531 + ], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 1, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "0/62/0": [ + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRAhgkBwEkCAEwCUEE3mWlRgzQdFFY8sclYjEv0uyAYGfTqVozOb5xR/ypUesqyIwaR1bqY6K4D2+zUx+FBvbRBBUj0PBwJ32cvUm+LTcKNQEoARgkAgE2AwQCBAEYMAQUnKark4iAc32+X9hGHNDon32qhdowBRRqGquZZYwbDAaOinVVrS9sWTozoBgwC0ABtt37m0318llNw7RtRoGFeHD4OxuGHNRS7JT28Oy0H4dNXb4Nu+xyQEK5zVri/QSUK3doq/PD8G0h33Ix4oOLGA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEyvr+z4yBxEDoiyCFg+i408LqC3j0UMvTszBv1051g2EMrAzBkj+0RZFsSl3eQ3D2c7mTcH6GERtlk4BqGvC1qDcKNQEpARgkAmAwBBRqGquZZYwbDAaOinVVrS9sWTozoDAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQCIuoikQZU9LkDKw7dcTVVXBDlTyBol3w070PIIw8BbaQD5qCeIv/3cI5/X5sAYTmemRq0ZPMjAw1dsN+wodzm8Y", + "254": 1 + } + ], + "0/62/1": [ + { + "1": "BIPshBqc9a7nNK00eRrviEzHfe/cfATY9VngqKv17+uAUpy3XujhZBjkAQyhYAaSKxVzSfVttY4FVQkpXIHZFlA=", + "2": 4939, + "3": 2, + "4": 2, + "5": "Maison", + "254": 1 + } + ], + "0/62/2": 16, + "0/62/3": 1, + "0/62/4": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEg+yEGpz1ruc0rTR5Gu+ITMd979x8BNj1WeCoq/Xv64BSnLde6OFkGOQBDKFgBpIrFXNJ9W21jgVVCSlcgdkWUDcKNQEpARgkAmAwBBRPkvAMbwLEubfgETM7L7icezGlHzAFFE+S8AxvAsS5t+ARMzsvuJx7MaUfGDALQIKyooBXllxj1uo4Zn4CBbZqECNdO3wwzlhl7ZEygrWa04gBa5rVqgg+JahrvXD6HPHu4XldWIULtqTCPPIm4OsY" + ], + "0/62/5": 1, + "0/62/65532": 0, + "0/62/65533": 2, + "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, 65532, 65533, 65528, 65529, 65531], + "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, 65532, 65533, 65528, 65529, 65531], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 6, + "1/3/65528": [], + "1/3/65529": [0], + "1/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "1/29/0": [ + { + "0": 123, + "1": 2 + } + ], + "1/29/1": [3, 29], + "1/29/2": [], + "1/29/3": [2, 3], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "2/29/0": [ + { + "0": 113, + "1": 3 + } + ], + "2/29/1": [29, 72, 73, 86, 1026], + "2/29/2": [], + "2/29/3": [], + "2/29/4": [ + { + "0": null, + "1": 8, + "2": 2 + } + ], + "2/29/65532": 1, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "2/72/0": ["pre-heating", "pre-heated", "cooling down"], + "2/72/1": 0, + "2/72/2": null, + "2/72/3": [ + { + "0": 0 + }, + { + "0": 1 + }, + { + "0": 3 + } + ], + "2/72/4": 1, + "2/72/5": { + "0": 0 + }, + "2/72/65532": 0, + "2/72/65533": 2, + "2/72/65528": [4], + "2/72/65529": [1, 2], + "2/72/65531": [0, 1, 2, 3, 4, 5, 65532, 65533, 65528, 65529, 65531], + "2/73/0": [ + { + "0": "Bake", + "1": 0, + "2": [ + { + "1": 16384 + } + ] + }, + { + "0": "Convection", + "1": 1, + "2": [ + { + "1": 16385 + } + ] + }, + { + "0": "Grill", + "1": 2, + "2": [ + { + "1": 16386 + } + ] + }, + { + "0": "Roast", + "1": 3, + "2": [ + { + "1": 16387 + } + ] + }, + { + "0": "Clean", + "1": 4, + "2": [ + { + "1": 16388 + } + ] + }, + { + "0": "Convection Bake", + "1": 5, + "2": [ + { + "1": 16389 + } + ] + }, + { + "0": "Convection Roast", + "1": 6, + "2": [ + { + "1": 16390 + } + ] + }, + { + "0": "Warming", + "1": 7, + "2": [ + { + "1": 16391 + } + ] + }, + { + "0": "Proofing", + "1": 8, + "2": [ + { + "1": 16392 + } + ] + } + ], + "2/73/1": 0, + "2/73/65532": 0, + "2/73/65533": 2, + "2/73/65528": [1], + "2/73/65529": [0], + "2/73/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "2/86/0": 7600, + "2/86/1": 7600, + "2/86/2": 28800, + "2/86/65532": 1, + "2/86/65533": 1, + "2/86/65528": [], + "2/86/65529": [0], + "2/86/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "2/1026/0": 6555, + "2/1026/1": 3000, + "2/1026/2": 30000, + "2/1026/65532": 0, + "2/1026/65533": 4, + "2/1026/65528": [], + "2/1026/65529": [], + "2/1026/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531], + "3/3/0": 0, + "3/3/1": 0, + "3/3/65532": 0, + "3/3/65533": 6, + "3/3/65528": [], + "3/3/65529": [0], + "3/3/65531": [0, 1, 65532, 65533, 65528, 65529, 65531], + "3/6/0": false, + "3/6/65532": 4, + "3/6/65533": 6, + "3/6/65528": [], + "3/6/65529": [0], + "3/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "3/29/0": [ + { + "0": 120, + "1": 1 + } + ], + "3/29/1": [3, 6, 29], + "3/29/2": [], + "3/29/3": [4], + "3/29/65532": 0, + "3/29/65533": 2, + "3/29/65528": [], + "3/29/65529": [], + "3/29/65531": [0, 1, 2, 3, 65532, 65533, 65528, 65529, 65531], + "4/6/0": false, + "4/6/65532": 4, + "4/6/65533": 6, + "4/6/65528": [], + "4/6/65529": [0], + "4/6/65531": [0, 65532, 65533, 65528, 65529, 65531], + "4/29/0": [ + { + "0": 119, + "1": 1 + } + ], + "4/29/1": [6, 29, 86, 1026], + "4/29/2": [], + "4/29/3": [], + "4/29/4": [ + { + "0": null, + "1": 8, + "2": 0 + } + ], + "4/29/65532": 1, + "4/29/65533": 2, + "4/29/65528": [], + "4/29/65529": [], + "4/29/65531": [0, 1, 2, 3, 4, 65532, 65533, 65528, 65529, 65531], + "4/86/4": 0, + "4/86/5": ["Low", "Medium", "High"], + "4/86/65532": 2, + "4/86/65533": 1, + "4/86/65528": [], + "4/86/65529": [0], + "4/86/65531": [4, 5, 65532, 65533, 65528, 65529, 65531], + "4/1026/0": 0, + "4/1026/1": null, + "4/1026/2": null, + "4/1026/65532": 0, + "4/1026/65533": 4, + "4/1026/65528": [], + "4/1026/65529": [], + "4/1026/65531": [0, 1, 2, 65532, 65533, 65528, 65529, 65531] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index d05ff71964b..713f0b25f45 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -1719,6 +1719,134 @@ 'state': 'previous', }) # --- +# name: test_selects[oven][select.mock_oven_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Bake', + 'Convection', + 'Grill', + 'Roast', + 'Clean', + 'Convection Bake', + 'Convection Roast', + 'Warming', + 'Proofing', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_oven_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': 'Mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-MatterOvenMode-73-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[oven][select.mock_oven_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Oven Mode', + 'options': list([ + 'Bake', + 'Convection', + 'Grill', + 'Roast', + 'Clean', + 'Convection Bake', + 'Convection Roast', + 'Warming', + 'Proofing', + ]), + }), + 'context': , + 'entity_id': 'select.mock_oven_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Bake', + }) +# --- +# name: test_selects[oven][select.mock_oven_temperature_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_oven_temperature_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature level', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'temperature_level', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-TemperatureControlSelectedTemperatureLevel-86-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[oven][select.mock_oven_temperature_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Oven Temperature level', + 'options': list([ + 'Low', + 'Medium', + 'High', + ]), + }), + 'context': , + 'entity_id': 'select.mock_oven_temperature_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Low', + }) +# --- # name: test_selects[silabs_evse_charging][select.evse_energy_management_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 9c44be97bc9..454e6e67a4c 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2639,6 +2639,228 @@ 'state': 'stopped', }) # --- +# name: test_sensors[oven][sensor.mock_oven_current_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'pre-heating', + 'pre-heated', + 'cooling down', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_oven_current_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current phase', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'current_phase', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-OvenCavityOperationalStateCurrentPhase-72-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_current_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Oven Current phase', + 'options': list([ + 'pre-heating', + 'pre-heated', + 'cooling down', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_oven_current_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pre-heating', + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_operational_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'stopped', + 'running', + 'error', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_oven_operational_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operational state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'operational_state', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-OvenCavityOperationalState-72-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_operational_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Oven Operational state', + 'options': list([ + 'stopped', + 'running', + 'error', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_oven_operational_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_temperature_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_oven_temperature_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': 'Temperature (2)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-TemperatureSensor-1026-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_temperature_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Oven Temperature (2)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_oven_temperature_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '65.55', + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_temperature_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_oven_temperature_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': 'Temperature (4)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-TemperatureSensor-1026-0', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[oven][sensor.mock_oven_temperature_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Mock Oven Temperature (4)', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.mock_oven_temperature_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_sensors[pressure_sensor][sensor.mock_pressure_sensor_pressure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index e37b3d9f2b4..08a3e0290c8 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -478,6 +478,102 @@ 'state': 'off', }) # --- +# name: test_switches[oven][switch.mock_oven_power_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_oven_power_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': 'Power (3)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-3-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[oven][switch.mock_oven_power_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Oven Power (3)', + }), + 'context': , + 'entity_id': 'switch.mock_oven_power_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switches[oven][switch.mock_oven_power_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.mock_oven_power_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': 'Power (4)', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-MatterPowerToggle-6-0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switches[oven][switch.mock_oven_power_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Mock Oven Power (4)', + }), + 'context': , + 'entity_id': 'switch.mock_oven_power_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switches[pump][switch.mock_pump_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 45c0a19a6897cfdd95ce633b94a121802153833d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 10 May 2025 09:44:55 +0200 Subject: [PATCH 0325/1175] Fix squeezebox test serializing mocks (#144600) --- tests/components/squeezebox/test_media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index f3292f1b469..824cc387139 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -799,6 +799,8 @@ async def test_squeezebox_server_discovery( """Mock the async_discover function of pysqueezebox.""" return callback(lms_factory(2)) + lms.async_prepared_status.return_value = {} + with ( patch( "homeassistant.components.squeezebox.Server", From 86cf01a9019e8eae58eaa81417ada78d026e526c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Sat, 10 May 2025 10:53:16 +0200 Subject: [PATCH 0326/1175] Delete deprecated program switches from Home Connect (#144606) --- .../components/home_connect/switch.py | 158 +-------- tests/components/home_connect/test_switch.py | 303 +----------------- 2 files changed, 4 insertions(+), 457 deletions(-) diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 05f0ed2ddc3..cb032a5815d 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -3,31 +3,18 @@ import logging from typing import Any, cast -from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey +from aiohomeconnect.model import OptionKey, SettingKey from aiohomeconnect.model.error import HomeConnectError -from aiohomeconnect.model.program import EnumerateProgram -from homeassistant.components.automation import automations_with_entity -from homeassistant.components.script import scripts_with_entity from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) from homeassistant.helpers.typing import UNDEFINED, UndefinedType from .common import setup_home_connect_entry from .const import BSH_POWER_OFF, BSH_POWER_ON, BSH_POWER_STANDBY, DOMAIN -from .coordinator import ( - HomeConnectApplianceData, - HomeConnectConfigEntry, - HomeConnectCoordinator, -) +from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry from .entity import HomeConnectEntity, HomeConnectOptionEntity from .utils import get_dict_from_home_connect_error @@ -154,11 +141,6 @@ def _get_entities_for_appliance( ) -> list[HomeConnectEntity]: """Get a list of entities.""" entities: list[HomeConnectEntity] = [] - entities.extend( - HomeConnectProgramSwitch(entry.runtime_data, appliance, program) - for program in appliance.programs - if program.key != ProgramKey.UNKNOWN - ) if SettingKey.BSH_COMMON_POWER_STATE in appliance.settings: entities.append( HomeConnectPowerSwitch( @@ -247,142 +229,6 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity): self._attr_is_on = self.appliance.settings[SettingKey(self.bsh_key)].value -class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): - """Switch class for Home Connect.""" - - def __init__( - self, - coordinator: HomeConnectCoordinator, - appliance: HomeConnectApplianceData, - program: EnumerateProgram, - ) -> None: - """Initialize the entity.""" - desc = " ".join(["Program", program.key.split(".")[-1]]) - if appliance.info.type == "WasherDryer": - desc = " ".join( - ["Program", program.key.split(".")[-3], program.key.split(".")[-1]] - ) - self.program = program - super().__init__( - coordinator, - appliance, - SwitchEntityDescription( - key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM, - entity_registry_enabled_default=False, - ), - ) - self._attr_name = f"{appliance.info.name} {desc}" - self._attr_unique_id = f"{appliance.info.ha_id}-{desc}" - self._attr_has_entity_name = False - - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - automations = automations_with_entity(self.hass, self.entity_id) - scripts = scripts_with_entity(self.hass, self.entity_id) - items = automations + scripts - if not items: - return - - entity_reg: er.EntityRegistry = er.async_get(self.hass) - entity_automations = [ - automation_entity - for automation_id in automations - if (automation_entity := entity_reg.async_get(automation_id)) - ] - entity_scripts = [ - script_entity - for script_id in scripts - if (script_entity := entity_reg.async_get(script_id)) - ] - - items_list = [ - f"- [{item.original_name}](/config/automation/edit/{item.unique_id})" - for item in entity_automations - ] + [ - f"- [{item.original_name}](/config/script/edit/{item.unique_id})" - for item in entity_scripts - ] - - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_program_switch_in_automations_scripts_{self.entity_id}", - breaks_in_ha_version="2025.6.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_program_switch_in_automations_scripts", - translation_placeholders={ - "entity_id": self.entity_id, - "items": "\n".join(items_list), - }, - ) - - async def async_will_remove_from_hass(self) -> None: - """Call when entity will be removed from hass.""" - async_delete_issue( - self.hass, - DOMAIN, - f"deprecated_program_switch_in_automations_scripts_{self.entity_id}", - ) - async_delete_issue( - self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}" - ) - - def create_action_handler_issue(self) -> None: - """Create deprecation issue.""" - async_create_issue( - self.hass, - DOMAIN, - f"deprecated_program_switch_{self.entity_id}", - breaks_in_ha_version="2025.6.0", - is_fixable=True, - is_persistent=True, - severity=IssueSeverity.WARNING, - translation_key="deprecated_program_switch", - translation_placeholders={ - "entity_id": self.entity_id, - }, - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Start the program.""" - self.create_action_handler_issue() - try: - await self.coordinator.client.start_program( - self.appliance.info.ha_id, program_key=self.program.key - ) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="start_program", - translation_placeholders={ - **get_dict_from_home_connect_error(err), - "program": self.program.key, - }, - ) from err - - async def async_turn_off(self, **kwargs: Any) -> None: - """Stop the program.""" - self.create_action_handler_issue() - try: - await self.coordinator.client.stop_program(self.appliance.info.ha_id) - except HomeConnectError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="stop_program", - translation_placeholders={ - **get_dict_from_home_connect_error(err), - }, - ) from err - - def update_native_value(self) -> None: - """Update the switch's status based on if the program related to this entity is currently active.""" - event = self.appliance.events.get(EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM) - self._attr_is_on = bool(event and event.value == self.program.key) - - class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): """Power switch class for Home Connect.""" diff --git a/tests/components/home_connect/test_switch.py b/tests/components/home_connect/test_switch.py index 40d2468fb3e..1131f0ab46e 100644 --- a/tests/components/home_connect/test_switch.py +++ b/tests/components/home_connect/test_switch.py @@ -1,16 +1,12 @@ """Tests for home_connect sensor entities.""" from collections.abc import Awaitable, Callable -from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, MagicMock from aiohomeconnect.model import ( ArrayOfEvents, - ArrayOfPrograms, ArrayOfSettings, - Event, - EventKey, EventMessage, EventType, GetSetting, @@ -26,19 +22,16 @@ from aiohomeconnect.model.error import ( HomeConnectError, SelectedProgramNotSetError, ) -from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption +from aiohomeconnect.model.program import ProgramDefinitionOption from aiohomeconnect.model.setting import SettingConstraints import pytest -from homeassistant.components import automation, script -from homeassistant.components.automation import automations_with_entity from homeassistant.components.home_connect.const import ( BSH_POWER_OFF, BSH_POWER_ON, BSH_POWER_STANDBY, DOMAIN, ) -from homeassistant.components.script import scripts_with_entity from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -52,15 +45,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - issue_registry as ir, -) -from homeassistant.setup import async_setup_component +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -from tests.typing import ClientSessionGenerator @pytest.fixture @@ -80,17 +67,6 @@ async def test_paired_depaired_devices_flow( appliance: HomeAppliance, ) -> None: """Test that removed devices are correctly removed from and added to hass on API events.""" - client.get_available_program = AsyncMock( - return_value=ProgramDefinition( - ProgramKey.UNKNOWN, - options=[ - ProgramDefinitionOption( - OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE, - "Boolean", - ) - ], - ) - ) assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -140,7 +116,6 @@ async def test_paired_depaired_devices_flow( ( SettingKey.BSH_COMMON_POWER_STATE, SettingKey.BSH_COMMON_CHILD_LOCK, - "Program Cotton", ), ) ], @@ -162,7 +137,6 @@ async def test_connected_devices( not be obtained while disconnected and once connected, the entities are added. """ get_settings_original_mock = client.get_settings - get_all_programs_mock = client.get_all_programs async def get_settings_side_effect(ha_id: str): if ha_id == appliance.ha_id: @@ -171,19 +145,10 @@ async def test_connected_devices( ) return await get_settings_original_mock.side_effect(ha_id) - async def get_all_programs_side_effect(ha_id: str): - if ha_id == appliance.ha_id: - raise HomeConnectApiError( - "SDK.Error.HomeAppliance.Connection.Initialization.Failed" - ) - return await get_all_programs_mock.side_effect(ha_id) - client.get_settings = AsyncMock(side_effect=get_settings_side_effect) - client.get_all_programs = AsyncMock(side_effect=get_all_programs_side_effect) assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED client.get_settings = get_settings_original_mock - client.get_all_programs = get_all_programs_mock device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) assert device @@ -226,7 +191,6 @@ async def test_switch_entity_availability( entity_ids = [ "switch.dishwasher_power", "switch.dishwasher_child_lock", - "switch.dishwasher_program_eco50", ] assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED @@ -321,82 +285,6 @@ async def test_switch_functionality( assert hass.states.is_state(entity_id, state) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize( - ("entity_id", "program_key", "initial_state", "appliance"), - [ - ( - "switch.dryer_program_mix", - ProgramKey.LAUNDRY_CARE_DRYER_MIX, - STATE_OFF, - "Dryer", - ), - ( - "switch.dryer_program_cotton", - ProgramKey.LAUNDRY_CARE_DRYER_COTTON, - STATE_ON, - "Dryer", - ), - ], - indirect=["appliance"], -) -async def test_program_switch_functionality( - hass: HomeAssistant, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - entity_id: str, - program_key: ProgramKey, - initial_state: str, - appliance: HomeAppliance, -) -> None: - """Test switch functionality.""" - - async def mock_stop_program(ha_id: str) -> None: - """Mock stop program.""" - await client.add_events( - [ - EventMessage( - ha_id, - EventType.NOTIFY, - ArrayOfEvents( - [ - Event( - key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM, - raw_key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value, - timestamp=0, - level="", - handling="", - value=ProgramKey.UNKNOWN, - ) - ] - ), - ), - ] - ) - - client.stop_program = AsyncMock(side_effect=mock_stop_program) - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - assert hass.states.is_state(entity_id, initial_state) - - await hass.services.async_call( - SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id} - ) - await hass.async_block_till_done() - assert hass.states.is_state(entity_id, STATE_ON) - client.start_program.assert_awaited_once_with( - appliance.ha_id, program_key=program_key - ) - - await hass.services.async_call( - SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id} - ) - await hass.async_block_till_done() - assert hass.states.is_state(entity_id, STATE_OFF) - client.stop_program.assert_awaited_once_with(appliance.ha_id) - - @pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ( @@ -406,18 +294,6 @@ async def test_program_switch_functionality( "exception_match", ), [ - ( - "switch.dishwasher_program_eco50", - SERVICE_TURN_ON, - "start_program", - r"Error.*start.*program.*", - ), - ( - "switch.dishwasher_program_eco50", - SERVICE_TURN_OFF, - "stop_program", - r"Error.*stop.*program.*", - ), ( "switch.dishwasher_power", SERVICE_TURN_OFF, @@ -455,15 +331,6 @@ async def test_switch_exception_handling( exception_match: str, ) -> None: """Test exception handling.""" - client_with_exception.get_all_programs.side_effect = None - client_with_exception.get_all_programs.return_value = ArrayOfPrograms( - [ - EnumerateProgram( - key=ProgramKey.DISHCARE_DISHWASHER_ECO_50, - raw_key=ProgramKey.DISHCARE_DISHWASHER_ECO_50.value, - ) - ] - ) client_with_exception.get_settings.side_effect = None client_with_exception.get_settings.return_value = ArrayOfSettings( [ @@ -780,172 +647,6 @@ async def test_power_switch_service_validation_errors( ) -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize( - "service", - [SERVICE_TURN_ON, SERVICE_TURN_OFF], -) -async def test_create_program_switch_deprecation_issue( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - service: str, -) -> None: - """Test that we create an issue when an automation or script is using a program switch entity or the entity is used by the user.""" - entity_id = "switch.washer_program_mix" - automation_script_issue_id = f"deprecated_program_switch_{entity_id}" - action_handler_issue_id = f"deprecated_program_switch_{entity_id}" - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "test", - "trigger": {"platform": "state", "entity_id": entity_id}, - "action": { - "action": "automation.turn_on", - "target": { - "entity_id": "automation.test", - }, - }, - } - }, - ) - assert await async_setup_component( - hass, - script.DOMAIN, - { - script.DOMAIN: { - "test": { - "sequence": [ - { - "action": "switch.turn_on", - "entity_id": entity_id, - }, - ], - } - } - }, - ) - - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - await hass.services.async_call( - SWITCH_DOMAIN, - service, - { - ATTR_ENTITY_ID: entity_id, - }, - blocking=True, - ) - - assert automations_with_entity(hass, entity_id)[0] == "automation.test" - assert scripts_with_entity(hass, entity_id)[0] == "script.test" - - assert len(issue_registry.issues) == 2 - assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id) - assert issue_registry.async_get_issue(DOMAIN, action_handler_issue_id) - - await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() - - # Assert the issue is no longer present - assert not issue_registry.async_get_issue(DOMAIN, automation_script_issue_id) - assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id) - assert len(issue_registry.issues) == 0 - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -@pytest.mark.parametrize( - "service", - [SERVICE_TURN_ON, SERVICE_TURN_OFF], -) -async def test_program_switch_deprecation_issue_fix( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - issue_registry: ir.IssueRegistry, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - service: str, -) -> None: - """Test we can fix the issues created when a program switch entity is in an automation or in a script or when is used.""" - entity_id = "switch.washer_program_mix" - automation_script_issue_id = f"deprecated_program_switch_{entity_id}" - action_handler_issue_id = f"deprecated_program_switch_{entity_id}" - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "alias": "test", - "trigger": {"platform": "state", "entity_id": entity_id}, - "action": { - "action": "automation.turn_on", - "target": { - "entity_id": "automation.test", - }, - }, - } - }, - ) - assert await async_setup_component( - hass, - script.DOMAIN, - { - script.DOMAIN: { - "test": { - "sequence": [ - { - "action": "switch.turn_on", - "entity_id": entity_id, - }, - ], - } - } - }, - ) - - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - await hass.services.async_call( - SWITCH_DOMAIN, - service, - { - ATTR_ENTITY_ID: entity_id, - }, - blocking=True, - ) - - assert automations_with_entity(hass, entity_id)[0] == "automation.test" - assert scripts_with_entity(hass, entity_id)[0] == "script.test" - - assert len(issue_registry.issues) == 2 - assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id) - assert issue_registry.async_get_issue(DOMAIN, action_handler_issue_id) - - for issue in issue_registry.issues.copy().values(): - _client = await hass_client() - resp = await _client.post( - "/api/repairs/issues/fix", - json={"handler": DOMAIN, "issue_id": issue.issue_id}, - ) - assert resp.status == HTTPStatus.OK - flow_id = (await resp.json())["flow_id"] - resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}") - - # Assert the issue is no longer present - assert not issue_registry.async_get_issue(DOMAIN, automation_script_issue_id) - assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id) - assert len(issue_registry.issues) == 0 - - @pytest.mark.parametrize( ( "set_active_program_options_side_effect", From 5e580327459b3bf72c2ee4885065701d2b267b32 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Sat, 10 May 2025 10:54:11 +0200 Subject: [PATCH 0327/1175] Add Codeowner to OpenWeatherMap (#144605) --- CODEOWNERS | 4 ++-- homeassistant/components/openweathermap/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bbc1b30efdc..ec6d5dc6254 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1111,8 +1111,8 @@ build.json @home-assistant/supervisor /tests/components/opentherm_gw/ @mvn23 /homeassistant/components/openuv/ @bachya /tests/components/openuv/ @bachya -/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi -/tests/components/openweathermap/ @fabaff @freekode @nzapponi +/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck +/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck /homeassistant/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish /homeassistant/components/opower/ @tronikos diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 88510aaae8c..2c32882b6ed 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -1,7 +1,7 @@ { "domain": "openweathermap", "name": "OpenWeatherMap", - "codeowners": ["@fabaff", "@freekode", "@nzapponi"], + "codeowners": ["@fabaff", "@freekode", "@nzapponi", "@wittypluck"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", "iot_class": "cloud_polling", From 1416580f8b9167baca8e3fc4d4674ac2666603bc Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sat, 10 May 2025 20:23:52 +0200 Subject: [PATCH 0328/1175] fix enphase_envoy diagnostics home endpoint name (#144634) --- homeassistant/components/enphase_envoy/diagnostics.py | 2 +- .../enphase_envoy/snapshots/test_diagnostics.ambr | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/diagnostics.py b/homeassistant/components/enphase_envoy/diagnostics.py index 6fcf73bebe9..97079255876 100644 --- a/homeassistant/components/enphase_envoy/diagnostics.py +++ b/homeassistant/components/enphase_envoy/diagnostics.py @@ -64,7 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]: "/ivp/ensemble/generator", "/ivp/meters", "/ivp/meters/readings", - "/home,", + "/home", ] for end_point in end_points: diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index acbd7de6c0e..650fb0bb810 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -896,8 +896,8 @@ '/api/v1/production/inverters': 'Testing request replies.', '/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}', '/api/v1/production_log': '{"headers":{"Hello":"World"},"code":200}', - '/home,': 'Testing request replies.', - '/home,_log': '{"headers":{"Hello":"World"},"code":200}', + '/home': 'Testing request replies.', + '/home_log': '{"headers":{"Hello":"World"},"code":200}', '/info': 'Testing request replies.', '/info_log': '{"headers":{"Hello":"World"},"code":200}', '/ivp/ensemble/dry_contacts': 'Testing request replies.', @@ -1390,7 +1390,7 @@ '/api/v1/production_log': dict({ 'Error': "EnvoyError('Test')", }), - '/home,_log': dict({ + '/home_log': dict({ 'Error': "EnvoyError('Test')", }), '/info_log': dict({ From 4501303beb31891d35ce185726a590073c4241a8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 10 May 2025 22:11:32 +0200 Subject: [PATCH 0329/1175] Fix licenses check for jaraco.itertools (#144631) --- .github/workflows/ci.yaml | 2 +- script/licenses.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae732ef4912..497e5b4b149 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,7 +37,7 @@ on: type: boolean env: - CACHE_VERSION: 1 + CACHE_VERSION: 2 UV_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1 HA_SHORT_VERSION: "2025.6" diff --git a/script/licenses.py b/script/licenses.py index f801603738a..44a046a099b 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -204,10 +204,6 @@ EXCEPTIONS = { "repoze.lru", "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 - # --- - # https://github.com/jaraco/skeleton/pull/170 - # https://github.com/jaraco/skeleton/pull/171 - "jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21 } TODO = { From 882565a8e5bc1632a615ab264860696d91af27f1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 10 May 2025 20:59:01 -0700 Subject: [PATCH 0330/1175] Bump ical to 9.2.1 (#144642) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 32af3e675b3..668ab6e34be 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index eba26e88d5a..c3ffce2890b 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index fb48ca72337..f93129be94c 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==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index b31fa3389dc..4df3f11cf10 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.0"] + "requirements": ["ical==9.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index cd7caf31cd0..99c80bea42a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1197,7 +1197,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.0 +ical==9.2.1 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 221ab72dc2f..d4b880bc628 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,7 +1018,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.0 +ical==9.2.1 # homeassistant.components.caldav icalendar==6.1.0 From e065f1e097528c9b4b60ef28698d290a59da43c9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 11 May 2025 07:06:42 +0200 Subject: [PATCH 0331/1175] Update pylint to 3.3.7 + astroid to 3.3.10 (#144630) * Update pylint to 3.3.7 + astroid to 3.3.10 * Remove unnecessary pylint disable comment --- requirements_test.txt | 4 ++-- tests/components/esphome/common.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 80be991cfcd..2839d7f7982 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==3.3.9 +astroid==3.3.10 coverage==7.6.12 freezegun==1.5.1 go2rtc-client==0.1.2 @@ -16,7 +16,7 @@ mock-open==1.4.0 mypy-dev==1.16.0a8 pre-commit==4.0.0 pydantic==2.11.3 -pylint==3.3.6 +pylint==3.3.7 pylint-per-file-ignores==1.4.0 pipdeptree==2.25.1 pytest-asyncio==0.26.0 diff --git a/tests/components/esphome/common.py b/tests/components/esphome/common.py index 426eee11341..814fa27215b 100644 --- a/tests/components/esphome/common.py +++ b/tests/components/esphome/common.py @@ -4,8 +4,6 @@ from datetime import datetime from homeassistant.components import assist_satellite from homeassistant.components.assist_satellite import AssistSatelliteEntity - -# pylint: disable-next=hass-component-root-import from homeassistant.components.esphome import DOMAIN from homeassistant.components.esphome.assist_satellite import EsphomeAssistSatellite from homeassistant.components.esphome.coordinator import REFRESH_INTERVAL From 996839cb677acc35ed7c276efb092fd697c73e45 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 10:12:35 +0200 Subject: [PATCH 0332/1175] Fix sentence-casing and spelling of "SIA-based" in `sia` (#144659) Fix sentence-casing and spelling of `SIA-based` in `sia` --- homeassistant/components/sia/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index e5eb4770db5..df2e11b5659 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -6,12 +6,12 @@ "port": "[%key:common::config_flow::data::port%]", "protocol": "Protocol", "account": "Account ID", - "encryption_key": "Encryption Key", - "ping_interval": "Ping Interval (min)", + "encryption_key": "Encryption key", + "ping_interval": "Ping interval (min)", "zones": "Number of zones for the account", "additional_account": "Additional accounts" }, - "title": "Create a connection for SIA based alarm systems." + "title": "Create a connection for SIA-based alarm systems." }, "additional_account": { "data": { From 54a7691a80840b1b4b8992322379652f5c40f19d Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sun, 11 May 2025 10:14:10 +0200 Subject: [PATCH 0333/1175] Fix typo in ntfy integration (#144650) fix typo in ntfy integratrion --- homeassistant/components/ntfy/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ntfy/strings.json b/homeassistant/components/ntfy/strings.json index a48d158c896..5b2e93bc97b 100644 --- a/homeassistant/components/ntfy/strings.json +++ b/homeassistant/components/ntfy/strings.json @@ -51,7 +51,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "account_mismatch": "The provided access token corresponds to the account {wrong_username}. Please re-authenticate with with the account **{username}**" + "account_mismatch": "The provided access token corresponds to the account {wrong_username}. Please re-authenticate with the account **{username}**" } }, "config_subentries": { From ebb61caa538c7191123d44a386a077b6d05862dc Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 12:20:30 +0200 Subject: [PATCH 0334/1175] Add missing hyphen to "file-based" in `file` (#144640) --- homeassistant/components/file/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/file/strings.json b/homeassistant/components/file/strings.json index bd8f23602e3..02f8c42755b 100644 --- a/homeassistant/components/file/strings.json +++ b/homeassistant/components/file/strings.json @@ -4,13 +4,13 @@ "user": { "description": "Make a choice", "menu_options": { - "sensor": "Set up a file based sensor", + "sensor": "Set up a file-based sensor", "notify": "Set up a notification service" } }, "sensor": { "title": "File sensor", - "description": "Set up a file based sensor", + "description": "[%key:component::file::config::step::user::menu_options::sensor%]", "data": { "file_path": "File path", "value_template": "Value template", From 58161b5fa2c4ad681371908f33d0a36bbfe898c4 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 11 May 2025 12:56:40 +0200 Subject: [PATCH 0335/1175] Bump python-linkplay to v0.2.5 (#144666) Bump linkplay to 0.2.5 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 69a7b71eeb6..ac89d2ff399 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.4"], + "requirements": ["python-linkplay==0.2.5"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 99c80bea42a..eea34defbbd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2437,7 +2437,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.4 +python-linkplay==0.2.5 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4b880bc628..b750302275f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1980,7 +1980,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.4 +python-linkplay==0.2.5 # homeassistant.components.matter python-matter-server==7.0.0 From 31a576b206b1084261ff8fe529ac6c969f7ed7e2 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 12:57:28 +0200 Subject: [PATCH 0336/1175] Add missing hyphen to "time-based" in `filter` (#144639) Fix spelling of "time-based" in `filter` Also sentence-case the complete string. --- homeassistant/components/filter/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filter/strings.json b/homeassistant/components/filter/strings.json index b0403227fd4..689cf730023 100644 --- a/homeassistant/components/filter/strings.json +++ b/homeassistant/components/filter/strings.json @@ -183,7 +183,7 @@ "outlier": "Outlier", "throttle": "Throttle", "time_throttle": "Time throttle", - "time_simple_moving_average": "Moving Average (Time based)" + "time_simple_moving_average": "Moving average (time-based)" } }, "type": { From 773a2a9db697e55a5b7d42b4502aa01287560302 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 12:57:44 +0200 Subject: [PATCH 0337/1175] Add missing hyphen to "time-based" in `integration` (#144638) --- homeassistant/components/integration/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index ed4f5de3ea7..ddd0d42ca39 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -18,7 +18,7 @@ "round": "Controls the number of decimal digits in the output.", "unit_prefix": "The output will be scaled according to the selected metric prefix.", "unit_time": "The output will be scaled according to the selected time unit.", - "max_sub_interval": "Applies time based integration if the source did not change for this duration. Use 0 for no time based updates." + "max_sub_interval": "Applies time-based integration if the source did not change for this duration. Use 0 for no time-based updates." } } } From 09515bf174f6c385cea89f697e4757f6e12ffe81 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 12:58:04 +0200 Subject: [PATCH 0338/1175] Add missing hyphen to "time-weighted" in `derivative` (#144637) --- homeassistant/components/derivative/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index bfdf861a019..f1b7375ae07 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -15,7 +15,7 @@ }, "data_description": { "round": "Controls the number of decimal digits in the output.", - "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "time_window": "If set, the sensor's value is a time-weighted moving average of derivatives within this window.", "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative." } } From 0dadd3122167a0ccb5481f89df86331c3da4b1cc Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 12:58:55 +0200 Subject: [PATCH 0339/1175] Add missing hyphen to "volume-weighted" in `kraken` (#144636) Also fix sentence-casing in one string. --- homeassistant/components/kraken/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kraken/strings.json b/homeassistant/components/kraken/strings.json index c636dbf8d1f..d30e2bb2dff 100644 --- a/homeassistant/components/kraken/strings.json +++ b/homeassistant/components/kraken/strings.json @@ -14,7 +14,7 @@ "init": { "data": { "scan_interval": "Update interval", - "tracked_asset_pairs": "Tracked Asset Pairs" + "tracked_asset_pairs": "Tracked asset pairs" } } } @@ -40,10 +40,10 @@ "name": "Volume last 24h" }, "volume_weighted_average_today": { - "name": "Volume weighted average today" + "name": "Volume-weighted average today" }, "volume_weighted_average_last_24h": { - "name": "Volume weighted average last 24h" + "name": "Volume-weighted average last 24h" }, "number_of_trades_today": { "name": "Number of trades today" From d0fe7de501620a5cd5c2e8b134adb097b7f6287b Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Sun, 11 May 2025 13:07:35 +0200 Subject: [PATCH 0340/1175] bump pyenphase to 1.26.1 (#144641) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 4516a35f4fe..e978ded7321 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["pyenphase"], "quality_scale": "platinum", - "requirements": ["pyenphase==1.26.0"], + "requirements": ["pyenphase==1.26.1"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index eea34defbbd..047c332c11a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.26.0 +pyenphase==1.26.1 # homeassistant.components.envisalink pyenvisalink==4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b750302275f..ac3cf94e318 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ pyeiscp==0.0.7 pyemoncms==0.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.26.0 +pyenphase==1.26.1 # homeassistant.components.everlights pyeverlights==0.1.0 From 40e2c7b9b7b4fbd2a179aaac67997e23d88bb1c2 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 13:53:21 +0200 Subject: [PATCH 0341/1175] Improve user-facing strings of `plaato` (#144633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make all references to "auth token" consistent, using sentence-casing - remove "Paste … here" so the description correctly refers to the field name 'Auth token' - make the clickable URL text longer by using "these instructions" instead of just "these" - slightly reword using "If you prefer to …" - add the missing hyphen to "built-in" --- homeassistant/components/plaato/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 23568258118..452875fc71d 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -11,10 +11,10 @@ }, "api_method": { "title": "Select API method", - "description": "To be able to query the API an `auth_token` is required which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions\n\n Selected device: **{device_type}** \n\nIf you rather use the built in webhook method (Airlock only) please check the box below and leave Auth Token blank", + "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Selected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", "data": { "use_webhook": "Use webhook", - "token": "Paste Auth Token here" + "token": "Auth token" } }, "webhook": { From 05796dcd51b0727e8a8289e959574612f17c06c5 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 13:53:42 +0200 Subject: [PATCH 0342/1175] Fix grammar in description of `unifi.remove_clients` action (#144632) --- homeassistant/components/unifi/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 8f4f2b420a5..5b88055e62a 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -117,7 +117,7 @@ }, "remove_clients": { "name": "Remove clients from the UniFi Network", - "description": "Cleans up clients that has only been associated with the controller for a short period of time." + "description": "Cleans up clients that have only been associated with the controller for a short period of time." } } } From 85535b2cbdb711dab07d874d7e2f5e8ba7a48cad Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 11 May 2025 17:00:44 +0200 Subject: [PATCH 0343/1175] Bump holidays to 0.72 (#144671) --- 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 d54d6955087..9809862cd52 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.70", "babel==2.15.0"] + "requirements": ["holidays==0.72", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 60196fb15b7..542b68169a3 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.70"] + "requirements": ["holidays==0.72"] } diff --git a/requirements_all.txt b/requirements_all.txt index 047c332c11a..dd8e9f374ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1155,7 +1155,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.70 +holidays==0.72 # homeassistant.components.frontend home-assistant-frontend==20250509.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac3cf94e318..e1f6db8af22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -985,7 +985,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.70 +holidays==0.72 # homeassistant.components.frontend home-assistant-frontend==20250509.0 From 3e6a2168063dbd368e06ba9f043824f0d6bddf81 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 11 May 2025 18:01:51 +0300 Subject: [PATCH 0344/1175] Fix strings typo for Comelit (#144672) --- homeassistant/components/comelit/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 2076ecb5c1e..8f2ae1433e5 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -76,7 +76,7 @@ "cannot_authenticate": { "message": "Error authenticating" }, - "updated_failed": { + "update_failed": { "message": "Failed to update data: {error}" } } From a540c6259444bd0b25dc98407a2d291476e26cfd Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 11 May 2025 17:07:33 +0200 Subject: [PATCH 0345/1175] Bump pylamarzocco to 2.0.2 (#144635) Co-authored-by: Shay Levy --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index fb6a3660c66..1fbef073394 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.1"] + "requirements": ["pylamarzocco==2.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd8e9f374ff..909dde8c8a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2090,7 +2090,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.1 +pylamarzocco==2.0.2 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1f6db8af22..f7603b05761 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1705,7 +1705,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.1 +pylamarzocco==2.0.2 # homeassistant.components.lastfm pylast==5.1.0 From 6f41fbeb22452bf40fa69be1971c9d0dfef1ae40 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sun, 11 May 2025 16:21:01 +0100 Subject: [PATCH 0346/1175] Add PARALLEL_UPDATES to Squeezebox (#144618) --- homeassistant/components/squeezebox/binary_sensor.py | 3 +++ homeassistant/components/squeezebox/button.py | 3 +++ homeassistant/components/squeezebox/media_player.py | 1 + homeassistant/components/squeezebox/sensor.py | 3 +++ 4 files changed, 10 insertions(+) diff --git a/homeassistant/components/squeezebox/binary_sensor.py b/homeassistant/components/squeezebox/binary_sensor.py index daae8703597..1045e526ee3 100644 --- a/homeassistant/components/squeezebox/binary_sensor.py +++ b/homeassistant/components/squeezebox/binary_sensor.py @@ -17,6 +17,9 @@ from . import SqueezeboxConfigEntry from .const import STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_RESCAN from .entity import LMSStatusEntity +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + SENSORS: tuple[BinarySensorEntityDescription, ...] = ( BinarySensorEntityDescription( key=STATUS_SENSOR_RESCAN, diff --git a/homeassistant/components/squeezebox/button.py b/homeassistant/components/squeezebox/button.py index 098df3a1b5c..887151036aa 100644 --- a/homeassistant/components/squeezebox/button.py +++ b/homeassistant/components/squeezebox/button.py @@ -18,6 +18,9 @@ from .entity import SqueezeboxEntity _LOGGER = logging.getLogger(__name__) +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + HARDWARE_MODELS_WITH_SCREEN = [ "Squeezebox Boom", "Squeezebox Radio", diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6e99099ccb1..315ea46c811 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -75,6 +75,7 @@ ATTR_QUERY_RESULT = "query_result" _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 ATTR_PARAMETERS = "parameters" ATTR_OTHER_PLAYER = "other_player" diff --git a/homeassistant/components/squeezebox/sensor.py b/homeassistant/components/squeezebox/sensor.py index 9d9490208ea..11c169910dc 100644 --- a/homeassistant/components/squeezebox/sensor.py +++ b/homeassistant/components/squeezebox/sensor.py @@ -29,6 +29,9 @@ from .const import ( ) from .entity import LMSStatusEntity +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_ALBUMS, From 61f8970aca9f16a91b092390a5a51ebcb2516723 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Sun, 11 May 2025 17:26:02 +0200 Subject: [PATCH 0347/1175] Fix typos in Miele device names to match enum (#144609) --- homeassistant/components/miele/strings.json | 4 ++-- tests/components/miele/snapshots/test_vacuum.ambr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 764fc76a877..959d8e421cd 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -91,10 +91,10 @@ "freezer": { "name": "Freezer" }, - "robot_vacuum_cleander": { + "robot_vacuum_cleaner": { "name": "Robot vacuum cleaner" }, - "steam_oven_microwave": { + "steam_oven_micro": { "name": "Steam oven micro" }, "dialog_oven": { diff --git a/tests/components/miele/snapshots/test_vacuum.ambr b/tests/components/miele/snapshots/test_vacuum.ambr index c3029e83fd8..71254f9c8b3 100644 --- a/tests/components/miele/snapshots/test_vacuum.ambr +++ b/tests/components/miele/snapshots/test_vacuum.ambr @@ -50,7 +50,7 @@ 'turbo', 'silent', ]), - 'friendly_name': 'robot_vacuum_cleaner', + 'friendly_name': 'Robot vacuum cleaner', 'supported_features': , }), 'context': , From 158bbf1f52fe8c8731f61fa7617ec00991484c3e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 11 May 2025 17:33:09 +0200 Subject: [PATCH 0348/1175] Remove unused constant from entity_platform tests (#144601) --- tests/helpers/test_entity_platform.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 8a1bdcb2f0c..77ac85ed4ed 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -56,7 +56,6 @@ from tests.common import ( _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" -PLATFORM = "test_platform" async def test_polling_only_updates_entities_it_should_poll( From ea4120a7d4dd93a09f8cde7f163d3c7aa963daea Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 17:44:27 +0200 Subject: [PATCH 0349/1175] Add missing hyphens to "condition-based" and "pre-entry" in `bmw_connected_drive` (#144685) --- .../bmw_connected_drive/strings.json | 4 +-- .../snapshots/test_binary_sensor.ambr | 28 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index d094116725f..3b8b6fc5ff0 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -69,7 +69,7 @@ "name": "Door lock state" }, "condition_based_services": { - "name": "Condition based services" + "name": "Condition-based services" }, "check_control_messages": { "name": "Check control messages" @@ -81,7 +81,7 @@ "name": "Connection status" }, "is_pre_entry_climatization_enabled": { - "name": "Pre entry climatization" + "name": "Pre-entry climatization" } }, "button": { diff --git a/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr index 569d39c1a5a..0e5a1a7622a 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr @@ -120,7 +120,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Condition based services', + 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -135,7 +135,7 @@ 'brake_fluid': 'OK', 'brake_fluid_date': '2022-10-01', 'device_class': 'problem', - 'friendly_name': 'i3 (+ REX) Condition based services', + 'friendly_name': 'i3 (+ REX) Condition-based services', 'vehicle_check': 'OK', 'vehicle_check_date': '2023-05-01', 'vehicle_tuv': 'OK', @@ -326,7 +326,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pre entry climatization', + 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -338,7 +338,7 @@ # name: test_entity_state_attrs[binary_sensor.i3_rex_pre_entry_climatization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'i3 (+ REX) Pre entry climatization', + 'friendly_name': 'i3 (+ REX) Pre-entry climatization', }), 'context': , 'entity_id': 'binary_sensor.i3_rex_pre_entry_climatization', @@ -520,7 +520,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Condition based services', + 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -536,7 +536,7 @@ 'brake_fluid_date': '2024-12-01', 'brake_fluid_distance': '50000 km', 'device_class': 'problem', - 'friendly_name': 'i4 eDrive40 Condition based services', + 'friendly_name': 'i4 eDrive40 Condition-based services', 'tire_wear_front': 'OK', 'tire_wear_rear': 'OK', 'vehicle_check': 'OK', @@ -730,7 +730,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pre entry climatization', + 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -742,7 +742,7 @@ # name: test_entity_state_attrs[binary_sensor.i4_edrive40_pre_entry_climatization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'i4 eDrive40 Pre entry climatization', + 'friendly_name': 'i4 eDrive40 Pre-entry climatization', }), 'context': , 'entity_id': 'binary_sensor.i4_edrive40_pre_entry_climatization', @@ -927,7 +927,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Condition based services', + 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -943,7 +943,7 @@ 'brake_fluid_date': '2024-12-01', 'brake_fluid_distance': '50000 km', 'device_class': 'problem', - 'friendly_name': 'iX xDrive50 Condition based services', + 'friendly_name': 'iX xDrive50 Condition-based services', 'tire_wear_front': 'OK', 'tire_wear_rear': 'OK', 'vehicle_check': 'OK', @@ -1138,7 +1138,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Pre entry climatization', + 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -1150,7 +1150,7 @@ # name: test_entity_state_attrs[binary_sensor.ix_xdrive50_pre_entry_climatization-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'iX xDrive50 Pre entry climatization', + 'friendly_name': 'iX xDrive50 Pre-entry climatization', }), 'context': , 'entity_id': 'binary_sensor.ix_xdrive50_pre_entry_climatization', @@ -1288,7 +1288,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Condition based services', + 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, 'supported_features': 0, @@ -1304,7 +1304,7 @@ 'brake_fluid_date': '2024-12-01', 'brake_fluid_distance': '50000 km', 'device_class': 'problem', - 'friendly_name': 'M340i xDrive Condition based services', + 'friendly_name': 'M340i xDrive Condition-based services', 'oil': 'OK', 'oil_date': '2024-12-01', 'oil_distance': '50000 km', From 867624fc5941b546e3d43968847e7452d12f79f9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 11 May 2025 20:38:53 +0300 Subject: [PATCH 0350/1175] Take into account coordinator availability for SamsungTV (#144545) --- homeassistant/components/samsungtv/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/entity.py b/homeassistant/components/samsungtv/entity.py index 80ebe461757..1918f6ef28c 100644 --- a/homeassistant/components/samsungtv/entity.py +++ b/homeassistant/components/samsungtv/entity.py @@ -54,7 +54,7 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity) @property def available(self) -> bool: """Return the availability of the device.""" - if self._bridge.auth_failed: + if not super().available or self._bridge.auth_failed: return False return ( self.coordinator.is_on From 8840970d647846fa72538f13977905e29a5e2da6 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 20:30:19 +0200 Subject: [PATCH 0351/1175] Add missing hyphen to "WebSocket-based" in `mqtt` (#144686) Co-authored-by: Jan Bouwhuis --- homeassistant/components/mqtt/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index fdf1ebd8089..7006df09897 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -57,7 +57,7 @@ "set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT brokers certificate.", "set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.", "transport": "The transport to be used for the connection to your MQTT broker.", - "ws_headers": "The WebSocket headers to pass through the WebSocket based connection to your MQTT broker.", + "ws_headers": "The WebSocket headers to pass through the WebSocket-based connection to your MQTT broker.", "ws_path": "The WebSocket path to be used for the connection to your MQTT broker." } }, From 494c7aa3da90791317e35483116c18c9d17e4243 Mon Sep 17 00:00:00 2001 From: Seweryn Zeman Date: Sun, 11 May 2025 20:33:17 +0200 Subject: [PATCH 0352/1175] Removed unused file_id param from open_ai_conversation request (#143878) --- homeassistant/components/openai_conversation/__init__.py | 1 - tests/components/openai_conversation/test_init.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py index 7da1becd333..71effe83884 100644 --- a/homeassistant/components/openai_conversation/__init__.py +++ b/homeassistant/components/openai_conversation/__init__.py @@ -140,7 +140,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: content.append( ResponseInputImageParam( type="input_image", - file_id=filename, image_url=f"data:{mime_type};base64,{base64_file}", detail="auto", ) diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index dc83aa48807..b4f816707e9 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -324,7 +324,6 @@ async def test_init_error( "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE1", "detail": "auto", - "file_id": "/a/b/c.jpg", }, ], }, @@ -349,13 +348,11 @@ async def test_init_error( "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE1", "detail": "auto", - "file_id": "/a/b/c.jpg", }, { "type": "input_image", "image_url": "data:image/jpeg;base64,BASE64IMAGE2", "detail": "auto", - "file_id": "d/e/f.jpg", }, ], }, From 597c386bc2bac667d496da88c352ae1fa3d9d931 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Sun, 11 May 2025 20:58:13 +0200 Subject: [PATCH 0353/1175] Fix missing sentence-casing in `alarmdecoder` (#144690) --- .../components/alarmdecoder/strings.json | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alarmdecoder/strings.json b/homeassistant/components/alarmdecoder/strings.json index ccf1d965855..3bc8363b90d 100644 --- a/homeassistant/components/alarmdecoder/strings.json +++ b/homeassistant/components/alarmdecoder/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Choose AlarmDecoder Protocol", + "title": "Choose AlarmDecoder protocol", "data": { "protocol": "Protocol" } @@ -12,8 +12,8 @@ "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", - "device_baudrate": "Device Baud Rate", - "device_path": "Device Path" + "device_baudrate": "Device baud rate", + "device_path": "Device path" }, "data_description": { "host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.", @@ -44,36 +44,36 @@ "arm_settings": { "title": "[%key:component::alarmdecoder::options::step::init::title%]", "data": { - "auto_bypass": "Auto Bypass on Arm", - "code_arm_required": "Code Required for Arming", - "alt_night_mode": "Alternative Night Mode" + "auto_bypass": "Auto-bypass on arm", + "code_arm_required": "Code required for arming", + "alt_night_mode": "Alternative night mode" } }, "zone_select": { "title": "[%key:component::alarmdecoder::options::step::init::title%]", "description": "Enter the zone number you'd like to to add, edit, or remove.", "data": { - "zone_number": "Zone Number" + "zone_number": "Zone number" } }, "zone_details": { "title": "[%key:component::alarmdecoder::options::step::init::title%]", - "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.", + "description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.", "data": { - "zone_name": "Zone Name", - "zone_type": "Zone Type", - "zone_rfid": "RF Serial", - "zone_loop": "RF Loop", - "zone_relayaddr": "Relay Address", - "zone_relaychan": "Relay Channel" + "zone_name": "Zone name", + "zone_type": "Zone type", + "zone_rfid": "RF serial", + "zone_loop": "RF loop", + "zone_relayaddr": "Relay address", + "zone_relaychan": "Relay channel" } } }, "error": { - "relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.", + "relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.", "int": "The field below must be an integer.", - "loop_rfid": "RF Loop cannot be used without RF Serial.", - "loop_range": "RF Loop must be an integer between 1 and 4." + "loop_rfid": "'RF loop' cannot be used without 'RF serial'.", + "loop_range": "'RF loop' must be an integer between 1 and 4." } }, "services": { From 4f6141581e6b1305abbedf002e2cc41edf573e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 11 May 2025 20:59:23 +0200 Subject: [PATCH 0354/1175] Bump dependency pymiele to 0.5.1 (#144688) --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index c0795922875..7a72d4c8a59 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.4.3"], + "requirements": ["pymiele==0.5.1"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 909dde8c8a4..8040e15e541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2132,7 +2132,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.3 +pymiele==0.5.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7603b05761..2b95448c13d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1744,7 +1744,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.4.3 +pymiele==0.5.1 # homeassistant.components.mochad pymochad==0.2.0 From 6516cd388f8860cac4bb0683977c4ea4b0cdc780 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 11 May 2025 22:00:21 +0300 Subject: [PATCH 0355/1175] Avoid closing shared session for Comelit (#144682) --- homeassistant/components/comelit/__init__.py | 1 - homeassistant/components/comelit/config_flow.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/comelit/__init__.py b/homeassistant/components/comelit/__init__.py index c2a7498afec..23be67fc1a1 100644 --- a/homeassistant/components/comelit/__init__.py +++ b/homeassistant/components/comelit/__init__.py @@ -77,6 +77,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> coordinator = entry.runtime_data if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): await coordinator.api.logout() - await coordinator.api.close() return unload_ok diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index f6bda97a781..10180236f79 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -73,7 +73,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, ) from err finally: await api.logout() - await api.close() return {"title": data[CONF_HOST]} From 80a04314fcd1d0ae3bbbaa297395aa2099ae8fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sun, 11 May 2025 21:05:43 +0200 Subject: [PATCH 0356/1175] Add program phases for Miele washer-dryer (#144664) --- homeassistant/components/miele/const.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 237302937e2..2b933873da4 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -316,6 +316,8 @@ STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = { MieleAppliance.TUMBLE_DRYER: STATE_PROGRAM_PHASE_TUMBLE_DRYER, MieleAppliance.DRYER_PROFESSIONAL: STATE_PROGRAM_PHASE_TUMBLE_DRYER, MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_TUMBLE_DRYER, + MieleAppliance.WASHER_DRYER: STATE_PROGRAM_PHASE_WASHING_MACHINE + | STATE_PROGRAM_PHASE_TUMBLE_DRYER, MieleAppliance.DISHWASHER: STATE_PROGRAM_PHASE_DISHWASHER, MieleAppliance.DISHWASHER_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER, MieleAppliance.DISHWASHER_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER, From 554cb2770376135bc092321983d1572b98cffa28 Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Sun, 11 May 2025 21:06:04 +0200 Subject: [PATCH 0357/1175] Close Octoprint aiohttp session on unload (#144670) --- homeassistant/components/octoprint/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 59fd04357eb..48d81b81f0c 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -181,11 +181,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = aiohttp.ClientSession(connector=connector) @callback - def _async_close_websession(event: Event) -> None: + def _async_close_websession(event: Event | None = None) -> None: """Close websession.""" session.detach() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession) + entry.async_on_unload(_async_close_websession) + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_close_websession) + ) client = OctoprintClient( host=entry.data[CONF_HOST], From b394c07a3d04e8ba475b42d792ac9220f9d96a2c Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sun, 11 May 2025 20:15:12 +0100 Subject: [PATCH 0358/1175] Override available property in button platform for Squeezebox (#144693) --- homeassistant/components/squeezebox/button.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/squeezebox/button.py b/homeassistant/components/squeezebox/button.py index 887151036aa..88018e4f9a9 100644 --- a/homeassistant/components/squeezebox/button.py +++ b/homeassistant/components/squeezebox/button.py @@ -153,6 +153,11 @@ class SqueezeboxButtonEntity(SqueezeboxEntity, ButtonEntity): f"{format_mac(self._player.player_id)}_{entity_description.key}" ) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.available and super().available + async def async_press(self) -> None: """Execute the button action.""" await self._player.async_query("button", self.entity_description.press_action) From 4faa920318b0d7ccee97a5ab3f1d325c790bc563 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 11 May 2025 15:38:21 -0400 Subject: [PATCH 0359/1175] Move Assist Pipeline tests to right file (#144696) --- tests/components/assist_pipeline/__init__.py | 18 + .../assist_pipeline/snapshots/test_init.ambr | 201 ---- .../snapshots/test_pipeline.ambr | 202 ++++ tests/components/assist_pipeline/test_init.py | 847 +---------------- .../assist_pipeline/test_pipeline.py | 862 +++++++++++++++++- 5 files changed, 1079 insertions(+), 1051 deletions(-) create mode 100644 tests/components/assist_pipeline/snapshots/test_pipeline.ambr diff --git a/tests/components/assist_pipeline/__init__.py b/tests/components/assist_pipeline/__init__.py index dd0f80e52ad..cc11fcc6c82 100644 --- a/tests/components/assist_pipeline/__init__.py +++ b/tests/components/assist_pipeline/__init__.py @@ -1,5 +1,10 @@ """Tests for the Voice Assistant integration.""" +from dataclasses import asdict +from unittest.mock import ANY + +from homeassistant.components import assist_pipeline + MANY_LANGUAGES = [ "ar", "bg", @@ -54,3 +59,16 @@ MANY_LANGUAGES = [ "zh-hk", "zh-tw", ] + + +def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: + """Process events to remove dynamic values.""" + processed = [] + for event in events: + as_dict = asdict(event) + as_dict.pop("timestamp") + if as_dict["type"] == assist_pipeline.PipelineEventType.RUN_START: + as_dict["data"]["pipeline"] = ANY + processed.append(as_dict) + + return processed diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index f772f877d3a..81972191868 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -461,204 +461,3 @@ }), ]) # --- -# name: test_pipeline_language_used_instead_of_conversation_language - list([ - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'language': 'en', - 'pipeline': , - }), - 'type': , - }), - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'device_id': None, - 'engine': 'conversation.home_assistant', - 'intent_input': 'test input', - 'language': 'en', - 'prefer_local_intents': False, - }), - 'type': , - }), - dict({ - 'data': dict({ - 'intent_output': dict({ - 'continue_conversation': False, - 'conversation_id': , - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - }), - }), - }), - 'processed_locally': True, - }), - 'type': , - }), - dict({ - 'data': None, - 'type': , - }), - ]) -# --- -# name: test_stt_language_used_instead_of_conversation_language - list([ - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'language': 'en', - 'pipeline': , - }), - 'type': , - }), - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'device_id': None, - 'engine': 'conversation.home_assistant', - 'intent_input': 'test input', - 'language': 'en-US', - 'prefer_local_intents': False, - }), - 'type': , - }), - dict({ - 'data': dict({ - 'intent_output': dict({ - 'continue_conversation': False, - 'conversation_id': , - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - }), - }), - }), - 'processed_locally': True, - }), - 'type': , - }), - dict({ - 'data': None, - 'type': , - }), - ]) -# --- -# name: test_tts_language_used_instead_of_conversation_language - list([ - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'language': 'en', - 'pipeline': , - }), - 'type': , - }), - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'device_id': None, - 'engine': 'conversation.home_assistant', - 'intent_input': 'test input', - 'language': 'en-us', - 'prefer_local_intents': False, - }), - 'type': , - }), - dict({ - 'data': dict({ - 'intent_output': dict({ - 'continue_conversation': False, - 'conversation_id': , - 'response': dict({ - 'card': dict({ - }), - 'data': dict({ - 'failed': list([ - ]), - 'success': list([ - ]), - 'targets': list([ - ]), - }), - 'language': 'en', - 'response_type': 'action_done', - 'speech': dict({ - }), - }), - }), - 'processed_locally': True, - }), - 'type': , - }), - dict({ - 'data': None, - 'type': , - }), - ]) -# --- -# name: test_wake_word_detection_aborted - list([ - dict({ - 'data': dict({ - 'conversation_id': 'mock-ulid', - 'language': 'en', - 'pipeline': , - 'tts_output': dict({ - 'mime_type': 'audio/mpeg', - 'token': 'mocked-token.mp3', - 'url': '/api/tts_proxy/mocked-token.mp3', - }), - }), - 'type': , - }), - dict({ - 'data': dict({ - 'entity_id': 'wake_word.test', - 'metadata': dict({ - 'bit_rate': , - 'channel': , - 'codec': , - 'format': , - 'sample_rate': , - }), - 'timeout': 0, - }), - 'type': , - }), - dict({ - 'data': dict({ - 'code': 'wake_word_detection_aborted', - 'message': '', - }), - 'type': , - }), - dict({ - 'data': None, - 'type': , - }), - ]) -# --- diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr new file mode 100644 index 00000000000..7c0ac254b6e --- /dev/null +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -0,0 +1,202 @@ +# serializer version: 1 +# name: test_pipeline_language_used_instead_of_conversation_language + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'conversation.home_assistant', + 'intent_input': 'test input', + 'language': 'en', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': False, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), + }), + 'processed_locally': True, + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- +# name: test_stt_language_used_instead_of_conversation_language + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'conversation.home_assistant', + 'intent_input': 'test input', + 'language': 'en-US', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': False, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), + }), + 'processed_locally': True, + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- +# name: test_tts_language_used_instead_of_conversation_language + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'conversation.home_assistant', + 'intent_input': 'test input', + 'language': 'en-us', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': False, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + }), + }), + }), + 'processed_locally': True, + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- +# name: test_wake_word_detection_aborted + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + 'tts_output': dict({ + 'mime_type': 'audio/mpeg', + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'entity_id': 'wake_word.test', + 'metadata': dict({ + 'bit_rate': , + 'channel': , + 'codec': , + 'format': , + 'sample_rate': , + }), + 'timeout': 0, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'code': 'wake_word_detection_aborted', + 'message': '', + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 0e04d1f0cd2..0294f9953db 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -2,44 +2,35 @@ import asyncio from collections.abc import Generator -from dataclasses import asdict import itertools as it from pathlib import Path import tempfile -from unittest.mock import ANY, Mock, patch +from unittest.mock import Mock, patch import wave import hass_nabucasa import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import ( - assist_pipeline, - conversation, - media_source, - stt, - tts, -) +from homeassistant.components import assist_pipeline, stt from homeassistant.components.assist_pipeline.const import ( BYTES_PER_CHUNK, CONF_DEBUG_RECORDING_DIR, DOMAIN, ) -from homeassistant.const import MATCH_ALL from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import chat_session, intent from homeassistant.setup import async_setup_component +from . import process_events from .conftest import ( BYTES_ONE_SECOND, MockSTTProvider, MockSTTProviderEntity, - MockTTSProvider, MockWakeWordEntity, make_10ms_chunk, ) -from tests.typing import ClientSessionGenerator, WebSocketGenerator +from tests.typing import WebSocketGenerator @pytest.fixture(autouse=True) @@ -58,19 +49,6 @@ def mock_tts_token() -> Generator[None]: yield -def process_events(events: list[assist_pipeline.PipelineEvent]) -> list[dict]: - """Process events to remove dynamic values.""" - processed = [] - for event in events: - as_dict = asdict(event) - as_dict.pop("timestamp") - if as_dict["type"] == assist_pipeline.PipelineEventType.RUN_START: - as_dict["data"]["pipeline"] = ANY - processed.append(as_dict) - - return processed - - async def test_pipeline_from_audio_stream_auto( hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity, @@ -677,823 +655,6 @@ async def test_pipeline_saved_audio_empty_queue( ) -async def test_wake_word_detection_aborted( - hass: HomeAssistant, - mock_stt_provider: MockSTTProvider, - mock_wake_word_provider_entity: MockWakeWordEntity, - init_components, - pipeline_data: assist_pipeline.pipeline.PipelineData, - mock_chat_session: chat_session.ChatSession, - snapshot: SnapshotAssertion, -) -> None: - """Test creating a pipeline from an audio stream with wake word.""" - - events: list[assist_pipeline.PipelineEvent] = [] - - async def audio_data(): - yield make_10ms_chunk(b"silence!") - yield make_10ms_chunk(b"wake word!") - yield make_10ms_chunk(b"part1") - yield make_10ms_chunk(b"part2") - yield b"" - - 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( - session=mock_chat_session, - device_id=None, - stt_metadata=stt.SpeechMetadata( - language="", - format=stt.AudioFormats.WAV, - codec=stt.AudioCodecs.PCM, - bit_rate=stt.AudioBitRates.BITRATE_16, - sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, - channel=stt.AudioChannels.CHANNEL_MONO, - ), - stt_stream=audio_data(), - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.WAKE_WORD, - end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=events.append, - tts_audio_output=None, - wake_word_settings=assist_pipeline.WakeWordSettings( - audio_seconds_to_buffer=1.5 - ), - audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), - ), - ) - await pipeline_input.validate() - - updates = pipeline.to_json() - updates.pop("id") - await pipeline_store.async_update_item( - pipeline_id, - updates, - ) - await pipeline_input.execute() - - assert process_events(events) == snapshot - - -def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: - """Test that pipeline run equality uses unique id.""" - - def event_callback(event): - pass - - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass) - run_1 = assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.STT, - end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=event_callback, - ) - run_2 = assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.STT, - end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=event_callback, - ) - - assert run_1 == run_1 # noqa: PLR0124 - assert run_1 != run_2 - assert run_1 != 1234 - - -async def test_tts_audio_output( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, - init_components, - pipeline_data: assist_pipeline.pipeline.PipelineData, - mock_chat_session: chat_session.ChatSession, - 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, {}) - - 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.", - session=mock_chat_session, - 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() - - # Verify TTS audio settings - assert pipeline_input.run.tts_stream.options is not None - assert pipeline_input.run.tts_stream.options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" - assert ( - pipeline_input.run.tts_stream.options.get(tts.ATTR_PREFERRED_SAMPLE_RATE) - == 16000 - ) - assert ( - pipeline_input.run.tts_stream.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 - await client.get(event.data["tts_output"]["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_wav_preferred_format( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, - init_components, - mock_chat_session: chat_session.ChatSession, - pipeline_data: assist_pipeline.pipeline.PipelineData, -) -> 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.", - session=mock_chat_session, - 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, - tts.ATTR_PREFERRED_SAMPLE_BYTES, - ] - ) - - 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 - await client.get(event.data["tts_output"]["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 options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_RATE)) == 16000 - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS)) == 1 - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_BYTES)) == 2 - - -async def test_tts_dict_preferred_format( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, - init_components, - mock_chat_session: chat_session.ChatSession, - pipeline_data: assist_pipeline.pipeline.PipelineData, -) -> 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.", - session=mock_chat_session, - 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={ - tts.ATTR_PREFERRED_FORMAT: "flac", - tts.ATTR_PREFERRED_SAMPLE_RATE: 48000, - tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 2, - tts.ATTR_PREFERRED_SAMPLE_BYTES: 2, - }, - ), - ) - 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, - tts.ATTR_PREFERRED_SAMPLE_BYTES, - ] - ) - - 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 - await client.get(event.data["tts_output"]["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 options.get(tts.ATTR_PREFERRED_FORMAT) == "flac" - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_RATE)) == 48000 - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS)) == 2 - assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_BYTES)) == 2 - - -async def test_sentence_trigger_overrides_conversation_agent( - hass: HomeAssistant, - init_components, - mock_chat_session: chat_session.ChatSession, - pipeline_data: assist_pipeline.pipeline.PipelineData, -) -> None: - """Test that sentence triggers are checked before a non-default conversation agent.""" - assert await async_setup_component( - hass, - "automation", - { - "automation": { - "trigger": { - "platform": "conversation", - "command": [ - "test trigger sentence", - ], - }, - "action": { - "set_conversation_response": "test trigger response", - }, - } - }, - ) - - 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( - intent_input="test trigger sentence", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - intent_agent="test-agent", # not the default agent - ), - ) - - # Ensure prepare succeeds - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), - ): - await pipeline_input.validate() - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse" - ) as mock_async_converse: - await pipeline_input.execute() - - # Sentence trigger should have been handled - mock_async_converse.assert_not_called() - - # Verify sentence trigger response - intent_end_event = next( - ( - e - for e in events - if e.type == assist_pipeline.PipelineEventType.INTENT_END - ), - None, - ) - assert (intent_end_event is not None) and intent_end_event.data - assert ( - intent_end_event.data["intent_output"]["response"]["speech"]["plain"][ - "speech" - ] - == "test trigger response" - ) - - -async def test_prefer_local_intents( - hass: HomeAssistant, - init_components, - mock_chat_session: chat_session.ChatSession, - pipeline_data: assist_pipeline.pipeline.PipelineData, -) -> None: - """Test that the default agent is checked first when local intents are preferred.""" - events: list[assist_pipeline.PipelineEvent] = [] - - # Reuse custom sentences in test config - class OrderBeerIntentHandler(intent.IntentHandler): - intent_type = "OrderBeer" - - async def async_handle( - self, intent_obj: intent.Intent - ) -> intent.IntentResponse: - response = intent_obj.create_response() - response.async_set_speech("Order confirmed") - return response - - handler = OrderBeerIntentHandler() - intent.async_register(hass, handler) - - # Fake a test agent and prefer local intents - pipeline_store = pipeline_data.pipeline_store - pipeline_id = pipeline_store.async_get_preferred_item() - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) - await assist_pipeline.pipeline.async_update_pipeline( - hass, pipeline, conversation_engine="test-agent", prefer_local_intents=True - ) - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="I'd like to order a stout please", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - - # Ensure prepare succeeds - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), - ): - await pipeline_input.validate() - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse" - ) as mock_async_converse: - await pipeline_input.execute() - - # Test agent should not have been called - mock_async_converse.assert_not_called() - - # Verify local intent response - intent_end_event = next( - ( - e - for e in events - if e.type == assist_pipeline.PipelineEventType.INTENT_END - ), - None, - ) - assert (intent_end_event is not None) and intent_end_event.data - assert ( - intent_end_event.data["intent_output"]["response"]["speech"]["plain"][ - "speech" - ] - == "Order confirmed" - ) - - -async def test_intent_continue_conversation( - hass: HomeAssistant, - init_components, - mock_chat_session: chat_session.ChatSession, - pipeline_data: assist_pipeline.pipeline.PipelineData, -) -> None: - """Test that a conversation agent flagging continue conversation gets response.""" - events: list[assist_pipeline.PipelineEvent] = [] - - # Fake a test agent and prefer local intents - pipeline_store = pipeline_data.pipeline_store - pipeline_id = pipeline_store.async_get_preferred_item() - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) - await assist_pipeline.pipeline.async_update_pipeline( - hass, pipeline, conversation_engine="test-agent" - ) - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="Set a timer", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - - # Ensure prepare succeeds - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), - ): - await pipeline_input.validate() - - response = intent.IntentResponse("en") - response.async_set_speech("For how long?") - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - return_value=conversation.ConversationResult( - response=response, - conversation_id=mock_chat_session.conversation_id, - continue_conversation=True, - ), - ) as mock_async_converse: - await pipeline_input.execute() - - mock_async_converse.assert_called() - - results = [ - event.data - for event in events - if event.type - in ( - assist_pipeline.PipelineEventType.INTENT_START, - assist_pipeline.PipelineEventType.INTENT_END, - ) - ] - assert results[1]["intent_output"]["continue_conversation"] is True - - # Change conversation agent to default one and register sentence trigger that should not be called - await assist_pipeline.pipeline.async_update_pipeline( - hass, pipeline, conversation_engine=None - ) - pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) - assert await async_setup_component( - hass, - "automation", - { - "automation": { - "trigger": { - "platform": "conversation", - "command": ["Hello"], - }, - "action": { - "set_conversation_response": "test trigger response", - }, - } - }, - ) - - # Because we did continue conversation, it should respond to the test agent again. - events.clear() - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="Hello", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - - # Ensure prepare succeeds - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), - ) as mock_prepare: - await pipeline_input.validate() - - # It requested test agent even if that was not default agent. - assert mock_prepare.mock_calls[0][1][1] == "test-agent" - - response = intent.IntentResponse("en") - response.async_set_speech("Timer set for 20 minutes") - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - return_value=conversation.ConversationResult( - response=response, - conversation_id=mock_chat_session.conversation_id, - ), - ) as mock_async_converse: - await pipeline_input.execute() - - mock_async_converse.assert_called() - - # Snapshot will show it was still handled by the test agent and not default agent - results = [ - event.data - for event in events - if event.type - in ( - assist_pipeline.PipelineEventType.INTENT_START, - assist_pipeline.PipelineEventType.INTENT_END, - ) - ] - assert results[0]["engine"] == "test-agent" - assert results[1]["intent_output"]["continue_conversation"] is False - - -async def test_stt_language_used_instead_of_conversation_language( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - init_components, - mock_chat_session: chat_session.ChatSession, - snapshot: SnapshotAssertion, -) -> None: - """Test that the STT language is used first when the conversation language is '*' (all languages).""" - client = await hass_ws_client(hass) - - events: list[assist_pipeline.PipelineEvent] = [] - - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", - "conversation_language": MATCH_ALL, - "language": "en", - "name": "test_name", - "stt_engine": "test", - "stt_language": "en-US", - "tts_engine": "test", - "tts_language": "en-US", - "tts_voice": "Arnold Schwarzenegger", - "wake_word_entity": None, - "wake_word_id": None, - } - ) - msg = await client.receive_json() - assert msg["success"] - pipeline_id = msg["result"]["id"] - pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="test input", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - await pipeline_input.validate() - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - return_value=conversation.ConversationResult( - intent.IntentResponse(pipeline.language) - ), - ) as mock_async_converse: - await pipeline_input.execute() - - # Check intent start event - assert process_events(events) == snapshot - intent_start: assist_pipeline.PipelineEvent | None = None - for event in events: - if event.type == assist_pipeline.PipelineEventType.INTENT_START: - intent_start = event - break - - assert intent_start is not None - - # STT language (en-US) should be used instead of '*' - assert intent_start.data.get("language") == pipeline.stt_language - - # Check input to async_converse - mock_async_converse.assert_called_once() - assert ( - mock_async_converse.call_args_list[0].kwargs.get("language") - == pipeline.stt_language - ) - - -async def test_tts_language_used_instead_of_conversation_language( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - init_components, - mock_chat_session: chat_session.ChatSession, - snapshot: SnapshotAssertion, -) -> None: - """Test that the TTS language is used after STT when the conversation language is '*' (all languages).""" - client = await hass_ws_client(hass) - - events: list[assist_pipeline.PipelineEvent] = [] - - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", - "conversation_language": MATCH_ALL, - "language": "en", - "name": "test_name", - "stt_engine": None, - "stt_language": None, - "tts_engine": None, - "tts_language": "en-us", - "tts_voice": "Arnold Schwarzenegger", - "wake_word_entity": None, - "wake_word_id": None, - } - ) - msg = await client.receive_json() - assert msg["success"] - pipeline_id = msg["result"]["id"] - pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="test input", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - await pipeline_input.validate() - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - return_value=conversation.ConversationResult( - intent.IntentResponse(pipeline.language) - ), - ) as mock_async_converse: - await pipeline_input.execute() - - # Check intent start event - assert process_events(events) == snapshot - intent_start: assist_pipeline.PipelineEvent | None = None - for event in events: - if event.type == assist_pipeline.PipelineEventType.INTENT_START: - intent_start = event - break - - assert intent_start is not None - - # STT language (en-US) should be used instead of '*' - assert intent_start.data.get("language") == pipeline.tts_language - - # Check input to async_converse - mock_async_converse.assert_called_once() - assert ( - mock_async_converse.call_args_list[0].kwargs.get("language") - == pipeline.tts_language - ) - - -async def test_pipeline_language_used_instead_of_conversation_language( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - init_components, - mock_chat_session: chat_session.ChatSession, - snapshot: SnapshotAssertion, -) -> None: - """Test that the pipeline language is used last when the conversation language is '*' (all languages).""" - client = await hass_ws_client(hass) - - events: list[assist_pipeline.PipelineEvent] = [] - - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline/create", - "conversation_engine": "homeassistant", - "conversation_language": MATCH_ALL, - "language": "en", - "name": "test_name", - "stt_engine": None, - "stt_language": None, - "tts_engine": None, - "tts_language": None, - "tts_voice": None, - "wake_word_entity": None, - "wake_word_id": None, - } - ) - msg = await client.receive_json() - assert msg["success"] - pipeline_id = msg["result"]["id"] - pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) - - pipeline_input = assist_pipeline.pipeline.PipelineInput( - intent_input="test input", - session=mock_chat_session, - run=assist_pipeline.pipeline.PipelineRun( - hass, - context=Context(), - pipeline=pipeline, - start_stage=assist_pipeline.PipelineStage.INTENT, - end_stage=assist_pipeline.PipelineStage.INTENT, - event_callback=events.append, - ), - ) - await pipeline_input.validate() - - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - return_value=conversation.ConversationResult( - intent.IntentResponse(pipeline.language) - ), - ) as mock_async_converse: - await pipeline_input.execute() - - # Check intent start event - assert process_events(events) == snapshot - intent_start: assist_pipeline.PipelineEvent | None = None - for event in events: - if event.type == assist_pipeline.PipelineEventType.INTENT_START: - intent_start = event - break - - assert intent_start is not None - - # STT language (en-US) should be used instead of '*' - assert intent_start.data.get("language") == pipeline.language - - # Check input to async_converse - mock_async_converse.assert_called_once() - assert ( - mock_async_converse.call_args_list[0].kwargs.get("language") - == pipeline.language - ) - - async def test_pipeline_from_audio_stream_with_cloud_auth_fail( hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity, diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index d67a0fd1726..4f15853b296 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1,13 +1,20 @@ """Websocket tests for Voice Assistant integration.""" -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Generator from typing import Any -from unittest.mock import ANY, patch +from unittest.mock import ANY, Mock, patch from hassil.recognize import Intent, IntentData, RecognizeResult import pytest +from syrupy.assertion import SnapshotAssertion -from homeassistant.components import conversation +from homeassistant.components import ( + assist_pipeline, + conversation, + media_source, + stt, + tts, +) from homeassistant.components.assist_pipeline.const import DOMAIN from homeassistant.components.assist_pipeline.pipeline import ( STORAGE_KEY, @@ -24,14 +31,22 @@ from homeassistant.components.assist_pipeline.pipeline import ( async_migrate_engine, async_update_pipeline, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import intent +from homeassistant.const import MATCH_ALL +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import chat_session, intent from homeassistant.setup import async_setup_component -from . import MANY_LANGUAGES -from .conftest import MockSTTProviderEntity, MockTTSProvider +from . import MANY_LANGUAGES, process_events +from .conftest import ( + MockSTTProvider, + MockSTTProviderEntity, + MockTTSProvider, + MockWakeWordEntity, + make_10ms_chunk, +) from tests.common import flush_store +from tests.typing import ClientSessionGenerator, WebSocketGenerator @pytest.fixture(autouse=True) @@ -119,6 +134,22 @@ async def test_load_pipelines(hass: HomeAssistant) -> None: assert store1.async_get_preferred_item() == store2.async_get_preferred_item() +@pytest.fixture(autouse=True) +def mock_chat_session_id() -> Generator[Mock]: + """Mock the conversation ID of chat sessions.""" + with patch( + "homeassistant.helpers.chat_session.ulid_now", return_value="mock-ulid" + ) as mock_ulid_now: + yield mock_ulid_now + + +@pytest.fixture(autouse=True) +def mock_tts_token() -> Generator[None]: + """Mock the TTS token for URLs.""" + with patch("secrets.token_urlsafe", return_value="mocked-token"): + yield + + async def test_loading_pipelines_from_storage( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: @@ -697,3 +728,820 @@ def test_fallback_intent_filter() -> None: ) is False ) + + +async def test_wake_word_detection_aborted( + hass: HomeAssistant, + mock_stt_provider: MockSTTProvider, + mock_wake_word_provider_entity: MockWakeWordEntity, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + mock_chat_session: chat_session.ChatSession, + snapshot: SnapshotAssertion, +) -> None: + """Test wake word stream is first detected, then aborted.""" + + events: list[assist_pipeline.PipelineEvent] = [] + + async def audio_data(): + yield make_10ms_chunk(b"silence!") + yield make_10ms_chunk(b"wake word!") + yield make_10ms_chunk(b"part1") + yield make_10ms_chunk(b"part2") + yield b"" + + 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( + session=mock_chat_session, + device_id=None, + stt_metadata=stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + tts_audio_output=None, + wake_word_settings=assist_pipeline.WakeWordSettings( + audio_seconds_to_buffer=1.5 + ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), + ), + ) + await pipeline_input.validate() + + updates = pipeline.to_json() + updates.pop("id") + await pipeline_store.async_update_item( + pipeline_id, + updates, + ) + await pipeline_input.execute() + + assert process_events(events) == snapshot + + +def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: + """Test that pipeline run equality uses unique id.""" + + def event_callback(event): + pass + + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass) + run_1 = assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.STT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=event_callback, + ) + run_2 = assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.STT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=event_callback, + ) + + assert run_1 == run_1 # noqa: PLR0124 + assert run_1 != run_2 + assert run_1 != 1234 + + +async def test_tts_audio_output( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + mock_chat_session: chat_session.ChatSession, + 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, {}) + + 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.", + session=mock_chat_session, + 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() + + # Verify TTS audio settings + assert pipeline_input.run.tts_stream.options is not None + assert pipeline_input.run.tts_stream.options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" + assert ( + pipeline_input.run.tts_stream.options.get(tts.ATTR_PREFERRED_SAMPLE_RATE) + == 16000 + ) + assert ( + pipeline_input.run.tts_stream.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 + await client.get(event.data["tts_output"]["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_wav_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + mock_chat_session: chat_session.ChatSession, + pipeline_data: assist_pipeline.pipeline.PipelineData, +) -> 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.", + session=mock_chat_session, + 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, + tts.ATTR_PREFERRED_SAMPLE_BYTES, + ] + ) + + 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 + await client.get(event.data["tts_output"]["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 options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_RATE)) == 16000 + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS)) == 1 + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_BYTES)) == 2 + + +async def test_tts_dict_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + mock_chat_session: chat_session.ChatSession, + pipeline_data: assist_pipeline.pipeline.PipelineData, +) -> 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.", + session=mock_chat_session, + 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={ + tts.ATTR_PREFERRED_FORMAT: "flac", + tts.ATTR_PREFERRED_SAMPLE_RATE: 48000, + tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 2, + tts.ATTR_PREFERRED_SAMPLE_BYTES: 2, + }, + ), + ) + 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, + tts.ATTR_PREFERRED_SAMPLE_BYTES, + ] + ) + + 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 + await client.get(event.data["tts_output"]["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 options.get(tts.ATTR_PREFERRED_FORMAT) == "flac" + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_RATE)) == 48000 + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS)) == 2 + assert int(options.get(tts.ATTR_PREFERRED_SAMPLE_BYTES)) == 2 + + +async def test_sentence_trigger_overrides_conversation_agent( + hass: HomeAssistant, + init_components, + mock_chat_session: chat_session.ChatSession, + pipeline_data: assist_pipeline.pipeline.PipelineData, +) -> None: + """Test that sentence triggers are checked before a non-default conversation agent.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": [ + "test trigger sentence", + ], + }, + "action": { + "set_conversation_response": "test trigger response", + }, + } + }, + ) + + 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( + intent_input="test trigger sentence", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + intent_agent="test-agent", # not the default agent + ), + ) + + # Ensure prepare succeeds + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + ): + await pipeline_input.validate() + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse" + ) as mock_async_converse: + await pipeline_input.execute() + + # Sentence trigger should have been handled + mock_async_converse.assert_not_called() + + # Verify sentence trigger response + intent_end_event = next( + ( + e + for e in events + if e.type == assist_pipeline.PipelineEventType.INTENT_END + ), + None, + ) + assert (intent_end_event is not None) and intent_end_event.data + assert ( + intent_end_event.data["intent_output"]["response"]["speech"]["plain"][ + "speech" + ] + == "test trigger response" + ) + + +async def test_prefer_local_intents( + hass: HomeAssistant, + init_components, + mock_chat_session: chat_session.ChatSession, + pipeline_data: assist_pipeline.pipeline.PipelineData, +) -> None: + """Test that the default agent is checked first when local intents are preferred.""" + events: list[assist_pipeline.PipelineEvent] = [] + + # Reuse custom sentences in test config + class OrderBeerIntentHandler(intent.IntentHandler): + intent_type = "OrderBeer" + + async def async_handle( + self, intent_obj: intent.Intent + ) -> intent.IntentResponse: + response = intent_obj.create_response() + response.async_set_speech("Order confirmed") + return response + + handler = OrderBeerIntentHandler() + intent.async_register(hass, handler) + + # Fake a test agent and prefer local intents + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + await assist_pipeline.pipeline.async_update_pipeline( + hass, pipeline, conversation_engine="test-agent", prefer_local_intents=True + ) + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="I'd like to order a stout please", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + + # Ensure prepare succeeds + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + ): + await pipeline_input.validate() + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse" + ) as mock_async_converse: + await pipeline_input.execute() + + # Test agent should not have been called + mock_async_converse.assert_not_called() + + # Verify local intent response + intent_end_event = next( + ( + e + for e in events + if e.type == assist_pipeline.PipelineEventType.INTENT_END + ), + None, + ) + assert (intent_end_event is not None) and intent_end_event.data + assert ( + intent_end_event.data["intent_output"]["response"]["speech"]["plain"][ + "speech" + ] + == "Order confirmed" + ) + + +async def test_intent_continue_conversation( + hass: HomeAssistant, + init_components, + mock_chat_session: chat_session.ChatSession, + pipeline_data: assist_pipeline.pipeline.PipelineData, +) -> None: + """Test that a conversation agent flagging continue conversation gets response.""" + events: list[assist_pipeline.PipelineEvent] = [] + + # Fake a test agent and prefer local intents + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + await assist_pipeline.pipeline.async_update_pipeline( + hass, pipeline, conversation_engine="test-agent" + ) + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="Set a timer", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + + # Ensure prepare succeeds + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + ): + await pipeline_input.validate() + + response = intent.IntentResponse("en") + response.async_set_speech("For how long?") + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + return_value=conversation.ConversationResult( + response=response, + conversation_id=mock_chat_session.conversation_id, + continue_conversation=True, + ), + ) as mock_async_converse: + await pipeline_input.execute() + + mock_async_converse.assert_called() + + results = [ + event.data + for event in events + if event.type + in ( + assist_pipeline.PipelineEventType.INTENT_START, + assist_pipeline.PipelineEventType.INTENT_END, + ) + ] + assert results[1]["intent_output"]["continue_conversation"] is True + + # Change conversation agent to default one and register sentence trigger that should not be called + await assist_pipeline.pipeline.async_update_pipeline( + hass, pipeline, conversation_engine=None + ) + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "conversation", + "command": ["Hello"], + }, + "action": { + "set_conversation_response": "test trigger response", + }, + } + }, + ) + + # Because we did continue conversation, it should respond to the test agent again. + events.clear() + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="Hello", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + + # Ensure prepare succeeds + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + ) as mock_prepare: + await pipeline_input.validate() + + # It requested test agent even if that was not default agent. + assert mock_prepare.mock_calls[0][1][1] == "test-agent" + + response = intent.IntentResponse("en") + response.async_set_speech("Timer set for 20 minutes") + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + return_value=conversation.ConversationResult( + response=response, + conversation_id=mock_chat_session.conversation_id, + ), + ) as mock_async_converse: + await pipeline_input.execute() + + mock_async_converse.assert_called() + + # Snapshot will show it was still handled by the test agent and not default agent + results = [ + event.data + for event in events + if event.type + in ( + assist_pipeline.PipelineEventType.INTENT_START, + assist_pipeline.PipelineEventType.INTENT_END, + ) + ] + assert results[0]["engine"] == "test-agent" + assert results[1]["intent_output"]["continue_conversation"] is False + + +async def test_stt_language_used_instead_of_conversation_language( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + mock_chat_session: chat_session.ChatSession, + snapshot: SnapshotAssertion, +) -> None: + """Test that the STT language is used first when the conversation language is '*' (all languages).""" + client = await hass_ws_client(hass) + + events: list[assist_pipeline.PipelineEvent] = [] + + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline/create", + "conversation_engine": "homeassistant", + "conversation_language": MATCH_ALL, + "language": "en", + "name": "test_name", + "stt_engine": "test", + "stt_language": "en-US", + "tts_engine": "test", + "tts_language": "en-US", + "tts_voice": "Arnold Schwarzenegger", + "wake_word_entity": None, + "wake_word_id": None, + } + ) + msg = await client.receive_json() + assert msg["success"] + pipeline_id = msg["result"]["id"] + pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="test input", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + return_value=conversation.ConversationResult( + intent.IntentResponse(pipeline.language) + ), + ) as mock_async_converse: + await pipeline_input.execute() + + # Check intent start event + assert process_events(events) == snapshot + intent_start: assist_pipeline.PipelineEvent | None = None + for event in events: + if event.type == assist_pipeline.PipelineEventType.INTENT_START: + intent_start = event + break + + assert intent_start is not None + + # STT language (en-US) should be used instead of '*' + assert intent_start.data.get("language") == pipeline.stt_language + + # Check input to async_converse + mock_async_converse.assert_called_once() + assert ( + mock_async_converse.call_args_list[0].kwargs.get("language") + == pipeline.stt_language + ) + + +async def test_tts_language_used_instead_of_conversation_language( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + mock_chat_session: chat_session.ChatSession, + snapshot: SnapshotAssertion, +) -> None: + """Test that the TTS language is used after STT when the conversation language is '*' (all languages).""" + client = await hass_ws_client(hass) + + events: list[assist_pipeline.PipelineEvent] = [] + + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline/create", + "conversation_engine": "homeassistant", + "conversation_language": MATCH_ALL, + "language": "en", + "name": "test_name", + "stt_engine": None, + "stt_language": None, + "tts_engine": None, + "tts_language": "en-us", + "tts_voice": "Arnold Schwarzenegger", + "wake_word_entity": None, + "wake_word_id": None, + } + ) + msg = await client.receive_json() + assert msg["success"] + pipeline_id = msg["result"]["id"] + pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="test input", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + return_value=conversation.ConversationResult( + intent.IntentResponse(pipeline.language) + ), + ) as mock_async_converse: + await pipeline_input.execute() + + # Check intent start event + assert process_events(events) == snapshot + intent_start: assist_pipeline.PipelineEvent | None = None + for event in events: + if event.type == assist_pipeline.PipelineEventType.INTENT_START: + intent_start = event + break + + assert intent_start is not None + + # STT language (en-US) should be used instead of '*' + assert intent_start.data.get("language") == pipeline.tts_language + + # Check input to async_converse + mock_async_converse.assert_called_once() + assert ( + mock_async_converse.call_args_list[0].kwargs.get("language") + == pipeline.tts_language + ) + + +async def test_pipeline_language_used_instead_of_conversation_language( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + mock_chat_session: chat_session.ChatSession, + snapshot: SnapshotAssertion, +) -> None: + """Test that the pipeline language is used last when the conversation language is '*' (all languages).""" + client = await hass_ws_client(hass) + + events: list[assist_pipeline.PipelineEvent] = [] + + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline/create", + "conversation_engine": "homeassistant", + "conversation_language": MATCH_ALL, + "language": "en", + "name": "test_name", + "stt_engine": None, + "stt_language": None, + "tts_engine": None, + "tts_language": None, + "tts_voice": None, + "wake_word_entity": None, + "wake_word_id": None, + } + ) + msg = await client.receive_json() + assert msg["success"] + pipeline_id = msg["result"]["id"] + pipeline = assist_pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="test input", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.INTENT, + event_callback=events.append, + ), + ) + await pipeline_input.validate() + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + return_value=conversation.ConversationResult( + intent.IntentResponse(pipeline.language) + ), + ) as mock_async_converse: + await pipeline_input.execute() + + # Check intent start event + assert process_events(events) == snapshot + intent_start: assist_pipeline.PipelineEvent | None = None + for event in events: + if event.type == assist_pipeline.PipelineEventType.INTENT_START: + intent_start = event + break + + assert intent_start is not None + + # STT language (en-US) should be used instead of '*' + assert intent_start.data.get("language") == pipeline.language + + # Check input to async_converse + mock_async_converse.assert_called_once() + assert ( + mock_async_converse.call_args_list[0].kwargs.get("language") + == pipeline.language + ) From ca89aa7a94e86f3928f1078f7a4cc94f6954eb70 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sun, 11 May 2025 22:42:02 +0200 Subject: [PATCH 0360/1175] Sort list items alphabetically in Bring integration (#144700) --- homeassistant/components/bring/todo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bring/todo.py b/homeassistant/components/bring/todo.py index c72b8c7ca0e..04902f3e724 100644 --- a/homeassistant/components/bring/todo.py +++ b/homeassistant/components/bring/todo.py @@ -108,7 +108,9 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity): description=item.specification, status=TodoItemStatus.NEEDS_ACTION, ) - for item in self.bring_list.content.items.purchase + for item in sorted( + self.bring_list.content.items.purchase, key=lambda i: i.itemId + ) ), *( TodoItem( From 58802b71c4646995c61d4836f177140135c4a42e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 12 May 2025 00:15:30 +0200 Subject: [PATCH 0361/1175] Bump reolink_aio to 0.13.3 (#144583) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 59a2741571f..a6f0b59426a 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.2"] + "requirements": ["reolink-aio==0.13.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8040e15e541..7483882c7bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2637,7 +2637,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.2 +reolink-aio==0.13.3 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b95448c13d..7ab5a9385d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2144,7 +2144,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.2 +reolink-aio==0.13.3 # homeassistant.components.rflink rflink==0.0.66 From 943998e57eb806a829c611c570c95142e8404f43 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 11 May 2025 18:01:20 -0700 Subject: [PATCH 0362/1175] Bump voluptuous-openapi to 0.1.0 (#144703) --- 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 aec8d3979f9..37618cb3d54 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -70,7 +70,7 @@ typing-extensions>=4.13.0,<5.0 ulid-transform==1.4.0 urllib3>=1.26.5,<2 uv==0.7.1 -voluptuous-openapi==0.0.7 +voluptuous-openapi==0.1.0 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 diff --git a/pyproject.toml b/pyproject.toml index 5c24e227648..68954726b56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ dependencies = [ "uv==0.7.1", "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", - "voluptuous-openapi==0.0.7", + "voluptuous-openapi==0.1.0", "yarl==1.20.0", "webrtc-models==0.3.0", "zeroconf==0.147.0", diff --git a/requirements.txt b/requirements.txt index 283651940f7..25f977d455f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,7 +57,7 @@ urllib3>=1.26.5,<2 uv==0.7.1 voluptuous==0.15.2 voluptuous-serialize==2.6.0 -voluptuous-openapi==0.0.7 +voluptuous-openapi==0.1.0 yarl==1.20.0 webrtc-models==0.3.0 zeroconf==0.147.0 From 77e91427225704dbfa4336bc24bb9b5429ff908a Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 12 May 2025 07:25:43 +0200 Subject: [PATCH 0363/1175] Increase test coverage for ntfy integration (#144701) Increase test coverage --- tests/components/ntfy/test_notify.py | 54 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/components/ntfy/test_notify.py b/tests/components/ntfy/test_notify.py index 76bf1049ae8..ec947ba5a1f 100644 --- a/tests/components/ntfy/test_notify.py +++ b/tests/components/ntfy/test_notify.py @@ -4,7 +4,11 @@ from collections.abc import AsyncGenerator from unittest.mock import patch from aiontfy import Message -from aiontfy.exceptions import NtfyException, NtfyHTTPError +from aiontfy.exceptions import ( + NtfyException, + NtfyHTTPError, + NtfyUnauthorizedAuthenticationError, +) from freezegun.api import freeze_time import pytest from syrupy.assertion import SnapshotAssertion @@ -15,7 +19,8 @@ from homeassistant.components.notify import ( DOMAIN as NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.ntfy.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -101,6 +106,10 @@ async def test_send_message( NtfyException, "Failed to publish notification due to a connection error", ), + ( + NtfyUnauthorizedAuthenticationError(40101, 401, "unauthorized"), + "Failed to authenticate with ntfy service. Please verify your credentials", + ), ], ) async def test_send_message_exception( @@ -135,3 +144,44 @@ async def test_send_message_exception( mock_aiontfy.publish.assert_called_once_with( Message(topic="mytopic", message="triggered", title="test") ) + + +async def test_send_message_reauth_flow( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_aiontfy: AsyncMock, +) -> None: + """Test unauthorized exception initiates reauth flow.""" + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + mock_aiontfy.publish.side_effect = ( + NtfyUnauthorizedAuthenticationError(40101, 401, "unauthorized"), + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_ENTITY_ID: "notify.mytopic", + ATTR_MESSAGE: "triggered", + ATTR_TITLE: "test", + }, + blocking=True, + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == config_entry.entry_id From 2333c10915ed2227632cd394f5a8b5745b38976f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 09:13:23 +0200 Subject: [PATCH 0364/1175] Rename samsung legacy test fixtures and constants (#144715) * Rename samsung legacy test fixtures and constants * More --- tests/components/samsungtv/conftest.py | 17 +- tests/components/samsungtv/const.py | 8 +- .../components/samsungtv/test_config_flow.py | 95 ++++----- tests/components/samsungtv/test_init.py | 2 +- .../components/samsungtv/test_media_player.py | 188 +++++++++--------- tests/components/samsungtv/test_remote.py | 8 +- 6 files changed, 157 insertions(+), 161 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index c33fd89ec56..024a06617a5 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -148,15 +148,16 @@ def upnp_notify_server_fixture(upnp_factory: Mock) -> Generator[Mock]: yield notify_server -@pytest.fixture(name="remote") -def remote_fixture() -> Generator[Mock]: +@pytest.fixture(name="remote_legacy") +def remote_legacy_fixture() -> Generator[Mock]: """Patch the samsungctl Remote.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote_class: - remote = Mock(Remote) - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - yield remote + remote_legacy = Mock(Remote) + remote_legacy.__enter__ = Mock() + remote_legacy.__exit__ = Mock() + with patch( + "homeassistant.components.samsungtv.bridge.Remote", return_value=remote_legacy + ): + yield remote_legacy @pytest.fixture(name="rest_api") diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 7078f088217..e4acc41d1c2 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -3,6 +3,7 @@ from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, DOMAIN, + LEGACY_PORT, METHOD_LEGACY, METHOD_WEBSOCKET, ) @@ -19,10 +20,9 @@ from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo from tests.common import load_json_object_fixture -MOCK_CONFIG = { - CONF_HOST: "fake_host", - CONF_NAME: "fake", - CONF_PORT: 55000, +ENTRYDATA_LEGACY = { + CONF_HOST: "10.10.12.34", + CONF_PORT: LEGACY_PORT, CONF_METHOD: METHOD_LEGACY, } MOCK_CONFIG_ENCRYPTED_WS = { diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 504eccc4f12..d57a594e5dc 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -26,6 +26,7 @@ from homeassistant.components.samsungtv.const import ( DEFAULT_MANUFACTURER, DOMAIN, LEGACY_PORT, + METHOD_LEGACY, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, @@ -54,6 +55,7 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component from .const import ( + ENTRYDATA_LEGACY, MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA, @@ -71,7 +73,6 @@ MOCK_USER_DATA = {CONF_HOST: "fake_host"} MOCK_DHCP_DATA = DhcpServiceInfo( ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname" ) -EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], @@ -86,16 +87,6 @@ MOCK_ZEROCONF_DATA = ZeroconfServiceInfo( }, type="mock_type", ) -MOCK_OLD_ENTRY = { - CONF_HOST: "10.10.12.34", - CONF_METHOD: "legacy", - CONF_PORT: None, -} -MOCK_LEGACY_ENTRY = { - CONF_HOST: EXISTING_IP, - CONF_METHOD: "legacy", - CONF_PORT: None, -} MOCK_DEVICE_INFO = { "device": { "type": "Samsung SmartTV", @@ -109,7 +100,7 @@ AUTODETECT_LEGACY = { "name": "HomeAssistant", "description": "HomeAssistant", "id": "ha.component.samsung", - "method": "legacy", + "method": METHOD_LEGACY, "port": LEGACY_PORT, "host": "fake_host", "timeout": TIMEOUT_REQUEST, @@ -144,7 +135,7 @@ DEVICEINFO_WEBSOCKET_NO_SSL = { pytestmark = pytest.mark.usefixtures("mock_setup_entry") -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_user_legacy(hass: HomeAssistant) -> None: """Test starting a flow by user.""" # show form @@ -162,7 +153,7 @@ async def test_user_legacy(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_METHOD] == "legacy" + assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None assert result["result"].unique_id is None @@ -196,7 +187,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "fake_host" assert result3["data"][CONF_HOST] == "fake_host" - assert result3["data"][CONF_METHOD] == "legacy" + assert result3["data"][CONF_METHOD] == METHOD_LEGACY assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result3["data"][CONF_MODEL] is None assert result3["result"].unique_id is None @@ -447,7 +438,7 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_ssdp(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" # confirm to add the entry @@ -469,7 +460,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: """Test starting a flow from discovery when the manufacturer data is missing.""" ssdp_data = deepcopy(MOCK_SSDP_DATA) @@ -487,7 +478,7 @@ async def test_ssdp_no_manufacturer(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "data", [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST] ) -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_ssdp_legacy_not_remote_control_receiver_udn( hass: HomeAssistant, data: SsdpServiceInfo ) -> None: @@ -499,7 +490,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_ssdp_noprefix(hass: HomeAssistant) -> None: """Test starting a flow from discovery when friendly name doesn't start with [TV].""" ssdp_data = deepcopy(MOCK_SSDP_DATA) @@ -731,7 +722,7 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_ssdp_wrong_manufacturer(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" ssdp_data = deepcopy(MOCK_SSDP_DATA) @@ -810,7 +801,7 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote", "remoteencws_failing") +@pytest.mark.usefixtures("remote_legacy", "remoteencws_failing") async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: """Test starting a flow from discovery twice.""" with patch( @@ -1036,7 +1027,7 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote", "remotews", "remoteencws", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "remotews", "remoteencws", "rest_api_failing") async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where device_info returns None.""" result = await hass.config_entries.flow.async_init( @@ -1217,14 +1208,14 @@ async def test_autodetect_not_supported(hass: HomeAssistant) -> None: assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_autodetect_legacy(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"][CONF_METHOD] == "legacy" + assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_MAC] is None assert result["data"][CONF_PORT] == LEGACY_PORT @@ -1256,7 +1247,7 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_old_entry(hass: HomeAssistant) -> None: """Test update of old entry sets unique id.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY) entry.add_to_hass(hass) config_entries_domain = hass.config_entries.async_entries(DOMAIN) @@ -1287,7 +1278,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY, unique_id=None) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -1309,7 +1300,7 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test incorrectly formatted mac is updated and unique id added.""" - entry_data = MOCK_OLD_ENTRY.copy() + entry_data = ENTRYDATA_LEGACY.copy() entry_data[CONF_MAC] = "aabbccddeeff" entry = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id=None) entry.add_to_hass(hass) @@ -1334,7 +1325,9 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry( - domain=DOMAIN, data={**MOCK_OLD_ENTRY, "host": "127.0.0.1"}, unique_id=None + domain=DOMAIN, + data={**ENTRYDATA_LEGACY, "host": "127.0.0.1"}, + unique_id=None, ) entry.add_to_hass(hass) @@ -1352,14 +1345,14 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "rest_api_failing") async def test_update_missing_model_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing model added via ssdp on legacy models.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_OLD_ENTRY, + data=ENTRYDATA_LEGACY, unique_id=None, ) entry.add_to_hass(hass) @@ -1382,7 +1375,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac, ssdp_location, and unique id added via ssdp.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY, unique_id=None) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -1402,7 +1395,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( @pytest.mark.usefixtures( - "remote", "remotews", "remoteencws_failing", "rest_api_failing" + "remote_legacy", "remotews", "remoteencws_failing", "rest_api_failing" ) async def test_update_zeroconf_discovery_preserved_unique_id( hass: HomeAssistant, @@ -1410,7 +1403,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( """Test zeroconf discovery preserves unique id.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:zz:ee:rr:oo"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:zz:ee:rr:oo"}, unique_id="original", ) entry.add_to_hass(hass) @@ -1434,7 +1427,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd entry = MockConfigEntry( domain=DOMAIN, data={ - **MOCK_OLD_ENTRY, + **ENTRYDATA_LEGACY, CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", }, unique_id=None, @@ -1467,7 +1460,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd entry = MockConfigEntry( domain=DOMAIN, data={ - **MOCK_OLD_ENTRY, + **ENTRYDATA_LEGACY, CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", }, unique_id=None, @@ -1501,7 +1494,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st entry = MockConfigEntry( domain=DOMAIN, data={ - **MOCK_OLD_ENTRY, + **ENTRYDATA_LEGACY, CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", CONF_SSDP_MAIN_TV_AGENT_LOCATION: "https://1.2.3.4:555/test", }, @@ -1538,7 +1531,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", ) entry.add_to_hass(hass) @@ -1569,7 +1562,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", ) entry.add_to_hass(hass) @@ -1599,7 +1592,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( """Test missing mac and unique id added.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, "host": "127.0.0.1"}, + data={**ENTRYDATA_LEGACY, "host": "127.0.0.1"}, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) entry.add_to_hass(hass) @@ -1618,14 +1611,14 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_legacy_missing_mac_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac added.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_LEGACY_ENTRY, + data=ENTRYDATA_LEGACY, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) entry.add_to_hass(hass) @@ -1634,7 +1627,7 @@ async def test_update_legacy_missing_mac_from_dhcp( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DhcpServiceInfo( - ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname" + ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname" ), ) await hass.async_block_till_done() @@ -1646,7 +1639,7 @@ async def test_update_legacy_missing_mac_from_dhcp( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock ) -> None: @@ -1654,7 +1647,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( rest_api.rest_device_info.side_effect = HttpApiError entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_LEGACY_ENTRY, + data=ENTRYDATA_LEGACY, ) entry.add_to_hass(hass) with ( @@ -1671,7 +1664,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DhcpServiceInfo( - ip=EXISTING_IP, macaddress="aabbccddeeff", hostname="fake_hostname" + ip="10.10.12.34", macaddress="aabbccddeeff", hostname="fake_hostname" ), ) await hass.async_block_till_done() @@ -1690,7 +1683,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( """Test missing ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, unique_id=None, ) entry.add_to_hass(hass) @@ -1718,7 +1711,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con """Test missing ssdp_location, and unique id added via ssdp with rendering control st.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, unique_id=None, ) entry.add_to_hass(hass) @@ -1742,10 +1735,10 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_form_reauth_legacy(hass: HomeAssistant) -> None: """Test reauthenticate legacy.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY) entry.add_to_hass(hass) result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -1911,7 +1904,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( """Test updating the wrong udn from ssdp via upnp udn match.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_OLD_ENTRY, + data=ENTRYDATA_LEGACY, unique_id="068e7781-006e-1000-bbbf-84a4668d8423", ) entry.add_to_hass(hass) @@ -1937,7 +1930,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( """Test updating the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_LEGACY, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, unique_id=None, ) entry.add_to_hass(hass) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 7e0d1c87fb1..d1d51a597c5 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -179,7 +179,7 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: assert len(flows_in_progress) == 1 -@pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") +@pytest.mark.usefixtures("remote_legacy", "remotews", "rest_api_failing") async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> None: """Test updating an imported legacy entry without a method.""" await setup_samsungtv_entry( diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 9171c49ef06..dba9c790825 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -81,7 +81,7 @@ from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry from .const import ( - MOCK_CONFIG, + ENTRYDATA_LEGACY, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_WIFI, @@ -119,10 +119,10 @@ MOCK_ENTRY_WS = { } -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_setup(hass: HomeAssistant) -> None: """Test setup of platform.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) assert hass.states.get(ENTITY_ID) @@ -212,10 +212,10 @@ async def test_setup_encrypted_websocket( remote_class.assert_called_once() -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_on(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Testing update tv on.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -225,10 +225,10 @@ async def test_update_on(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> assert state.state == STATE_ON -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_off(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Testing update tv off.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -359,12 +359,12 @@ async def test_update_off_encryptedws( rest_api.rest_device_info.assert_called_once() -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_access_denied( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Testing update tv access denied exception.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -464,12 +464,12 @@ async def test_update_ws_unauthorized_error( assert state.state == STATE_UNAVAILABLE -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_update_unhandled_response( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Testing update tv unhandled response exception.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -483,12 +483,12 @@ async def test_update_unhandled_response( assert state.state == STATE_ON -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_connection_closed_during_update_can_recover( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Testing update tv connection closed exception can recover.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -509,23 +509,23 @@ async def test_connection_closed_during_update_can_recover( assert state.state == STATE_ON -async def test_send_key(hass: HomeAssistant, remote: Mock) -> None: +async def test_send_key(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for send key.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_VOLUP")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_VOLUP")] assert state.state == STATE_ON -async def test_send_key_broken_pipe(hass: HomeAssistant, remote: Mock) -> None: +async def test_send_key_broken_pipe(hass: HomeAssistant, remote_legacy: Mock) -> None: """Testing broken pipe Exception.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - remote.control = Mock(side_effect=BrokenPipeError("Boom")) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) + remote_legacy.control = Mock(side_effect=BrokenPipeError("Boom")) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -534,11 +534,11 @@ async def test_send_key_broken_pipe(hass: HomeAssistant, remote: Mock) -> None: async def test_send_key_connection_closed_retry_succeed( - hass: HomeAssistant, remote: Mock + hass: HomeAssistant, remote_legacy: Mock ) -> None: """Test retry on connection closed.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - remote.control = Mock( + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) + remote_legacy.control = Mock( side_effect=[exceptions.ConnectionClosed("Boom"), DEFAULT_MOCK, DEFAULT_MOCK] ) await hass.services.async_call( @@ -546,18 +546,20 @@ async def test_send_key_connection_closed_retry_succeed( ) state = hass.states.get(ENTITY_ID) # key because of retry two times - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [ + assert remote_legacy.control.call_count == 2 + assert remote_legacy.control.call_args_list == [ call("KEY_VOLUP"), call("KEY_VOLUP"), ] assert state.state == STATE_ON -async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> None: +async def test_send_key_unhandled_response( + hass: HomeAssistant, remote_legacy: Mock +) -> None: """Testing unhandled response exception.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) + remote_legacy.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -619,10 +621,10 @@ async def test_send_key_os_error_ws_encrypted( assert state.state == STATE_ON -async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: +async def test_send_key_os_error(hass: HomeAssistant, remote_legacy: Mock) -> None: """Testing broken pipe Exception.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - remote.control = Mock(side_effect=OSError("Boom")) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) + remote_legacy.control = Mock(side_effect=OSError("Boom")) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -630,18 +632,18 @@ async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: assert state.state == STATE_ON -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_name(hass: HomeAssistant) -> None: """Test for name property.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_FRIENDLY_NAME] == "Mock Title" -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_state(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None: """Test for state property.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -667,18 +669,18 @@ async def test_state(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> Non assert state.state == STATE_UNAVAILABLE -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_supported_features(hass: HomeAssistant) -> None: """Test for supported_features property.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote_legacy") async def test_device_class(hass: HomeAssistant) -> None: """Test for device_class property.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_DEVICE_CLASS] == MediaPlayerDeviceClass.TV @@ -821,24 +823,24 @@ async def test_turn_off_encrypted_websocket_key_type( assert "Unknown power_off command for" not in caplog.text -async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: +async def test_turn_off_legacy(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for turn_off.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_POWEROFF")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_POWEROFF")] async def test_turn_off_os_error( - hass: HomeAssistant, remote: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, remote_legacy: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) - await setup_samsungtv_entry(hass, MOCK_CONFIG) - remote.close = Mock(side_effect=OSError("BOOM")) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) + remote_legacy.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -873,31 +875,31 @@ async def test_turn_off_encryptedws_os_error( assert "Error closing connection" in caplog.text -async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: +async def test_volume_up(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for volume_up.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_VOLUP")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_VOLUP")] -async def test_volume_down(hass: HomeAssistant, remote: Mock) -> None: +async def test_volume_down(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for volume_down.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_VOLDOWN")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_VOLDOWN")] -async def test_mute_volume(hass: HomeAssistant, remote: Mock) -> None: +async def test_mute_volume(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for mute_volume.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_MUTE, @@ -905,66 +907,66 @@ async def test_mute_volume(hass: HomeAssistant, remote: Mock) -> None: True, ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_MUTE")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_MUTE")] -async def test_media_play(hass: HomeAssistant, remote: Mock) -> None: +async def test_media_play(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for media_play.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_PLAY")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_PLAY")] await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY_PAUSE")] + assert remote_legacy.control.call_count == 2 + assert remote_legacy.control.call_args_list == [call("KEY_PLAY"), call("KEY_PAUSE")] -async def test_media_pause(hass: HomeAssistant, remote: Mock) -> None: +async def test_media_pause(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for media_pause.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_PAUSE")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_PAUSE")] await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY_PLAY")] + assert remote_legacy.control.call_count == 2 + assert remote_legacy.control.call_args_list == [call("KEY_PAUSE"), call("KEY_PLAY")] -async def test_media_next_track(hass: HomeAssistant, remote: Mock) -> None: +async def test_media_next_track(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for media_next_track.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_CHUP")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_CHUP")] -async def test_media_previous_track(hass: HomeAssistant, remote: Mock) -> None: +async def test_media_previous_track(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for media_previous_track.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_CHDOWN")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_CHDOWN")] @pytest.mark.usefixtures("remotews", "rest_api") @@ -988,21 +990,21 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: assert mock_send_magic_packet.called -async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: +async def test_turn_on_without_turnon(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test turn on.""" await async_setup_component(hass, "homeassistant", {}) - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with pytest.raises(ServiceNotSupported, match="does not support action"): await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # nothing called as not supported feature - assert remote.control.call_count == 0 + assert remote_legacy.control.call_count == 0 -async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: +async def test_play_media(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for play_media.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with patch("homeassistant.components.samsungtv.bridge.asyncio.sleep") as sleep: await hass.services.async_call( MP_DOMAIN, @@ -1015,8 +1017,8 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: True, ) # keys and update called - assert remote.control.call_count == 4 - assert remote.control.call_args_list == [ + assert remote_legacy.control.call_count == 4 + assert remote_legacy.control.call_args_list == [ call("KEY_5"), call("KEY_7"), call("KEY_6"), @@ -1029,7 +1031,7 @@ async def test_play_media_invalid_type(hass: HomeAssistant) -> None: """Test for play_media with invalid media type.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) remote.reset_mock() await hass.services.async_call( MP_DOMAIN, @@ -1049,7 +1051,7 @@ async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: """Test for play_media with invalid channel as string.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: url = "https://example.com" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) remote.reset_mock() await hass.services.async_call( MP_DOMAIN, @@ -1068,7 +1070,7 @@ async def test_play_media_channel_as_string(hass: HomeAssistant) -> None: async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: """Test for play_media with invalid channel as non positive integer.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) remote.reset_mock() await hass.services.async_call( MP_DOMAIN, @@ -1084,9 +1086,9 @@ async def test_play_media_channel_as_non_positive(hass: HomeAssistant) -> None: assert remote.control.call_count == 0 -async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: +async def test_select_source(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test for select_source.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, @@ -1094,8 +1096,8 @@ async def test_select_source(hass: HomeAssistant, remote: Mock) -> None: True, ) # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_HDMI")] + assert remote_legacy.control.call_count == 1 + assert remote_legacy.control.call_args_list == [call("KEY_HDMI")] async def test_select_source_invalid_source(hass: HomeAssistant) -> None: @@ -1104,7 +1106,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: source = "INVALID" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) remote.reset_mock() with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 4149352ba3f..9f847d87395 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -17,7 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_samsungtv_entry -from .const import MOCK_CONFIG, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS +from .const import ENTRYDATA_LEGACY, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS from tests.common import MockConfigEntry @@ -119,15 +119,15 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: assert mock_send_magic_packet.called -async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: +async def test_turn_on_without_turnon(hass: HomeAssistant, remote_legacy: Mock) -> None: """Test turn on.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) + await setup_samsungtv_entry(hass, ENTRYDATA_LEGACY) with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( REMOTE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # nothing called as not supported feature - assert remote.control.call_count == 0 + assert remote_legacy.control.call_count == 0 assert exc_info.value.translation_domain == DOMAIN assert exc_info.value.translation_key == "service_unsupported" assert exc_info.value.translation_placeholders == { From fbe1811e2b71f75555814388f98aecb35c7e367a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 09:23:55 +0200 Subject: [PATCH 0365/1175] Improve SamsungTV test coverage (#144717) --- tests/components/samsungtv/test_init.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index d1d51a597c5..a8c93aaec67 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,5 +1,6 @@ """Tests for the Samsung TV Integration.""" +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.samsungtv.const import ( - CONF_MANUFACTURER, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, @@ -179,12 +179,20 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: assert len(flows_in_progress) == 1 -@pytest.mark.usefixtures("remote_legacy", "remotews", "rest_api_failing") -async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> None: - """Test updating an imported legacy entry without a method.""" - await setup_samsungtv_entry( - hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"} - ) +@pytest.mark.usefixtures("remote_legacy", "remoteencws_failing", "rest_api_failing") +@pytest.mark.parametrize( + "entry_data", + [ + {CONF_HOST: "1.2.3.4"}, # Missing port/method + {CONF_HOST: "1.2.3.4", CONF_PORT: LEGACY_PORT}, # Missing method + {CONF_HOST: "1.2.3.4", CONF_METHOD: METHOD_LEGACY}, # Missing port + ], +) +async def test_update_imported_legacy( + hass: HomeAssistant, entry_data: dict[str, Any] +) -> None: + """Test updating an imported legacy entry.""" + await setup_samsungtv_entry(hass, entry_data) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 From 0616bf16f4cf6d4bcce79090070a2a13c0fcbc67 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 12 May 2025 00:37:57 -0700 Subject: [PATCH 0366/1175] Bump ical to 9.2.2 (#144713) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 668ab6e34be..296ac519e1d 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.1"] + "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.2"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index c3ffce2890b..2fa603d51ff 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index f93129be94c..735c11e645a 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==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 4df3f11cf10..33a46ea3dc8 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.1"] + "requirements": ["ical==9.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7483882c7bc..bcadc74ddd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1197,7 +1197,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.1 +ical==9.2.2 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ab5a9385d0..b7c7a85eee4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,7 +1018,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.1 +ical==9.2.2 # homeassistant.components.caldav icalendar==6.1.0 From 5276a3688e697597df0a9c081199e28e00598fd0 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Mon, 12 May 2025 09:39:30 +0200 Subject: [PATCH 0367/1175] Fix wrong state in Husqvarna Automower (#144684) --- .../components/husqvarna_automower/lawn_mower.py | 8 ++++---- tests/components/husqvarna_automower/test_lawn_mower.py | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/lawn_mower.py b/homeassistant/components/husqvarna_automower/lawn_mower.py index 9ae214524a7..5a728265651 100644 --- a/homeassistant/components/husqvarna_automower/lawn_mower.py +++ b/homeassistant/components/husqvarna_automower/lawn_mower.py @@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity): mower_attributes = self.mower_attributes if mower_attributes.mower.state in PAUSED_STATES: return LawnMowerActivity.PAUSED - if mower_attributes.mower.state in MowerStates.IN_OPERATION: - if mower_attributes.mower.activity == MowerActivities.GOING_HOME: - return LawnMowerActivity.RETURNING - return LawnMowerActivity.MOWING if (mower_attributes.mower.state == "RESTRICTED") or ( mower_attributes.mower.activity in DOCKED_ACTIVITIES ): return LawnMowerActivity.DOCKED + if mower_attributes.mower.state in MowerStates.IN_OPERATION: + if mower_attributes.mower.activity == MowerActivities.GOING_HOME: + return LawnMowerActivity.RETURNING + return LawnMowerActivity.MOWING return LawnMowerActivity.ERROR @property diff --git a/tests/components/husqvarna_automower/test_lawn_mower.py b/tests/components/husqvarna_automower/test_lawn_mower.py index 42b737652b7..c62cf6653c4 100644 --- a/tests/components/husqvarna_automower/test_lawn_mower.py +++ b/tests/components/husqvarna_automower/test_lawn_mower.py @@ -37,6 +37,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed MowerStates.IN_OPERATION, LawnMowerActivity.MOWING, ), + ( + MowerActivities.PARKED_IN_CS, + MowerStates.IN_OPERATION, + LawnMowerActivity.DOCKED, + ), ], ) async def test_lawn_mower_states( From 646c23094016c24d32ea759a332b83d6d83edc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 12 May 2025 09:42:27 +0200 Subject: [PATCH 0368/1175] Add target temp sensor to Miele washing machines (#144507) --- homeassistant/components/miele/icons.json | 3 +++ homeassistant/components/miele/sensor.py | 24 +++++++++++++++++++++ homeassistant/components/miele/strings.json | 3 +++ 3 files changed, 30 insertions(+) diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 02374a10f90..48df141ac9b 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -32,6 +32,9 @@ "core_target_temperature": { "default": "mdi:thermometer-probe" }, + "target_temperature": { + "default": "mdi:thermometer-check" + }, "drying_step": { "default": "mdi:water-outline" }, diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index b5b74db5bcc..64948cf7b83 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -382,6 +382,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( MieleAppliance.OVEN, MieleAppliance.OVEN_MICROWAVE, MieleAppliance.STEAM_OVEN_COMBI, + MieleAppliance.STEAM_OVEN_MK2, ), description=MieleSensorDescription( key="state_core_target_temperature", @@ -398,6 +399,29 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( ), ), ), + MieleSensorDefinition( + types=( + MieleAppliance.WASHING_MACHINE, + MieleAppliance.WASHER_DRYER, + MieleAppliance.OVEN, + MieleAppliance.OVEN_MICROWAVE, + MieleAppliance.STEAM_OVEN_MICRO, + MieleAppliance.STEAM_OVEN_COMBI, + MieleAppliance.STEAM_OVEN_MK2, + ), + description=MieleSensorDescription( + key="state_target_temperature", + translation_key="target_temperature", + zone=1, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=( + lambda value: cast(int, value.state_target_temperature[0].temperature) + / 100.0 + ), + ), + ), MieleSensorDefinition( types=( MieleAppliance.OVEN, diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 959d8e421cd..adffe9b378c 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -876,6 +876,9 @@ "core_temperature": { "name": "Core temperature" }, + "target_temperature": { + "name": "Target temperature" + }, "core_target_temperature": { "name": "Core target temperature" } From e493fe1105c438017015cf877d389a296b82e8f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 10:27:29 +0200 Subject: [PATCH 0369/1175] Rename samsung websocket test fixtures and constants (#144719) --- tests/components/samsungtv/conftest.py | 40 ++--- tests/components/samsungtv/const.py | 8 +- .../components/samsungtv/test_config_flow.py | 104 +++++++------ .../components/samsungtv/test_diagnostics.py | 2 +- tests/components/samsungtv/test_init.py | 16 +- .../components/samsungtv/test_media_player.py | 142 ++++++++++-------- tests/components/samsungtv/test_remote.py | 2 +- 7 files changed, 168 insertions(+), 146 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 024a06617a5..d064d3d37a7 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -219,45 +219,47 @@ def remoteencws_failing_fixture() -> Generator[None]: yield -@pytest.fixture(name="remotews") -def remotews_fixture() -> Generator[Mock]: +@pytest.fixture(name="remote_websocket") +def remote_websocket_fixture() -> Generator[Mock]: """Patch the samsungtvws SamsungTVWS.""" - remotews = Mock(SamsungTVWSAsyncRemote) - remotews.__aenter__ = AsyncMock(return_value=remotews) - remotews.__aexit__ = AsyncMock() - remotews.token = "FAKE_TOKEN" - remotews.app_list_data = None + remote_websocket = Mock(SamsungTVWSAsyncRemote) + remote_websocket.__aenter__ = AsyncMock(return_value=remote_websocket) + remote_websocket.__aexit__ = AsyncMock() + remote_websocket.token = "FAKE_TOKEN" + remote_websocket.app_list_data = None async def _start_listening( ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None, ): - remotews.ws_event_callback = ws_event_callback + remote_websocket.ws_event_callback = ws_event_callback async def _send_commands(commands: list[SamsungTVCommand]): if ( len(commands) == 1 and isinstance(commands[0], ChannelEmitCommand) and commands[0].params["event"] == "ed.installedApp.get" - and remotews.app_list_data is not None + and remote_websocket.app_list_data is not None ): - remotews.raise_mock_ws_event_callback( + remote_websocket.raise_mock_ws_event_callback( ED_INSTALLED_APP_EVENT, - remotews.app_list_data, + remote_websocket.app_list_data, ) def _mock_ws_event_callback(event: str, response: Any): - if remotews.ws_event_callback: - remotews.ws_event_callback(event, response) + if remote_websocket.ws_event_callback: + remote_websocket.ws_event_callback(event, response) - remotews.start_listening.side_effect = _start_listening - remotews.send_commands.side_effect = _send_commands - remotews.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback) + remote_websocket.start_listening.side_effect = _start_listening + remote_websocket.send_commands.side_effect = _send_commands + remote_websocket.raise_mock_ws_event_callback = Mock( + side_effect=_mock_ws_event_callback + ) with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", - ) as remotews_class: - remotews_class.return_value = remotews - yield remotews + return_value=remote_websocket, + ): + yield remote_websocket @pytest.fixture(name="remoteencws") diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index e4acc41d1c2..c2b3f9b5ee3 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -6,6 +6,7 @@ from homeassistant.components.samsungtv.const import ( LEGACY_PORT, METHOD_LEGACY, METHOD_WEBSOCKET, + WEBSOCKET_SSL_PORT, ) from homeassistant.const import ( CONF_HOST, @@ -37,12 +38,11 @@ MOCK_ENTRYDATA_ENCRYPTED_WS = { CONF_TOKEN: "037739871315caef138547b03e348b72", CONF_SESSION_ID: "2", } -MOCK_ENTRYDATA_WS = { +ENTRYDATA_WEBSOCKET = { CONF_HOST: "10.10.12.34", CONF_METHOD: METHOD_WEBSOCKET, - CONF_PORT: 8002, - CONF_MODEL: "any", - CONF_NAME: "any", + CONF_PORT: WEBSOCKET_SSL_PORT, + CONF_MODEL: "UE43LS003", } MOCK_ENTRY_WS_WITH_MAC = { CONF_HOST: "fake_host", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d57a594e5dc..cd98cb0460f 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -56,8 +56,8 @@ from homeassistant.setup import async_setup_component from .const import ( ENTRYDATA_LEGACY, + ENTRYDATA_WEBSOCKET, MOCK_ENTRYDATA_ENCRYPTED_WS, - MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, @@ -193,7 +193,7 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: assert result3["result"].unique_id is None -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_user_websocket(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( @@ -518,7 +518,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" -@pytest.mark.usefixtures("remotews", "rest_api_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api_failing") async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow from discovery with authentication.""" with patch( @@ -553,7 +553,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: assert result["result"].unique_id == "068e7781-006e-1000-bbbf-84a4668d8423" -@pytest.mark.usefixtures("remotews", "rest_api_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api_failing") async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: """Test starting a flow from discovery for not supported device.""" with patch( @@ -568,7 +568,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( hass: HomeAssistant, ) -> None: @@ -597,7 +597,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_location( hass: HomeAssistant, ) -> None: @@ -711,8 +711,8 @@ async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", - ) as remotews, - patch.object(remotews, "open", side_effect=WebSocketException("Boom")), + ) as remote_websocket, + patch.object(remote_websocket, "open", side_effect=WebSocketException("Boom")), ): # device not supported result = await hass.config_entries.flow.async_init( @@ -823,7 +823,7 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") async def test_ssdp_already_configured(hass: HomeAssistant) -> None: """Test starting a flow from discovery when already configured.""" with patch( @@ -851,7 +851,9 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: assert entry.unique_id == "123" -@pytest.mark.usefixtures("remotews", "rest_api_non_ssl_only", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api_non_ssl_only", "remoteencws_failing" +) async def test_dhcp_wireless(hass: HomeAssistant) -> None: """Test starting a flow from dhcp.""" # confirm to add the entry @@ -877,7 +879,7 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from dhcp.""" # Even though it is named "wifiMac", it matches the mac of the wired connection @@ -907,7 +909,9 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api_non_ssl_only", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api_non_ssl_only", "remoteencws_failing" +) @pytest.mark.parametrize( ("source1", "data1", "source2", "data2", "is_matching_result"), [ @@ -979,7 +983,7 @@ async def test_dhcp_zeroconf_already_in_progress( assert return_values == [is_matching_result] -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" result = await hass.config_entries.flow.async_init( @@ -1004,7 +1008,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where the device is actually a soundbar.""" rest_api.rest_device_info.return_value = { @@ -1027,7 +1031,9 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote_legacy", "remotews", "remoteencws", "rest_api_failing") +@pytest.mark.usefixtures( + "remote_legacy", "remote_websocket", "remoteencws", "rest_api_failing" +) async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where device_info returns None.""" result = await hass.config_entries.flow.async_init( @@ -1040,7 +1046,7 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf and dhcp.""" result = await hass.config_entries.flow.async_init( @@ -1072,7 +1078,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" - ) as remotews, + ) as remote_websocket, patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", ) as rest_api_class, @@ -1095,7 +1101,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: } ) remote.token = "123456789" - remotews.return_value = remote + remote_websocket.return_value = remote result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -1103,7 +1109,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: 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) + remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -1123,7 +1129,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" - ) as remotews, + ) as remote_websocket, patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", ) as rest_api_class, @@ -1145,7 +1151,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: ) remote.token = "123456789" - remotews.return_value = remote + remote_websocket.return_value = remote result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA @@ -1154,7 +1160,7 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" - remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) + remote_websocket.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() @@ -1244,7 +1250,7 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: assert rest_device_info.call_count == 2 -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_old_entry(hass: HomeAssistant) -> None: """Test update of old entry sets unique id.""" entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY) @@ -1273,7 +1279,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1295,7 +1301,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1319,7 +1325,7 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_zeroconf( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1370,7 +1376,7 @@ async def test_update_missing_model_added_from_ssdp( assert entry.data[CONF_MODEL] == "UE55H6400" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1395,7 +1401,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( @pytest.mark.usefixtures( - "remote_legacy", "remotews", "remoteencws_failing", "rest_api_failing" + "remote_legacy", "remote_websocket", "remoteencws_failing", "rest_api_failing" ) async def test_update_zeroconf_discovery_preserved_unique_id( hass: HomeAssistant, @@ -1419,7 +1425,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( assert entry.unique_id == "original" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1452,7 +1458,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1486,7 +1492,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1524,7 +1530,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1555,7 +1561,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1585,7 +1591,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1676,7 +1682,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( assert entry.unique_id is None -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1704,7 +1710,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1753,10 +1759,10 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: assert result2["reason"] == "reauth_successful" -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET) entry.add_to_hass(hass) assert entry.state is ConfigEntryState.NOT_LOADED @@ -1776,16 +1782,16 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api") async def test_form_reauth_websocket_cannot_connect( - hass: HomeAssistant, remotews: Mock + hass: HomeAssistant, remote_websocket: Mock ) -> None: """Test reauthenticate websocket when we cannot connect on the first attempt.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET) entry.add_to_hass(hass) result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - with patch.object(remotews, "open", side_effect=ConnectionFailure): + with patch.object(remote_websocket, "open", side_effect=ConnectionFailure): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, @@ -1807,7 +1813,7 @@ async def test_form_reauth_websocket_cannot_connect( async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: """Test reauthenticate websocket when the device is not supported.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) + entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_WEBSOCKET) entry.add_to_hass(hass) result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM @@ -1897,7 +1903,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: assert entry.data[CONF_SESSION_ID] == "1" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1923,7 +1929,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1949,14 +1955,14 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP updates the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, + data={**ENTRYDATA_WEBSOCKET, CONF_MAC: "aa:bb:aa:aa:aa:aa"}, source=config_entries.SOURCE_SSDP, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) @@ -1976,14 +1982,14 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP does not update the wrong udn from ssdp via host match.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ss:ss:dd:pp"}, + data={**ENTRYDATA_WEBSOCKET, CONF_MAC: "aa:bb:ss:ss:dd:pp"}, source=config_entries.SOURCE_SSDP, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) @@ -2003,7 +2009,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") async def test_ssdp_update_mac(hass: HomeAssistant) -> None: """Ensure that MAC address is correctly updated from SSDP.""" with patch( diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 3f40c51d5d0..37f90d5913c 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -18,7 +18,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index a8c93aaec67..bdd0dcf881e 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -40,9 +40,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from . import setup_samsungtv_entry from .const import ( + ENTRYDATA_WEBSOCKET, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS, - MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) @@ -57,7 +57,7 @@ MOCK_CONFIG = { } -@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing", "rest_api") async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) @@ -101,7 +101,7 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY -@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing", "rest_api") async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) @@ -111,7 +111,7 @@ async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -126,13 +126,13 @@ async def test_setup_h_j_model( assert "H and J series use an encrypted protocol" in caplog.text -@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_setup_updates_from_ssdp( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion ) -> None: """Test setting up the entry fetches data from ssdp cache.""" entry = MockConfigEntry( - domain="samsungtv", data=MOCK_ENTRYDATA_WS, entry_id="sample-entry-id" + domain="samsungtv", data=ENTRYDATA_WEBSOCKET, entry_id="sample-entry-id" ) entry.add_to_hass(hass) @@ -200,7 +200,7 @@ async def test_update_imported_legacy( assert entries[0].data[CONF_PORT] == LEGACY_PORT -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: """Test incorrectly formatted mac is corrected.""" with patch( @@ -230,7 +230,7 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") @pytest.mark.xfail async def test_cleanup_mac( hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index dba9c790825..6cbd489e114 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -126,7 +126,7 @@ async def test_setup(hass: HomeAssistant) -> None: assert hass.states.get(ENTITY_ID) -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_setup_websocket(hass: HomeAssistant) -> None: """Test setup of platform.""" with patch( @@ -243,7 +243,10 @@ async def test_update_off(hass: HomeAssistant, freezer: FrozenDateTimeFactory) - async def test_update_off_ws_no_power_state( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, remotews: Mock, rest_api: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + remote_websocket: Mock, + rest_api: Mock, ) -> None: """Testing update tv off.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) @@ -254,8 +257,8 @@ async def test_update_off_ws_no_power_state( state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON - remotews.start_listening = Mock(side_effect=WebSocketException("Boom")) - remotews.is_alive.return_value = False + remote_websocket.start_listening = Mock(side_effect=WebSocketException("Boom")) + remote_websocket.is_alive.return_value = False freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -266,9 +269,12 @@ async def test_update_off_ws_no_power_state( rest_api.rest_device_info.assert_not_called() -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remote_websocket") async def test_update_off_ws_with_power_state( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, remotews: Mock, rest_api: Mock + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + remote_websocket: Mock, + rest_api: Mock, ) -> None: """Testing update tv off.""" with ( @@ -276,7 +282,7 @@ async def test_update_off_ws_with_power_state( rest_api, "rest_device_info", side_effect=HttpApiError ) as mock_device_info, patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") + remote_websocket, "start_listening", side_effect=WebSocketException("Boom") ) as mock_start_listening, ): await setup_samsungtv_entry(hass, MOCK_CONFIGWS) @@ -296,14 +302,14 @@ async def test_update_off_ws_with_power_state( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - remotews.start_listening.assert_called_once() + remote_websocket.start_listening.assert_called_once() rest_api.rest_device_info.assert_called_once() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON # After initial update, start_listening shouldn't be called - remotews.start_listening.reset_mock() + remote_websocket.start_listening.reset_mock() # Second update uses device_info(ON) rest_api.rest_device_info.reset_mock() @@ -330,7 +336,7 @@ async def test_update_off_ws_with_power_state( state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE - remotews.start_listening.assert_not_called() + remote_websocket.start_listening.assert_not_called() async def test_update_off_encryptedws( @@ -391,7 +397,7 @@ async def test_update_access_denied( async def test_update_ws_connection_failure( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - remotews: Mock, + remote_websocket: Mock, caplog: pytest.LogCaptureFixture, ) -> None: """Testing update tv connection failure exception.""" @@ -399,11 +405,11 @@ async def test_update_ws_connection_failure( with ( patch.object( - remotews, + remote_websocket, "start_listening", side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), ), - patch.object(remotews, "is_alive", return_value=False), + patch.object(remote_websocket, "is_alive", return_value=False), ): freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -421,16 +427,18 @@ async def test_update_ws_connection_failure( @pytest.mark.usefixtures("rest_api") async def test_update_ws_connection_closed( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, remotews: Mock + hass: HomeAssistant, freezer: FrozenDateTimeFactory, remote_websocket: Mock ) -> None: """Testing update tv connection failure exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) with ( patch.object( - remotews, "start_listening", side_effect=ConnectionClosedError(None, None) + remote_websocket, + "start_listening", + side_effect=ConnectionClosedError(None, None), ), - patch.object(remotews, "is_alive", return_value=False), + patch.object(remote_websocket, "is_alive", return_value=False), ): freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -442,14 +450,16 @@ async def test_update_ws_connection_closed( @pytest.mark.usefixtures("rest_api") async def test_update_ws_unauthorized_error( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, remotews: Mock + hass: HomeAssistant, freezer: FrozenDateTimeFactory, remote_websocket: Mock ) -> None: """Testing update tv unauthorized failure exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) with ( - patch.object(remotews, "start_listening", side_effect=UnauthorizedError), - patch.object(remotews, "is_alive", return_value=False), + patch.object( + remote_websocket, "start_listening", side_effect=UnauthorizedError + ), + patch.object(remote_websocket, "is_alive", return_value=False), ): freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -570,10 +580,12 @@ async def test_send_key_unhandled_response( @pytest.mark.usefixtures("rest_api") -async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) -> None: +async def test_send_key_websocketexception( + hass: HomeAssistant, remote_websocket: Mock +) -> None: """Testing unhandled response exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands = Mock(side_effect=WebSocketException("Boom")) + remote_websocket.send_commands = Mock(side_effect=WebSocketException("Boom")) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -596,10 +608,12 @@ async def test_send_key_websocketexception_encrypted( @pytest.mark.usefixtures("rest_api") -async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: +async def test_send_key_os_error_ws( + hass: HomeAssistant, remote_websocket: Mock +) -> None: """Testing unhandled response exception.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands = Mock(side_effect=OSError("Boom")) + remote_websocket.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -687,10 +701,10 @@ async def test_device_class(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api") async def test_turn_off_websocket( - hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, remote_websocket: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off.""" - remotews.app_list_data = load_json_object_fixture( + remote_websocket.app_list_data = load_json_object_fixture( "ws_installed_app_event.json", DOMAIN ) with patch( @@ -699,20 +713,20 @@ async def test_turn_off_websocket( ): await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_commands.call_count == 1 - commands = remotews.send_commands.call_args_list[0].args[0] + assert remote_websocket.send_commands.call_count == 1 + commands = remote_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["DataOfCmd"] == "KEY_POWER" # commands not sent : power off in progress - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -724,11 +738,11 @@ async def test_turn_off_websocket( True, ) assert "TV is powering off, not sending launch_app command" in caplog.text - remotews.send_commands.assert_not_called() + remote_websocket.send_commands.assert_not_called() async def test_turn_off_websocket_frame( - hass: HomeAssistant, remotews: Mock, rest_api: Mock + hass: HomeAssistant, remote_websocket: Mock, rest_api: Mock ) -> None: """Test for turn_off.""" rest_api.rest_device_info.return_value = load_json_object_fixture( @@ -740,14 +754,14 @@ async def test_turn_off_websocket_frame( ): await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_commands.call_count == 1 - commands = remotews.send_commands.call_args_list[0].args[0] + assert remote_websocket.send_commands.call_count == 1 + commands = remote_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 3 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["Cmd"] == "Press" @@ -849,12 +863,12 @@ async def test_turn_off_os_error( @pytest.mark.usefixtures("rest_api") async def test_turn_off_ws_os_error( - hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, remote_websocket: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.close = Mock(side_effect=OSError("BOOM")) + remote_websocket.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -969,7 +983,7 @@ async def test_media_previous_track(hass: HomeAssistant, remote_legacy: Mock) -> assert remote_legacy.control.call_args_list == [call("KEY_CHDOWN")] -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( @@ -1126,10 +1140,10 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_api") -async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: +async def test_play_media_app(hass: HomeAssistant, remote_websocket: Mock) -> None: """Test for play_media.""" await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, @@ -1141,21 +1155,21 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: }, True, ) - assert remotews.send_commands.call_count == 1 - commands = remotews.send_commands.call_args_list[0].args[0] + assert remote_websocket.send_commands.call_count == 1 + commands = remote_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], ChannelEmitCommand) assert commands[0].params["data"]["appId"] == "3201608010191" @pytest.mark.usefixtures("rest_api") -async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: +async def test_select_source_app(hass: HomeAssistant, remote_websocket: Mock) -> None: """Test for select_source.""" - remotews.app_list_data = load_json_object_fixture( + remote_websocket.app_list_data = load_json_object_fixture( "ws_installed_app_event.json", DOMAIN ) await setup_samsungtv_entry(hass, MOCK_CONFIGWS) - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, @@ -1163,8 +1177,8 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, ) - assert remotews.send_commands.call_count == 1 - commands = remotews.send_commands.call_args_list[0].args[0] + assert remote_websocket.send_commands.call_count == 1 + commands = remote_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], ChannelEmitCommand) assert commands[0].params["data"]["appId"] == "3201608010191" @@ -1173,7 +1187,7 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: @pytest.mark.usefixtures("rest_api") async def test_websocket_unsupported_remote_control( hass: HomeAssistant, - remotews: Mock, + remote_websocket: Mock, freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: @@ -1183,12 +1197,12 @@ async def test_websocket_unsupported_remote_control( assert entry.data[CONF_METHOD] == METHOD_WEBSOCKET assert entry.data[CONF_PORT] == 8001 - remotews.send_commands.reset_mock() + remote_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) - remotews.raise_mock_ws_event_callback( + remote_websocket.raise_mock_ws_event_callback( "ms.error", { "event": "ms.error", @@ -1197,8 +1211,8 @@ async def test_websocket_unsupported_remote_control( ) # key called - assert remotews.send_commands.call_count == 1 - commands = remotews.send_commands.call_args_list[0].args[0] + assert remote_websocket.send_commands.call_count == 1 + commands = remote_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["DataOfCmd"] == "KEY_POWER" @@ -1227,7 +1241,7 @@ async def test_websocket_unsupported_remote_control( assert state.state == STATE_UNAVAILABLE -@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "upnp_notify_server") async def test_volume_control_upnp(hass: HomeAssistant, dmr_device: Mock) -> None: """Test for Upnp volume control.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) @@ -1261,7 +1275,7 @@ async def test_volume_control_upnp(hass: HomeAssistant, dmr_device: Mock) -> Non dmr_device.async_set_volume_level.assert_called_once_with(0.6) -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_upnp_not_available( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1279,7 +1293,7 @@ async def test_upnp_not_available( assert "Upnp services are not available" in caplog.text -@pytest.mark.usefixtures("remotews", "rest_api", "upnp_factory") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "upnp_factory") async def test_upnp_missing_service( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1297,7 +1311,7 @@ async def test_upnp_missing_service( assert "Upnp services are not available" in caplog.text -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_upnp_shutdown( hass: HomeAssistant, dmr_device: Mock, @@ -1318,7 +1332,7 @@ async def test_upnp_shutdown( upnp_notify_server.async_stop_server.assert_called_once() -@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server") +@pytest.mark.usefixtures("remote_websocket", "rest_api", "upnp_notify_server") async def test_upnp_subscribe_events(hass: HomeAssistant, dmr_device: Mock) -> None: """Test for Upnp event feedback.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) @@ -1338,7 +1352,7 @@ async def test_upnp_subscribe_events(hass: HomeAssistant, dmr_device: Mock) -> N assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is True -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_upnp_subscribe_events_upnperror( hass: HomeAssistant, dmr_device: Mock, @@ -1353,7 +1367,7 @@ async def test_upnp_subscribe_events_upnperror( assert "Error while subscribing during device connect" in caplog.text -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_upnp_subscribe_events_upnpresponseerror( hass: HomeAssistant, dmr_device: Mock, @@ -1376,7 +1390,7 @@ async def test_upnp_subscribe_events_upnpresponseerror( async def test_upnp_re_subscribe_events( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - remotews: Mock, + remote_websocket: Mock, dmr_device: Mock, ) -> None: """Test for Upnp event feedback.""" @@ -1389,9 +1403,9 @@ async def test_upnp_re_subscribe_events( with ( patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") + remote_websocket, "start_listening", side_effect=WebSocketException("Boom") ), - patch.object(remotews, "is_alive", return_value=False), + patch.object(remote_websocket, "is_alive", return_value=False), ): freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -1420,7 +1434,7 @@ async def test_upnp_re_subscribe_events( async def test_upnp_failed_re_subscribe_events( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - remotews: Mock, + remote_websocket: Mock, dmr_device: Mock, caplog: pytest.LogCaptureFixture, error: Exception, @@ -1435,9 +1449,9 @@ async def test_upnp_failed_re_subscribe_events( with ( patch.object( - remotews, "start_listening", side_effect=WebSocketException("Boom") + remote_websocket, "start_listening", side_effect=WebSocketException("Boom") ), - patch.object(remotews, "is_alive", return_value=False), + patch.object(remote_websocket, "is_alive", return_value=False), ): freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index 9f847d87395..a4ea43ee77c 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -98,7 +98,7 @@ async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> N assert command.body["param3"] == "dash" -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( From 7eded95315686e223447ec7b9621b1d2172f1403 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 12 May 2025 12:23:44 +0300 Subject: [PATCH 0370/1175] Bump aiocomelit to 0.12.1 (#144720) --- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 2097d1c25f6..58f347b4ba3 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aiocomelit"], "quality_scale": "bronze", - "requirements": ["aiocomelit==0.12.0"] + "requirements": ["aiocomelit==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index bcadc74ddd0..5dbaf454548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -211,7 +211,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.0 +aiocomelit==0.12.1 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7c7a85eee4..e3d76e8833d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,7 +199,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.0 +aiocomelit==0.12.1 # homeassistant.components.dhcp aiodhcpwatcher==1.1.1 From 63e38b4d8dc764b84f7f5ce33017a0b1a449ca1b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 11:36:22 +0200 Subject: [PATCH 0371/1175] Rename samsung encrypted websocket test fixtures and constants (#144726) * Rename samsung encrypted websocket test fixtures and constants * More * More --- tests/components/samsungtv/conftest.py | 32 +++-- tests/components/samsungtv/const.py | 14 +- .../samsungtv/snapshots/test_diagnostics.ambr | 6 +- .../components/samsungtv/test_config_flow.py | 136 ++++++++++++------ .../samsungtv/test_device_trigger.py | 14 +- .../components/samsungtv/test_diagnostics.py | 10 +- tests/components/samsungtv/test_init.py | 20 ++- .../components/samsungtv/test_media_player.py | 64 +++++---- tests/components/samsungtv/test_remote.py | 44 +++--- tests/components/samsungtv/test_trigger.py | 18 +-- 10 files changed, 215 insertions(+), 143 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d064d3d37a7..d0c53020d85 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -209,8 +209,8 @@ def rest_api_failure_fixture() -> Generator[None]: yield -@pytest.fixture(name="remoteencws_failing") -def remoteencws_failing_fixture() -> Generator[None]: +@pytest.fixture(name="remote_encrypted_websocket_failing") +def remote_encrypted_websocket_failing_fixture() -> Generator[None]: """Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", @@ -262,30 +262,34 @@ def remote_websocket_fixture() -> Generator[Mock]: yield remote_websocket -@pytest.fixture(name="remoteencws") -def remoteencws_fixture() -> Generator[Mock]: +@pytest.fixture(name="remote_encrypted_websocket") +def remote_encrypted_websocket_fixture() -> Generator[Mock]: """Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote.""" - remoteencws = Mock(SamsungTVEncryptedWSAsyncRemote) - remoteencws.__aenter__ = AsyncMock(return_value=remoteencws) - remoteencws.__aexit__ = AsyncMock() + remote_encrypted_websocket = Mock(SamsungTVEncryptedWSAsyncRemote) + remote_encrypted_websocket.__aenter__ = AsyncMock( + return_value=remote_encrypted_websocket + ) + remote_encrypted_websocket.__aexit__ = AsyncMock() def _start_listening( ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None, ): - remoteencws.ws_event_callback = ws_event_callback + remote_encrypted_websocket.ws_event_callback = ws_event_callback def _mock_ws_event_callback(event: str, response: Any): - if remoteencws.ws_event_callback: - remoteencws.ws_event_callback(event, response) + if remote_encrypted_websocket.ws_event_callback: + remote_encrypted_websocket.ws_event_callback(event, response) - remoteencws.start_listening.side_effect = _start_listening - remoteencws.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback) + remote_encrypted_websocket.start_listening.side_effect = _start_listening + remote_encrypted_websocket.raise_mock_ws_event_callback = Mock( + side_effect=_mock_ws_event_callback + ) with patch( "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote", ) as remotews_class: - remotews_class.return_value = remoteencws - yield remoteencws + remotews_class.return_value = remote_encrypted_websocket + yield remote_encrypted_websocket @pytest.fixture(name="mac_address", autouse=True) diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index c2b3f9b5ee3..64d1a76bc86 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -3,7 +3,9 @@ from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, WEBSOCKET_SSL_PORT, @@ -26,14 +28,10 @@ ENTRYDATA_LEGACY = { CONF_PORT: LEGACY_PORT, CONF_METHOD: METHOD_LEGACY, } -MOCK_CONFIG_ENCRYPTED_WS = { - CONF_HOST: "fake_host", - CONF_NAME: "fake", - CONF_PORT: 8000, -} -MOCK_ENTRYDATA_ENCRYPTED_WS = { - **MOCK_CONFIG_ENCRYPTED_WS, - CONF_METHOD: "encrypted", +ENTRYDATA_ENCRYPTED_WEBSOCKET = { + CONF_HOST: "10.10.12.34", + CONF_PORT: ENCRYPTED_WEBSOCKET_PORT, + CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET, CONF_MAC: "aa:bb:cc:dd:ee:ff", CONF_TOKEN: "037739871315caef138547b03e348b72", CONF_SESSION_ID: "2", diff --git a/tests/components/samsungtv/snapshots/test_diagnostics.ambr b/tests/components/samsungtv/snapshots/test_diagnostics.ambr index 7650623a4fb..e5c9ab1f88e 100644 --- a/tests/components/samsungtv/snapshots/test_diagnostics.ambr +++ b/tests/components/samsungtv/snapshots/test_diagnostics.ambr @@ -45,10 +45,9 @@ 'device_info': None, 'entry': dict({ 'data': dict({ - 'host': 'fake_host', + 'host': '10.10.12.34', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'encrypted', - 'name': 'fake', 'port': 8000, 'session_id': '**REDACTED**', 'token': '**REDACTED**', @@ -104,11 +103,10 @@ }), 'entry': dict({ 'data': dict({ - 'host': 'fake_host', + 'host': '10.10.12.34', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'encrypted', 'model': 'UE48JU6400', - 'name': 'fake', 'port': 8000, 'session_id': '**REDACTED**', 'token': '**REDACTED**', diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index cd98cb0460f..83639171576 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -55,9 +55,9 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.setup import async_setup_component from .const import ( + ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_LEGACY, ENTRYDATA_WEBSOCKET, - MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_SSDP_DATA, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, @@ -193,7 +193,9 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: assert result3["result"].unique_id is None -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_user_websocket(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( @@ -220,7 +222,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api_non_ssl_only") async def test_user_encrypted_websocket( hass: HomeAssistant, ) -> None: @@ -313,7 +315,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing") async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with ( @@ -334,7 +336,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing") async def test_user_websocket_access_denied( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -358,7 +360,7 @@ async def test_user_websocket_access_denied( assert "Please check the Device Connection Manager on your TV" in caplog.text -@pytest.mark.usefixtures("rest_api", "remoteencws_failing") +@pytest.mark.usefixtures("rest_api", "remote_encrypted_websocket_failing") async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with ( @@ -568,7 +570,9 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( hass: HomeAssistant, ) -> None: @@ -597,7 +601,9 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_location( hass: HomeAssistant, ) -> None: @@ -626,7 +632,7 @@ async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_loc assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api_non_ssl_only") async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location( hass: HomeAssistant, ) -> None: @@ -737,7 +743,7 @@ async def test_ssdp_wrong_manufacturer(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remoteencws_failing") +@pytest.mark.usefixtures("remote_encrypted_websocket_failing") async def test_ssdp_not_successful(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with ( @@ -769,7 +775,7 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remoteencws_failing") +@pytest.mark.usefixtures("remote_encrypted_websocket_failing") async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with ( @@ -801,7 +807,7 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote_legacy", "remoteencws_failing") +@pytest.mark.usefixtures("remote_legacy", "remote_encrypted_websocket_failing") async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: """Test starting a flow from discovery twice.""" with patch( @@ -823,7 +829,7 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") async def test_ssdp_already_configured(hass: HomeAssistant) -> None: """Test starting a flow from discovery when already configured.""" with patch( @@ -852,7 +858,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: @pytest.mark.usefixtures( - "remote_websocket", "rest_api_non_ssl_only", "remoteencws_failing" + "remote_websocket", "rest_api_non_ssl_only", "remote_encrypted_websocket_failing" ) async def test_dhcp_wireless(hass: HomeAssistant) -> None: """Test starting a flow from dhcp.""" @@ -879,7 +885,9 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from dhcp.""" # Even though it is named "wifiMac", it matches the mac of the wired connection @@ -910,7 +918,7 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: @pytest.mark.usefixtures( - "remote_websocket", "rest_api_non_ssl_only", "remoteencws_failing" + "remote_websocket", "rest_api_non_ssl_only", "remote_encrypted_websocket_failing" ) @pytest.mark.parametrize( ("source1", "data1", "source2", "data2", "is_matching_result"), @@ -983,7 +991,9 @@ async def test_dhcp_zeroconf_already_in_progress( assert return_values == [is_matching_result] -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" result = await hass.config_entries.flow.async_init( @@ -1008,7 +1018,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where the device is actually a soundbar.""" rest_api.rest_device_info.return_value = { @@ -1032,7 +1042,10 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> @pytest.mark.usefixtures( - "remote_legacy", "remote_websocket", "remoteencws", "rest_api_failing" + "remote_legacy", + "remote_websocket", + "remote_encrypted_websocket", + "rest_api_failing", ) async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where device_info returns None.""" @@ -1046,7 +1059,9 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf and dhcp.""" result = await hass.config_entries.flow.async_init( @@ -1068,7 +1083,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: assert result2["reason"] == "already_in_progress" -@pytest.mark.usefixtures("remoteencws_failing") +@pytest.mark.usefixtures("remote_encrypted_websocket_failing") async def test_autodetect_websocket(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with ( @@ -1118,7 +1133,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -@pytest.mark.usefixtures("remoteencws_failing") +@pytest.mark.usefixtures("remote_encrypted_websocket_failing") async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: """Test for send key with autodetection of protocol.""" mac_address.return_value = "gg:ee:tt:mm:aa:cc" @@ -1250,7 +1265,9 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: assert rest_device_info.call_count == 2 -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_old_entry(hass: HomeAssistant) -> None: """Test update of old entry sets unique id.""" entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY) @@ -1279,7 +1296,9 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1301,7 +1320,9 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1325,7 +1346,9 @@ async def test_update_incorrectly_formatted_mac_unique_id_added_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_added_from_zeroconf( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1376,7 +1399,9 @@ async def test_update_missing_model_added_from_ssdp( assert entry.data[CONF_MODEL] == "UE55H6400" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1401,7 +1426,10 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( @pytest.mark.usefixtures( - "remote_legacy", "remote_websocket", "remoteencws_failing", "rest_api_failing" + "remote_legacy", + "remote_websocket", + "remote_encrypted_websocket_failing", + "rest_api_failing", ) async def test_update_zeroconf_discovery_preserved_unique_id( hass: HomeAssistant, @@ -1425,7 +1453,9 @@ async def test_update_zeroconf_discovery_preserved_unique_id( assert entry.unique_id == "original" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1458,7 +1488,9 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1492,7 +1524,9 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1530,7 +1564,9 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1561,7 +1597,9 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1682,7 +1720,9 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( assert entry.unique_id is None -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_ssdp_location_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1710,7 +1750,9 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1833,10 +1875,10 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: assert result2["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: """Test reauth flow for encrypted TVs.""" - encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} + encrypted_entry_data = deepcopy(ENTRYDATA_ENCRYPTED_WEBSOCKET) del encrypted_entry_data[CONF_TOKEN] del encrypted_entry_data[CONF_SESSION_ID] @@ -1889,7 +1931,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: assert entry.state is ConfigEntryState.LOADED authenticator_mock.assert_called_once() - assert authenticator_mock.call_args[0] == ("fake_host",) + assert authenticator_mock.call_args[0] == ("10.10.12.34",) authenticator_mock.return_value.start_pairing.assert_called_once() assert authenticator_mock.return_value.try_pin.call_count == 2 @@ -1903,7 +1945,9 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: assert entry.data[CONF_SESSION_ID] == "1" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1929,7 +1973,9 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1955,7 +2001,9 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_update_incorrect_udn_matching_mac_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -1982,7 +2030,9 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote_websocket", "rest_api", "remoteencws_failing") +@pytest.mark.usefixtures( + "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" +) async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: @@ -2009,7 +2059,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") async def test_ssdp_update_mac(hass: HomeAssistant) -> None: """Ensure that MAC address is correctly updated from SSDP.""" with patch( diff --git a/tests/components/samsungtv/test_device_trigger.py b/tests/components/samsungtv/test_device_trigger.py index f142339547c..adb80293744 100644 --- a/tests/components/samsungtv/test_device_trigger.py +++ b/tests/components/samsungtv/test_device_trigger.py @@ -16,17 +16,17 @@ from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry -from .const import MOCK_ENTRYDATA_ENCRYPTED_WS +from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET from tests.common import MockConfigEntry, async_get_device_automations -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test we get the expected triggers.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) device = device_registry.async_get_device( identifiers={(DOMAIN, "be9554b9-c9fb-41f4-8920-22da015376a4")} @@ -46,14 +46,14 @@ async def test_get_triggers( assert turn_on_trigger in triggers -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_if_fires_on_turn_on_request( hass: HomeAssistant, device_registry: dr.DeviceRegistry, service_calls: list[ServiceCall], ) -> None: """Test for turn_on and turn_off triggers firing.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) entity_id = "media_player.mock_title" device = device_registry.async_get_device( @@ -109,12 +109,12 @@ async def test_if_fires_on_turn_on_request( assert service_calls[2].data["id"] == 0 -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_failure_scenarios( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test failure scenarios.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) # Test wrong trigger platform type with pytest.raises(HomeAssistantError): diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 37f90d5913c..252c3a35c87 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.core import HomeAssistant from . import setup_samsungtv_entry -from .const import MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS +from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, MOCK_ENTRY_WS_WITH_MAC from tests.common import load_json_object_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -32,7 +32,7 @@ async def test_entry_diagnostics( ) == snapshot(exclude=props("created_at", "modified_at")) -@pytest.mark.usefixtures("remoteencws") +@pytest.mark.usefixtures("remote_encrypted_websocket") async def test_entry_diagnostics_encrypted( hass: HomeAssistant, rest_api: Mock, @@ -43,14 +43,14 @@ async def test_entry_diagnostics_encrypted( rest_api.rest_device_info.return_value = load_json_object_fixture( "device_info_UE48JU6400.json", DOMAIN ) - config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) == snapshot(exclude=props("created_at", "modified_at")) -@pytest.mark.usefixtures("remoteencws") +@pytest.mark.usefixtures("remote_encrypted_websocket") async def test_entry_diagnostics_encrypte_offline( hass: HomeAssistant, rest_api: Mock, @@ -59,7 +59,7 @@ async def test_entry_diagnostics_encrypte_offline( ) -> None: """Test config entry diagnostics.""" rest_api.rest_device_info.side_effect = HttpApiError - config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index bdd0dcf881e..7d191771ea0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -40,9 +40,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from . import setup_samsungtv_entry from .const import ( + ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_WEBSOCKET, MOCK_ENTRY_WS_WITH_MAC, - MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) @@ -57,7 +57,9 @@ MOCK_CONFIG = { } -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing", "rest_api") +@pytest.mark.usefixtures( + "remote_websocket", "remote_encrypted_websocket_failing", "rest_api" +) async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) @@ -101,7 +103,9 @@ async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing", "rest_api") +@pytest.mark.usefixtures( + "remote_websocket", "remote_encrypted_websocket_failing", "rest_api" +) async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await setup_samsungtv_entry(hass, MOCK_CONFIG) @@ -111,7 +115,7 @@ async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" -@pytest.mark.usefixtures("remote_websocket", "remoteencws_failing") +@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -162,10 +166,10 @@ async def test_setup_updates_from_ssdp( ) -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: """Test reauth flow is triggered for encrypted TVs.""" - encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} + encrypted_entry_data = {**ENTRYDATA_ENCRYPTED_WEBSOCKET} del encrypted_entry_data[CONF_TOKEN] del encrypted_entry_data[CONF_SESSION_ID] @@ -179,7 +183,9 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: assert len(flows_in_progress) == 1 -@pytest.mark.usefixtures("remote_legacy", "remoteencws_failing", "rest_api_failing") +@pytest.mark.usefixtures( + "remote_legacy", "remote_encrypted_websocket_failing", "rest_api_failing" +) @pytest.mark.parametrize( "entry_data", [ diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 6cbd489e114..3ca03e5b158 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -81,9 +81,9 @@ from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry from .const import ( + ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_LEGACY, MOCK_ENTRY_WS_WITH_MAC, - MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_WIFI, ) @@ -201,7 +201,7 @@ async def test_setup_encrypted_websocket( remote.__aexit__ = AsyncMock() remote_class.return_value = remote - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -342,19 +342,21 @@ async def test_update_off_ws_with_power_state( async def test_update_off_encryptedws( hass: HomeAssistant, freezer: FrozenDateTimeFactory, - remoteencws: Mock, + remote_encrypted_websocket: Mock, rest_api: Mock, ) -> None: """Testing update tv off.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) rest_api.rest_device_info.assert_called_once() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON - remoteencws.start_listening = Mock(side_effect=WebSocketException("Boom")) - remoteencws.is_alive.return_value = False + remote_encrypted_websocket.start_listening = Mock( + side_effect=WebSocketException("Boom") + ) + remote_encrypted_websocket.is_alive.return_value = False freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) @@ -595,11 +597,13 @@ async def test_send_key_websocketexception( @pytest.mark.usefixtures("rest_api") async def test_send_key_websocketexception_encrypted( - hass: HomeAssistant, remoteencws: Mock + hass: HomeAssistant, remote_encrypted_websocket: Mock ) -> None: """Testing unhandled response exception.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - remoteencws.send_commands = Mock(side_effect=WebSocketException("Boom")) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) + remote_encrypted_websocket.send_commands = Mock( + side_effect=WebSocketException("Boom") + ) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -623,11 +627,11 @@ async def test_send_key_os_error_ws( @pytest.mark.usefixtures("rest_api") async def test_send_key_os_error_ws_encrypted( - hass: HomeAssistant, remoteencws: Mock + hass: HomeAssistant, remote_encrypted_websocket: Mock ) -> None: """Testing unhandled response exception.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - remoteencws.send_commands = Mock(side_effect=OSError("Boom")) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) + remote_encrypted_websocket.send_commands = Mock(side_effect=OSError("Boom")) await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -774,36 +778,38 @@ async def test_turn_off_websocket_frame( async def test_turn_off_encrypted_websocket( - hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + remote_encrypted_websocket: Mock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test for turn_off.""" - entry_data = deepcopy(MOCK_ENTRYDATA_ENCRYPTED_WS) + entry_data = deepcopy(ENTRYDATA_ENCRYPTED_WEBSOCKET) entry_data[CONF_MODEL] = "UE48UNKNOWN" await setup_samsungtv_entry(hass, entry_data) - remoteencws.send_commands.reset_mock() + remote_encrypted_websocket.send_commands.reset_mock() caplog.clear() await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remoteencws.send_commands.call_count == 1 - commands = remoteencws.send_commands.call_args_list[0].args[0] + assert remote_encrypted_websocket.send_commands.call_count == 1 + commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 2 assert isinstance(command := commands[0], SamsungTVEncryptedCommand) assert command.body["param3"] == "KEY_POWEROFF" assert isinstance(command := commands[1], SamsungTVEncryptedCommand) assert command.body["param3"] == "KEY_POWER" - assert "Unknown power_off command for UE48UNKNOWN (fake_host)" in caplog.text + assert "Unknown power_off command for UE48UNKNOWN (10.10.12.34)" in caplog.text # commands not sent : power off in progress - remoteencws.send_commands.reset_mock() + remote_encrypted_websocket.send_commands.reset_mock() await hass.services.async_call( MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text - remoteencws.send_commands.assert_not_called() + remote_encrypted_websocket.send_commands.assert_not_called() @pytest.mark.parametrize( @@ -812,25 +818,25 @@ async def test_turn_off_encrypted_websocket( ) async def test_turn_off_encrypted_websocket_key_type( hass: HomeAssistant, - remoteencws: Mock, + remote_encrypted_websocket: Mock, caplog: pytest.LogCaptureFixture, model: str, expected_key_type: str, ) -> None: """Test for turn_off.""" - entry_data = deepcopy(MOCK_ENTRYDATA_ENCRYPTED_WS) + entry_data = deepcopy(ENTRYDATA_ENCRYPTED_WEBSOCKET) entry_data[CONF_MODEL] = model await setup_samsungtv_entry(hass, entry_data) - remoteencws.send_commands.reset_mock() + remote_encrypted_websocket.send_commands.reset_mock() caplog.clear() await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remoteencws.send_commands.call_count == 1 - commands = remoteencws.send_commands.call_args_list[0].args[0] + assert remote_encrypted_websocket.send_commands.call_count == 1 + commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(command := commands[0], SamsungTVEncryptedCommand) assert command.body["param3"] == expected_key_type @@ -877,12 +883,14 @@ async def test_turn_off_ws_os_error( @pytest.mark.usefixtures("rest_api") async def test_turn_off_encryptedws_os_error( - hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + remote_encrypted_websocket: Mock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) - remoteencws.close = Mock(side_effect=OSError("BOOM")) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) + remote_encrypted_websocket.close = Mock(side_effect=OSError("BOOM")) await hass.services.async_call( MP_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index a4ea43ee77c..b64c61acb4a 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -17,39 +17,45 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_samsungtv_entry -from .const import ENTRYDATA_LEGACY, MOCK_ENTRY_WS_WITH_MAC, MOCK_ENTRYDATA_ENCRYPTED_WS +from .const import ( + ENTRYDATA_ENCRYPTED_WEBSOCKET, + ENTRYDATA_LEGACY, + MOCK_ENTRY_WS_WITH_MAC, +) from tests.common import MockConfigEntry ENTITY_ID = f"{REMOTE_DOMAIN}.mock_title" -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_setup(hass: HomeAssistant) -> None: """Test setup with basic config.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) assert hass.states.get(ENTITY_ID) -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_unique_id( hass: HomeAssistant, entity_registry: er.EntityRegistry ) -> None: """Test unique id.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) main = entity_registry.async_get(ENTITY_ID) assert main.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") async def test_main_services( - hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + remote_encrypted_websocket: Mock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test for turn_off.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) - remoteencws.send_commands.reset_mock() + remote_encrypted_websocket.send_commands.reset_mock() await hass.services.async_call( REMOTE_DOMAIN, @@ -59,8 +65,8 @@ async def test_main_services( ) # key called - assert remoteencws.send_commands.call_count == 1 - commands = remoteencws.send_commands.call_args_list[0].args[0] + assert remote_encrypted_websocket.send_commands.call_count == 1 + commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 2 assert isinstance(command := commands[0], SamsungTVEncryptedCommand) assert command.body["param3"] == "KEY_POWEROFF" @@ -68,7 +74,7 @@ async def test_main_services( assert command.body["param3"] == "KEY_POWER" # commands not sent : power off in progress - remoteencws.send_commands.reset_mock() + remote_encrypted_websocket.send_commands.reset_mock() await hass.services.async_call( REMOTE_DOMAIN, SERVICE_SEND_COMMAND, @@ -76,13 +82,15 @@ async def test_main_services( blocking=True, ) assert "TV is powering off, not sending keys: ['dash']" in caplog.text - remoteencws.send_commands.assert_not_called() + remote_encrypted_websocket.send_commands.assert_not_called() -@pytest.mark.usefixtures("remoteencws", "rest_api") -async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> None: +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") +async def test_send_command_service( + hass: HomeAssistant, remote_encrypted_websocket: Mock +) -> None: """Test the send command.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) await hass.services.async_call( REMOTE_DOMAIN, @@ -91,8 +99,8 @@ async def test_send_command_service(hass: HomeAssistant, remoteencws: Mock) -> N blocking=True, ) - assert remoteencws.send_commands.call_count == 1 - commands = remoteencws.send_commands.call_args_list[0].args[0] + assert remote_encrypted_websocket.send_commands.call_count == 1 + commands = remote_encrypted_websocket.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(command := commands[0], SamsungTVEncryptedCommand) assert command.body["param3"] == "dash" diff --git a/tests/components/samsungtv/test_trigger.py b/tests/components/samsungtv/test_trigger.py index dce64c5e580..e2155bca834 100644 --- a/tests/components/samsungtv/test_trigger.py +++ b/tests/components/samsungtv/test_trigger.py @@ -12,12 +12,12 @@ from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry -from .const import MOCK_ENTRYDATA_ENCRYPTED_WS +from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET from tests.common import MockEntity, MockEntityPlatform -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_turn_on_trigger_device_id( hass: HomeAssistant, @@ -26,7 +26,7 @@ async def test_turn_on_trigger_device_id( entity_domain: str, ) -> None: """Test for turn_on triggers by device_id firing.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) entity_id = f"{entity_domain}.mock_title" @@ -84,13 +84,13 @@ async def test_turn_on_trigger_device_id( mock_send_magic_packet.assert_called() -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_turn_on_trigger_entity_id( hass: HomeAssistant, service_calls: list[ServiceCall], entity_domain: str ) -> None: """Test for turn_on triggers by entity_id firing.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) entity_id = f"{entity_domain}.mock_title" @@ -126,13 +126,13 @@ async def test_turn_on_trigger_entity_id( assert service_calls[1].data["id"] == 0 -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_wrong_trigger_platform_type( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_domain: str ) -> None: """Test wrong trigger platform type.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) entity_id = f"{entity_domain}.fake" await async_setup_component( @@ -163,13 +163,13 @@ async def test_wrong_trigger_platform_type( ) -@pytest.mark.usefixtures("remoteencws", "rest_api") +@pytest.mark.usefixtures("remote_encrypted_websocket", "rest_api") @pytest.mark.parametrize("entity_domain", ["media_player", "remote"]) async def test_trigger_invalid_entity_id( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, entity_domain: str ) -> None: """Test turn on trigger using invalid entity_id.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + await setup_samsungtv_entry(hass, ENTRYDATA_ENCRYPTED_WEBSOCKET) entity_id = f"{entity_domain}.fake" platform = MockEntityPlatform(hass) From cba12fb5989659bf731c66fed9dd953372dda122 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 May 2025 12:00:32 +0200 Subject: [PATCH 0372/1175] Refactor frontend user store (#144723) * Refactor frontend user store * Address review comments --- homeassistant/components/frontend/storage.py | 102 +++++++++---------- homeassistant/core_config.py | 8 +- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index a33a9de7ac5..c75fa360db7 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -10,53 +10,63 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api import ActiveConnection -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import Store from homeassistant.util.hass_dict import HassKey -DATA_STORAGE: HassKey[tuple[dict[str, Store], dict[str, dict]]] = HassKey( - "frontend_storage" -) +DATA_STORAGE: HassKey[dict[str, UserStore]] = HassKey("frontend_storage") STORAGE_VERSION_USER_DATA = 1 -@callback -def _initialize_frontend_storage(hass: HomeAssistant) -> None: - """Set up frontend storage.""" - if DATA_STORAGE in hass.data: - return - hass.data[DATA_STORAGE] = ({}, {}) - - async def async_setup_frontend_storage(hass: HomeAssistant) -> None: """Set up frontend storage.""" - _initialize_frontend_storage(hass) websocket_api.async_register_command(hass, websocket_set_user_data) websocket_api.async_register_command(hass, websocket_get_user_data) -async def async_user_store( - hass: HomeAssistant, user_id: str -) -> tuple[Store, dict[str, Any]]: +async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore: """Access a user store.""" - _initialize_frontend_storage(hass) - stores, data = hass.data[DATA_STORAGE] + stores = hass.data.setdefault(DATA_STORAGE, {}) if (store := stores.get(user_id)) is None: - store = stores[user_id] = Store( + store = stores[user_id] = UserStore(hass, user_id) + await store.async_load() + + return store + + +class UserStore: + """User store for frontend data.""" + + def __init__(self, hass: HomeAssistant, user_id: str) -> None: + """Initialize the user store.""" + self._store = _UserStore(hass, user_id) + self.data: dict[str, Any] = {} + + async def async_load(self) -> None: + """Load the data from the store.""" + self.data = await self._store.async_load() or {} + + async def async_set_item(self, key: str, value: Any) -> None: + """Set an item item and save the store.""" + self.data[key] = value + await self._store.async_save(self.data) + + +class _UserStore(Store[dict[str, Any]]): + """User store for frontend data.""" + + def __init__(self, hass: HomeAssistant, user_id: str) -> None: + """Initialize the user store.""" + super().__init__( hass, STORAGE_VERSION_USER_DATA, f"frontend.user_data_{user_id}", ) - if user_id not in data: - data[user_id] = await store.async_load() or {} - return store, data[user_id] - - -def with_store( +def with_user_store( orig_func: Callable[ - [HomeAssistant, ActiveConnection, dict[str, Any], Store, dict[str, Any]], + [HomeAssistant, ActiveConnection, dict[str, Any], UserStore], Coroutine[Any, Any, None], ], ) -> Callable[ @@ -65,17 +75,17 @@ def with_store( """Decorate function to provide data.""" @wraps(orig_func) - async def with_store_func( + async def with_user_store_func( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Provide user specific data and store to function.""" user_id = connection.user.id - store, user_data = await async_user_store(hass, user_id) + store = await async_user_store(hass, user_id) - await orig_func(hass, connection, msg, store, user_data) + await orig_func(hass, connection, msg, store) - return with_store_func + return with_user_store_func @websocket_api.websocket_command( @@ -86,41 +96,31 @@ def with_store( } ) @websocket_api.async_response -@with_store +@with_user_store async def websocket_set_user_data( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - store: Store, - data: dict[str, Any], + store: UserStore, ) -> None: - """Handle set global data command. - - Async friendly. - """ - data[msg["key"]] = msg["value"] - await store.async_save(data) - connection.send_message(websocket_api.result_message(msg["id"])) + """Handle set user data command.""" + await store.async_set_item(msg["key"], msg["value"]) + connection.send_result(msg["id"]) @websocket_api.websocket_command( {vol.Required("type"): "frontend/get_user_data", vol.Optional("key"): str} ) @websocket_api.async_response -@with_store +@with_user_store async def websocket_get_user_data( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any], - store: Store, - data: dict[str, Any], + store: UserStore, ) -> None: - """Handle get global data command. - - Async friendly. - """ - connection.send_message( - websocket_api.result_message( - msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data} - ) + """Handle get user data command.""" + data = store.data + connection.send_result( + msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data} ) diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py index ccae24a907b..f1ba96daae4 100644 --- a/homeassistant/core_config.py +++ b/homeassistant/core_config.py @@ -866,17 +866,17 @@ class Config: # pylint: disable-next=import-outside-toplevel from .components.frontend import storage as frontend_store - _, owner_data = await frontend_store.async_user_store( + owner_store = await frontend_store.async_user_store( self.hass, owner.id ) if ( - "language" in owner_data - and "language" in owner_data["language"] + "language" in owner_store.data + and "language" in owner_store.data["language"] ): with suppress(vol.InInvalid): data["language"] = cv.language( - owner_data["language"]["language"] + owner_store.data["language"]["language"] ) # pylint: disable-next=broad-except except Exception: From 4dde3143385cb75dbf9b1fe6b0adaa0064995da6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 13:45:20 +0200 Subject: [PATCH 0373/1175] Remove obsolete tests in SamsungTV (#144735) --- .../samsungtv/snapshots/test_init.ambr | 86 ------------------- tests/components/samsungtv/test_init.py | 54 +----------- 2 files changed, 1 insertion(+), 139 deletions(-) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 48201781004..443484e38c7 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -1,90 +1,4 @@ # serializer version: 1 -# name: test_cleanup_mac - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - 'aa:bb:cc:dd:ee:ff', - ), - tuple( - 'mac', - 'none', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'samsungtv', - 'any', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': None, - 'model': '82GXARRS', - 'model_id': None, - 'name': 'fake', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- -# name: test_cleanup_mac.1 - list([ - DeviceRegistryEntrySnapshot({ - 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , - 'configuration_url': None, - 'connections': set({ - tuple( - 'mac', - 'aa:bb:cc:dd:ee:ff', - ), - tuple( - 'mac', - 'none', - ), - }), - 'disabled_by': None, - 'entry_type': None, - 'hw_version': None, - 'id': , - 'identifiers': set({ - tuple( - 'samsungtv', - 'any', - ), - }), - 'is_new': False, - 'labels': set({ - }), - 'manufacturer': None, - 'model': '82GXARRS', - 'model_id': '82GXARRS', - 'name': 'fake', - 'name_by_user': None, - 'primary_config_entry': , - 'serial_number': None, - 'suggested_area': None, - 'sw_version': None, - 'via_device_id': None, - }), - ]) -# --- # name: test_setup_updates_from_ssdp StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 7d191771ea0..0b72a112301 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -36,13 +36,12 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import entity_registry as er from . import setup_samsungtv_entry from .const import ( ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_WEBSOCKET, - MOCK_ENTRY_WS_WITH_MAC, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) @@ -234,54 +233,3 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" - - -@pytest.mark.usefixtures("remote_websocket", "rest_api") -@pytest.mark.xfail -async def test_cleanup_mac( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion -) -> None: - """Test for `none` mac cleanup #103512. - - Reverted due to device registry collisions in #119249 / #119082 - """ - entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_ENTRY_WS_WITH_MAC, - entry_id="123456", - unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", - version=2, - minor_version=1, - ) - entry.add_to_hass(hass) - - # Setup initial device registry, with incorrect MAC - device_registry.async_get_or_create( - config_entry_id="123456", - connections={ - (dr.CONNECTION_NETWORK_MAC, "none"), - (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff"), - }, - identifiers={("samsungtv", "be9554b9-c9fb-41f4-8920-22da015376a4")}, - model="82GXARRS", - name="fake", - ) - device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries == snapshot - assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, "none"), - (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff"), - } - - # Run setup, and ensure the NONE mac is removed - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) - assert device_entries == snapshot - assert device_entries[0].connections == { - (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") - } - - assert entry.version == 2 - assert entry.minor_version == 2 From 7b23f217120b67504a5b096f27ff9fb778befdf7 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 12 May 2025 14:47:49 +0200 Subject: [PATCH 0374/1175] Remove deprecated camera async_handle_web_rtc_offer function (#144561) --- homeassistant/components/camera/__init__.py | 76 +----- homeassistant/components/camera/webrtc.py | 2 - tests/components/camera/conftest.py | 17 +- tests/components/camera/test_init.py | 17 +- tests/components/camera/test_webrtc.py | 257 +------------------- 5 files changed, 27 insertions(+), 342 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 194f316c13a..ee9d1cbc94f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -55,7 +55,6 @@ from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, all_with_deprecated_constants, check_if_deprecated_constant, - deprecated_function, dir_with_deprecated_constants, ) from homeassistant.helpers.entity import Entity, EntityDescription @@ -86,10 +85,10 @@ from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 from .webrtc import ( DATA_ICE_SERVERS, CameraWebRTCProvider, - WebRTCAnswer, + WebRTCAnswer, # noqa: F401 WebRTCCandidate, # noqa: F401 WebRTCClientConfiguration, - WebRTCError, + WebRTCError, # noqa: F401 WebRTCMessage, # noqa: F401 WebRTCSendMessage, async_get_supported_provider, @@ -473,9 +472,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): self.async_update_token() self._create_stream_lock: asyncio.Lock | None = None self._webrtc_provider: CameraWebRTCProvider | None = None - self._supports_native_sync_webrtc = ( - type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer - ) self._supports_native_async_webrtc = ( type(self).async_handle_async_webrtc_offer != Camera.async_handle_async_webrtc_offer @@ -579,15 +575,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """ return None - async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: - """Handle the WebRTC offer and return an answer. - - This is used by cameras with CameraEntityFeature.STREAM - and StreamType.WEB_RTC. - - Integrations can override with a native WebRTC implementation. - """ - async def async_handle_async_webrtc_offer( self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage ) -> None: @@ -600,42 +587,6 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): Integrations can override with a native WebRTC implementation. """ - if self._supports_native_sync_webrtc: - try: - answer = await deprecated_function( - "async_handle_async_webrtc_offer", - breaks_in_ha_version="2025.6", - )(self.async_handle_web_rtc_offer)(offer_sdp) - except ValueError as ex: - _LOGGER.error("Error handling WebRTC offer: %s", ex) - send_message( - WebRTCError( - "webrtc_offer_failed", - str(ex), - ) - ) - except TimeoutError: - # This catch was already here and should stay through the deprecation - _LOGGER.error("Timeout handling WebRTC offer") - send_message( - WebRTCError( - "webrtc_offer_failed", - "Timeout handling WebRTC offer", - ) - ) - else: - if answer: - send_message(WebRTCAnswer(answer)) - else: - _LOGGER.error("Error handling WebRTC offer: No answer") - send_message( - WebRTCError( - "webrtc_offer_failed", - "No answer on WebRTC offer", - ) - ) - return - if self._webrtc_provider: await self._webrtc_provider.async_handle_async_webrtc_offer( self, offer_sdp, session_id, send_message @@ -764,9 +715,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): new_provider = None # Skip all providers if the camera has a native WebRTC implementation - if not ( - self._supports_native_sync_webrtc or self._supports_native_async_webrtc - ): + if not self._supports_native_async_webrtc: # Camera doesn't have a native WebRTC implementation new_provider = await self._async_get_supported_webrtc_provider( async_get_supported_provider @@ -798,17 +747,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the WebRTC client configuration and extend it with the registered ice servers.""" config = self._async_get_webrtc_client_configuration() - if not self._supports_native_sync_webrtc: - # Until 2024.11, the frontend was not resolving any ice servers - # The async approach was added 2024.11 and new integrations need to use it - ice_servers = [ - server - for servers in self.hass.data.get(DATA_ICE_SERVERS, []) - for server in servers() - ] - config.configuration.ice_servers.extend(ice_servers) - - config.get_candidates_upfront = self._supports_native_sync_webrtc + ice_servers = [ + server + for servers in self.hass.data.get(DATA_ICE_SERVERS, []) + for server in servers() + ] + config.configuration.ice_servers.extend(ice_servers) return config @@ -838,7 +782,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return the camera capabilities.""" frontend_stream_types = set() if CameraEntityFeature.STREAM in self.supported_features_compat: - if self._supports_native_sync_webrtc or self._supports_native_async_webrtc: + if self._supports_native_async_webrtc: # The camera has a native WebRTC implementation frontend_stream_types.add(StreamType.WEB_RTC) else: diff --git a/homeassistant/components/camera/webrtc.py b/homeassistant/components/camera/webrtc.py index 723d44409fd..9ad50430f83 100644 --- a/homeassistant/components/camera/webrtc.py +++ b/homeassistant/components/camera/webrtc.py @@ -111,13 +111,11 @@ class WebRTCClientConfiguration: configuration: RTCConfiguration = field(default_factory=RTCConfiguration) data_channel: str | None = None - get_candidates_upfront: bool = False def to_frontend_dict(self) -> dict[str, Any]: """Return a dict that can be used by the frontend.""" data: dict[str, Any] = { "configuration": self.configuration.to_dict(), - "getCandidatesUpfront": self.get_candidates_upfront, } if self.data_channel is not None: data["dataChannel"] = self.data_channel diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index b529ee3e9b9..dcc02cf99fe 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -165,13 +165,15 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None: async def stream_source(self) -> str | None: return STREAM_SOURCE - class SyncCamera(BaseCamera): - """Mock Camera with native sync WebRTC support.""" + class AsyncNoCandidateCamera(BaseCamera): + """Mock Camera with native async WebRTC support but not implemented candidate support.""" - _attr_name = "Sync" + _attr_name = "Async No Candidate" - async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: - return WEBRTC_ANSWER + async def async_handle_async_webrtc_offer( + self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage + ) -> None: + send_message(WebRTCAnswer(WEBRTC_ANSWER)) class AsyncCamera(BaseCamera): """Mock Camera with native async WebRTC support.""" @@ -221,7 +223,10 @@ async def mock_test_webrtc_cameras(hass: HomeAssistant) -> None: ), ) setup_test_component_platform( - hass, camera.DOMAIN, [SyncCamera(), AsyncCamera()], from_config_entry=True + hass, + camera.DOMAIN, + [AsyncNoCandidateCamera(), AsyncCamera()], + from_config_entry=True, ) mock_platform(hass, f"{domain}.config_flow", Mock()) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 7fd5cd0855f..2348ca58673 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -968,24 +968,19 @@ async def test_camera_capabilities_webrtc( """Test WebRTC camera capabilities.""" await _test_capabilities( - hass, hass_ws_client, "camera.sync", {StreamType.WEB_RTC}, {StreamType.WEB_RTC} + hass, hass_ws_client, "camera.async", {StreamType.WEB_RTC}, {StreamType.WEB_RTC} ) -@pytest.mark.parametrize( - ("entity_id", "expect_native_async_webrtc"), - [("camera.sync", False), ("camera.async", True)], -) @pytest.mark.usefixtures("mock_test_webrtc_cameras", "register_test_provider") async def test_webrtc_provider_not_added_for_native_webrtc( - hass: HomeAssistant, entity_id: str, expect_native_async_webrtc: bool + hass: HomeAssistant, ) -> None: """Test that a WebRTC provider is not added to a camera when the camera has native WebRTC support.""" - camera_obj = get_camera_from_entity_id(hass, entity_id) + camera_obj = get_camera_from_entity_id(hass, "camera.async") assert camera_obj assert camera_obj._webrtc_provider is None - assert camera_obj._supports_native_sync_webrtc is not expect_native_async_webrtc - assert camera_obj._supports_native_async_webrtc is expect_native_async_webrtc + assert camera_obj._supports_native_async_webrtc is True @pytest.mark.usefixtures("mock_camera", "mock_stream_source") @@ -1016,14 +1011,12 @@ async def test_camera_capabilities_changing_non_native_support( @pytest.mark.usefixtures("mock_test_webrtc_cameras") -@pytest.mark.parametrize(("entity_id"), ["camera.sync", "camera.async"]) async def test_camera_capabilities_changing_native_support( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - entity_id: str, ) -> None: """Test WebRTC camera capabilities.""" - cam = get_camera_from_entity_id(hass, entity_id) + cam = get_camera_from_entity_id(hass, "camera.async") assert cam.supported_features == camera.CameraEntityFeature.STREAM await _test_capabilities( diff --git a/tests/components/camera/test_webrtc.py b/tests/components/camera/test_webrtc.py index c7ea82f7b9d..e6b13afc171 100644 --- a/tests/components/camera/test_webrtc.py +++ b/tests/components/camera/test_webrtc.py @@ -1,7 +1,6 @@ """Test camera WebRTC.""" from collections.abc import AsyncGenerator -import logging from typing import Any from unittest.mock import AsyncMock, Mock, patch @@ -10,9 +9,7 @@ from webrtc_models import RTCIceCandidate, RTCIceCandidateInit, RTCIceServer from homeassistant.components.camera import ( DATA_ICE_SERVERS, - DOMAIN as CAMERA_DOMAIN, Camera, - CameraEntityFeature, CameraWebRTCProvider, StreamType, WebRTCAnswer, @@ -25,22 +22,12 @@ from homeassistant.components.camera import ( get_camera_from_entity_id, ) from homeassistant.components.websocket_api import TYPE_RESULT -from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.core import HomeAssistant, callback from homeassistant.core_config import async_process_ha_core_config -from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from .common import STREAM_SOURCE, WEBRTC_ANSWER, SomeTestProvider -from tests.common import ( - MockConfigEntry, - MockModule, - mock_config_flow, - mock_integration, - mock_platform, - setup_test_component_platform, -) from tests.typing import WebSocketGenerator WEBRTC_OFFER = "v=0\r\n" @@ -57,84 +44,6 @@ class Go2RTCProvider(SomeTestProvider): return "go2rtc" -class MockCamera(Camera): - """Mock Camera Entity.""" - - _attr_name = "Test" - _attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM - - def __init__(self) -> None: - """Initialize the mock entity.""" - super().__init__() - self._sync_answer: str | None | Exception = WEBRTC_ANSWER - - def set_sync_answer(self, value: str | None | Exception) -> None: - """Set sync offer answer.""" - self._sync_answer = value - - async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: - """Handle the WebRTC offer and return the answer.""" - if isinstance(self._sync_answer, Exception): - raise self._sync_answer - return self._sync_answer - - async def stream_source(self) -> str | None: - """Return the source of the stream. - - This is used by cameras with CameraEntityFeature.STREAM - and StreamType.HLS. - """ - return "rtsp://stream" - - -@pytest.fixture -async def init_test_integration( - hass: HomeAssistant, -) -> MockCamera: - """Initialize components.""" - - entry = MockConfigEntry(domain=TEST_INTEGRATION_DOMAIN) - entry.add_to_hass(hass) - - async def async_setup_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setups( - config_entry, [CAMERA_DOMAIN] - ) - return True - - async def async_unload_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Unload test config entry.""" - await hass.config_entries.async_forward_entry_unload( - config_entry, CAMERA_DOMAIN - ) - return True - - mock_integration( - hass, - MockModule( - TEST_INTEGRATION_DOMAIN, - async_setup_entry=async_setup_entry_init, - async_unload_entry=async_unload_entry_init, - ), - ) - test_camera = MockCamera() - setup_test_component_platform( - hass, CAMERA_DOMAIN, [test_camera], from_config_entry=True - ) - mock_platform(hass, f"{TEST_INTEGRATION_DOMAIN}.config_flow", Mock()) - - with mock_config_flow(TEST_INTEGRATION_DOMAIN, ConfigFlow): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return test_camera - - @pytest.mark.usefixtures("mock_camera", "mock_stream_source") async def test_async_register_webrtc_provider( hass: HomeAssistant, @@ -302,7 +211,6 @@ async def test_ws_get_client_config( }, ], }, - "getCandidatesUpfront": False, } @callback @@ -341,30 +249,6 @@ async def test_ws_get_client_config( }, ], }, - "getCandidatesUpfront": False, - } - - -@pytest.mark.usefixtures("mock_test_webrtc_cameras") -async def test_ws_get_client_config_sync_offer( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator -) -> None: - """Test get WebRTC client config, when camera is supporting sync offer.""" - await async_setup_component(hass, "camera", {}) - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - await client.send_json_auto_id( - {"type": "camera/webrtc/get_client_config", "entity_id": "camera.sync"} - ) - msg = await client.receive_json() - - # Assert WebSocket response - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"] == { - "configuration": {}, - "getCandidatesUpfront": True, } @@ -391,7 +275,6 @@ async def test_ws_get_client_config_custom_config( assert msg["success"] assert msg["result"] == { "configuration": {"iceServers": [{"urls": ["stun:custom_stun_server:3478"]}]}, - "getCandidatesUpfront": False, } @@ -625,144 +508,6 @@ async def test_websocket_webrtc_offer_missing_offer( assert response["error"]["code"] == "invalid_format" -@pytest.mark.parametrize( - ("error", "expected_message"), - [ - (ValueError("value error"), "value error"), - (HomeAssistantError("offer failed"), "offer failed"), - (TimeoutError(), "Timeout handling WebRTC offer"), - ], -) -async def test_websocket_webrtc_offer_failure( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - init_test_integration: MockCamera, - error: Exception, - expected_message: str, -) -> None: - """Test WebRTC stream that fails handling the offer.""" - client = await hass_ws_client(hass) - init_test_integration.set_sync_answer(error) - - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.test", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Error - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "error", - "code": "webrtc_offer_failed", - "message": expected_message, - } - - -@pytest.mark.usefixtures("mock_test_webrtc_cameras") -async def test_websocket_webrtc_offer_sync( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test sync WebRTC stream offer.""" - client = await hass_ws_client(hass) - - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.sync", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - - assert ( - "tests.components.camera.conftest", - logging.WARNING, - ( - "async_handle_web_rtc_offer was called from camera, this is a deprecated " - "function which will be removed in HA Core 2025.6. Use " - "async_handle_async_webrtc_offer instead" - ), - ) in caplog.record_tuples - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == {"type": "answer", "answer": WEBRTC_ANSWER} - - -async def test_websocket_webrtc_offer_sync_no_answer( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - caplog: pytest.LogCaptureFixture, - init_test_integration: MockCamera, -) -> None: - """Test sync WebRTC stream offer with no answer.""" - client = await hass_ws_client(hass) - init_test_integration.set_sync_answer(None) - - await client.send_json_auto_id( - { - "type": "camera/webrtc/offer", - "entity_id": "camera.test", - "offer": WEBRTC_OFFER, - } - ) - response = await client.receive_json() - - assert response["type"] == TYPE_RESULT - assert response["success"] - subscription_id = response["id"] - - # Session id - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"]["type"] == "session" - - # Answer - response = await client.receive_json() - assert response["id"] == subscription_id - assert response["type"] == "event" - assert response["event"] == { - "type": "error", - "code": "webrtc_offer_failed", - "message": "No answer on WebRTC offer", - } - assert ( - "homeassistant.components.camera", - logging.ERROR, - "Error handling WebRTC offer: No answer", - ) in caplog.record_tuples - - @pytest.mark.usefixtures("mock_camera") async def test_websocket_webrtc_offer_invalid_stream_type( hass: HomeAssistant, hass_ws_client: WebSocketGenerator @@ -901,7 +646,7 @@ async def test_ws_webrtc_candidate_not_supported( await client.send_json_auto_id( { "type": "camera/webrtc/candidate", - "entity_id": "camera.sync", + "entity_id": "camera.async_no_candidate", "session_id": "session_id", "candidate": {"candidate": "candidate"}, } From f1e5f73d7e01ccfd925d261985fe08eeadde34d9 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 12 May 2025 15:35:06 +0200 Subject: [PATCH 0375/1175] Improve user-facing strings of `velbus` (#144716) - add the missing hyphen to "password-protected" - resolve missing genitive in `sync_clock` action description - resolve singular/plural mismatch in `set_memo_text` action description --- homeassistant/components/velbus/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/strings.json b/homeassistant/components/velbus/strings.json index 35f94e54470..4ef7ccf62c2 100644 --- a/homeassistant/components/velbus/strings.json +++ b/homeassistant/components/velbus/strings.json @@ -21,7 +21,7 @@ "tls": "Enable this if you use a secure connection to your Velbus interface, like a Signum.", "host": "The IP address or hostname of the Velbus interface.", "port": "The port number of the Velbus interface.", - "password": "The password of the Velbus interface, this is only needed if the interface is password protected." + "password": "The password of the Velbus interface, this is only needed if the interface is password-protected." }, "description": "TCP/IP configuration, in case you use a Signum, VelServ, velbus-tcp or any other Velbus to TCP/IP interface." }, @@ -58,7 +58,7 @@ "services": { "sync_clock": { "name": "Sync clock", - "description": "Syncs the Velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.", + "description": "Syncs the clock of the Velbus modules to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink.", "fields": { "interface": { "name": "Interface", @@ -104,7 +104,7 @@ }, "set_memo_text": { "name": "Set memo text", - "description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text.", + "description": "Sets the memo text to the display of modules like VMBGPO, VMBGPOD. Be sure the pages of the modules are configured to display the memo text.", "fields": { "interface": { "name": "[%key:component::velbus::services::sync_clock::fields::interface::name%]", From 05324dedd00e76a85badadc25a94c1831c8f6c12 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 May 2025 15:38:31 +0200 Subject: [PATCH 0376/1175] Deduplicate condition schemas (#144739) --- homeassistant/helpers/config_validation.py | 50 +++++++++------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0ce2c9e02e0..4c760bd9d70 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1084,9 +1084,12 @@ def renamed( return validator +type ValueSchemas = dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]] + + def key_value_schemas( key: str, - value_schemas: dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any]]], + value_schemas: ValueSchemas, default_schema: VolSchemaType | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: @@ -1735,26 +1738,25 @@ CONDITION_SHORTHAND_SCHEMA = vol.Schema( } ) +BUILT_IN_CONDITIONS: ValueSchemas = { + "and": AND_CONDITION_SCHEMA, + "device": DEVICE_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, + "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, + "or": OR_CONDITION_SCHEMA, + "state": STATE_CONDITION_SCHEMA, + "sun": SUN_CONDITION_SCHEMA, + "template": TEMPLATE_CONDITION_SCHEMA, + "time": TIME_CONDITION_SCHEMA, + "trigger": TRIGGER_CONDITION_SCHEMA, + "zone": ZONE_CONDITION_SCHEMA, +} + CONDITION_SCHEMA: vol.Schema = vol.Schema( vol.Any( vol.All( expand_condition_shorthand, - key_value_schemas( - CONF_CONDITION, - { - "and": AND_CONDITION_SCHEMA, - "device": DEVICE_CONDITION_SCHEMA, - "not": NOT_CONDITION_SCHEMA, - "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, - "or": OR_CONDITION_SCHEMA, - "state": STATE_CONDITION_SCHEMA, - "sun": SUN_CONDITION_SCHEMA, - "template": TEMPLATE_CONDITION_SCHEMA, - "time": TIME_CONDITION_SCHEMA, - "trigger": TRIGGER_CONDITION_SCHEMA, - "zone": ZONE_CONDITION_SCHEMA, - }, - ), + key_value_schemas(CONF_CONDITION, BUILT_IN_CONDITIONS), ), dynamic_template_condition, ) @@ -1780,19 +1782,7 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( expand_condition_shorthand, key_value_schemas( CONF_CONDITION, - { - "and": AND_CONDITION_SCHEMA, - "device": DEVICE_CONDITION_SCHEMA, - "not": NOT_CONDITION_SCHEMA, - "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, - "or": OR_CONDITION_SCHEMA, - "state": STATE_CONDITION_SCHEMA, - "sun": SUN_CONDITION_SCHEMA, - "template": TEMPLATE_CONDITION_SCHEMA, - "time": TIME_CONDITION_SCHEMA, - "trigger": TRIGGER_CONDITION_SCHEMA, - "zone": ZONE_CONDITION_SCHEMA, - }, + BUILT_IN_CONDITIONS, dynamic_template_condition_action, "a list of conditions or a valid template", ), From 73a59523f522ec2d7579917be9ef64432b445d49 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 12 May 2025 15:51:21 +0200 Subject: [PATCH 0377/1175] Merge websocket test constants in samsungtv tests (#144741) --- tests/components/samsungtv/const.py | 9 +-------- .../components/samsungtv/snapshots/test_diagnostics.ambr | 5 ++--- tests/components/samsungtv/test_diagnostics.py | 4 ++-- tests/components/samsungtv/test_media_player.py | 4 ++-- tests/components/samsungtv/test_remote.py | 8 ++------ 5 files changed, 9 insertions(+), 21 deletions(-) diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 64d1a76bc86..16ffb6b9c05 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_MAC, CONF_METHOD, CONF_MODEL, - CONF_NAME, CONF_PORT, CONF_TOKEN, ) @@ -40,14 +39,8 @@ ENTRYDATA_WEBSOCKET = { CONF_HOST: "10.10.12.34", CONF_METHOD: METHOD_WEBSOCKET, CONF_PORT: WEBSOCKET_SSL_PORT, - CONF_MODEL: "UE43LS003", -} -MOCK_ENTRY_WS_WITH_MAC = { - CONF_HOST: "fake_host", - CONF_METHOD: "websocket", CONF_MAC: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "fake", - CONF_PORT: 8002, + CONF_MODEL: "UE43LS003", CONF_TOKEN: "123456789", } diff --git a/tests/components/samsungtv/snapshots/test_diagnostics.ambr b/tests/components/samsungtv/snapshots/test_diagnostics.ambr index e5c9ab1f88e..fb7bcd83ae7 100644 --- a/tests/components/samsungtv/snapshots/test_diagnostics.ambr +++ b/tests/components/samsungtv/snapshots/test_diagnostics.ambr @@ -13,11 +13,10 @@ }), 'entry': dict({ 'data': dict({ - 'host': 'fake_host', + 'host': '10.10.12.34', 'mac': 'aa:bb:cc:dd:ee:ff', 'method': 'websocket', - 'model': '82GXARRS', - 'name': 'fake', + 'model': 'UE43LS003', 'port': 8002, 'token': '**REDACTED**', }), diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 252c3a35c87..1704b0c0422 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.core import HomeAssistant from . import setup_samsungtv_entry -from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, MOCK_ENTRY_WS_WITH_MAC +from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_WEBSOCKET from tests.common import load_json_object_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -25,7 +25,7 @@ async def test_entry_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS_WITH_MAC) + config_entry = await setup_samsungtv_entry(hass, ENTRYDATA_WEBSOCKET) assert await get_diagnostics_for_config_entry( hass, hass_client, config_entry diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 3ca03e5b158..58797b67423 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -83,7 +83,7 @@ from . import setup_samsungtv_entry from .const import ( ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_LEGACY, - MOCK_ENTRY_WS_WITH_MAC, + ENTRYDATA_WEBSOCKET, SAMPLE_DEVICE_INFO_WIFI, ) @@ -996,7 +996,7 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_ENTRY_WS_WITH_MAC, + data=ENTRYDATA_WEBSOCKET, unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", ) entry.add_to_hass(hass) diff --git a/tests/components/samsungtv/test_remote.py b/tests/components/samsungtv/test_remote.py index b64c61acb4a..ec161773c1e 100644 --- a/tests/components/samsungtv/test_remote.py +++ b/tests/components/samsungtv/test_remote.py @@ -17,11 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_samsungtv_entry -from .const import ( - ENTRYDATA_ENCRYPTED_WEBSOCKET, - ENTRYDATA_LEGACY, - MOCK_ENTRY_WS_WITH_MAC, -) +from .const import ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_LEGACY, ENTRYDATA_WEBSOCKET from tests.common import MockConfigEntry @@ -111,7 +107,7 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_ENTRY_WS_WITH_MAC, + data=ENTRYDATA_WEBSOCKET, unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", ) entry.add_to_hass(hass) From b192ca4bad73b16f9d313277d4d92d5a5201694e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 12 May 2025 16:01:42 +0200 Subject: [PATCH 0378/1175] Make it possible to subscribe to frontend user store (#144724) --- homeassistant/components/frontend/storage.py | 47 ++++++- tests/components/frontend/test_storage.py | 121 ++++++++++++++++++- 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index c75fa360db7..11d155dbcb4 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api import ActiveConnection -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store from homeassistant.util.hass_dict import HassKey @@ -22,6 +22,7 @@ async def async_setup_frontend_storage(hass: HomeAssistant) -> None: """Set up frontend storage.""" websocket_api.async_register_command(hass, websocket_set_user_data) websocket_api.async_register_command(hass, websocket_get_user_data) + websocket_api.async_register_command(hass, websocket_subscribe_user_data) async def async_user_store(hass: HomeAssistant, user_id: str) -> UserStore: @@ -41,6 +42,7 @@ class UserStore: """Initialize the user store.""" self._store = _UserStore(hass, user_id) self.data: dict[str, Any] = {} + self.subscriptions: dict[str | None, list[Callable[[], None]]] = {} async def async_load(self) -> None: """Load the data from the store.""" @@ -50,6 +52,23 @@ class UserStore: """Set an item item and save the store.""" self.data[key] = value await self._store.async_save(self.data) + for cb in self.subscriptions.get(None, []): + cb() + for cb in self.subscriptions.get(key, []): + cb() + + @callback + def async_subscribe( + self, key: str | None, on_update_callback: Callable[[], None] + ) -> Callable[[], None]: + """Save the data to the store.""" + self.subscriptions.setdefault(key, []).append(on_update_callback) + + def unsubscribe() -> None: + """Unsubscribe from the store.""" + self.subscriptions[key].remove(on_update_callback) + + return unsubscribe class _UserStore(Store[dict[str, Any]]): @@ -124,3 +143,29 @@ async def websocket_get_user_data( connection.send_result( msg["id"], {"value": data.get(msg["key"]) if "key" in msg else data} ) + + +@websocket_api.websocket_command( + {vol.Required("type"): "frontend/subscribe_user_data", vol.Optional("key"): str} +) +@websocket_api.async_response +@with_user_store +async def websocket_subscribe_user_data( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], + store: UserStore, +) -> None: + """Handle subscribe to user data command.""" + key: str | None = msg.get("key") + + def on_data_update() -> None: + """Handle user data update.""" + data = store.data + connection.send_event( + msg["id"], {"value": data.get(key) if key is not None else data} + ) + + connection.subscriptions[msg["id"]] = store.async_subscribe(key, on_data_update) + on_data_update() + connection.send_result(msg["id"]) diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py index 360ca151551..f4a61b743c5 100644 --- a/tests/components/frontend/test_storage.py +++ b/tests/components/frontend/test_storage.py @@ -79,12 +79,46 @@ async def test_get_user_data( assert res["result"]["value"]["test-complex"][0]["foo"] == "bar" +@pytest.mark.parametrize( + ("subscriptions", "events"), + [ + ([], []), + ([(1, {}, {})], [(1, {"test-key": "test-value"})]), + ([(1, {"key": "test-key"}, None)], [(1, "test-value")]), + ([(1, {"key": "other-key"}, None)], []), + ], +) async def test_set_user_data_empty( - hass: HomeAssistant, hass_ws_client: WebSocketGenerator + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + subscriptions: list[tuple[int, dict[str, str], Any]], + events: list[tuple[int, Any]], ) -> None: - """Test set_user_data command.""" + """Test set_user_data command. + + Also test subscribing. + """ client = await hass_ws_client(hass) + for msg_id, key, event_data in subscriptions: + await client.send_json( + { + "id": msg_id, + "type": "frontend/subscribe_user_data", + } + | key + ) + + event = await client.receive_json() + assert event == { + "id": msg_id, + "type": "event", + "event": {"value": event_data}, + } + + res = await client.receive_json() + assert res["success"], res + # test creating await client.send_json( @@ -104,6 +138,10 @@ async def test_set_user_data_empty( } ) + for msg_id, event_data in events: + event = await client.receive_json() + assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}} + res = await client.receive_json() assert res["success"], res @@ -116,11 +154,63 @@ async def test_set_user_data_empty( assert res["result"]["value"] == "test-value" +@pytest.mark.parametrize( + ("subscriptions", "events"), + [ + ( + [], + [[], []], + ), + ( + [(1, {}, {"test-key": "test-value", "test-complex": "string"})], + [ + [ + ( + 1, + { + "test-complex": "string", + "test-key": "test-value", + "test-non-existent-key": "test-value-new", + }, + ) + ], + [ + ( + 1, + { + "test-complex": [{"foo": "bar"}], + "test-key": "test-value", + "test-non-existent-key": "test-value-new", + }, + ) + ], + ], + ), + ( + [(1, {"key": "test-key"}, "test-value")], + [[], []], + ), + ( + [(1, {"key": "test-non-existent-key"}, None)], + [[(1, "test-value-new")], []], + ), + ( + [(1, {"key": "test-complex"}, "string")], + [[], [(1, [{"foo": "bar"}])]], + ), + ( + [(1, {"key": "other-key"}, None)], + [[], []], + ), + ], +) async def test_set_user_data( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, hass_storage: dict[str, Any], hass_admin_user: MockUser, + subscriptions: list[tuple[int, dict[str, str], Any]], + events: list[list[tuple[int, Any]]], ) -> None: """Test set_user_data command with initial data.""" storage_key = f"{DOMAIN}.user_data_{hass_admin_user.id}" @@ -131,6 +221,25 @@ async def test_set_user_data( client = await hass_ws_client(hass) + for msg_id, key, event_data in subscriptions: + await client.send_json( + { + "id": msg_id, + "type": "frontend/subscribe_user_data", + } + | key + ) + + event = await client.receive_json() + assert event == { + "id": msg_id, + "type": "event", + "event": {"value": event_data}, + } + + res = await client.receive_json() + assert res["success"], res + # test creating await client.send_json( @@ -142,6 +251,10 @@ async def test_set_user_data( } ) + for msg_id, event_data in events[0]: + event = await client.receive_json() + assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}} + res = await client.receive_json() assert res["success"], res @@ -164,6 +277,10 @@ async def test_set_user_data( } ) + for msg_id, event_data in events[1]: + event = await client.receive_json() + assert event == {"id": msg_id, "type": "event", "event": {"value": event_data}} + res = await client.receive_json() assert res["success"], res From 38674f0dc2967e0e43819fc1332d02ec84dd5c73 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 12 May 2025 16:47:14 +0200 Subject: [PATCH 0379/1175] Add missing hyphen to "password-protected" in `Shelly` (#144746) --- homeassistant/components/shelly/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index bc6f44a971b..28f3a993462 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -34,7 +34,7 @@ } }, "confirm_discovery": { - "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device." + "description": "Do you want to set up the {model} at {host}?\n\nBattery-powered devices that are password-protected must be woken up before continuing with setting up.\nBattery-powered devices that are not password-protected will be added when the device wakes up, you can now manually wake the device up using a button on it or wait for the next data update from the device." }, "reconfigure": { "description": "Update configuration for {device_name}.\n\nBefore setup, battery-powered devices must be woken up, you can now wake the device up using a button on it.", From d471de5645b8bf02d500dbddb6b9120578c15c55 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 12 May 2025 16:54:22 +0200 Subject: [PATCH 0380/1175] Spelling fixes in user-facing strings of `fronius` (#144744) --- homeassistant/components/fronius/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index ef55c51cb14..9cd3b7c8a54 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -107,7 +107,7 @@ "ac_module_temperature_sensor_faulty_l2": "AC module temperature sensor faulty (L2)", "dc_component_measured_in_grid_too_high": "DC component measured in the grid too high", "fixed_voltage_mode_out_of_range": "Fixed voltage mode has been selected instead of MPP voltage mode and the fixed voltage has been set to too low or too high a value", - "safety_cut_out_triggered": "Safety cut out via option card or RECERBO has triggered", + "safety_cut_out_triggered": "Safety cut-out via option card or RECERBO has triggered", "no_communication_between_power_stage_and_control_system": "No communication possible between power stage set and control system", "hardware_id_problem": "Hardware ID problem", "unique_id_conflict": "Unique ID conflict", @@ -148,7 +148,7 @@ "update_file_does_not_match_device": "Update file does not match the device, update file too old", "write_or_read_error_occurred": "Write or read error occurred", "file_could_not_be_opened": "File could not be opened", - "log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write protected or full)", + "log_file_cannot_be_saved": "Log file cannot be saved (e.g. USB flash drive is write-protected or full)", "initialisation_error_file_system_error_on_usb": "Initialization error in file system on USB flash drive", "error_during_logging_data_recording": "Error during recording of logging data", "error_during_update_process": "Error occurred during update process", @@ -166,7 +166,7 @@ "invalid_device_type": "Invalid device type", "insulation_measurement_triggered": "Insulation measurement triggered", "inverter_settings_changed_restart_required": "Inverter settings have been changed, inverter restart required", - "wired_shut_down_triggered": "Wired shut down triggered", + "wired_shut_down_triggered": "Wired shutdown triggered", "grid_frequency_exceeded_limit_reconnecting": "The grid frequency has exceeded a limit value when reconnecting", "mains_voltage_dependent_power_reduction": "Mains voltage-dependent power reduction", "too_little_dc_power_for_feed_in_operation": "Too little DC power for feed-in operation", From 2266e9741773d2338f1eb523e48b613b86698f5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 May 2025 12:15:05 -0400 Subject: [PATCH 0381/1175] Add a test for Assist Pipeline streaming deltas to TTS (#144711) * Add a test for Assist Pipeline streaming deltas to TTS * Adjust tests to new TTS engine --- tests/components/assist_pipeline/conftest.py | 23 ++- .../assist_pipeline/snapshots/test_init.ambr | 16 +- .../snapshots/test_pipeline.ambr | 154 ++++++++++++++++ .../snapshots/test_websocket.ambr | 32 ++-- .../assist_pipeline/test_pipeline.py | 173 ++++++++++++++++-- .../assist_pipeline/test_websocket.py | 18 +- 6 files changed, 370 insertions(+), 46 deletions(-) diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index a0549f27f05..e20452a1f93 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -37,7 +37,7 @@ from tests.common import ( mock_platform, ) from tests.components.stt.common import MockSTTProvider, MockSTTProviderEntity -from tests.components.tts.common import MockTTSProvider +from tests.components.tts.common import MockTTSEntity, MockTTSProvider _TRANSCRIPT = "test transcript" @@ -68,6 +68,15 @@ async def mock_tts_provider() -> MockTTSProvider: return provider +@pytest.fixture +def mock_tts_entity() -> MockTTSEntity: + """Test TTS entity.""" + entity = MockTTSEntity("en") + entity._attr_unique_id = "test_tts" + entity._attr_supported_languages = ["en-US"] + return entity + + @pytest.fixture async def mock_stt_provider() -> MockSTTProvider: """Mock STT provider.""" @@ -198,6 +207,7 @@ async def init_supporting_components( mock_stt_provider: MockSTTProvider, mock_stt_provider_entity: MockSTTProviderEntity, mock_tts_provider: MockTTSProvider, + mock_tts_entity: MockTTSEntity, mock_wake_word_provider_entity: MockWakeWordEntity, mock_wake_word_provider_entity2: MockWakeWordEntity2, config_flow_fixture, @@ -209,7 +219,7 @@ async def init_supporting_components( ) -> bool: """Set up test config entry.""" await hass.config_entries.async_forward_entry_setups( - config_entry, [Platform.STT, Platform.WAKE_WORD] + config_entry, [Platform.STT, Platform.TTS, Platform.WAKE_WORD] ) return True @@ -230,6 +240,14 @@ async def init_supporting_components( """Set up test stt platform via config entry.""" async_add_entities([mock_stt_provider_entity]) + async def async_setup_entry_tts_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + ) -> None: + """Set up test tts platform via config entry.""" + async_add_entities([mock_tts_entity]) + async def async_setup_entry_wake_word_platform( hass: HomeAssistant, config_entry: ConfigEntry, @@ -253,6 +271,7 @@ async def init_supporting_components( "test.tts", MockTTSPlatform( async_get_engine=AsyncMock(return_value=mock_tts_provider), + async_setup_entry=async_setup_entry_tts_platform, ), ) mock_platform( diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 81972191868..816430f58d0 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -74,17 +74,17 @@ }), dict({ 'data': dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }), 'type': , }), dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -395,17 +395,17 @@ }), dict({ 'data': dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }), 'type': , }), dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index 7c0ac254b6e..717823fe4e4 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -1,4 +1,158 @@ # serializer version: 1 +# name: test_chat_log_tts_streaming[to_stream_tts0] + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + 'tts_output': dict({ + 'mime_type': 'audio/mpeg', + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'test-agent', + 'intent_input': 'Set a timer', + 'language': 'en', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'role': 'assistant', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'hello,', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': ' ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'how', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': ' ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'are', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': ' ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '?', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': True, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': 'hello, how are you?', + }), + }), + }), + }), + 'processed_locally': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'tts.test', + 'language': 'en_US', + 'tts_input': 'hello, how are you?', + 'voice': None, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'tts_output': dict({ + 'media_id': 'media-source://tts/tts.test?message=hello,+how+are+you?&language=en_US&tts_options=%7B%7D', + 'mime_type': 'audio/mpeg', + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- # name: test_pipeline_language_used_instead_of_conversation_language list([ dict({ diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 57ae0095236..41bdba9f3cd 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -71,16 +71,16 @@ # --- # name: test_audio_pipeline.5 dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }) # --- # name: test_audio_pipeline.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -162,16 +162,16 @@ # --- # name: test_audio_pipeline_debug.5 dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }) # --- # name: test_audio_pipeline_debug.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -265,16 +265,16 @@ # --- # name: test_audio_pipeline_with_enhancements.5 dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }) # --- # name: test_audio_pipeline_with_enhancements.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -378,16 +378,16 @@ # --- # name: test_audio_pipeline_with_wake_word_no_timeout.7 dict({ - 'engine': 'test', - 'language': 'en-US', + 'engine': 'tts.test', + 'language': 'en_US', 'tts_input': "Sorry, I couldn't understand that", - 'voice': 'james_earl_jones', + 'voice': None, }) # --- # name: test_audio_pipeline_with_wake_word_no_timeout.8 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", + 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 4f15853b296..e318862a2f2 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -40,6 +40,7 @@ from . import MANY_LANGUAGES, process_events from .conftest import ( MockSTTProvider, MockSTTProviderEntity, + MockTTSEntity, MockTTSProvider, MockWakeWordEntity, make_10ms_chunk, @@ -62,6 +63,12 @@ async def load_homeassistant(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "homeassistant", {}) +@pytest.fixture +async def disable_tts_entity(mock_tts_entity: tts.TextToSpeechEntity) -> None: + """Disable the TTS entity.""" + mock_tts_entity._attr_entity_registry_enabled_default = False + + @pytest.mark.usefixtures("init_components") async def test_load_pipelines(hass: HomeAssistant) -> None: """Make sure that we can load/save data correctly.""" @@ -283,6 +290,7 @@ async def test_migrate_pipeline_store( @pytest.mark.usefixtures("init_supporting_components") +@pytest.mark.usefixtures("disable_tts_entity") async def test_create_default_pipeline(hass: HomeAssistant) -> None: """Test async_create_default_pipeline.""" assert await async_setup_component(hass, "assist_pipeline", {}) @@ -430,6 +438,7 @@ async def test_default_pipeline_no_stt_tts( ], ) @pytest.mark.usefixtures("init_supporting_components") +@pytest.mark.usefixtures("disable_tts_entity") async def test_default_pipeline( hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity, @@ -474,6 +483,7 @@ async def test_default_pipeline( @pytest.mark.usefixtures("init_supporting_components") +@pytest.mark.usefixtures("disable_tts_entity") async def test_default_pipeline_unsupported_stt_language( hass: HomeAssistant, mock_stt_provider_entity: MockSTTProviderEntity ) -> None: @@ -504,6 +514,7 @@ async def test_default_pipeline_unsupported_stt_language( @pytest.mark.usefixtures("init_supporting_components") +@pytest.mark.usefixtures("disable_tts_entity") async def test_default_pipeline_unsupported_tts_language( hass: HomeAssistant, mock_tts_provider: MockTTSProvider ) -> None: @@ -825,7 +836,7 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: async def test_tts_audio_output( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, + mock_tts_entity: MockTTSProvider, init_components, pipeline_data: assist_pipeline.pipeline.PipelineData, mock_chat_session: chat_session.ChatSession, @@ -869,7 +880,7 @@ async def test_tts_audio_output( == 1 ) - with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio: + with patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio: await pipeline_input.execute() for event in events: @@ -881,14 +892,14 @@ async def test_tts_audio_output( # 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) + extra_options = set(options).difference(mock_tts_entity.supported_options) assert len(extra_options) == 0, extra_options async def test_tts_wav_preferred_format( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, + mock_tts_entity: MockTTSEntity, init_components, mock_chat_session: chat_session.ChatSession, pipeline_data: assist_pipeline.pipeline.PipelineData, @@ -920,7 +931,7 @@ async def test_tts_wav_preferred_format( await pipeline_input.validate() # Make the TTS provider support preferred format options - supported_options = list(mock_tts_provider.supported_options or []) + supported_options = list(mock_tts_entity.supported_options or []) supported_options.extend( [ tts.ATTR_PREFERRED_FORMAT, @@ -931,8 +942,8 @@ async def test_tts_wav_preferred_format( ) with ( - patch.object(mock_tts_provider, "_supported_options", supported_options), - patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio, + patch.object(mock_tts_entity, "_supported_options", supported_options), + patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio, ): await pipeline_input.execute() @@ -955,7 +966,7 @@ async def test_tts_wav_preferred_format( async def test_tts_dict_preferred_format( hass: HomeAssistant, hass_client: ClientSessionGenerator, - mock_tts_provider: MockTTSProvider, + mock_tts_entity: MockTTSEntity, init_components, mock_chat_session: chat_session.ChatSession, pipeline_data: assist_pipeline.pipeline.PipelineData, @@ -992,7 +1003,7 @@ async def test_tts_dict_preferred_format( await pipeline_input.validate() # Make the TTS provider support preferred format options - supported_options = list(mock_tts_provider.supported_options or []) + supported_options = list(mock_tts_entity.supported_options or []) supported_options.extend( [ tts.ATTR_PREFERRED_FORMAT, @@ -1003,8 +1014,8 @@ async def test_tts_dict_preferred_format( ) with ( - patch.object(mock_tts_provider, "_supported_options", supported_options), - patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio, + patch.object(mock_tts_entity, "_supported_options", supported_options), + patch.object(mock_tts_entity, "get_tts_audio") as mock_get_tts_audio, ): await pipeline_input.execute() @@ -1545,3 +1556,143 @@ async def test_pipeline_language_used_instead_of_conversation_language( mock_async_converse.call_args_list[0].kwargs.get("language") == pipeline.language ) + + +@pytest.mark.parametrize( + "to_stream_tts", + [ + [ + "hello,", + " ", + "how", + " ", + "are", + " ", + "you", + "?", + ] + ], +) +async def test_chat_log_tts_streaming( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_components, + mock_chat_session: chat_session.ChatSession, + snapshot: SnapshotAssertion, + mock_tts_entity: MockTTSEntity, + pipeline_data: assist_pipeline.pipeline.PipelineData, + to_stream_tts: list[str], +) -> None: + """Test that chat log events are streamed to the TTS entity.""" + 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) + await assist_pipeline.pipeline.async_update_pipeline( + hass, pipeline, conversation_engine="test-agent" + ) + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + intent_input="Set a timer", + session=mock_chat_session, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.INTENT, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + ), + ) + + received_tts = [] + + async def async_stream_tts_audio( + request: tts.TTSAudioRequest, + ) -> tts.TTSAudioResponse: + """Mock stream TTS audio.""" + + async def gen_data(): + async for msg in request.message_gen: + received_tts.append(msg) + yield msg.encode() + + return tts.TTSAudioResponse( + extension="mp3", + data_gen=gen_data(), + ) + + mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", + return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + ): + await pipeline_input.validate() + + async def mock_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, + extra_system_prompt: str | None = None, + ): + """Mock converse.""" + conversation_input = conversation.ConversationInput( + text=text, + context=context, + conversation_id=conversation_id, + device_id=device_id, + language=language, + agent_id=agent_id, + extra_system_prompt=extra_system_prompt, + ) + + async def stream_llm_response(): + yield {"role": "assistant"} + for chunk in to_stream_tts: + yield {"content": chunk} + + with ( + chat_session.async_get_chat_session(hass, conversation_id) as session, + conversation.async_get_chat_log( + hass, + session, + conversation_input, + ) as chat_log, + ): + async for _content in chat_log.async_add_delta_content_stream( + agent_id, stream_llm_response() + ): + pass + intent_response = intent.IntentResponse(language) + intent_response.async_set_speech("".join(to_stream_tts)) + return conversation.ConversationResult( + response=intent_response, + conversation_id=chat_log.conversation_id, + continue_conversation=chat_log.continue_conversation, + ) + + with patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + mock_converse, + ): + await pipeline_input.execute() + + stream = tts.async_get_stream(hass, events[0].data["tts_output"]["token"]) + assert stream is not None + tts_result = "".join( + [chunk.decode() async for chunk in stream.async_stream_result()] + ) + + streamed_text = "".join(to_stream_tts) + assert tts_result == streamed_text + assert len(received_tts) == 1 + assert "".join(received_tts) == streamed_text + + assert process_events(events) == snapshot diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index 060c0dce660..bf9818f2a5f 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -1153,9 +1153,9 @@ async def test_get_pipeline( "name": "Home Assistant", "stt_engine": "stt.mock_stt", "stt_language": "en-US", - "tts_engine": "test", - "tts_language": "en-US", - "tts_voice": "james_earl_jones", + "tts_engine": "tts.test", + "tts_language": "en_US", + "tts_voice": None, "wake_word_entity": None, "wake_word_id": None, "prefer_local_intents": False, @@ -1179,9 +1179,9 @@ async def test_get_pipeline( # It found these defaults "stt_engine": "stt.mock_stt", "stt_language": "en-US", - "tts_engine": "test", - "tts_language": "en-US", - "tts_voice": "james_earl_jones", + "tts_engine": "tts.test", + "tts_language": "en_US", + "tts_voice": None, "wake_word_entity": None, "wake_word_id": None, "prefer_local_intents": False, @@ -1266,9 +1266,9 @@ async def test_list_pipelines( "name": "Home Assistant", "stt_engine": "stt.mock_stt", "stt_language": "en-US", - "tts_engine": "test", - "tts_language": "en-US", - "tts_voice": "james_earl_jones", + "tts_engine": "tts.test", + "tts_language": "en_US", + "tts_voice": None, "wake_word_entity": None, "wake_word_id": None, "prefer_local_intents": False, From da0d65ca5bda76d373499b674b87ee5574a035bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 12 May 2025 18:59:38 +0200 Subject: [PATCH 0382/1175] Log instead of ValueError for missing cloud translation key (#144732) * Log instead of ValueError for missing translation key * Update homeassistant/components/cloud/client.py --- homeassistant/components/cloud/client.py | 7 ++++++- tests/components/cloud/test_client.py | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 916bac4f73d..d413a447bd7 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -404,7 +404,12 @@ class CloudClient(Interface): ) -> None: """Create a repair issue.""" if translation_key not in VALID_REPAIR_TRANSLATION_KEYS: - raise ValueError(f"Invalid translation key {translation_key}") + _LOGGER.error( + "Invalid translation key %s for repair issue %s", + translation_key, + identifier, + ) + return async_create_issue( hass=self._hass, domain=DOMAIN, diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 14fcbbd5e5b..283e2ff39f1 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -482,19 +482,20 @@ async def test_async_create_repair_issue_unknown( cloud: MagicMock, mock_cloud_setup: None, issue_registry: ir.IssueRegistry, + caplog: pytest.LogCaptureFixture, ) -> None: """Test not creating repair issue for unknown repairs.""" identifier = "abc123" - with pytest.raises( - ValueError, - match="Invalid translation key unknown_translation_key", - ): - await cloud.client.async_create_repair_issue( - identifier=identifier, - translation_key="unknown_translation_key", - placeholders={"custom_domains": "example.com"}, - severity="error", - ) + await cloud.client.async_create_repair_issue( + identifier=identifier, + translation_key="unknown_translation_key", + placeholders={"custom_domains": "example.com"}, + severity="error", + ) + assert ( + "Invalid translation key unknown_translation_key for repair issue abc123" + in caplog.text + ) issue = issue_registry.async_get_issue(domain=DOMAIN, issue_id=identifier) assert issue is None From a6ff52b300314f25c639c40106615b93152488aa Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 12 May 2025 19:12:49 +0200 Subject: [PATCH 0383/1175] Fix outdated help center URL in `plaato` (#144748) * Fix outdated help center URL in `plaato` * Remove excessive space character --- homeassistant/components/plaato/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index 452875fc71d..e1ac078aff1 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -11,7 +11,7 @@ }, "api_method": { "title": "Select API method", - "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Selected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", + "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://intercom.help/plaato/en/articles/5004720-auth_token)\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", "data": { "use_webhook": "Use webhook", "token": "Auth token" From 00faadcfea1568a9f1a996937c3d5291cb012868 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 12 May 2025 19:36:53 +0200 Subject: [PATCH 0384/1175] Improve config flow description in ntfy integration (#144581) --- homeassistant/components/ntfy/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ntfy/strings.json b/homeassistant/components/ntfy/strings.json index 5b2e93bc97b..13704d960be 100644 --- a/homeassistant/components/ntfy/strings.json +++ b/homeassistant/components/ntfy/strings.json @@ -18,7 +18,7 @@ "sections": { "auth": { "name": "Authentication", - "description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password.", + "description": "Depending on whether the server is configured to support access control, some topics may be read/write protected so that only users with the correct credentials can subscribe or publish to them. To publish/subscribe to protected topics, you can provide a username and password. Home Assistant will automatically generate an access token to authenticate with ntfy.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" From d2ef3ca1004e80370f5f05a69b5484899de233c4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 12 May 2025 19:37:45 +0200 Subject: [PATCH 0385/1175] Fill in Plaato URL via placeholders (#144754) --- homeassistant/components/plaato/config_flow.py | 7 ++++++- homeassistant/components/plaato/strings.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 9adfb4a14fe..6a05b209f2c 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -32,6 +32,8 @@ from .const import ( PLACEHOLDER_WEBHOOK_URL, ) +AUTH_TOKEN_URL = "https://intercom.help/plaato/en/articles/5004720-auth_token" + class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" @@ -153,7 +155,10 @@ class PlaatoConfigFlow(ConfigFlow, domain=DOMAIN): step_id="api_method", data_schema=data_schema, errors=errors, - description_placeholders={PLACEHOLDER_DEVICE_TYPE: device_type.name}, + description_placeholders={ + PLACEHOLDER_DEVICE_TYPE: device_type.name, + "auth_token_url": AUTH_TOKEN_URL, + }, ) async def _get_webhook_id(self): diff --git a/homeassistant/components/plaato/strings.json b/homeassistant/components/plaato/strings.json index e1ac078aff1..66c5d18e0e7 100644 --- a/homeassistant/components/plaato/strings.json +++ b/homeassistant/components/plaato/strings.json @@ -11,7 +11,7 @@ }, "api_method": { "title": "Select API method", - "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions](https://intercom.help/plaato/en/articles/5004720-auth_token)\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", + "description": "To be able to query the API an 'auth token' is required which can be obtained by following [these instructions]({auth_token_url})\n\nSelected device: **{device_type}** \n\nIf you prefer to use the built-in webhook method (Airlock only) please check the box below and leave 'Auth token' blank", "data": { "use_webhook": "Use webhook", "token": "Auth token" From c022c32d2ff38569a64a63a30922386f07f6e853 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Mon, 12 May 2025 19:44:24 +0200 Subject: [PATCH 0386/1175] Simplify unique config_entry check for LCN (#135756) * Simplify check for unique config_entry * Fix tests * Fix reconfigure flow * Add check for unchanging IP/port combination * Remove explicit check for unchanged IP/port combination --- homeassistant/components/lcn/config_flow.py | 45 +++++++++------------ homeassistant/components/lcn/strings.json | 6 +-- tests/components/lcn/test_config_flow.py | 4 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 946c7ac3724..62a9920fb73 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -19,7 +19,6 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -44,21 +43,6 @@ CONFIG_SCHEMA = vol.Schema(CONFIG_DATA) USER_SCHEMA = vol.Schema(USER_DATA) -def get_config_entry( - hass: HomeAssistant, data: ConfigType -) -> config_entries.ConfigEntry | None: - """Check config entries for already configured entries based on the ip address/port.""" - return next( - ( - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.data[CONF_IP_ADDRESS] == data[CONF_IP_ADDRESS] - and entry.data[CONF_PORT] == data[CONF_PORT] - ), - None, - ) - - async def validate_connection(data: ConfigType) -> str | None: """Validate if a connection to LCN can be established.""" error = None @@ -120,19 +104,20 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="user", data_schema=USER_SCHEMA) - errors = None - if get_config_entry(self.hass, user_input): - errors = {CONF_BASE: "already_configured"} - elif (error := await validate_connection(user_input)) is not None: - errors = {CONF_BASE: error} + self._async_abort_entries_match( + { + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PORT: user_input[CONF_PORT], + } + ) - if errors is not None: + if (error := await validate_connection(user_input)) is not None: return self.async_show_form( step_id="user", data_schema=self.add_suggested_values_to_schema( USER_SCHEMA, user_input ), - errors=errors, + errors={CONF_BASE: error}, ) data: dict = { @@ -152,15 +137,21 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: user_input[CONF_HOST] = reconfigure_entry.data[CONF_HOST] - await self.hass.config_entries.async_unload(reconfigure_entry.entry_id) - if (error := await validate_connection(user_input)) is not None: - errors = {CONF_BASE: error} + self._async_abort_entries_match( + { + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PORT: user_input[CONF_PORT], + } + ) - if errors is None: + await self.hass.config_entries.async_unload(reconfigure_entry.entry_id) + + if (error := await validate_connection(user_input)) is None: return self.async_update_reload_and_abort( reconfigure_entry, data_updates=user_input ) + errors = {CONF_BASE: error} await self.hass.config_entries.async_setup(reconfigure_entry.entry_id) return self.async_show_form( diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 4295ceb384d..9d806bce104 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -66,11 +66,11 @@ "error": { "authentication_error": "Authentication failed. Wrong username or password.", "license_error": "Maximum number of connections was reached. An additional licence key is required.", - "connection_refused": "Unable to connect to PCHK. Check IP and port.", - "already_configured": "PCHK connection using the same ip address/port is already configured." + "connection_refused": "Unable to connect to PCHK. Check IP and port." }, "abort": { - "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "already_configured": "PCHK connection using the same ip address/port is already configured." } }, "issues": { diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 478f2c0949e..ef99a19dee4 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -94,8 +94,8 @@ async def test_step_user_existing_host( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_data ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_BASE: "already_configured"} + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" @pytest.mark.parametrize( From 4994229215979f6a150fc3cdc2950e0f305a77b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 May 2025 13:44:39 -0400 Subject: [PATCH 0387/1175] Track if TTS entity supports streaming input (#144697) * Track if entity supports streaming * Make class method --- homeassistant/components/tts/__init__.py | 16 ++++++++++-- homeassistant/components/tts/entity.py | 7 +++++ tests/components/tts/common.py | 1 + tests/components/tts/test_entity.py | 33 ++++++++++++++++++++++++ tests/components/tts/test_init.py | 5 +++- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index b279af31803..526be21ad76 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -469,6 +469,7 @@ class ResultStream: use_file_cache: bool language: str options: dict + supports_streaming_input: bool _manager: SpeechManager @@ -484,7 +485,10 @@ class ResultStream: @callback def async_set_message(self, message: str) -> None: - """Set message to be generated.""" + """Set message to be generated. + + This method will leverage a disk cache to speed up generation. + """ self._result_cache.set_result( self._manager.async_cache_message_in_memory( engine=self.engine, @@ -497,7 +501,10 @@ class ResultStream: @callback def async_set_message_stream(self, message_stream: AsyncGenerator[str]) -> None: - """Set a stream that will generate the message.""" + """Set a stream that will generate the message. + + This method can result in faster first byte when generating long responses. + """ self._result_cache.set_result( self._manager.async_cache_message_stream_in_memory( engine=self.engine, @@ -726,6 +733,10 @@ class SpeechManager: if (engine_instance := get_engine_instance(self.hass, engine)) is None: raise HomeAssistantError(f"Provider {engine} not found") + supports_streaming_input = ( + isinstance(engine_instance, TextToSpeechEntity) + and engine_instance.async_supports_streaming_input() + ) language, options = self.process_options(engine_instance, language, options) if use_file_cache is None: use_file_cache = self.use_file_cache @@ -741,6 +752,7 @@ class SpeechManager: engine=engine, language=language, options=options, + supports_streaming_input=supports_streaming_input, _manager=self, ) self.token_to_stream[token] = result_stream diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index 199d673398e..1f01a41c5ab 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -89,6 +89,13 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH """Return a mapping with the default options.""" return self._attr_default_options + @classmethod + def async_supports_streaming_input(cls) -> bool: + """Return if the TTS engine supports streaming input.""" + return ( + cls.async_stream_tts_audio is not TextToSpeechEntity.async_stream_tts_audio + ) + @callback def async_get_supported_voices(self, language: str) -> list[Voice] | None: """Return a list of supported voices for a language.""" diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index c21db66dfac..171334c136a 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -281,6 +281,7 @@ class MockResultStream(ResultStream): content_type=f"audio/mock-{extension}", engine="test-engine", use_file_cache=True, + supports_streaming_input=True, language="en", options={}, _manager=hass.data[DATA_TTS_MANAGER], diff --git a/tests/components/tts/test_entity.py b/tests/components/tts/test_entity.py index d82ec6a5d2b..8648ca95e93 100644 --- a/tests/components/tts/test_entity.py +++ b/tests/components/tts/test_entity.py @@ -1,5 +1,7 @@ """Tests for the TTS entity.""" +from typing import Any + import pytest from homeassistant.components import tts @@ -142,3 +144,34 @@ async def test_tts_entity_subclass_properties( if record.exc_info is not None ] ) + + +def test_streaming_supported() -> None: + """Test streaming support.""" + base_entity = tts.TextToSpeechEntity() + assert base_entity.async_supports_streaming_input() is False + + class StreamingEntity(tts.TextToSpeechEntity): + async def async_stream_tts_audio(self) -> None: + pass + + streaming_entity = StreamingEntity() + assert streaming_entity.async_supports_streaming_input() is True + + class NonStreamingEntity(tts.TextToSpeechEntity): + async def async_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> tts.TtsAudioType: + pass + + non_streaming_entity = NonStreamingEntity() + assert non_streaming_entity.async_supports_streaming_input() is False + + class SyncNonStreamingEntity(tts.TextToSpeechEntity): + def get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> tts.TtsAudioType: + pass + + sync_non_streaming_entity = SyncNonStreamingEntity() + assert sync_non_streaming_entity.async_supports_streaming_input() is False diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index ea281506f3a..ccb62959eba 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -4,7 +4,7 @@ import asyncio from http import HTTPStatus from pathlib import Path from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -1885,6 +1885,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No stream = tts.async_create_stream(hass, mock_tts_entity.entity_id) assert stream.language == mock_tts_entity.default_language assert stream.options == (mock_tts_entity.default_options or {}) + assert stream.supports_streaming_input is False assert tts.async_get_stream(hass, stream.token) is stream stream.async_set_message("beer") result_data = b"".join([chunk async for chunk in stream.async_stream_result()]) @@ -1905,6 +1906,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No ) mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio + mock_tts_entity.async_supports_streaming_input = Mock(return_value=True) async def stream_message(): """Mock stream message.""" @@ -1913,6 +1915,7 @@ async def test_stream(hass: HomeAssistant, mock_tts_entity: MockTTSEntity) -> No yield "o" stream = tts.async_create_stream(hass, mock_tts_entity.entity_id) + assert stream.supports_streaming_input is True stream.async_set_message_stream(stream_message()) result_data = b"".join([chunk async for chunk in stream.async_stream_result()]) assert result_data == b"hello" From 158b795c70e4e6915c9a41716630b66a7daf57a2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 12 May 2025 19:45:02 +0200 Subject: [PATCH 0388/1175] Update xknx to 3.8.0 (#144753) --- 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 9c3ac0c12d9..36c4bc71273 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -10,7 +10,7 @@ "iot_class": "local_push", "loggers": ["xknx", "xknxproject"], "requirements": [ - "xknx==3.6.0", + "xknx==3.8.0", "xknxproject==3.8.2", "knx-frontend==2025.4.1.91934" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5dbaf454548..c4b3460ddc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3101,7 +3101,7 @@ xbox-webapi==2.1.0 xiaomi-ble==0.38.0 # homeassistant.components.knx -xknx==3.6.0 +xknx==3.8.0 # homeassistant.components.knx xknxproject==3.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3d76e8833d..13e81d72e62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2509,7 +2509,7 @@ xbox-webapi==2.1.0 xiaomi-ble==0.38.0 # homeassistant.components.knx -xknx==3.6.0 +xknx==3.8.0 # homeassistant.components.knx xknxproject==3.8.2 From 1d0584a90d3b5b274d117820befa6f25acf241e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Mon, 12 May 2025 19:45:34 +0200 Subject: [PATCH 0389/1175] Bump gcal-sync to 7.0.1 (#144718) Co-authored-by: Allen Porter --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 296ac519e1d..b43ded01d6e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.2"] + "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c4b3460ddc0..9c1d08febf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -983,7 +983,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.0 +gcal-sync==7.0.1 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13e81d72e62..7d2b09d68f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -837,7 +837,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.0 +gcal-sync==7.0.1 # homeassistant.components.geniushub geniushub-client==0.7.1 From b5445c00615d68f887b55b40361683d617618292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 12 May 2025 19:48:20 +0200 Subject: [PATCH 0390/1175] Allow subscription_expired repair issue in cloud (#144316) Co-authored-by: Martin Hjelmare --- homeassistant/components/cloud/client.py | 1 + homeassistant/components/cloud/strings.json | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index d413a447bd7..a857185f07f 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -43,6 +43,7 @@ VALID_REPAIR_TRANSLATION_KEYS = { "no_subscription", "warn_bad_custom_domain_configuration", "reset_bad_custom_domain_configuration", + "subscription_expired", } diff --git a/homeassistant/components/cloud/strings.json b/homeassistant/components/cloud/strings.json index 2dc2acc82d0..e7d219ff69e 100644 --- a/homeassistant/components/cloud/strings.json +++ b/homeassistant/components/cloud/strings.json @@ -73,6 +73,10 @@ "reset_bad_custom_domain_configuration": { "title": "Custom domain ignored", "description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. This domain has now been ignored and will not be used for Home Assistant Cloud. If you want to use this domain, please fix the DNS configuration and restart Home Assistant. If you do not need this anymore, you can remove it from the account page." + }, + "subscription_expired": { + "title": "Subscription has expired", + "description": "Your Home Assistant Cloud subscription has expired. Resubscribe at {account_url}." } }, "services": { From 15a4514c7dd595660337c034309648c14281dd50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Mon, 12 May 2025 21:11:12 +0200 Subject: [PATCH 0391/1175] Add MAC connection through DHCP discovery to Home Connect devices (#144611) * Add MAC connection through DHCP discovery to Home Connect devices * Update snapshots --- .../components/home_connect/config_flow.py | 22 ++++- .../home_connect/fixtures/appliances.json | 4 +- .../snapshots/test_diagnostics.ambr | 80 +++++++++---------- .../home_connect/test_config_flow.py | 67 ++++++++++++++-- 4 files changed, 123 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py index 2b3b2aacf0c..9c7da4d98df 100644 --- a/homeassistant/components/home_connect/config_flow.py +++ b/homeassistant/components/home_connect/config_flow.py @@ -8,7 +8,8 @@ import jwt import voluptuous as vol from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import DOMAIN @@ -58,3 +59,22 @@ class OAuth2FlowHandler( ) self._abort_if_unique_id_configured() return await super().async_oauth_create_entry(data) + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle a DHCP discovery.""" + device_registry = dr.async_get(self.hass) + if device_entry := device_registry.async_get_device( + identifiers={ + (DOMAIN, discovery_info.hostname), + (DOMAIN, discovery_info.hostname.split("-")[-1]), + } + ): + device_registry.async_update_device( + device_entry.id, + new_connections={ + (dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress) + }, + ) + return await super().async_step_dhcp(discovery_info) diff --git a/tests/components/home_connect/fixtures/appliances.json b/tests/components/home_connect/fixtures/appliances.json index 081dd44764f..3d2e236b28c 100644 --- a/tests/components/home_connect/fixtures/appliances.json +++ b/tests/components/home_connect/fixtures/appliances.json @@ -97,7 +97,7 @@ "connected": true, "type": "Hob", "enumber": "HCS000000/05", - "haId": "BOSCH-HCS000000-D00000000005" + "haId": "BOSCH-HCS000000-68A40E000000" }, { "name": "CookProcessor", @@ -106,7 +106,7 @@ "connected": true, "type": "CookProcessor", "enumber": "HCS000000/06", - "haId": "BOSCH-HCS000000-D00000000006" + "haId": "123456789012345678" }, { "name": "DNE", diff --git a/tests/components/home_connect/snapshots/test_diagnostics.ambr b/tests/components/home_connect/snapshots/test_diagnostics.ambr index 535119b941c..e18489d5220 100644 --- a/tests/components/home_connect/snapshots/test_diagnostics.ambr +++ b/tests/components/home_connect/snapshots/test_diagnostics.ambr @@ -1,6 +1,26 @@ # serializer version: 1 # name: test_async_get_config_entry_diagnostics dict({ + '123456789012345678': dict({ + 'brand': 'BOSCH', + 'connected': True, + 'e_number': 'HCS000000/06', + 'ha_id': '123456789012345678', + 'name': 'CookProcessor', + 'programs': list([ + ]), + 'settings': dict({ + }), + 'status': dict({ + 'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed', + 'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready', + 'BSH.Common.Status.RemoteControlActive': True, + 'BSH.Common.Status.RemoteControlStartAllowed': True, + 'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open', + }), + 'type': 'CookProcessor', + 'vib': 'HCS000006', + }), 'BOSCH-000000000-000000000000': dict({ 'brand': 'BOSCH', 'connected': True, @@ -21,6 +41,26 @@ 'type': 'DNE', 'vib': 'HCS000000', }), + 'BOSCH-HCS000000-68A40E000000': dict({ + 'brand': 'BOSCH', + 'connected': True, + 'e_number': 'HCS000000/05', + 'ha_id': 'BOSCH-HCS000000-68A40E000000', + 'name': 'Hob', + 'programs': list([ + ]), + 'settings': dict({ + }), + 'status': dict({ + 'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed', + 'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready', + 'BSH.Common.Status.RemoteControlActive': True, + 'BSH.Common.Status.RemoteControlStartAllowed': True, + 'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open', + }), + 'type': 'Hob', + 'vib': 'HCS000005', + }), 'BOSCH-HCS000000-D00000000001': dict({ 'brand': 'BOSCH', 'connected': True, @@ -114,46 +154,6 @@ 'type': 'Hood', 'vib': 'HCS000004', }), - 'BOSCH-HCS000000-D00000000005': dict({ - 'brand': 'BOSCH', - 'connected': True, - 'e_number': 'HCS000000/05', - 'ha_id': 'BOSCH-HCS000000-D00000000005', - 'name': 'Hob', - 'programs': list([ - ]), - 'settings': dict({ - }), - 'status': dict({ - 'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed', - 'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready', - 'BSH.Common.Status.RemoteControlActive': True, - 'BSH.Common.Status.RemoteControlStartAllowed': True, - 'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open', - }), - 'type': 'Hob', - 'vib': 'HCS000005', - }), - 'BOSCH-HCS000000-D00000000006': dict({ - 'brand': 'BOSCH', - 'connected': True, - 'e_number': 'HCS000000/06', - 'ha_id': 'BOSCH-HCS000000-D00000000006', - 'name': 'CookProcessor', - 'programs': list([ - ]), - 'settings': dict({ - }), - 'status': dict({ - 'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed', - 'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready', - 'BSH.Common.Status.RemoteControlActive': True, - 'BSH.Common.Status.RemoteControlStartAllowed': True, - 'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open', - }), - 'type': 'CookProcessor', - 'vib': 'HCS000006', - }), 'BOSCH-HCS01OVN1-43E0065FE245': dict({ 'brand': 'BOSCH', 'connected': True, diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 73aed382780..3d239d63bd0 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,9 +1,11 @@ """Test the Home Connect config flow.""" +from collections.abc import Awaitable, Callable from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import MagicMock, patch from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from aiohomeconnect.model import HomeAppliance import pytest from homeassistant import config_entries, setup @@ -11,7 +13,7 @@ from homeassistant.components.home_connect.const import 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 +from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import FAKE_ACCESS_TOKEN, FAKE_REFRESH_TOKEN @@ -337,17 +339,17 @@ async def test_zeroconf_flow_already_setup( @pytest.mark.usefixtures("current_request_with_host") -@pytest.mark.parametrize("dchp_discovery", DHCP_DISCOVERY) +@pytest.mark.parametrize("dhcp_discovery", DHCP_DISCOVERY) async def test_dhcp_flow( hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, - dchp_discovery: DhcpServiceInfo, + dhcp_discovery: DhcpServiceInfo, ) -> None: """Test DHCP discovery.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dchp_discovery + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -391,8 +393,6 @@ async def test_dhcp_flow( @pytest.mark.usefixtures("current_request_with_host") async def test_dhcp_flow_already_setup( hass: HomeAssistant, - hass_client_no_auth: ClientSessionGenerator, - aioclient_mock: AiohttpClientMocker, config_entry: MockConfigEntry, ) -> None: """Test DHCP discovery with already setup device.""" @@ -403,3 +403,56 @@ async def test_dhcp_flow_already_setup( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("current_request_with_host") +@pytest.mark.parametrize( + ("dhcp_discovery", "appliance"), + [ + ( + DhcpServiceInfo( + ip="1.1.1.1", + hostname="bosch-cookprocessor-123456789012345678", + macaddress="c8:d7:78:00:00:00", + ), + "CookProcessor", + ), + ( + DhcpServiceInfo( + ip="1.1.1.1", + hostname="BOSCH-HCS000000-68A40E000000", + macaddress="68:a4:0e:00:00:00", + ), + "Hob", + ), + ], + indirect=["appliance"], +) +async def test_dhcp_flow_complete_device_information( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + client: MagicMock, + config_entry: MockConfigEntry, + integration_setup: Callable[[MagicMock], Awaitable[bool]], + dhcp_discovery: DhcpServiceInfo, + appliance: HomeAppliance, +) -> None: + """Test DHCP discovery with complete device information.""" + assert await integration_setup(client) + assert config_entry.state is ConfigEntryState.LOADED + + device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) + assert device + assert device.connections == set() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + device = device_registry.async_get_device(identifiers={(DOMAIN, appliance.ha_id)}) + assert device + assert device.connections == { + (dr.CONNECTION_NETWORK_MAC, dhcp_discovery.macaddress) + } From 3eed552c562c2180c3993f023ea608f45fa95ae4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 12 May 2025 21:18:55 +0200 Subject: [PATCH 0392/1175] Repair Z-Wave unknown controller (#144738) Co-authored-by: Franck Nijhof --- homeassistant/components/zwave_js/__init__.py | 33 +++++ homeassistant/components/zwave_js/repairs.py | 44 +++++++ .../components/zwave_js/strings.json | 11 ++ tests/components/zwave_js/conftest.py | 6 +- tests/components/zwave_js/test_repairs.py | 116 ++++++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index e73bd01deba..349baecc21d 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -278,6 +278,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # and we'll handle the clean up below. await driver_events.setup(driver) + if (old_unique_id := entry.unique_id) is not None and old_unique_id != ( + new_unique_id := str(driver.controller.home_id) + ): + device_registry = dr.async_get(hass) + controller_model = "Unknown model" + if ( + (own_node := driver.controller.own_node) + and ( + controller_device_entry := device_registry.async_get_device( + identifiers={get_device_id(driver, own_node)} + ) + ) + and (model := controller_device_entry.model) + ): + controller_model = model + async_create_issue( + hass, + DOMAIN, + f"migrate_unique_id.{entry.entry_id}", + data={ + "config_entry_id": entry.entry_id, + "config_entry_title": entry.title, + "controller_model": controller_model, + "new_unique_id": new_unique_id, + "old_unique_id": old_unique_id, + }, + is_fixable=True, + severity=IssueSeverity.ERROR, + translation_key="migrate_unique_id", + ) + else: + async_delete_issue(hass, DOMAIN, f"migrate_unique_id.{entry.entry_id}") + # If the listen task is already failed, we need to raise ConfigEntryNotReady if listen_task.done(): listen_error, error_message = _get_listen_task_error(listen_task) diff --git a/homeassistant/components/zwave_js/repairs.py b/homeassistant/components/zwave_js/repairs.py index e515ae10549..f1deb91d869 100644 --- a/homeassistant/components/zwave_js/repairs.py +++ b/homeassistant/components/zwave_js/repairs.py @@ -57,6 +57,47 @@ class DeviceConfigFileChangedFlow(RepairsFlow): ) +class MigrateUniqueIDFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, data: dict[str, str]) -> None: + """Initialize.""" + self.description_placeholders: dict[str, str] = { + "config_entry_title": data["config_entry_title"], + "controller_model": data["controller_model"], + "new_unique_id": data["new_unique_id"], + "old_unique_id": data["old_unique_id"], + } + self._config_entry_id: str = data["config_entry_id"] + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + config_entry = self.hass.config_entries.async_get_entry( + self._config_entry_id + ) + # If config entry was removed, we can ignore the issue. + if config_entry is not None: + self.hass.config_entries.async_update_entry( + config_entry, + unique_id=self.description_placeholders["new_unique_id"], + ) + return self.async_create_entry(data={}) + + return self.async_show_form( + step_id="confirm", + description_placeholders=self.description_placeholders, + ) + + async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, data: dict[str, str] | None ) -> RepairsFlow: @@ -65,4 +106,7 @@ async def async_create_fix_flow( if issue_id.split(".")[0] == "device_config_file_changed": assert data return DeviceConfigFileChangedFlow(data) + if issue_id.split(".")[0] == "migrate_unique_id": + assert data + return MigrateUniqueIDFlow(data) return ConfirmRepairFlow() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 56ae4e12401..2a8e2c6ea2d 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -273,6 +273,17 @@ "invalid_server_version": { "description": "The version of Z-Wave Server you are currently running is too old for this version of Home Assistant. Please update the Z-Wave Server to the latest version to fix this issue.", "title": "Newer version of Z-Wave Server needed" + }, + "migrate_unique_id": { + "fix_flow": { + "step": { + "confirm": { + "description": "A Z-Wave controller of model {controller_model} with a different ID ({new_unique_id}) than the previously connected controller ({old_unique_id}) was connected to the {config_entry_title} configuration entry.\n\nReasons for a different controller ID could be:\n\n1. The controller was factory reset, with a 3rd party application.\n2. A controller Non Volatile Memory (NVM) backup was restored to the controller, with a 3rd party application.\n3. A different controller was connected to this configuration entry.\n\nIf a different controller was connected, you should instead set up a new configuration entry for the new controller.\n\nIf you are sure that the current controller is the correct controller you can confirm this by pressing Submit, and the configuration entry will remember the new controller ID.", + "title": "An unknown controller was detected" + } + } + }, + "title": "An unknown controller was detected" } }, "services": { diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e4e757ad363..609a0229bcf 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -843,7 +843,11 @@ async def integration_fixture( platforms: list[Platform], ) -> MockConfigEntry: """Set up the zwave_js integration.""" - entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry = MockConfigEntry( + domain="zwave_js", + data={"url": "ws://test.org"}, + unique_id=str(client.driver.controller.home_id), + ) entry.add_to_hass(hass) with patch("homeassistant.components.zwave_js.PLATFORMS", platforms): await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/zwave_js/test_repairs.py b/tests/components/zwave_js/test_repairs.py index 1d0f74c7269..d8c3de92b3b 100644 --- a/tests/components/zwave_js/test_repairs.py +++ b/tests/components/zwave_js/test_repairs.py @@ -12,6 +12,7 @@ from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, issue_registry as ir +from tests.common import MockConfigEntry from tests.components.repairs import ( async_process_repairs_platforms, process_repair_fix_flow, @@ -268,3 +269,118 @@ async def test_abort_confirm( assert data["type"] == "abort" assert data["reason"] == "cannot_connect" assert data["description_placeholders"] == {"device_name": device.name} + + +@pytest.mark.usefixtures("client") +async def test_migrate_unique_id( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the migrate unique id flow.""" + old_unique_id = "123456789" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://test.org", + }, + unique_id=old_unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + issue_id = issue["issue_id"] + assert issue_id == f"migrate_unique_id.{config_entry.entry_id}" + + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + assert data["description_placeholders"] == { + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": "3245146787", + "old_unique_id": old_unique_id, + } + + # Apply fix + data = await process_repair_fix_flow(http_client, flow_id) + + assert data["type"] == "create_entry" + assert config_entry.unique_id == "3245146787" + + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 + + +@pytest.mark.usefixtures("client") +async def test_migrate_unique_id_missing_config_entry( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the migrate unique id flow with missing config entry.""" + old_unique_id = "123456789" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://test.org", + }, + unique_id=old_unique_id, + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + http_client = await hass_client() + + # Assert the issue is present + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + issue = msg["result"]["issues"][0] + issue_id = issue["issue_id"] + assert issue_id == f"migrate_unique_id.{config_entry.entry_id}" + + await hass.config_entries.async_remove(config_entry.entry_id) + + assert not hass.config_entries.async_get_entry(config_entry.entry_id) + + data = await start_repair_fix_flow(http_client, DOMAIN, issue_id) + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + assert data["description_placeholders"] == { + "config_entry_title": "Z-Wave JS", + "controller_model": "ZW090", + "new_unique_id": "3245146787", + "old_unique_id": old_unique_id, + } + + # Apply fix + data = await process_repair_fix_flow(http_client, flow_id) + + assert data["type"] == "create_entry" + + await ws_client.send_json_auto_id({"type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 From 026687299d10a88aa11339db983db85b1b5e43f0 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 12 May 2025 21:28:40 +0200 Subject: [PATCH 0393/1175] Assert resulting data in devolo Home Network test_form_reauth (#144760) --- tests/components/devolo_home_network/test_config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 16d3e6a8b9e..589a828f29f 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -303,5 +303,4 @@ async def test_form_reauth(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 - - await hass.config_entries.async_unload(entry.entry_id) + assert entry.data[CONF_PASSWORD] == "test-right-password" From e58750555e688daa6fa1a03ead335792be7de0ac Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 12 May 2025 23:21:14 +0200 Subject: [PATCH 0394/1175] Rework platform setup tests for devolo Home Network (#143114) * Rework platform setup tests for devolo Home Network * Fix sensor test * Remove unload --- .../devolo_home_network/test_binary_sensor.py | 27 ++++--- .../devolo_home_network/test_button.py | 31 ++++---- .../test_device_tracker.py | 2 - .../devolo_home_network/test_image.py | 24 +++---- .../devolo_home_network/test_sensor.py | 72 ++++++++----------- .../devolo_home_network/test_switch.py | 24 +++---- .../devolo_home_network/test_update.py | 18 ++--- 7 files changed, 86 insertions(+), 112 deletions(-) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 8197ec1a1e5..e793c509b13 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -7,11 +7,9 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.devolo_home_network.const import ( - CONNECTED_TO_ROUTER, - LONG_UPDATE_INTERVAL, -) +from homeassistant.components.binary_sensor import DOMAIN as PLATFORM +from homeassistant.components.devolo_home_network.const import LONG_UPDATE_INTERVAL +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -24,19 +22,20 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device") -async def test_binary_sensor_setup(hass: HomeAssistant) -> None: +async def test_binary_sensor_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the binary sensor component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - assert ( - hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}") - is None - ) - - await hass.config_entries.async_unload(entry.entry_id) + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_connected_to_router" + ).disabled @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -50,7 +49,7 @@ async def test_update_attached_to_router( """Test state change of a attached_to_router binary sensor device.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() - state_key = f"{BINARY_SENSOR_DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}" + state_key = f"{PLATFORM}.{device_name}_connected_to_router" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -81,5 +80,3 @@ async def test_update_attached_to_router( state = hass.states.get(state_key) assert state is not None assert state.state == STATE_ON - - await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_button.py b/tests/components/devolo_home_network/test_button.py index b2d410b03f9..8a8028454ea 100644 --- a/tests/components/devolo_home_network/test_button.py +++ b/tests/components/devolo_home_network/test_button.py @@ -8,7 +8,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as PLATFORM, SERVICE_PRESS from homeassistant.components.devolo_home_network.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -19,22 +19,27 @@ from .mock import MockDevice @pytest.mark.usefixtures("mock_device") -async def test_button_setup(hass: HomeAssistant) -> None: +async def test_button_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the button component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - assert ( - hass.states.get(f"{PLATFORM}.{device_name}_identify_device_with_a_blinking_led") - is not None - ) - assert hass.states.get(f"{PLATFORM}.{device_name}_start_plc_pairing") is not None - assert hass.states.get(f"{PLATFORM}.{device_name}_restart_device") is not None - assert hass.states.get(f"{PLATFORM}.{device_name}_start_wps") is not None - - await hass.config_entries.async_unload(entry.entry_id) + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_identify_device_with_a_blinking_led" + ).disabled + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_start_plc_pairing" + ).disabled + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_restart_device" + ).disabled + assert not entity_registry.async_get(f"{PLATFORM}.{device_name}_start_wps").disabled @pytest.mark.parametrize( @@ -107,8 +112,6 @@ async def test_button( blocking=True, ) - await hass.config_entries.async_unload(entry.entry_id) - async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None: """Test setting unautherized triggers the reauth flow.""" @@ -139,5 +142,3 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None assert "context" in flow assert flow["context"]["source"] == SOURCE_REAUTH assert flow["context"]["entry_id"] == entry.entry_id - - await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index ac86eb54961..2af6a1e3759 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -70,8 +70,6 @@ async def test_device_tracker( assert state is not None assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_unload(entry.entry_id) - async def test_restoring_clients( hass: HomeAssistant, diff --git a/tests/components/devolo_home_network/test_image.py b/tests/components/devolo_home_network/test_image.py index f13db4fce9d..54a8af3af6e 100644 --- a/tests/components/devolo_home_network/test_image.py +++ b/tests/components/devolo_home_network/test_image.py @@ -9,7 +9,8 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.devolo_home_network.const import SHORT_UPDATE_INTERVAL -from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN +from homeassistant.components.image import DOMAIN as PLATFORM +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 @@ -24,21 +25,20 @@ from tests.typing import ClientSessionGenerator @pytest.mark.usefixtures("mock_device") -async def test_image_setup(hass: HomeAssistant) -> None: +async def test_image_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the image component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - assert ( - hass.states.get( - f"{IMAGE_DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code" - ) - is not None - ) - - await hass.config_entries.async_unload(entry.entry_id) + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_guest_wi_fi_credentials_as_qr_code" + ).disabled @pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") @@ -53,7 +53,7 @@ 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"{IMAGE_DOMAIN}.{device_name}_guest_wi_fi_credentials_as_qr_code" + state_key = f"{PLATFORM}.{device_name}_guest_wi_fi_credentials_as_qr_code" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -95,5 +95,3 @@ async def test_guest_wifi_qr( resp = await client.get(f"/api/image_proxy/{state_key}") assert resp.status == HTTPStatus.OK assert await resp.read() != body - - await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index cf0207a2800..d01eb9f9e38 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -27,49 +27,41 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device") -async def test_sensor_setup(hass: HomeAssistant) -> None: +async def test_sensor_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the sensor component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - assert ( - hass.states.get(f"{PLATFORM}.{device_name}_connected_wi_fi_clients") is not None - ) - assert hass.states.get(f"{PLATFORM}.{device_name}_connected_plc_devices") is None - assert ( - hass.states.get(f"{PLATFORM}.{device_name}_neighboring_wi_fi_networks") is None - ) - assert ( - hass.states.get( - f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}" - ) - is not None - ) - assert ( - hass.states.get( - f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[1].user_device_name}" - ) - is not None - ) - assert ( - hass.states.get( - f"{PLATFORM}.{device_name}_plc_downlink_phyrate_{PLCNET.devices[2].user_device_name}" - ) - is None - ) - assert ( - hass.states.get( - f"{PLATFORM}.{device_name}_plc_uplink_phyrate_{PLCNET.devices[2].user_device_name}" - ) - is None - ) - assert ( - hass.states.get(f"{PLATFORM}.{device_name}_last_restart_of_the_device") is None - ) - - await hass.config_entries.async_unload(entry.entry_id) + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_connected_wi_fi_clients" + ).disabled + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_connected_plc_devices" + ).disabled + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_neighboring_wi_fi_networks" + ).disabled + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[1].user_device_name}" + ).disabled + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[1].user_device_name}" + ).disabled + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_plc_downlink_phy_rate_{PLCNET.devices[2].user_device_name}" + ).disabled + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_plc_uplink_phy_rate_{PLCNET.devices[2].user_device_name}" + ).disabled + assert entity_registry.async_get( + f"{PLATFORM}.{device_name}_last_restart_of_the_device" + ).disabled @pytest.mark.parametrize( @@ -145,8 +137,6 @@ async def test_sensor( assert state is not None assert state.state == expected_state - await hass.config_entries.async_unload(entry.entry_id) - async def test_update_plc_phyrates( hass: HomeAssistant, @@ -198,8 +188,6 @@ async def test_update_plc_phyrates( assert state is not None assert state.state == str(PLCNET.data_rates[0].tx_rate) - await hass.config_entries.async_unload(entry.entry_id) - async def test_update_last_update_auth_failed( hass: HomeAssistant, mock_device: MockDevice @@ -222,5 +210,3 @@ async def test_update_last_update_auth_failed( assert "context" in flow assert flow["context"]["source"] == SOURCE_REAUTH assert flow["context"]["entry_id"] == entry.entry_id - - await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py index 7a342780877..1ab2a1c354b 100644 --- a/tests/components/devolo_home_network/test_switch.py +++ b/tests/components/devolo_home_network/test_switch.py @@ -35,17 +35,23 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device") -async def test_switch_setup(hass: HomeAssistant) -> None: +async def test_switch_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the switch component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - 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) + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_enable_guest_wi_fi" + ).disabled + assert not entity_registry.async_get( + f"{PLATFORM}.{device_name}_enable_leds" + ).disabled async def test_update_guest_wifi_status_auth_failed( @@ -70,8 +76,6 @@ async def test_update_guest_wifi_status_auth_failed( assert flow["context"]["source"] == SOURCE_REAUTH assert flow["context"]["entry_id"] == entry.entry_id - await hass.config_entries.async_unload(entry.entry_id) - async def test_update_enable_guest_wifi( hass: HomeAssistant, @@ -153,8 +157,6 @@ async def test_update_enable_guest_wifi( assert state is not None assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_unload(entry.entry_id) - async def test_update_enable_leds( hass: HomeAssistant, @@ -230,8 +232,6 @@ async def test_update_enable_leds( assert state is not None assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_unload(entry.entry_id) - @pytest.mark.parametrize( ("name", "get_method", "update_interval"), @@ -325,5 +325,3 @@ async def test_auth_failed( assert "context" in flow assert flow["context"]["source"] == SOURCE_REAUTH assert flow["context"]["entry_id"] == entry.entry_id - - await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/devolo_home_network/test_update.py b/tests/components/devolo_home_network/test_update.py index 4fe7a173309..034d1bad7f6 100644 --- a/tests/components/devolo_home_network/test_update.py +++ b/tests/components/devolo_home_network/test_update.py @@ -11,7 +11,7 @@ from homeassistant.components.devolo_home_network.const import ( FIRMWARE_UPDATE_INTERVAL, ) from homeassistant.components.update import DOMAIN as PLATFORM, SERVICE_INSTALL -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -25,16 +25,18 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device") -async def test_update_setup(hass: HomeAssistant) -> None: +async def test_update_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: """Test default setup of the update component.""" entry = configure_integration(hass) device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + assert entry.state is ConfigEntryState.LOADED - assert hass.states.get(f"{PLATFORM}.{device_name}_firmware") is not None - - await hass.config_entries.async_unload(entry.entry_id) + assert not entity_registry.async_get(f"{PLATFORM}.{device_name}_firmware").disabled async def test_update_firmware( @@ -85,8 +87,6 @@ async def test_update_firmware( assert device_info is not None assert device_info.sw_version == mock_device.firmware_version - await hass.config_entries.async_unload(entry.entry_id) - async def test_device_failure_check( hass: HomeAssistant, @@ -137,8 +137,6 @@ async def test_device_failure_update( blocking=True, ) - await hass.config_entries.async_unload(entry.entry_id) - async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None: """Test updating unauthorized triggers the reauth flow.""" @@ -168,5 +166,3 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None assert "context" in flow assert flow["context"]["source"] == SOURCE_REAUTH assert flow["context"]["entry_id"] == entry.entry_id - - await hass.config_entries.async_unload(entry.entry_id) From ba3181d4e77a640df6fb25b3f7e63f219f922af0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 12 May 2025 23:52:27 +0200 Subject: [PATCH 0395/1175] Update pipdeptree to 2.26.1 (#144775) --- requirements_test.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2839d7f7982..aa989cdd0ed 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,7 +18,7 @@ pre-commit==4.0.0 pydantic==2.11.3 pylint==3.3.7 pylint-per-file-ignores==1.4.0 -pipdeptree==2.25.1 +pipdeptree==2.26.1 pytest-asyncio==0.26.0 pytest-aiohttp==1.1.0 pytest-cov==6.0.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 306b5901370..71e671ad9ac 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -24,7 +24,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree==2.25.1 tqdm==4.67.1 ruff==0.11.0 \ + stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" From 0719753be3c42129729c3ded794e75ff6a4b4a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 12 May 2025 23:53:54 +0200 Subject: [PATCH 0396/1175] Set PARALLEL_UPDATES and update quality_scale for Miele integration (#144770) Set PARALLEL_UPDATES and update quality_scale --- homeassistant/components/miele/binary_sensor.py | 2 ++ homeassistant/components/miele/button.py | 2 ++ homeassistant/components/miele/climate.py | 2 ++ homeassistant/components/miele/fan.py | 2 ++ homeassistant/components/miele/light.py | 2 ++ .../components/miele/quality_scale.yaml | 17 +++++++++++------ homeassistant/components/miele/sensor.py | 2 ++ homeassistant/components/miele/switch.py | 2 ++ homeassistant/components/miele/vacuum.py | 2 ++ 9 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/miele/binary_sensor.py b/homeassistant/components/miele/binary_sensor.py index 9b0868beed4..b43bd86010e 100644 --- a/homeassistant/components/miele/binary_sensor.py +++ b/homeassistant/components/miele/binary_sensor.py @@ -23,6 +23,8 @@ from .const import MieleAppliance from .coordinator import MieleConfigEntry from .entity import MieleEntity +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/miele/button.py b/homeassistant/components/miele/button.py index b749ce364f0..4086c002743 100644 --- a/homeassistant/components/miele/button.py +++ b/homeassistant/components/miele/button.py @@ -17,6 +17,8 @@ from .const import DOMAIN, PROCESS_ACTION, MieleActions, MieleAppliance from .coordinator import MieleConfigEntry from .entity import MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 4324444d987..85235322616 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -26,6 +26,8 @@ from .const import DEVICE_TYPE_TAGS, DISABLED_TEMP_ENTITIES, DOMAIN, MieleApplia from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator from .entity import MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/miele/fan.py b/homeassistant/components/miele/fan.py index e8bea197f58..5faaa46b33c 100644 --- a/homeassistant/components/miele/fan.py +++ b/homeassistant/components/miele/fan.py @@ -27,6 +27,8 @@ from .const import DOMAIN, POWER_OFF, POWER_ON, VENTILATION_STEP, MieleAppliance from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator from .entity import MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) SPEED_RANGE = (1, 4) diff --git a/homeassistant/components/miele/light.py b/homeassistant/components/miele/light.py index 678c2f92382..e918b93b12a 100644 --- a/homeassistant/components/miele/light.py +++ b/homeassistant/components/miele/light.py @@ -23,6 +23,8 @@ from .const import AMBIENT_LIGHT, DOMAIN, LIGHT, LIGHT_OFF, LIGHT_ON, MieleAppli from .coordinator import MieleConfigEntry from .entity import MieleDevice, MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/miele/quality_scale.yaml b/homeassistant/components/miele/quality_scale.yaml index e9d229c6a1b..d0c3677db40 100644 --- a/homeassistant/components/miele/quality_scale.yaml +++ b/homeassistant/components/miele/quality_scale.yaml @@ -32,18 +32,23 @@ rules: Handled by a setting in manifest.json as there is no account information in API # Silver - action-exceptions: todo + action-exceptions: + status: done + comment: No custom actions are defined config-entry-unloading: done docs-configuration-parameters: status: exempt comment: No configuration parameters - docs-installation-parameters: todo + docs-installation-parameters: + status: exempt + comment: | + Integration uses account linking via Nabu casa so no installation parameters are needed. entity-unavailable: done integration-owner: done - log-when-unavailable: todo - parallel-updates: - status: exempt - comment: Handled by coordinator + log-when-unavailable: + status: done + comment: Handled by DataUpdateCoordinator + parallel-updates: done reauthentication-flow: done test-coverage: todo diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 64948cf7b83..5a0b9212971 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -39,6 +39,8 @@ from .const import ( from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator from .entity import MieleEntity +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) DISABLED_TEMPERATURE = -32768 diff --git a/homeassistant/components/miele/switch.py b/homeassistant/components/miele/switch.py index 4cd237aa724..af46ef2c917 100644 --- a/homeassistant/components/miele/switch.py +++ b/homeassistant/components/miele/switch.py @@ -28,6 +28,8 @@ from .const import ( from .coordinator import MieleConfigEntry from .entity import MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/miele/vacuum.py b/homeassistant/components/miele/vacuum.py index 02d85cabdef..1e14d33f461 100644 --- a/homeassistant/components/miele/vacuum.py +++ b/homeassistant/components/miele/vacuum.py @@ -24,6 +24,8 @@ from .const import DOMAIN, PROCESS_ACTION, PROGRAM_ID, MieleActions, MieleApplia from .coordinator import MieleConfigEntry from .entity import MieleEntity +PARALLEL_UPDATES = 1 + _LOGGER = logging.getLogger(__name__) # The following const classes define program speeds and programs for the vacuum cleaner. From e69ca0cf80a658b56789d2197c6dde594728e673 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Tue, 13 May 2025 00:00:17 +0200 Subject: [PATCH 0397/1175] Bump aiodhcpwatcher to 1.2.0 (#144769) --- 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 c425aafdb00..c3b0121ff2b 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -14,7 +14,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==1.1.1", + "aiodhcpwatcher==1.2.0", "aiodiscover==2.7.0", "cached-ipaddress==0.10.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 37618cb3d54..59437b4c2ae 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==1.1.1 +aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 aiodns==3.4.0 aiohasupervisor==0.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9c1d08febf8..16592ce1d3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -214,7 +214,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.1 # homeassistant.components.dhcp -aiodhcpwatcher==1.1.1 +aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp aiodiscover==2.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d2b09d68f7..8b29082b212 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,7 +202,7 @@ aiobotocore==2.21.1 aiocomelit==0.12.1 # homeassistant.components.dhcp -aiodhcpwatcher==1.1.1 +aiodhcpwatcher==1.2.0 # homeassistant.components.dhcp aiodiscover==2.7.0 From 0128d859995a1e5877e135e06ae5d7ebea96ddff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 May 2025 00:03:37 +0200 Subject: [PATCH 0398/1175] Move sun conditions to the sun integration (#144742) --- homeassistant/components/sun/condition.py | 136 ++ homeassistant/helpers/condition.py | 109 +- homeassistant/helpers/config_validation.py | 30 +- tests/components/sun/test_condition.py | 1235 +++++++++++++++++ .../components/websocket_api/test_commands.py | 5 +- tests/helpers/test_condition.py | 1223 +--------------- tests/helpers/test_config_validation.py | 7 +- 7 files changed, 1405 insertions(+), 1340 deletions(-) create mode 100644 homeassistant/components/sun/condition.py create mode 100644 tests/components/sun/test_condition.py diff --git a/homeassistant/components/sun/condition.py b/homeassistant/components/sun/condition.py new file mode 100644 index 00000000000..c52ada51e06 --- /dev/null +++ b/homeassistant/components/sun/condition.py @@ -0,0 +1,136 @@ +"""Offer sun based automation rules.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import cast + +import voluptuous as vol + +from homeassistant.const import CONF_CONDITION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.condition import ( + ConditionCheckerType, + condition_trace_set_result, + condition_trace_update_result, + trace_condition_function, +) +from homeassistant.helpers.sun import get_astral_event_date +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.util import dt as dt_util + +CONDITION_SCHEMA = vol.All( + vol.Schema( + { + **cv.CONDITION_BASE_SCHEMA, + vol.Required(CONF_CONDITION): "sun", + vol.Optional("before"): cv.sun_event, + vol.Optional("before_offset"): cv.time_period, + vol.Optional("after"): vol.All( + vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE) + ), + vol.Optional("after_offset"): cv.time_period, + } + ), + cv.has_at_least_one_key("before", "after"), +) + + +def sun( + hass: HomeAssistant, + before: str | None = None, + after: str | None = None, + before_offset: timedelta | None = None, + after_offset: timedelta | None = None, +) -> bool: + """Test if current time matches sun requirements.""" + utcnow = dt_util.utcnow() + today = dt_util.as_local(utcnow).date() + before_offset = before_offset or timedelta(0) + after_offset = after_offset or timedelta(0) + + sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) + sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) + + has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after) + has_sunset_condition = SUN_EVENT_SUNSET in (before, after) + + after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date() + if after_sunrise and has_sunrise_condition: + tomorrow = today + timedelta(days=1) + sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow) + + after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date() + if after_sunset and has_sunset_condition: + tomorrow = today + timedelta(days=1) + sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow) + + # Special case: before sunrise OR after sunset + # This will handle the very rare case in the polar region when the sun rises/sets + # but does not set/rise. + # However this entire condition does not handle those full days of darkness + # or light, the following should be used instead: + # + # condition: + # condition: state + # entity_id: sun.sun + # state: 'above_horizon' (or 'below_horizon') + # + if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET: + wanted_time_before = cast(datetime, sunrise) + before_offset + condition_trace_update_result(wanted_time_before=wanted_time_before) + wanted_time_after = cast(datetime, sunset) + after_offset + condition_trace_update_result(wanted_time_after=wanted_time_after) + return utcnow < wanted_time_before or utcnow > wanted_time_after + + if sunrise is None and has_sunrise_condition: + # There is no sunrise today + condition_trace_set_result(False, message="no sunrise today") + return False + + if sunset is None and has_sunset_condition: + # There is no sunset today + condition_trace_set_result(False, message="no sunset today") + return False + + if before == SUN_EVENT_SUNRISE: + wanted_time_before = cast(datetime, sunrise) + before_offset + condition_trace_update_result(wanted_time_before=wanted_time_before) + if utcnow > wanted_time_before: + return False + + if before == SUN_EVENT_SUNSET: + wanted_time_before = cast(datetime, sunset) + before_offset + condition_trace_update_result(wanted_time_before=wanted_time_before) + if utcnow > wanted_time_before: + return False + + if after == SUN_EVENT_SUNRISE: + wanted_time_after = cast(datetime, sunrise) + after_offset + condition_trace_update_result(wanted_time_after=wanted_time_after) + if utcnow < wanted_time_after: + return False + + if after == SUN_EVENT_SUNSET: + wanted_time_after = cast(datetime, sunset) + after_offset + condition_trace_update_result(wanted_time_after=wanted_time_after) + if utcnow < wanted_time_after: + return False + + return True + + +def async_condition_from_config(config: ConfigType) -> ConditionCheckerType: + """Wrap action method with sun based condition.""" + before = config.get("before") + after = config.get("after") + before_offset = config.get("before_offset") + after_offset = config.get("after_offset") + + @trace_condition_function + def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + """Validate time based if-condition.""" + return sun(hass, before, after, before_offset, after_offset) + + return sun_if diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index fa2dd42589b..c1b87dd755a 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -42,8 +42,6 @@ from homeassistant.const import ( ENTITY_MATCH_ANY, STATE_UNAVAILABLE, STATE_UNKNOWN, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, WEEKDAYS, ) from homeassistant.core import HomeAssistant, State, callback @@ -60,7 +58,6 @@ from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe from . import config_validation as cv, entity_registry as er -from .sun import get_astral_event_date from .template import Template, render_complex from .trace import ( TraceElement, @@ -85,7 +82,6 @@ _PLATFORM_ALIASES = { "numeric_state": None, "or": None, "state": None, - "sun": None, "template": None, "time": None, "trigger": None, @@ -655,105 +651,6 @@ def state_from_config(config: ConfigType) -> ConditionCheckerType: return if_state -def sun( - hass: HomeAssistant, - before: str | None = None, - after: str | None = None, - before_offset: timedelta | None = None, - after_offset: timedelta | None = None, -) -> bool: - """Test if current time matches sun requirements.""" - utcnow = dt_util.utcnow() - today = dt_util.as_local(utcnow).date() - before_offset = before_offset or timedelta(0) - after_offset = after_offset or timedelta(0) - - sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) - sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) - - has_sunrise_condition = SUN_EVENT_SUNRISE in (before, after) - has_sunset_condition = SUN_EVENT_SUNSET in (before, after) - - after_sunrise = today > dt_util.as_local(cast(datetime, sunrise)).date() - if after_sunrise and has_sunrise_condition: - tomorrow = today + timedelta(days=1) - sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow) - - after_sunset = today > dt_util.as_local(cast(datetime, sunset)).date() - if after_sunset and has_sunset_condition: - tomorrow = today + timedelta(days=1) - sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow) - - # Special case: before sunrise OR after sunset - # This will handle the very rare case in the polar region when the sun rises/sets - # but does not set/rise. - # However this entire condition does not handle those full days of darkness - # or light, the following should be used instead: - # - # condition: - # condition: state - # entity_id: sun.sun - # state: 'above_horizon' (or 'below_horizon') - # - if before == SUN_EVENT_SUNRISE and after == SUN_EVENT_SUNSET: - wanted_time_before = cast(datetime, sunrise) + before_offset - condition_trace_update_result(wanted_time_before=wanted_time_before) - wanted_time_after = cast(datetime, sunset) + after_offset - condition_trace_update_result(wanted_time_after=wanted_time_after) - return utcnow < wanted_time_before or utcnow > wanted_time_after - - if sunrise is None and has_sunrise_condition: - # There is no sunrise today - condition_trace_set_result(False, message="no sunrise today") - return False - - if sunset is None and has_sunset_condition: - # There is no sunset today - condition_trace_set_result(False, message="no sunset today") - return False - - if before == SUN_EVENT_SUNRISE: - wanted_time_before = cast(datetime, sunrise) + before_offset - condition_trace_update_result(wanted_time_before=wanted_time_before) - if utcnow > wanted_time_before: - return False - - if before == SUN_EVENT_SUNSET: - wanted_time_before = cast(datetime, sunset) + before_offset - condition_trace_update_result(wanted_time_before=wanted_time_before) - if utcnow > wanted_time_before: - return False - - if after == SUN_EVENT_SUNRISE: - wanted_time_after = cast(datetime, sunrise) + after_offset - condition_trace_update_result(wanted_time_after=wanted_time_after) - if utcnow < wanted_time_after: - return False - - if after == SUN_EVENT_SUNSET: - wanted_time_after = cast(datetime, sunset) + after_offset - condition_trace_update_result(wanted_time_after=wanted_time_after) - if utcnow < wanted_time_after: - return False - - return True - - -def sun_from_config(config: ConfigType) -> ConditionCheckerType: - """Wrap action method with sun based condition.""" - before = config.get("before") - after = config.get("after") - before_offset = config.get("before_offset") - after_offset = config.get("after_offset") - - @trace_condition_function - def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: - """Validate time based if-condition.""" - return sun(hass, before, after, before_offset, after_offset) - - return sun_if - - def template( hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None ) -> bool: @@ -1054,8 +951,10 @@ async def async_validate_condition_config( return config platform = await _async_get_condition_platform(hass, config) - if platform is not None and hasattr(platform, "async_validate_condition_config"): - return await platform.async_validate_condition_config(hass, config) + if platform is not None: + if hasattr(platform, "async_validate_condition_config"): + return await platform.async_validate_condition_config(hass, config) + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) if platform is None and condition in ("numeric_state", "state"): validator = cast( Callable[[HomeAssistant, ConfigType], ConfigType], diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4c760bd9d70..31a3e365071 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1090,7 +1090,7 @@ type ValueSchemas = dict[Hashable, VolSchemaType | Callable[[Any], dict[str, Any def key_value_schemas( key: str, value_schemas: ValueSchemas, - default_schema: VolSchemaType | None = None, + default_schema: VolSchemaType | Callable[[Any], dict[str, Any]] | None = None, default_description: str | None = None, ) -> Callable[[Any], dict[Hashable, Any]]: """Create a validator that validates based on a value for specific key. @@ -1745,18 +1745,35 @@ BUILT_IN_CONDITIONS: ValueSchemas = { "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, "or": OR_CONDITION_SCHEMA, "state": STATE_CONDITION_SCHEMA, - "sun": SUN_CONDITION_SCHEMA, "template": TEMPLATE_CONDITION_SCHEMA, "time": TIME_CONDITION_SCHEMA, "trigger": TRIGGER_CONDITION_SCHEMA, "zone": ZONE_CONDITION_SCHEMA, } + +# This is first round of validation, we don't want to mutate the config here already, +# just ensure basics as condition type and alias are there. +def _base_condition_validator(value: Any) -> Any: + vol.Schema( + { + **CONDITION_BASE_SCHEMA, + CONF_CONDITION: vol.NotIn(BUILT_IN_CONDITIONS), + }, + extra=vol.ALLOW_EXTRA, + )(value) + return value + + CONDITION_SCHEMA: vol.Schema = vol.Schema( vol.Any( vol.All( expand_condition_shorthand, - key_value_schemas(CONF_CONDITION, BUILT_IN_CONDITIONS), + key_value_schemas( + CONF_CONDITION, + BUILT_IN_CONDITIONS, + _base_condition_validator, + ), ), dynamic_template_condition, ) @@ -1783,7 +1800,10 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( key_value_schemas( CONF_CONDITION, BUILT_IN_CONDITIONS, - dynamic_template_condition_action, + vol.Any( + dynamic_template_condition_action, + _base_condition_validator, + ), "a list of conditions or a valid template", ), ) @@ -1842,7 +1862,7 @@ def _base_trigger_list_flatten(triggers: list[Any]) -> list[Any]: return flatlist -# This is first round of validation, we don't want to process the config here already, +# This is first round of validation, we don't want to mutate the config here already, # just ensure basics as platform and ID are there. def _base_trigger_validator(value: Any) -> Any: _base_trigger_validator_schema(value) diff --git a/tests/components/sun/test_condition.py b/tests/components/sun/test_condition.py new file mode 100644 index 00000000000..52c0d885461 --- /dev/null +++ b/tests/components/sun/test_condition.py @@ -0,0 +1,1235 @@ +"""The tests for sun conditions.""" + +from datetime import datetime + +from freezegun import freeze_time +import pytest + +from homeassistant.components import automation +from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import trace +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.typing import WebSocketGenerator + + +@pytest.fixture(autouse=True) +def prepare_condition_trace() -> None: + """Clear previous trace.""" + trace.trace_clear() + + +def _find_run_id(traces, trace_type, item_id): + """Find newest run_id for a script or automation.""" + for _trace in reversed(traces): + if _trace["domain"] == trace_type and _trace["item_id"] == item_id: + return _trace["run_id"] + + return None + + +async def assert_automation_condition_trace(hass_ws_client, automation_id, expected): + """Test the result of automation condition.""" + msg_id = 1 + + def next_id(): + nonlocal msg_id + msg_id += 1 + return msg_id + + client = await hass_ws_client() + + # List traces + await client.send_json( + {"id": next_id(), "type": "trace/list", "domain": "automation"} + ) + response = await client.receive_json() + assert response["success"] + run_id = _find_run_id(response["result"], "automation", automation_id) + + # Get trace + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": "automation", + "item_id": "sun", + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + trace = response["result"] + assert len(trace["trace"]["condition/0"]) == 1 + condition_trace = trace["trace"]["condition/0"][0]["result"] + assert condition_trace == expected + + +async def test_if_action_before_sunrise_no_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunrise. + + Before sunrise is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, + ) + + +async def test_if_action_after_sunrise_no_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunrise. + + After sunrise is true from sunrise until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s -> 'after sunrise' not true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = sunrise + 1s -> 'after sunrise' true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, + ) + + +async def test_if_action_before_sunrise_with_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunrise with offset. + + Before sunrise is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "before": SUN_EVENT_SUNRISE, + "before_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunrise + 1h -> 'before sunrise' with offset +1h true + now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC midnight -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'before sunrise' with offset +1h true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -1s -> 'before sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, + ) + + +async def test_if_action_before_sunset_with_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunset with offset. + + Before sunset is true from midnight until sunset, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "before": "sunset", + "before_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = local midnight -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true + now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1h -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = UTC midnight -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = UTC midnight - 1s -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 4 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunrise -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 5 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunrise -1s -> 'before sunset' with offset +1h true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = local midnight-1s -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, + ) + + +async def test_if_action_after_sunrise_with_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunrise with offset. + + After sunrise is true from sunrise until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": SUN_EVENT_SUNRISE, + "after_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunrise + 1h -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC noon -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local noon -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local noon - 1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 4 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = sunset + 1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 5 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight-1s -> 'after sunrise' with offset +1h true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, + ) + + # now = local midnight -> 'after sunrise' with offset +1h not true + now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 6 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T14:33:57.053037+00:00"}, + ) + + +async def test_if_action_after_sunset_with_offset( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunset with offset. + + After sunset is true from sunset until midnight, local time. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": "sunset", + "after_offset": "+1:00:00", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true + now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = sunset + 1h -> 'after sunset' with offset +1h true + now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + # now = midnight-1s -> 'after sunset' with offset +1h true + now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-09-16T02:55:06.099767+00:00"}, + ) + + # now = midnight -> 'after sunset' with offset +1h not true + now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, + ) + + +async def test_if_action_after_and_before_during( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunrise and before sunset. + + This is true from sunrise until sunset. + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "after": SUN_EVENT_SUNRISE, + "before": SUN_EVENT_SUNSET, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": False, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true + now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-09-17T01:53:44.723614+00:00"}, + ) + + # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset - 1s -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = 9AM local -> 'after sunrise' + 'before sunset' true + now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + +async def test_if_action_before_or_after_during( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunrise or after sunset. + + This is true from midnight until sunrise and from sunset until midnight + """ + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": { + "condition": "sun", + "before": SUN_EVENT_SUNRISE, + "after": SUN_EVENT_SUNSET, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local + # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC + # now = sunrise - 1s -> 'before sunrise' | 'after sunset' true + now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset + 1s -> 'before sunrise' | 'after sunset' true + now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunrise + 1s -> 'before sunrise' | 'after sunset' false + now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": False, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = sunset - 1s -> 'before sunrise' | 'after sunset' false + now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": False, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = midnight + 1s local -> 'before sunrise' | 'after sunset' true + now = datetime(2015, 9, 16, 7, 0, 1, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 3 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + # now = midnight - 1s local -> 'before sunrise' | 'after sunset' true + now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 4 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + { + "result": True, + "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", + "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", + }, + ) + + +async def test_if_action_before_sunrise_no_offset_kotzebue( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunrise is true from sunrise until midnight, local time. + """ + await hass.config.async_set_time_zone("America/Anchorage") + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = sunrise - 1h -> 'before sunrise' true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-23T15:12:19.155123+00:00"}, + ) + + +async def test_if_action_after_sunrise_no_offset_kotzebue( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunrise is true from midnight until sunrise, local time. + """ + await hass.config.async_set_time_zone("America/Anchorage") + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunrise -> 'after sunrise' true + now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = sunrise - 1h -> 'after sunrise' not true + now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-23T15:12:19.155123+00:00"}, + ) + + +async def test_if_action_before_sunset_no_offset_kotzebue( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunset is true from midnight until sunset, local time. + """ + await hass.config.async_set_time_zone("America/Anchorage") + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunset + 1s -> 'before sunset' not true + now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 0 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = sunset - 1h-> 'before sunset' true + now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_before": "2015-07-24T11:17:54.446913+00:00"}, + ) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_before": "2015-07-23T11:22:18.467277+00:00"}, + ) + + +async def test_if_action_after_sunset_no_offset_kotzebue( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + service_calls: list[ServiceCall], +) -> None: + """Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunset is true from sunset until midnight, local time. + """ + await hass.config.async_set_time_zone("America/Anchorage") + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local + # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC + # now = sunset -> 'after sunset' true + now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = sunset - 1s -> 'after sunset' not true + now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, + ) + + # now = local midnight -> 'after sunset' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 1 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": False, "wanted_time_after": "2015-07-24T11:17:54.446913+00:00"}, + ) + + # now = local midnight - 1s -> 'after sunset' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with freeze_time(now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(service_calls) == 2 + await assert_automation_condition_trace( + hass_ws_client, + "sun", + {"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"}, + ) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 80e6b8be056..4ca2098550b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2529,9 +2529,8 @@ async def test_validate_config_works( "state": "paulus", }, ( - "Unexpected value for condition: 'non_existing'. Expected and, device," - " not, numeric_state, or, state, sun, template, time, trigger, zone " - "@ data[0]" + "Invalid condition \"non_existing\" specified {'condition': " + "'non_existing', 'entity_id': 'hello.world', 'state': 'paulus'}" ), ), # Raises HomeAssistantError diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index aac64f6139a..7285301f12b 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,6 +1,6 @@ """Test the condition helper.""" -from datetime import datetime, timedelta +from datetime import timedelta from typing import Any from unittest.mock import AsyncMock, patch @@ -8,7 +8,6 @@ from freezegun import freeze_time import pytest import voluptuous as vol -from homeassistant.components import automation from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -17,10 +16,8 @@ from homeassistant.const import ( CONF_DOMAIN, STATE_UNAVAILABLE, STATE_UNKNOWN, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.helpers import ( condition, @@ -32,8 +29,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.typing import WebSocketGenerator - def assert_element(trace_element, expected_element, path): """Assert a trace element is as expected. @@ -2242,1220 +2237,6 @@ async def test_condition_template_invalid_results(hass: HomeAssistant) -> None: assert not test(hass) -def _find_run_id(traces, trace_type, item_id): - """Find newest run_id for a script or automation.""" - for _trace in reversed(traces): - if _trace["domain"] == trace_type and _trace["item_id"] == item_id: - return _trace["run_id"] - - return None - - -async def assert_automation_condition_trace(hass_ws_client, automation_id, expected): - """Test the result of automation condition.""" - msg_id = 1 - - def next_id(): - nonlocal msg_id - msg_id += 1 - return msg_id - - client = await hass_ws_client() - - # List traces - await client.send_json( - {"id": next_id(), "type": "trace/list", "domain": "automation"} - ) - response = await client.receive_json() - assert response["success"] - run_id = _find_run_id(response["result"], "automation", automation_id) - - # Get trace - await client.send_json( - { - "id": next_id(), - "type": "trace/get", - "domain": "automation", - "item_id": "sun", - "run_id": run_id, - } - ) - response = await client.receive_json() - assert response["success"] - trace = response["result"] - assert len(trace["trace"]["condition/0"]) == 1 - condition_trace = trace["trace"]["condition/0"][0]["result"] - assert condition_trace == expected - - -async def test_if_action_before_sunrise_no_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunrise. - - Before sunrise is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = sunrise -> 'before sunrise' true - now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T13:33:18.342542+00:00"}, - ) - - -async def test_if_action_after_sunrise_no_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunrise. - - After sunrise is true from sunrise until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s -> 'after sunrise' not true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = sunrise + 1s -> 'after sunrise' true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = local midnight -> 'after sunrise' not true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, - ) - - # now = local midnight - 1s -> 'after sunrise' true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T13:33:18.342542+00:00"}, - ) - - -async def test_if_action_before_sunrise_with_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunrise with offset. - - Before sunrise is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "before": SUN_EVENT_SUNRISE, - "before_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise + 1s + 1h -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 33, 19, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunrise + 1h -> 'before sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 33, 18, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = UTC midnight -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local midnight -> 'before sunrise' with offset +1h true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local midnight - 1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunset -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunset -1s -> 'before sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-16T14:33:18.342542+00:00"}, - ) - - -async def test_if_action_before_sunset_with_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunset with offset. - - Before sunset is true from midnight until sunset, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "before": "sunset", - "before_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = local midnight -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true - now = datetime(2015, 9, 17, 2, 53, 46, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = sunset + 1h -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = UTC midnight -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 3 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = UTC midnight - 1s -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 4 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = sunrise -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 33, 18, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 5 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = sunrise -1s -> 'before sunset' with offset +1h true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 6 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = local midnight-1s -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 6 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-17T02:53:44.723614+00:00"}, - ) - - -async def test_if_action_after_sunrise_with_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunrise with offset. - - After sunrise is true from sunrise until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": SUN_EVENT_SUNRISE, - "after_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s + 1h -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 14, 33, 17, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunrise + 1h -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 14, 33, 58, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = UTC noon -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local noon -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local noon - 1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 3 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunset -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 4 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = sunset + 1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 1, 53, 45, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 5 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local midnight-1s -> 'after sunrise' with offset +1h true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 6 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T14:33:18.342542+00:00"}, - ) - - # now = local midnight -> 'after sunrise' with offset +1h not true - now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 6 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-17T14:33:57.053037+00:00"}, - ) - - -async def test_if_action_after_sunset_with_offset( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunset with offset. - - After sunset is true from sunset until midnight, local time. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": "sunset", - "after_offset": "+1:00:00", - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunset - 1s + 1h -> 'after sunset' with offset +1h not true - now = datetime(2015, 9, 17, 2, 53, 44, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = sunset + 1h -> 'after sunset' with offset +1h true - now = datetime(2015, 9, 17, 2, 53, 45, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, - ) - - # now = midnight-1s -> 'after sunset' with offset +1h true - now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-09-16T02:55:06.099767+00:00"}, - ) - - # now = midnight -> 'after sunset' with offset +1h not true - now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-09-17T02:53:44.723614+00:00"}, - ) - - -async def test_if_action_after_and_before_during( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunrise and before sunset. - - This is true from sunrise until sunset. - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "after": SUN_EVENT_SUNRISE, - "before": SUN_EVENT_SUNSET, - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": False, - "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true - now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-09-17T01:53:44.723614+00:00"}, - ) - - # now = sunrise + 1s -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = sunset - 1s -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = 9AM local -> 'after sunrise' + 'before sunset' true - now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 3 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_before": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_after": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - -async def test_if_action_before_or_after_during( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunrise or after sunset. - - This is true from midnight until sunrise and from sunset until midnight - """ - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": { - "condition": "sun", - "before": SUN_EVENT_SUNRISE, - "after": SUN_EVENT_SUNSET, - }, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-09-16 06:33:18 local, sunset: 2015-09-16 18:53:45 local - # sunrise: 2015-09-16 13:33:18 UTC, sunset: 2015-09-17 01:53:45 UTC - # now = sunrise - 1s -> 'before sunrise' | 'after sunset' true - now = datetime(2015, 9, 16, 13, 33, 17, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = sunset + 1s -> 'before sunrise' | 'after sunset' true - now = datetime(2015, 9, 17, 1, 53, 46, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = sunrise + 1s -> 'before sunrise' | 'after sunset' false - now = datetime(2015, 9, 16, 13, 33, 19, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": False, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = sunset - 1s -> 'before sunrise' | 'after sunset' false - now = datetime(2015, 9, 17, 1, 53, 44, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": False, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = midnight + 1s local -> 'before sunrise' | 'after sunset' true - now = datetime(2015, 9, 16, 7, 0, 1, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 3 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - # now = midnight - 1s local -> 'before sunrise' | 'after sunset' true - now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 4 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - { - "result": True, - "wanted_time_after": "2015-09-17T01:53:44.723614+00:00", - "wanted_time_before": "2015-09-16T13:33:18.342542+00:00", - }, - ) - - -async def test_if_action_before_sunrise_no_offset_kotzebue( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - After sunrise is true from sunrise until midnight, local time. - """ - await hass.config.async_set_time_zone("America/Anchorage") - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunrise + 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 15, 21, 13, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = sunrise - 1h -> 'before sunrise' true - now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-07-23T15:12:19.155123+00:00"}, - ) - - -async def test_if_action_after_sunrise_no_offset_kotzebue( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - Before sunrise is true from midnight until sunrise, local time. - """ - await hass.config.async_set_time_zone("America/Anchorage") - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunrise -> 'after sunrise' true - now = datetime(2015, 7, 24, 15, 21, 12, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = sunrise - 1h -> 'after sunrise' not true - now = datetime(2015, 7, 24, 14, 21, 12, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = local midnight -> 'after sunrise' not true - now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-07-24T15:16:46.975735+00:00"}, - ) - - # now = local midnight - 1s -> 'after sunrise' true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-07-23T15:12:19.155123+00:00"}, - ) - - -async def test_if_action_before_sunset_no_offset_kotzebue( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was before sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - Before sunset is true from midnight until sunset, local time. - """ - await hass.config.async_set_time_zone("America/Anchorage") - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunset + 1s -> 'before sunset' not true - now = datetime(2015, 7, 25, 11, 13, 34, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 0 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, - ) - - # now = sunset - 1h-> 'before sunset' true - now = datetime(2015, 7, 25, 10, 13, 33, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-07-25T11:13:32.501837+00:00"}, - ) - - # now = local midnight -> 'before sunrise' true - now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_before": "2015-07-24T11:17:54.446913+00:00"}, - ) - - # now = local midnight - 1s -> 'before sunrise' not true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_before": "2015-07-23T11:22:18.467277+00:00"}, - ) - - -async def test_if_action_after_sunset_no_offset_kotzebue( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - service_calls: list[ServiceCall], -) -> None: - """Test if action was after sunrise. - - Local timezone: Alaska time - Location: Kotzebue, which has a very skewed local timezone with sunrise - at 7 AM and sunset at 3AM during summer - After sunset is true from sunset until midnight, local time. - """ - await hass.config.async_set_time_zone("America/Anchorage") - hass.config.latitude = 66.5 - hass.config.longitude = 162.4 - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "id": "sun", - "trigger": {"platform": "event", "event_type": "test_event"}, - "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, - "action": {"service": "test.automation"}, - } - }, - ) - - # sunrise: 2015-07-24 07:21:12 local, sunset: 2015-07-25 03:13:33 local - # sunrise: 2015-07-24 15:21:12 UTC, sunset: 2015-07-25 11:13:33 UTC - # now = sunset -> 'after sunset' true - now = datetime(2015, 7, 25, 11, 13, 33, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, - ) - - # now = sunset - 1s -> 'after sunset' not true - now = datetime(2015, 7, 25, 11, 13, 32, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-07-25T11:13:32.501837+00:00"}, - ) - - # now = local midnight -> 'after sunset' not true - now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 1 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": False, "wanted_time_after": "2015-07-24T11:17:54.446913+00:00"}, - ) - - # now = local midnight - 1s -> 'after sunset' true - now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) - with freeze_time(now): - hass.bus.async_fire("test_event") - await hass.async_block_till_done() - assert len(service_calls) == 2 - await assert_automation_condition_trace( - hass_ws_client, - "sun", - {"result": True, "wanted_time_after": "2015-07-23T11:22:18.467277+00:00"}, - ) - - async def test_trigger(hass: HomeAssistant) -> None: """Test trigger condition.""" config = {"alias": "Trigger Cond", "condition": "trigger", "id": "123456"} diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index ecf5271dafd..aec687be40a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1460,11 +1460,6 @@ def test_key_value_schemas_with_default() -> None: [ ({"delay": "{{ invalid"}, "should be format 'HH:MM'"), ({"wait_template": "{{ invalid"}, "invalid template"), - ({"condition": "invalid"}, "Unexpected value for condition: 'invalid'"), - ( - {"condition": "not", "conditions": {"condition": "invalid"}}, - "Unexpected value for condition: 'invalid'", - ), # The validation error message could be improved to explain that this is not # a valid shorthand template ( @@ -1496,7 +1491,7 @@ def test_key_value_schemas_with_default() -> None: ) @pytest.mark.usefixtures("hass") def test_script(caplog: pytest.LogCaptureFixture, config: dict, error: str) -> None: - """Test script validation is user friendly.""" + """Test script action validation is user friendly.""" with pytest.raises(vol.Invalid, match=error): cv.script_action(config) From b15c9ad130229bd4137f75fc8cd30b27392276d5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 13 May 2025 07:19:07 +0200 Subject: [PATCH 0399/1175] Link Shelly device entry with Shelly BT scanner entry (#144626) * Add BT address to DeviceInfo.connections * Cleaning * Use bluetooth_source property * Add test * Add connections property --- .../components/shelly/coordinator.py | 21 +++++++++++++++++-- tests/components/shelly/test_coordinator.py | 18 ++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index f980ba8f914..e4af35484c8 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -33,7 +33,11 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + CONNECTION_NETWORK_MAC, + format_mac, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .bluetooth import async_connect_scanner @@ -160,6 +164,11 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( """Sleep period of the device.""" return self.config_entry.data.get(CONF_SLEEP_PERIOD, 0) + @property + def connections(self) -> set[tuple[str, str]]: + """Connections of the device.""" + return {(CONNECTION_NETWORK_MAC, self.mac)} + def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" self._pending_platforms = pending_platforms @@ -167,7 +176,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( device_entry = dev_reg.async_get_or_create( config_entry_id=self.config_entry.entry_id, name=self.name, - connections={(CONNECTION_NETWORK_MAC, self.mac)}, + connections=self.connections, identifiers={(DOMAIN, self.mac)}, manufacturer="Shelly", model=get_shelly_model_name(self.model, self.sleep_period, self.device), @@ -523,6 +532,14 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """ return format_mac(bluetooth_mac_from_primary_mac(self.mac)).upper() + @property + def connections(self) -> set[tuple[str, str]]: + """Connections of the device.""" + connections = super().connections + if not self.sleep_period: + connections.add((CONNECTION_BLUETOOTH, self.bluetooth_source)) + return connections + async def async_device_online(self, source: str) -> None: """Handle device going online.""" if not self.sleep_period: diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index cf7f82014a0..aae452538bb 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1078,3 +1078,21 @@ async def test_xmod_model_lookup( ) assert device assert device.model == xmod_model + + +async def test_device_entry_bt_address( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_rpc_device: Mock, +) -> None: + """Check if BT address is added to device entry connections.""" + entry = await init_integration(hass, 2) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, entry.entry_id)}, + connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, + ) + + assert device + assert len(device.connections) == 2 + assert (dr.CONNECTION_BLUETOOTH, "12:34:56:78:9A:BE") in device.connections From eec617b391972b418b425c078864d4de699aef4d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 May 2025 07:54:37 +0200 Subject: [PATCH 0400/1175] Add comments to samsungtv config flow tests (#144787) --- .../components/samsungtv/test_config_flow.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 83639171576..dd744bd82ca 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -17,7 +17,10 @@ from websockets import frames from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant import config_entries -from homeassistant.components.samsungtv.config_flow import SamsungTVConfigFlow +from homeassistant.components.samsungtv.config_flow import ( + SamsungTVConfigFlow, + _strip_uuid, +) from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_SESSION_ID, @@ -45,6 +48,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import BaseServiceInfo, FlowResultType +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, @@ -2001,11 +2005,9 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures( - "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" -) +@pytest.mark.usefixtures("remote_websocket") async def test_update_incorrect_udn_matching_mac_from_dhcp( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP updates the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( @@ -2016,6 +2018,12 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( ) entry.add_to_hass(hass) + assert entry.data[CONF_HOST] == MOCK_DHCP_DATA.ip + assert entry.data[CONF_MAC] == dr.format_mac( + rest_api.rest_device_info.return_value["device"]["wifiMac"] + ) + assert entry.unique_id != _strip_uuid(rest_api.rest_device_info.return_value["id"]) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -2026,15 +2034,14 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - assert entry.data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" + + # Same IP + same MAC => unique id updated assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures( - "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" -) +@pytest.mark.usefixtures("remote_websocket") async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP does not update the wrong udn from ssdp via host match.""" entry = MockConfigEntry( @@ -2045,6 +2052,12 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( ) entry.add_to_hass(hass) + assert entry.data[CONF_HOST] == MOCK_DHCP_DATA.ip + assert entry.data[CONF_MAC] != dr.format_mac( + rest_api.rest_device_info.return_value["device"]["wifiMac"] + ) + assert entry.unique_id != _strip_uuid(rest_api.rest_device_info.return_value["id"]) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -2055,7 +2068,8 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" - assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" + + # Same IP + different MAC => unique id not updated assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" From d4c2356c70be7280050b8a213c41ccfa2da1b3c1 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 13 May 2025 16:05:33 +1000 Subject: [PATCH 0401/1175] Create stream on demand in Teslemetry (#144777) Create stream on demand --- .../components/teslemetry/__init__.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 1eb1ea54091..7b46caf2b43 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -95,13 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - energysites: list[TeslemetryEnergyData] = [] # Create the stream - stream = TeslemetryStream( - session, - access_token, - server=f"{region.lower()}.teslemetry.com", - parse_timestamp=True, - manual=True, - ) + stream: TeslemetryStream | None = None for product in products: if ( @@ -123,6 +117,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - serial_number=vin, ) + # Create stream if required + if not stream: + stream = TeslemetryStream( + session, + access_token, + server=f"{region.lower()}.teslemetry.com", + parse_timestamp=True, + manual=True, + ) + remove_listener = stream.async_add_listener( create_handle_vehicle_stream(vin, coordinator), {"vin": vin}, @@ -240,7 +244,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - entry.runtime_data = TeslemetryData(vehicles, energysites, scopes) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream") + if stream: + entry.async_create_background_task(hass, stream.listen(), "Teslemetry Stream") return True From 3e07f6543e2edc3721f0f24a969f07df1b2c644a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 13 May 2025 08:14:55 +0200 Subject: [PATCH 0402/1175] Update debugpy to v1.8.14 (#144755) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 21211d334df..0b9f8ea55f5 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -6,5 +6,5 @@ "integration_type": "service", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["debugpy==1.8.13"] + "requirements": ["debugpy==1.8.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 16592ce1d3f..3d9ca46c149 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ datapoint==0.9.9 dbus-fast==2.43.0 # homeassistant.components.debugpy -debugpy==1.8.13 +debugpy==1.8.14 # homeassistant.components.decora_wifi # decora-wifi==1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b29082b212..3333523b1cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -647,7 +647,7 @@ datapoint==0.9.9 dbus-fast==2.43.0 # homeassistant.components.debugpy -debugpy==1.8.13 +debugpy==1.8.14 # homeassistant.components.ecovacs deebot-client==13.1.0 From b0fb16d48d9c46f364f5451cfe3859981029621f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 May 2025 09:54:26 +0200 Subject: [PATCH 0403/1175] Remove obsolete compatibility code from SamsungTV (#144800) --- .../components/samsungtv/__init__.py | 33 +--------- tests/components/samsungtv/test_init.py | 63 +------------------ 2 files changed, 4 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index e306e00691f..0fbd0f6d1e0 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -21,26 +21,19 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.debounce import Debouncer -from .bridge import ( - SamsungTVBridge, - async_get_device_info, - mac_from_device_info, - model_requires_encryption, -) +from .bridge import SamsungTVBridge, mac_from_device_info, model_requires_encryption from .const import ( CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, ENTRY_RELOAD_COOLDOWN, - LEGACY_PORT, LOGGER, METHOD_ENCRYPTED_WEBSOCKET, - METHOD_LEGACY, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, ) @@ -180,30 +173,10 @@ async def _async_create_bridge_with_updated_data( """Create a bridge object and update any missing data in the config entry.""" updated_data: dict[str, str | int] = {} host: str = entry.data[CONF_HOST] - port: int | None = entry.data.get(CONF_PORT) - method: str | None = entry.data.get(CONF_METHOD) + method: str = entry.data[CONF_METHOD] load_info_attempted = False info: dict[str, Any] | None = None - if not port or not method: - LOGGER.debug("Attempting to get port or method for %s", host) - if method == METHOD_LEGACY: - port = LEGACY_PORT - else: - # When we imported from yaml we didn't setup the method - # because we didn't know it - _result, port, method, info = await async_get_device_info(hass, host) - load_info_attempted = True - if not port or not method: - raise ConfigEntryNotReady( - translation_domain=DOMAIN, - translation_key="failed_to_determine_connection_method", - ) - - LOGGER.debug("Updated port to %s and method to %s for %s", port, method, host) - updated_data[CONF_PORT] = port - updated_data[CONF_METHOD] = method - bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) mac: str | None = entry.data.get(CONF_MAC) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 0b72a112301..e2e7f2323ed 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,6 +1,5 @@ """Tests for the Samsung TV Integration.""" -from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -16,8 +15,6 @@ from homeassistant.components.samsungtv.const import ( CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, - LEGACY_PORT, - METHOD_LEGACY, METHOD_WEBSOCKET, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, @@ -53,6 +50,7 @@ MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", CONF_METHOD: METHOD_WEBSOCKET, + CONF_PORT: 8001, } @@ -78,42 +76,6 @@ async def test_setup(hass: HomeAssistant) -> None: ) -async def test_setup_without_port_device_offline(hass: HomeAssistant) -> None: - """Test import from yaml when the device is offline.""" - with ( - patch("homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError), - patch( - "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", - side_effect=OSError, - ), - patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", - side_effect=OSError, - ), - patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=None, - ), - ): - await setup_samsungtv_entry(hass, MOCK_CONFIG) - - config_entries_domain = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries_domain) == 1 - assert config_entries_domain[0].state is ConfigEntryState.SETUP_RETRY - - -@pytest.mark.usefixtures( - "remote_websocket", "remote_encrypted_websocket_failing", "rest_api" -) -async def test_setup_without_port_device_online(hass: HomeAssistant) -> None: - """Test import from yaml when the device is online.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - - config_entries_domain = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries_domain) == 1 - assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" - - @pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture @@ -182,29 +144,6 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: assert len(flows_in_progress) == 1 -@pytest.mark.usefixtures( - "remote_legacy", "remote_encrypted_websocket_failing", "rest_api_failing" -) -@pytest.mark.parametrize( - "entry_data", - [ - {CONF_HOST: "1.2.3.4"}, # Missing port/method - {CONF_HOST: "1.2.3.4", CONF_PORT: LEGACY_PORT}, # Missing method - {CONF_HOST: "1.2.3.4", CONF_METHOD: METHOD_LEGACY}, # Missing port - ], -) -async def test_update_imported_legacy( - hass: HomeAssistant, entry_data: dict[str, Any] -) -> None: - """Test updating an imported legacy entry.""" - await setup_samsungtv_entry(hass, entry_data) - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - assert entries[0].data[CONF_METHOD] == METHOD_LEGACY - assert entries[0].data[CONF_PORT] == LEGACY_PORT - - @pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: """Test incorrectly formatted mac is corrected.""" From c121631fef9ed8d3a88805556a4c40af8feeaba3 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 13 May 2025 10:35:32 +0200 Subject: [PATCH 0404/1175] Refactor config flow tests to improve result variable usage in Overkiz (#143374) * Refactor test setup for unique ID migration in Overkiz integration * Refactor test cases to unify result variable usage in Overkiz config flow tests (resultn -> result) * Revert change in test_init --- tests/components/overkiz/test_config_flow.py | 216 +++++++++---------- 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 5c98b4e9260..410c2ebb5f1 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -82,21 +82,21 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with ( patch("pyoverkiz.client.OverkizClient.login", return_value=True), @@ -105,7 +105,7 @@ async def test_form_cloud(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> N return_value=MOCK_GATEWAY_RESPONSE, ), ): - await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) @@ -125,13 +125,13 @@ async def test_form_only_cloud_supported( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER2}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with ( patch("pyoverkiz.client.OverkizClient.login", return_value=True), @@ -140,7 +140,7 @@ async def test_form_only_cloud_supported( return_value=MOCK_GATEWAY_RESPONSE, ), ): - await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) @@ -160,28 +160,28 @@ async def test_form_local_happy_flow( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "local"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "local" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local" with patch.multiple( "pyoverkiz.client.OverkizClient", login=AsyncMock(return_value=True), get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "gateway-1234-5678-1234.local:8443", @@ -192,9 +192,9 @@ async def test_form_local_happy_flow( await hass.async_block_till_done() - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == "gateway-1234-5678-1234.local:8443" - assert result4["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "gateway-1234-5678-1234.local:8443" + assert result["data"] == { "host": "gateway-1234-5678-1234.local:8443", "token": TEST_TOKEN, "verify_ssl": True, @@ -227,32 +227,32 @@ async def test_form_invalid_auth_cloud( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) await hass.async_block_till_done() - assert result4["type"] is FlowResultType.FORM - assert result4["errors"] == {"base": error} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} @pytest.mark.parametrize( @@ -283,24 +283,24 @@ async def test_form_invalid_auth_local( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "local"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "local" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": TEST_HOST, @@ -311,8 +311,8 @@ async def test_form_invalid_auth_local( await hass.async_block_till_done() - assert result4["type"] is FlowResultType.FORM - assert result4["errors"] == {"base": error} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} @pytest.mark.parametrize( @@ -331,25 +331,25 @@ async def test_form_invalid_cozytouch_auth( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER_COZYTOUCH}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with patch("pyoverkiz.client.OverkizClient.login", side_effect=side_effect): - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) await hass.async_block_till_done() - assert result3["type"] is FlowResultType.FORM - assert result3["errors"] == {"base": error} - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + assert result["step_id"] == "cloud" async def test_cloud_abort_on_duplicate_entry( @@ -369,21 +369,21 @@ async def test_cloud_abort_on_duplicate_entry( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with ( patch("pyoverkiz.client.OverkizClient.login", return_value=True), @@ -392,13 +392,13 @@ async def test_cloud_abort_on_duplicate_entry( return_value=MOCK_GATEWAY_RESPONSE, ), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] is FlowResultType.ABORT - assert result4["reason"] == "already_configured" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" async def test_local_abort_on_duplicate_entry( @@ -425,21 +425,21 @@ async def test_local_abort_on_duplicate_entry( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "local"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "local" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local" with patch.multiple( "pyoverkiz.client.OverkizClient", @@ -447,7 +447,7 @@ async def test_local_abort_on_duplicate_entry( get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE), get_setup_option=AsyncMock(return_value=True), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": TEST_HOST, @@ -456,8 +456,8 @@ async def test_local_abort_on_duplicate_entry( }, ) - assert result4["type"] is FlowResultType.ABORT - assert result4["reason"] == "already_configured" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" async def test_cloud_allow_multiple_unique_entries( @@ -478,21 +478,21 @@ async def test_cloud_allow_multiple_unique_entries( assert result["type"] is FlowResultType.FORM - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with ( patch("pyoverkiz.client.OverkizClient.login", return_value=True), @@ -501,14 +501,14 @@ async def test_cloud_allow_multiple_unique_entries( return_value=MOCK_GATEWAY_RESPONSE, ), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == TEST_EMAIL - assert result4["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_EMAIL + assert result["data"] == { "api_type": "cloud", "username": TEST_EMAIL, "password": TEST_PASSWORD, @@ -544,7 +544,7 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: return_value=MOCK_GATEWAY_RESPONSE, ), ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ "username": TEST_EMAIL, @@ -552,8 +552,8 @@ async def test_cloud_reauth_success(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -586,7 +586,7 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: return_value=MOCK_GATEWAY2_RESPONSE, ), ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ "username": TEST_EMAIL, @@ -594,8 +594,8 @@ async def test_cloud_reauth_wrong_account(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_wrong_account" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_wrong_account" async def test_local_reauth_legacy(hass: HomeAssistant) -> None: @@ -759,15 +759,15 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) @@ -776,7 +776,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch("pyoverkiz.client.OverkizClient.get_gateways", return_value=None), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "username": TEST_EMAIL, @@ -784,9 +784,9 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No }, ) - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == TEST_EMAIL - assert result4["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_EMAIL + assert result["data"] == { "username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_SERVER, @@ -830,21 +830,21 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "cloud"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "cloud" with ( patch("pyoverkiz.client.OverkizClient.login", return_value=True), @@ -853,14 +853,14 @@ async def test_zeroconf_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) - return_value=MOCK_GATEWAY_RESPONSE, ), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == TEST_EMAIL - assert result4["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_EMAIL + assert result["data"] == { "username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_SERVER, @@ -883,28 +883,28 @@ async def test_local_zeroconf_flow( assert result["type"] is FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"hub": TEST_SERVER}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "local_or_cloud" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local_or_cloud" - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_type": "local"}, ) - assert result3["type"] is FlowResultType.FORM - assert result3["step_id"] == "local" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "local" with patch.multiple( "pyoverkiz.client.OverkizClient", login=AsyncMock(return_value=True), get_gateways=AsyncMock(return_value=MOCK_GATEWAY_RESPONSE), ): - result4 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "gateway-1234-5678-9123.local:8443", @@ -913,11 +913,11 @@ async def test_local_zeroconf_flow( }, ) - assert result4["type"] is FlowResultType.CREATE_ENTRY - assert result4["title"] == "gateway-1234-5678-9123.local:8443" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "gateway-1234-5678-9123.local:8443" # Verify no username/password in data - assert result4["data"] == { + assert result["data"] == { "host": "gateway-1234-5678-9123.local:8443", "token": TEST_TOKEN, "verify_ssl": False, From 2db60340c2a0ab2b7b03ba7f8bc38985a6d92615 Mon Sep 17 00:00:00 2001 From: Jeremiah Paige Date: Tue, 13 May 2025 01:43:03 -0700 Subject: [PATCH 0405/1175] Add typing to wsdot (#143117) * increase wsdot typing * remove Final types * help out mypy * simplify wsdot types * minor wsdot type changes * type wsdot state --- homeassistant/components/wsdot/sensor.py | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 8ae93c809f2..b3eb2715562 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -65,7 +65,7 @@ def setup_platform( name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID) sensors.append( WashingtonStateTravelTimeSensor( - name, config.get(CONF_API_KEY), travel_time.get(CONF_ID) + name, config[CONF_API_KEY], travel_time.get(CONF_ID) ) ) @@ -82,20 +82,20 @@ class WashingtonStateTransportSensor(SensorEntity): _attr_icon = ICON - def __init__(self, name, access_code): + def __init__(self, name: str, access_code: str) -> None: """Initialize the sensor.""" - self._data = {} + self._data: dict[str, str | int | None] = {} self._access_code = access_code self._name = name - self._state = None + self._state: int | None = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._state @@ -106,7 +106,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = UnitOfTime.MINUTES - def __init__(self, name, access_code, travel_time_id): + def __init__(self, name: str, access_code: str, travel_time_id: str) -> None: """Construct a travel time sensor.""" self._travel_time_id = travel_time_id WashingtonStateTransportSensor.__init__(self, name, access_code) @@ -123,13 +123,17 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): _LOGGER.warning("Invalid response from WSDOT API") else: self._data = response.json() - self._state = self._data.get(ATTR_CURRENT_TIME) + _state = self._data.get(ATTR_CURRENT_TIME) + if not isinstance(_state, int): + self._state = None + else: + self._state = _state @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return other details about the sensor state.""" if self._data is not None: - attrs = {} + attrs: dict[str, str | int | None | datetime] = {} for key in ( ATTR_AVG_TIME, ATTR_NAME, @@ -144,12 +148,15 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): return None -def _parse_wsdot_timestamp(timestamp): +def _parse_wsdot_timestamp(timestamp: Any) -> datetime | None: """Convert WSDOT timestamp to datetime.""" - if not timestamp: + if not isinstance(timestamp, str): return None # ex: Date(1485040200000-0800) - milliseconds, tzone = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp).groups() + timestamp_parts = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp) + if timestamp_parts is None: + return None + milliseconds, tzone = timestamp_parts.groups() return datetime.fromtimestamp( int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone))) ) From a7787d6080203acea87558f379854c12f3c0c7a3 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 13 May 2025 10:46:46 +0200 Subject: [PATCH 0406/1175] Fix blocking call in azure storage (#144803) --- .../components/azure_storage/__init__.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/azure_storage/__init__.py b/homeassistant/components/azure_storage/__init__.py index f22e7b70c12..00e419fd3c9 100644 --- a/homeassistant/components/azure_storage/__init__.py +++ b/homeassistant/components/azure_storage/__init__.py @@ -39,11 +39,20 @@ async def async_setup_entry( session = async_create_clientsession( hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60) ) - container_client = ContainerClient( - account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/", - container_name=entry.data[CONF_CONTAINER_NAME], - credential=entry.data[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=session), + + def create_container_client() -> ContainerClient: + """Create a ContainerClient.""" + + return ContainerClient( + account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/", + container_name=entry.data[CONF_CONTAINER_NAME], + credential=entry.data[CONF_STORAGE_ACCOUNT_KEY], + transport=AioHttpTransport(session=session), + ) + + # has a blocking call to open in cpython + container_client: ContainerClient = await hass.async_add_executor_job( + create_container_client ) try: From 5c6984d3261674981f79791228abe1e0fb29d2a0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 May 2025 10:47:26 +0200 Subject: [PATCH 0407/1175] Do not abort on invalid host in SamsungTV user flow (#144794) --- .../components/samsungtv/config_flow.py | 35 +++++++------ .../components/samsungtv/strings.json | 2 +- tests/components/samsungtv/conftest.py | 2 +- .../components/samsungtv/test_config_flow.py | 49 ++++++++++--------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 9867e44254e..806a4db4bf6 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -56,7 +56,6 @@ from .const import ( RESULT_INVALID_PIN, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, - RESULT_UNKNOWN_HOST, SUCCESSFUL_RESULTS, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, @@ -252,32 +251,40 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self._mac = mac return True - async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> None: + async def _async_set_name_host_from_input(self, user_input: dict[str, Any]) -> bool: try: self._host = await self.hass.async_add_executor_job( socket.gethostbyname, user_input[CONF_HOST] ) except socket.gaierror as err: - raise AbortFlow(RESULT_UNKNOWN_HOST) from err + LOGGER.debug("Failed to get IP for %s: %s", user_input[CONF_HOST], err) + return False self._title = self._host + return True 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] | None = None if user_input is not None: - await self._async_set_name_host_from_input(user_input) - await self._async_create_bridge() - assert self._bridge - self._async_abort_entries_match({CONF_HOST: self._host}) - if self._bridge.method != METHOD_LEGACY: - # Legacy bridge does not provide device info - await self._async_set_device_unique_id(raise_on_progress=False) - if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: - return await self.async_step_encrypted_pairing() - return await self.async_step_pairing({}) + if await self._async_set_name_host_from_input(user_input): + await self._async_create_bridge() + assert self._bridge + self._async_abort_entries_match({CONF_HOST: self._host}) + if self._bridge.method != METHOD_LEGACY: + # Legacy bridge does not provide device info + await self._async_set_device_unique_id(raise_on_progress=False) + if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_encrypted_pairing() + return await self.async_step_pairing({}) + errors = {"base": "invalid_host"} - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input), + errors=errors, + ) async def async_step_pairing( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index 17fde5db5bf..431c9bd3ec6 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -43,6 +43,7 @@ }, "error": { "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]", + "invalid_host": "Host is invalid, please try again.", "invalid_pin": "PIN is invalid, please try again." }, "abort": { @@ -52,7 +53,6 @@ "id_missing": "This Samsung device doesn't have a SerialNumber.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "not_supported": "This Samsung device is currently not supported.", - "unknown": "[%key:common::config_flow::error::unknown%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d0c53020d85..6fe784addd7 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -67,7 +67,7 @@ def fake_host_fixture() -> Generator[None]: """Patch gethostbyname.""" with patch( "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", - return_value="fake_host", + return_value="10.20.43.21", ): yield diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index dd744bd82ca..f62c3cc1ae8 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -2,6 +2,7 @@ from copy import deepcopy from ipaddress import ip_address +import socket from unittest.mock import ANY, AsyncMock, Mock, call, patch import pytest @@ -106,35 +107,22 @@ AUTODETECT_LEGACY = { "id": "ha.component.samsung", "method": METHOD_LEGACY, "port": LEGACY_PORT, - "host": "fake_host", + "host": "10.20.43.21", "timeout": TIMEOUT_REQUEST, } -AUTODETECT_WEBSOCKET_PLAIN = { - "host": "fake_host", - "name": "HomeAssistant", - "port": 8001, - "timeout": TIMEOUT_REQUEST, - "token": None, -} AUTODETECT_WEBSOCKET_SSL = { - "host": "fake_host", + "host": "10.20.43.21", "name": "HomeAssistant", "port": 8002, "timeout": TIMEOUT_REQUEST, "token": None, } DEVICEINFO_WEBSOCKET_SSL = { - "host": "fake_host", + "host": "10.20.43.21", "session": ANY, "port": 8002, "timeout": TIMEOUT_WEBSOCKET, } -DEVICEINFO_WEBSOCKET_NO_SSL = { - "host": "fake_host", - "session": ANY, - "port": 8001, - "timeout": TIMEOUT_WEBSOCKET, -} pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -149,14 +137,27 @@ async def test_user_legacy(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - # entry was added + # Wrong host allow to retry + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + side_effect=socket.gaierror("[Error -2] Name or Service not known"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_host"} + + # Good host creates entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) # legacy tv entry created assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == "fake_host" - assert result["data"][CONF_HOST] == "fake_host" + assert result["title"] == "10.20.43.21" + assert result["data"][CONF_HOST] == "10.20.43.21" assert result["data"][CONF_METHOD] == METHOD_LEGACY assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None @@ -189,8 +190,8 @@ async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: # legacy tv entry created assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "fake_host" - assert result3["data"][CONF_HOST] == "fake_host" + assert result3["title"] == "10.20.43.21" + assert result3["data"][CONF_HOST] == "10.20.43.21" assert result3["data"][CONF_METHOD] == METHOD_LEGACY assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result3["data"][CONF_MODEL] is None @@ -219,7 +220,7 @@ async def test_user_websocket(hass: HomeAssistant) -> None: # websocket tv entry created assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == "10.20.43.21" assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" @@ -267,7 +268,7 @@ async def test_user_encrypted_websocket( assert result4["type"] is FlowResultType.CREATE_ENTRY assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" - assert result4["data"][CONF_HOST] == "fake_host" + assert result4["data"][CONF_HOST] == "10.20.43.21" assert result4["data"][CONF_MAC] == "aa:bb:aa:aa:aa:aa" assert result4["data"][CONF_MANUFACTURER] == "Samsung" assert result4["data"][CONF_MODEL] == "UE48JU6400" @@ -398,7 +399,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == "10.20.43.21" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" From 55b9dee448dc412a6a4e32c9e086970eb67562ed Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 13 May 2025 13:12:00 +0200 Subject: [PATCH 0408/1175] Fix Z-Wave unique id after controller reset (#144813) --- homeassistant/components/zwave_js/api.py | 23 +++++++++++ .../components/zwave_js/config_flow.py | 26 +----------- homeassistant/components/zwave_js/helpers.py | 26 ++++++++++++ tests/components/zwave_js/conftest.py | 40 ++++++++++++++++++ tests/components/zwave_js/test_api.py | 40 ++++++++++++++++++ tests/components/zwave_js/test_config_flow.py | 41 +------------------ 6 files changed, 133 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5eb59c6c288..f480c822a8c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -71,6 +71,7 @@ from homeassistant.components.websocket_api import ( ActiveConnection, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -88,13 +89,16 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, + LOGGER, RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( + CannotConnect, async_enable_statistics, async_get_node_from_device_id, async_get_provisioning_entry_from_device_id, + async_get_version_info, get_device_id, ) @@ -2865,6 +2869,25 @@ async def websocket_hard_reset_controller( async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + # When resetting the controller, the controller home id is also changed. + # The controller state in the client is stale after resetting the controller, + # so get the new home id with a new client using the helper function. + # The client state will be refreshed by reloading the config entry, + # after the unique id of the config entry has been updated. + try: + version_info = await async_get_version_info(hass, entry.data[CONF_URL]) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + LOGGER.error( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller reset" + ) + else: + hass.config_entries.async_update_entry( + entry, unique_id=str(version_info.home_id) + ) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 407af9e902b..e52a5e784e8 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -9,14 +9,13 @@ import logging from pathlib import Path from typing import Any -import aiohttp from awesomeversion import AwesomeVersion from serial.tools import list_ports import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.exceptions import FailedCommand from zwave_js_server.model.driver import Driver -from zwave_js_server.version import VersionInfo, get_server_version +from zwave_js_server.version import VersionInfo from homeassistant.components import usb from homeassistant.components.hassio import ( @@ -36,7 +35,6 @@ from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo @@ -69,6 +67,7 @@ from .const import ( DOMAIN, RESTORE_NVM_DRIVER_READY_TIMEOUT, ) +from .helpers import CannotConnect, async_get_version_info _LOGGER = logging.getLogger(__name__) @@ -79,7 +78,6 @@ ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" -SERVER_VERSION_TIMEOUT = 10 ADDON_LOG_LEVELS = { "error": "Error", @@ -130,22 +128,6 @@ async def validate_input(hass: HomeAssistant, user_input: dict) -> VersionInfo: raise InvalidInput("cannot_connect") from err -async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: - """Return Z-Wave JS version info.""" - try: - async with asyncio.timeout(SERVER_VERSION_TIMEOUT): - version_info: VersionInfo = await get_server_version( - ws_address, async_get_clientsession(hass) - ) - except (TimeoutError, aiohttp.ClientError) as err: - # We don't want to spam the log if the add-on isn't started - # or takes a long time to start. - _LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) - raise CannotConnect from err - - return version_info - - def get_usb_ports() -> dict[str, str]: """Return a dict of USB ports and their friendly names.""" ports = list_ports.comports() @@ -1357,10 +1339,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return client.driver -class CannotConnect(HomeAssistantError): - """Indicate connection error.""" - - class InvalidInput(HomeAssistantError): """Error to indicate input data is invalid.""" diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index ded87b590a4..bfa093f7db9 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,11 +2,13 @@ from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import astuple, dataclass import logging from typing import Any, cast +import aiohttp import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -25,6 +27,7 @@ from zwave_js_server.model.value import ( ValueDataType, get_value_id_str, ) +from zwave_js_server.version import VersionInfo, get_server_version from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -38,6 +41,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.group import expand_entity_ids from homeassistant.helpers.typing import ConfigType, VolSchemaType @@ -54,6 +58,8 @@ from .const import ( LOGGER, ) +SERVER_VERSION_TIMEOUT = 10 + @dataclass class ZwaveValueID: @@ -568,3 +574,23 @@ def get_network_identifier_for_notification( return f"`{config_entry.title}`, with the home ID `{home_id}`," return f"with the home ID `{home_id}`" return "" + + +async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> VersionInfo: + """Return Z-Wave JS version info.""" + try: + async with asyncio.timeout(SERVER_VERSION_TIMEOUT): + version_info: VersionInfo = await get_server_version( + ws_address, async_get_clientsession(hass) + ) + except (TimeoutError, aiohttp.ClientError) as err: + # We don't want to spam the log if the add-on isn't started + # or takes a long time to start. + LOGGER.debug("Failed to connect to Z-Wave JS server: %s", err) + raise CannotConnect from err + + return version_info + + +class CannotConnect(HomeAssistantError): + """Indicate connection error.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 609a0229bcf..e0485ced091 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,6 +1,7 @@ """Provide common Z-Wave JS fixtures.""" import asyncio +from collections.abc import Generator import copy import io from typing import Any, cast @@ -15,6 +16,7 @@ from zwave_js_server.version import VersionInfo from homeassistant.components.zwave_js import PLATFORMS from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.util.json import JsonArrayType @@ -587,6 +589,44 @@ def mock_client_fixture( yield client +@pytest.fixture(name="server_version_side_effect") +def server_version_side_effect_fixture() -> Any | None: + """Return the server version side effect.""" + return None + + +@pytest.fixture(name="get_server_version", autouse=True) +def mock_get_server_version( + server_version_side_effect: Any | None, server_version_timeout: int +) -> Generator[AsyncMock]: + """Mock server version.""" + version_info = VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=1234, + min_schema_version=0, + max_schema_version=1, + ) + with ( + patch( + "homeassistant.components.zwave_js.helpers.get_server_version", + side_effect=server_version_side_effect, + return_value=version_info, + ) as mock_version, + patch( + "homeassistant.components.zwave_js.helpers.SERVER_VERSION_TIMEOUT", + new=server_version_timeout, + ), + ): + yield mock_version + + +@pytest.fixture(name="server_version_timeout") +def mock_server_version_timeout() -> int: + """Patch the timeout for getting server version.""" + return SERVER_VERSION_TIMEOUT + + @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state) -> Node: """Mock a multisensor 6 node.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 150ee39925b..7d4f9fe7a36 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -7,6 +7,7 @@ import json from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch +from aiohttp import ClientError import pytest from zwave_js_server.const import ( ExclusionStrategy, @@ -5096,14 +5097,17 @@ async def test_subscribe_node_statistics( async def test_hard_reset_controller( hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, client: MagicMock, + get_server_version: AsyncMock, integration: MockConfigEntry, hass_ws_client: WebSocketGenerator, ) -> None: """Test that the hard_reset_controller WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) + assert entry.unique_id == "3245146787" async def async_send_command_driver_ready( message: dict[str, Any], @@ -5138,6 +5142,40 @@ async def test_hard_reset_controller( assert client.async_send_command.call_args_list[0] == call( {"command": "driver.hard_reset"}, 25 ) + assert entry.unique_id == "1234" + + client.async_send_command.reset_mock() + + # Test client connect error when getting the server version. + + get_server_version.side_effect = ClientError("Boom!") + + await ws_client.send_json_auto_id( + { + TYPE: "zwave_js/hard_reset_controller", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, client.driver.controller.nodes[1])} + ) + assert device is not None + assert msg["result"] == device.id + assert msg["success"] + + assert client.async_send_command.call_count == 3 + # The first call is the relevant hard reset command. + # 25 is the require_schema parameter. + assert client.async_send_command.call_args_list[0] == call( + {"command": "driver.hard_reset"}, 25 + ) + assert ( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller reset" + ) in caplog.text client.async_send_command.reset_mock() @@ -5178,6 +5216,8 @@ async def test_hard_reset_controller( {"command": "driver.hard_reset"}, 25 ) + client.async_send_command.reset_mock() + # Test FailedZWaveCommand is caught with patch( "zwave_js_server.model.driver.Driver.async_hard_reset", diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 3f1d894030f..e651a92339b 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -17,8 +17,9 @@ from zwave_js_server.exceptions import FailedCommand from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow -from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE +from homeassistant.components.zwave_js.config_flow import TITLE from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN +from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.hassio import HassioServiceInfo @@ -95,44 +96,6 @@ def mock_supervisor_fixture() -> Generator[None]: yield -@pytest.fixture(name="server_version_side_effect") -def server_version_side_effect_fixture() -> Any | None: - """Return the server version side effect.""" - return None - - -@pytest.fixture(name="get_server_version", autouse=True) -def mock_get_server_version( - server_version_side_effect: Any | None, server_version_timeout: int -) -> Generator[AsyncMock]: - """Mock server version.""" - version_info = VersionInfo( - driver_version="mock-driver-version", - server_version="mock-server-version", - home_id=1234, - min_schema_version=0, - max_schema_version=1, - ) - with ( - patch( - "homeassistant.components.zwave_js.config_flow.get_server_version", - side_effect=server_version_side_effect, - return_value=version_info, - ) as mock_version, - patch( - "homeassistant.components.zwave_js.config_flow.SERVER_VERSION_TIMEOUT", - new=server_version_timeout, - ), - ): - yield mock_version - - -@pytest.fixture(name="server_version_timeout") -def mock_server_version_timeout() -> int: - """Patch the timeout for getting server version.""" - return SERVER_VERSION_TIMEOUT - - @pytest.fixture(name="addon_setup_time", autouse=True) def mock_addon_setup_time() -> Generator[None]: """Mock add-on setup sleep time.""" From d197debbc04f2886f917abe55c32b3fe9ab6d576 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 May 2025 14:02:07 +0200 Subject: [PATCH 0409/1175] Improve SamsungTV config flow type hints (#144820) --- homeassistant/components/samsungtv/config_flow.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 806a4db4bf6..c7406cc740f 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -109,9 +109,11 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 2 MINOR_VERSION = 2 + _host: str + _bridge: SamsungTVBridge + def __init__(self) -> None: """Initialize flow.""" - self._host: str = "" self._mac: str | None = None self._udn: str | None = None self._upnp_udn: str | None = None @@ -124,13 +126,11 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self._name: str | None = None self._title: str = "" self._id: int | None = None - self._bridge: SamsungTVBridge | None = None self._device_info: dict[str, Any] | None = None self._authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = None def _base_config_entry(self) -> dict[str, Any]: """Generate the base config entry without the method.""" - assert self._bridge is not None return { CONF_HOST: self._host, CONF_MAC: self._mac, @@ -144,7 +144,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): def _get_entry_from_bridge(self) -> ConfigFlowResult: """Get device entry.""" - assert self._bridge data = self._base_config_entry() if self._bridge.token: data[CONF_TOKEN] = self._bridge.token @@ -164,7 +163,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self, raise_on_progress: bool = True ) -> None: """Set the unique id from the udn.""" - assert self._host is not None # Set the unique id without raising on progress in case # there are two SSDP flows with for each ST await self.async_set_unique_id(self._udn, raise_on_progress=False) @@ -270,7 +268,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: if await self._async_set_name_host_from_input(user_input): await self._async_create_bridge() - assert self._bridge self._async_abort_entries_match({CONF_HOST: self._host}) if self._bridge.method != METHOD_LEGACY: # Legacy bridge does not provide device info @@ -290,7 +287,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a pairing by accepting the message on the TV.""" - assert self._bridge is not None errors: dict[str, str] = {} if user_input is not None: result = await self._bridge.async_try_connect() @@ -312,7 +308,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a encrypted pairing.""" - assert self._host is not None await self._async_start_encrypted_pairing(self._host) assert self._authenticator is not None errors: dict[str, str] = {} @@ -427,7 +422,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): @callback def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" - assert self._host is not None if (entry := self._async_update_existing_matching_entry()) and entry.unique_id: # If we have the unique id and the mac we abort # as we do not need anything else @@ -525,7 +519,6 @@ class SamsungTVConfigFlow(ConfigFlow, domain=DOMAIN): """Handle user-confirmation of discovered node.""" if user_input is not None: await self._async_create_bridge() - assert self._bridge if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: return await self.async_step_encrypted_pairing() return await self.async_step_pairing({}) From 7928c15849d65fe27e3dbdd141eb1da48f1f60b1 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 13 May 2025 14:23:41 +0200 Subject: [PATCH 0410/1175] Fix blocking call in azure_storage config flow (#144818) * Fix blocking call in azure_storage config flow * Fix blocking call in azure_storage config_flow as well * move session getting to event flow --- .../components/azure_storage/config_flow.py | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/azure_storage/config_flow.py b/homeassistant/components/azure_storage/config_flow.py index 2862d290f95..25bd39a6608 100644 --- a/homeassistant/components/azure_storage/config_flow.py +++ b/homeassistant/components/azure_storage/config_flow.py @@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__) class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for azure storage.""" - def get_account_url(self, account_name: str) -> str: - """Get the account URL.""" - return f"https://{account_name}.blob.core.windows.net/" + async def get_container_client( + self, account_name: str, container_name: str, storage_account_key: str + ) -> ContainerClient: + """Get the container client. + + ContainerClient has a blocking call to open in cpython + """ + + session = async_get_clientsession(self.hass) + + def create_container_client() -> ContainerClient: + return ContainerClient( + account_url=f"https://{account_name}.blob.core.windows.net/", + container_name=container_name, + credential=storage_account_key, + transport=AioHttpTransport(session=session), + ) + + return await self.hass.async_add_executor_job(create_container_client) async def validate_config( self, container_client: ContainerClient @@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match( {CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]} ) - container_client = ContainerClient( - account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]), + container_client = await self.get_container_client( + account_name=user_input[CONF_ACCOUNT_NAME], container_name=user_input[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) errors = await self.validate_config(container_client) @@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): reauth_entry = self._get_reauth_entry() if user_input is not None: - container_client = ContainerClient( - account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]), + container_client = await self.get_container_client( + account_name=reauth_entry.data[CONF_ACCOUNT_NAME], container_name=reauth_entry.data[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) + errors = await self.validate_config(container_client) if not errors: return self.async_update_reload_and_abort( @@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN): reconfigure_entry = self._get_reconfigure_entry() if user_input is not None: - container_client = ContainerClient( - account_url=self.get_account_url( - reconfigure_entry.data[CONF_ACCOUNT_NAME] - ), + container_client = await self.get_container_client( + account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME], container_name=user_input[CONF_CONTAINER_NAME], - credential=user_input[CONF_STORAGE_ACCOUNT_KEY], - transport=AioHttpTransport(session=async_get_clientsession(self.hass)), + storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY], ) errors = await self.validate_config(container_client) if not errors: From d409b8621743b84fb966dc08ef32028deaa11213 Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 13 May 2025 23:21:56 +1000 Subject: [PATCH 0411/1175] Bump automower-ble to 0.2.1 (#144817) --- homeassistant/components/husqvarna_automower_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/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json index 7566b5c9d32..6eb618cbb04 100644 --- a/homeassistant/components/husqvarna_automower_ble/manifest.json +++ b/homeassistant/components/husqvarna_automower_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "iot_class": "local_polling", - "requirements": ["automower-ble==0.2.0"] + "requirements": ["automower-ble==0.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3d9ca46c149..985ab5d5bd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -542,7 +542,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.0 +automower-ble==0.2.1 # homeassistant.components.generic # homeassistant.components.stream diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3333523b1cc..e6a031684dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ aurorapy==0.2.7 autarco==3.1.0 # homeassistant.components.husqvarna_automower_ble -automower-ble==0.2.0 +automower-ble==0.2.1 # homeassistant.components.generic # homeassistant.components.stream From 3bbe4baaf7693a80b6ad44a18d17923a585c9eb8 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 14 May 2025 00:16:05 +0800 Subject: [PATCH 0412/1175] Update codeowner for switchbot Integration (#144829) update codeowners --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/manifest.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ec6d5dc6254..ed87cfc635c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1498,8 +1498,8 @@ build.json @home-assistant/supervisor /tests/components/switch_as_x/ @home-assistant/core /homeassistant/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski -/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski +/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang +/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang /homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur /tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur /homeassistant/components/switcher_kis/ @thecode @YogevBokobza diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 986a68a9e3e..8c3dcac8f65 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -32,7 +32,8 @@ "@RenierM26", "@murtas", "@Eloston", - "@dsypniewski" + "@dsypniewski", + "@zerzhang" ], "config_flow": true, "dependencies": ["bluetooth_adapters"], From e2dd897ac7923b70b07abfc262bc0ff57874a403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Tue, 13 May 2025 18:19:49 +0200 Subject: [PATCH 0413/1175] Bump dependency pymiele -> 0.5.2 (#144758) --- homeassistant/components/miele/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/manifest.json b/homeassistant/components/miele/manifest.json index 7a72d4c8a59..c9a20e977f9 100644 --- a/homeassistant/components/miele/manifest.json +++ b/homeassistant/components/miele/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "loggers": ["pymiele"], "quality_scale": "bronze", - "requirements": ["pymiele==0.5.1"], + "requirements": ["pymiele==0.5.2"], "single_config_entry": true, "zeroconf": ["_mieleathome._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 985ab5d5bd1..377cbb5de43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2132,7 +2132,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.1 +pymiele==0.5.2 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6a031684dd..e1b185a1645 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1744,7 +1744,7 @@ pymeteoclimatic==0.1.0 pymicro-vad==1.0.1 # homeassistant.components.miele -pymiele==0.5.1 +pymiele==0.5.2 # homeassistant.components.mochad pymochad==0.2.0 From 26796f87cd4481f424c6afb3a4f27a6dba8bb6e6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 13 May 2025 18:20:43 +0200 Subject: [PATCH 0414/1175] Add device registry snapshots to samsungtv tests (#144804) * Add device registry snapshots to samsungtv tests * Simplify * Adjust * Reduce --- tests/components/samsungtv/__init__.py | 9 +- .../samsungtv/snapshots/test_init.ambr | 113 ++++++++++++++++++ tests/components/samsungtv/test_init.py | 53 ++++---- 3 files changed, 147 insertions(+), 28 deletions(-) diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 182ea850b52..54b23f45efe 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -5,8 +5,9 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from homeassistant.components.samsungtv.const import DOMAIN +from homeassistant.components.samsungtv.const import DOMAIN, METHOD_LEGACY from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_METHOD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -20,7 +21,11 @@ async def setup_samsungtv_entry( domain=DOMAIN, data=data, entry_id="123456", - unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", + unique_id=( + None + if data[CONF_METHOD] == METHOD_LEGACY + else "be9554b9-c9fb-41f4-8920-22da015376a4" + ), ) entry.add_to_hass(hass) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 443484e38c7..96dfed3b1ce 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -1,4 +1,117 @@ # serializer version: 1 +# name: test_setup[encrypted] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'samsungtv', + 'be9554b9-c9fb-41f4-8920-22da015376a4', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Mock Title', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_setup[legacy] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'samsungtv', + '123456', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Mock Title', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- +# name: test_setup[websocket] + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + tuple( + 'mac', + 'aa:bb:cc:dd:ee:ff', + ), + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'samsungtv', + 'be9554b9-c9fb-41f4-8920-22da015376a4', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': 'UE43LS003', + 'name': 'Mock Title', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- # name: test_setup_updates_from_ssdp StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index e2e7f2323ed..abafb1854ba 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,43 +1,40 @@ """Tests for the Samsung TV Integration.""" +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest from samsungtvws.async_remote import SamsungTVWSAsyncRemote from syrupy.assertion import SnapshotAssertion -from homeassistant.components.media_player import ( - DOMAIN as MP_DOMAIN, - MediaPlayerEntityFeature, -) +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, + METHOD_ENCRYPTED_WEBSOCKET, + METHOD_LEGACY, METHOD_WEBSOCKET, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, ) -from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, CONF_HOST, CONF_MAC, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TOKEN, - SERVICE_VOLUME_UP, ) 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 . import setup_samsungtv_entry from .const import ( ENTRYDATA_ENCRYPTED_WEBSOCKET, + ENTRYDATA_LEGACY, ENTRYDATA_WEBSOCKET, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, @@ -54,26 +51,30 @@ MOCK_CONFIG = { } -@pytest.mark.usefixtures( - "remote_websocket", "remote_encrypted_websocket_failing", "rest_api" +@pytest.mark.parametrize( + "entry_data", + [ENTRYDATA_LEGACY, ENTRYDATA_ENCRYPTED_WEBSOCKET, ENTRYDATA_WEBSOCKET], + ids=[METHOD_LEGACY, METHOD_ENCRYPTED_WEBSOCKET, METHOD_WEBSOCKET], ) -async def test_setup(hass: HomeAssistant) -> None: - """Test Samsung TV integration is setup.""" - await setup_samsungtv_entry(hass, MOCK_CONFIG) - state = hass.states.get(ENTITY_ID) +@pytest.mark.usefixtures( + "remote_encrypted_websocket", + "remote_legacy", + "remote_websocket", + "rest_api_failing", +) +async def test_setup( + hass: HomeAssistant, + entry_data: dict[str, Any], + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test Samsung TV integration loads and fill device registry.""" + entry = await setup_samsungtv_entry(hass, entry_data) - # test name and turn_on - assert state - assert state.name == "Mock Title" - assert ( - state.attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_SAMSUNGTV | MediaPlayerEntityFeature.TURN_ON - ) + assert entry.state is ConfigEntryState.LOADED - # Ensure service is registered - await hass.services.async_call( - MP_DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) + device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + assert device_entries == snapshot @pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") From cd61f37df7c4fdadc1641bc5abb4c948d030f1b7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 13 May 2025 20:53:08 +0200 Subject: [PATCH 0415/1175] Remove support for condition platforms defining only a CONDITION_SCHEMA (#144832) --- homeassistant/components/sun/condition.py | 9 ++++++++- homeassistant/helpers/condition.py | 11 ++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sun/condition.py b/homeassistant/components/sun/condition.py index c52ada51e06..205f1bb8b5c 100644 --- a/homeassistant/components/sun/condition.py +++ b/homeassistant/components/sun/condition.py @@ -20,7 +20,7 @@ from homeassistant.helpers.sun import get_astral_event_date from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util import dt as dt_util -CONDITION_SCHEMA = vol.All( +_CONDITION_SCHEMA = vol.All( vol.Schema( { **cv.CONDITION_BASE_SCHEMA, @@ -37,6 +37,13 @@ CONDITION_SCHEMA = vol.All( ) +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + return _CONDITION_SCHEMA(config) # type: ignore[no-any-return] + + def sun( hass: HomeAssistant, before: str | None = None, diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index c1b87dd755a..fbdf2dce7b1 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -94,12 +94,7 @@ INPUT_ENTITY_ID = re.compile( class ConditionProtocol(Protocol): - """Define the format of device_condition modules. - - Each module must define either CONDITION_SCHEMA or async_validate_condition_config. - """ - - CONDITION_SCHEMA: vol.Schema + """Define the format of condition modules.""" async def async_validate_condition_config( self, hass: HomeAssistant, config: ConfigType @@ -952,9 +947,7 @@ async def async_validate_condition_config( platform = await _async_get_condition_platform(hass, config) if platform is not None: - if hasattr(platform, "async_validate_condition_config"): - return await platform.async_validate_condition_config(hass, config) - return cast(ConfigType, platform.CONDITION_SCHEMA(config)) + return await platform.async_validate_condition_config(hass, config) if platform is None and condition in ("numeric_state", "state"): validator = cast( Callable[[HomeAssistant, ConfigType], ConfigType], From de2cbb7f5c383efc57d8950b1052b6daabfdcea7 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 13 May 2025 22:47:21 +0200 Subject: [PATCH 0416/1175] Improve user-facing strings of `incomfort` (#144844) * Improvements in user-facing strings of `incomfort` Fix spelling of "IP address" and "timeout" Remove "temperature" from "Shortcut outside sensor temperature" as this makes no sense and leads to completely wrong translations. This is to indicate an electrical shortcut on the sensor so this should be the last word. This also matches the naming in the user manual. * Suggestion from review Co-authored-by: Jan Bouwhuis --------- Co-authored-by: Jan Bouwhuis --- homeassistant/components/incomfort/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index bc9085c3f20..40673a67609 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -9,7 +9,7 @@ "password": "[%key:common::config_flow::data::password%]" }, "data_description": { - "host": "Hostname or IP-address of the Intergas gateway.", + "host": "Hostname or IP address of the Intergas gateway.", "username": "The username to log in to the gateway. This is `admin` in most cases.", "password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices." } @@ -49,7 +49,7 @@ "auth_error": "Invalid credentials.", "no_heaters": "No heaters found.", "not_found": "No gateway found.", - "timeout_error": "Time out when connecting to the gateway.", + "timeout_error": "Timeout when connecting to the gateway.", "unknown": "Unknown error when connecting to the gateway." } }, @@ -143,7 +143,7 @@ "sensor_fault_s2_e22": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", "sensor_fault_s2_e23": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", "sensor_fault_s2_e24": "[%key:component::incomfort::entity::water_heater::boiler::state::sensor_fault_s2_e20%]", - "shortcut_outside_sensor_temperature_e27": "Shortcut outside sensor temperature", + "shortcut_outside_sensor_temperature_e27": "Shortcut outside temperature sensor", "gas_valve_relay_faulty_e29": "Gas valve relay faulty", "gas_valve_relay_faulty_e30": "[%key:component::incomfort::entity::water_heater::boiler::state::gas_valve_relay_faulty_e29%]" } From 6d809b0b5ad5993a973c21122f4da713645b96e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 13 May 2025 21:57:15 +0100 Subject: [PATCH 0417/1175] Add service response support to admin services (#144837) --- homeassistant/helpers/service.py | 29 ++++++++++++++++++++++------- tests/helpers/test_service.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 4873d935537..f157e82bc53 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable import dataclasses from enum import Enum from functools import cache, partial @@ -1094,9 +1094,15 @@ async def _handle_entity_call( async def _async_admin_handler( hass: HomeAssistant, - service_job: HassJob[[ServiceCall], Awaitable[None] | None], + service_job: HassJob[ + [ServiceCall], + Coroutine[Any, Any, ServiceResponse | EntityServiceResponse] + | ServiceResponse + | EntityServiceResponse + | None, + ], call: ServiceCall, -) -> None: +) -> ServiceResponse | EntityServiceResponse | None: """Run an admin service.""" if call.context.user_id: user = await hass.auth.async_get_user(call.context.user_id) @@ -1105,9 +1111,10 @@ async def _async_admin_handler( if not user.is_admin: raise Unauthorized(context=call.context) - result = hass.async_run_hass_job(service_job, call) - if result is not None: - await result + task = hass.async_run_hass_job(service_job, call) + if task is not None: + return await task + return None @bind_hass @@ -1116,8 +1123,15 @@ def async_register_admin_service( hass: HomeAssistant, domain: str, service: str, - service_func: Callable[[ServiceCall], Awaitable[None] | None], + service_func: Callable[ + [ServiceCall], + Coroutine[Any, Any, ServiceResponse | EntityServiceResponse] + | ServiceResponse + | EntityServiceResponse + | None, + ], schema: VolSchemaType = vol.Schema({}, extra=vol.PREVENT_EXTRA), + supports_response: SupportsResponse = SupportsResponse.NONE, ) -> None: """Register a service that requires admin access.""" hass.services.async_register( @@ -1129,6 +1143,7 @@ def async_register_admin_service( HassJob(service_func, f"admin service {domain}.{service}"), ), schema, + supports_response, ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 4582bce3e05..38e7e1ae452 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -32,6 +32,7 @@ from homeassistant.core import ( HassJob, HomeAssistant, ServiceCall, + ServiceResponse, SupportsResponse, ) from homeassistant.helpers import ( @@ -1648,6 +1649,33 @@ async def test_register_admin_service( assert calls[0].context.user_id == hass_admin_user.id +@pytest.mark.parametrize( + "supports_response", + [SupportsResponse.ONLY, SupportsResponse.OPTIONAL], +) +async def test_register_admin_service_return_response( + hass: HomeAssistant, supports_response: SupportsResponse +) -> None: + """Test the register admin service for a service that returns response data.""" + + async def mock_service(call: ServiceCall) -> ServiceResponse: + """Service handler coroutine.""" + assert call.return_response + return {"test-reply": "test-value1"} + + service.async_register_admin_service( + hass, "test", "test", mock_service, supports_response=supports_response + ) + result = await hass.services.async_call( + "test", + "test", + service_data={}, + blocking=True, + return_response=True, + ) + assert result == {"test-reply": "test-value1"} + + async def test_domain_control_not_async(hass: HomeAssistant, mock_entities) -> None: """Test domain verification in a service call with an unknown user.""" calls = [] From c76239806d2199d6143fe0e456517fbe8c79bf77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 13 May 2025 19:39:53 -0500 Subject: [PATCH 0418/1175] Bump aioesphomeapi to 31.0.0 (#144778) * aioesphomeapi update * Bump aioesphomeapi to 31.0.0 There are some breaking changes with the protobuf naming and types required some refactoring changelog: https://github.com/esphome/aioesphomeapi/compare/v30.2.0...v31.0.0 * actually include the commit to bump the lib --- homeassistant/components/esphome/fan.py | 6 +- homeassistant/components/esphome/light.py | 13 +- .../components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_fan.py | 2 +- tests/components/esphome/test_light.py | 181 +++++++----------- tests/components/esphome/test_sensor.py | 2 +- 9 files changed, 91 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 7cdc3570d61..a4d840845a6 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -63,7 +63,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): if self._supports_speed_levels: data["speed_level"] = math.ceil( percentage_to_ranged_value( - (1, self._static_info.supported_speed_levels), percentage + (1, self._static_info.supported_speed_count), percentage ) ) else: @@ -121,7 +121,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): ) return ranged_value_to_percentage( - (1, self._static_info.supported_speed_levels), self._state.speed_level + (1, self._static_info.supported_speed_count), self._state.speed_level ) @property @@ -164,7 +164,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): if not supports_speed_levels: self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) else: - self._attr_speed_count = static_info.supported_speed_levels + self._attr_speed_count = static_info.supported_speed_count async_setup_entry = partial( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index d8d827f18a1..3e278b5b2d6 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, cast from aioesphomeapi import ( APIVersion, + ColorMode as ESPHomeColorMode, EntityInfo, LightColorCapability, LightInfo, @@ -106,15 +107,15 @@ def _mired_to_kelvin(mired_temperature: float) -> int: @lru_cache -def _color_mode_to_ha(mode: int) -> str: +def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode: """Convert an esphome color mode to a HA color mode constant. Choose the color mode that best matches the feature-set. """ - candidates = [] + candidates: list[tuple[ColorMode, LightColorCapability]] = [] for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items(): for caps in cap_lists: - if caps == mode: + if caps.value == mode: # exact match return ha_mode if (mode & caps) == caps: @@ -131,8 +132,8 @@ def _color_mode_to_ha(mode: int) -> str: @lru_cache def _filter_color_modes( - supported: list[int], features: LightColorCapability -) -> tuple[int, ...]: + supported: list[ESPHomeColorMode], features: LightColorCapability +) -> tuple[ESPHomeColorMode, ...]: """Filter the given supported color modes. Excluding all values that don't have the requested features. @@ -156,7 +157,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int: class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """A light implementation for ESPHome.""" - _native_supported_color_modes: tuple[int, ...] + _native_supported_color_modes: tuple[ESPHomeColorMode, ...] _supports_color_mode = False @property diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d43ea86126a..d1fb3a49166 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==30.2.0", + "aioesphomeapi==31.0.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.15.1" ], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 611d7056ff7..5baa092613b 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -88,9 +88,9 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): return if ( state_class == EsphomeSensorStateClass.MEASUREMENT - and static_info.last_reset_type == LastResetType.AUTO + and static_info.legacy_last_reset_type == LastResetType.AUTO ): - # Legacy, last_reset_type auto was the equivalent to the + # Legacy, legacy_last_reset_type auto was the equivalent to the # TOTAL_INCREASING state class self._attr_state_class = SensorStateClass.TOTAL_INCREASING else: diff --git a/requirements_all.txt b/requirements_all.txt index 377cbb5de43..34dcee1337f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==30.2.0 +aioesphomeapi==31.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1b185a1645..45a1775b72b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -229,7 +229,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==30.2.0 +aioesphomeapi==31.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_fan.py b/tests/components/esphome/test_fan.py index a56ec1caeba..05a95fe0e00 100644 --- a/tests/components/esphome/test_fan.py +++ b/tests/components/esphome/test_fan.py @@ -148,7 +148,7 @@ async def test_fan_entity_with_all_features_new_api( key=1, name="my fan", unique_id="my_fan", - supported_speed_levels=4, + supported_speed_count=4, supports_direction=True, supports_speed=True, supports_oscillation=True, diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index d3302cab75c..0d2e8338c06 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -5,6 +5,7 @@ from unittest.mock import call from aioesphomeapi import ( APIClient, APIVersion, + ColorMode as ESPColorMode, LightColorCapability, LightInfo, LightState, @@ -58,7 +59,7 @@ async def test_light_on_off( unique_id="my_light", min_mireds=153, max_mireds=400, - supported_color_modes=[LightColorCapability.ON_OFF], + supported_color_modes=[ESPColorMode.ON_OFF], ) ] states = [LightState(key=1, state=True)] @@ -218,12 +219,14 @@ async def test_light_brightness_on_off( unique_id="my_light", min_mireds=153, max_mireds=400, - supported_color_modes=[ - LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS - ], + supported_color_modes=[ESPColorMode.ON_OFF, ESPColorMode.BRIGHTNESS], + ) + ] + states = [ + LightState( + key=1, state=True, brightness=100, color_mode=ESPColorMode.BRIGHTNESS ) ] - states = [LightState(key=1, state=True, brightness=100)] user_service = [] await mock_generic_device_entry( mock_client=mock_client, @@ -234,6 +237,10 @@ async def test_light_brightness_on_off( state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS await hass.services.async_call( LIGHT_DOMAIN, @@ -246,8 +253,7 @@ async def test_light_brightness_on_off( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.BRIGHTNESS.value, ) ] ) @@ -264,8 +270,7 @@ async def test_light_brightness_on_off( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.BRIGHTNESS.value, brightness=pytest.approx(0.4980392156862745), ) ] @@ -407,13 +412,18 @@ async def test_light_brightness_on_off_with_unknown_color_mode( min_mireds=153, max_mireds=400, supported_color_modes=[ - LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LIGHT_COLOR_CAPABILITY_UNKNOWN + ESPColorMode.ON_OFF, + ESPColorMode.BRIGHTNESS, + LIGHT_COLOR_CAPABILITY_UNKNOWN, ], ) ] - states = [LightState(key=1, state=True, brightness=100)] + entity_info[0].supported_color_modes.append(LIGHT_COLOR_CAPABILITY_UNKNOWN) + states = [ + LightState( + key=1, state=True, brightness=100, color_mode=LIGHT_COLOR_CAPABILITY_UNKNOWN + ) + ] user_service = [] await mock_generic_device_entry( mock_client=mock_client, @@ -436,9 +446,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LIGHT_COLOR_CAPABILITY_UNKNOWN, + color_mode=LIGHT_COLOR_CAPABILITY_UNKNOWN, ) ] ) @@ -455,9 +463,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LIGHT_COLOR_CAPABILITY_UNKNOWN, + color_mode=ESPColorMode.BRIGHTNESS, brightness=pytest.approx(0.4980392156862745), ) ] @@ -517,13 +523,10 @@ async def test_rgb_color_temp_light( ) -> None: """Test a generic light that supports color temp and RGB.""" color_modes = [ - LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS, - LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LightColorCapability.COLOR_TEMPERATURE, - LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LightColorCapability.RGB, + ESPColorMode.ON_OFF, + ESPColorMode.BRIGHTNESS, + ESPColorMode.COLOR_TEMPERATURE, + ESPColorMode.RGB, ] mock_client.api_version = APIVersion(1, 7) @@ -538,7 +541,11 @@ async def test_rgb_color_temp_light( supported_color_modes=color_modes, ) ] - states = [LightState(key=1, state=True, brightness=100)] + states = [ + LightState( + key=1, state=True, brightness=100, color_mode=ESPColorMode.BRIGHTNESS + ) + ] user_service = [] await mock_generic_device_entry( mock_client=mock_client, @@ -561,8 +568,7 @@ async def test_rgb_color_temp_light( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.BRIGHTNESS, ) ] ) @@ -579,8 +585,7 @@ async def test_rgb_color_temp_light( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.BRIGHTNESS, brightness=pytest.approx(0.4980392156862745), ) ] @@ -598,9 +603,7 @@ async def test_rgb_color_temp_light( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS - | LightColorCapability.COLOR_TEMPERATURE, + color_mode=ESPColorMode.COLOR_TEMPERATURE, color_temperature=400, ) ] @@ -908,12 +911,14 @@ async def test_light_rgbww_with_cold_warm_white_support( min_mireds=153, max_mireds=400, supported_color_modes=[ - LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS + ESPColorMode.RGB, + ESPColorMode.WHITE, + ESPColorMode.COLOR_TEMPERATURE, + ESPColorMode.COLD_WARM_WHITE, + ESPColorMode.ON_OFF, + ESPColorMode.BRIGHTNESS, + ESPColorMode.RGB_COLD_WARM_WHITE, + ESPColorMode.RGB_WHITE, ], ) ] @@ -928,12 +933,7 @@ async def test_light_rgbww_with_cold_warm_white_support( blue=1, warm_white=1, cold_white=1, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.RGB_COLD_WARM_WHITE, ) ] user_service = [] @@ -946,7 +946,13 @@ async def test_light_rgbww_with_cold_warm_white_support( state = hass.states.get("light.test_mylight") assert state is not None assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGBWW] + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.RGB, + ColorMode.RGBW, + ColorMode.RGBWW, + ColorMode.WHITE, + ] assert state.attributes[ATTR_COLOR_MODE] == ColorMode.RGBWW assert state.attributes[ATTR_RGBWW_COLOR] == (255, 255, 255, 255, 255) @@ -961,12 +967,7 @@ async def test_light_rgbww_with_cold_warm_white_support( call( key=1, state=True, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.RGB_COLD_WARM_WHITE, ) ] ) @@ -983,12 +984,7 @@ async def test_light_rgbww_with_cold_warm_white_support( call( key=1, state=True, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.RGB_COLD_WARM_WHITE, brightness=pytest.approx(0.4980392156862745), ) ] @@ -1011,14 +1007,7 @@ async def test_light_rgbww_with_cold_warm_white_support( key=1, state=True, color_brightness=1.0, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, - cold_white=0, - warm_white=0, + color_mode=ESPColorMode.RGB, rgb=(pytest.approx(0.3333333333333333), 1.0, 0.0), brightness=pytest.approx(0.4980392156862745), ) @@ -1037,16 +1026,9 @@ async def test_light_rgbww_with_cold_warm_white_support( call( key=1, state=True, - color_brightness=pytest.approx(0.4235294117647059), - cold_white=1, - warm_white=1, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, - rgb=(0, pytest.approx(0.5462962962962963), 1.0), + color_brightness=1.0, + color_mode=ESPColorMode.RGB, + rgb=(1.0, 1.0, 1.0), ) ] ) @@ -1063,16 +1045,10 @@ async def test_light_rgbww_with_cold_warm_white_support( call( key=1, state=True, - color_brightness=pytest.approx(0.4235294117647059), - cold_white=1, - warm_white=1, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, - rgb=(0, pytest.approx(0.5462962962962963), 1.0), + color_brightness=1.0, + white=1, + color_mode=ESPColorMode.RGB_WHITE, + rgb=(1.0, 1.0, 1.0), ) ] ) @@ -1095,12 +1071,7 @@ async def test_light_rgbww_with_cold_warm_white_support( color_brightness=1, cold_white=1, warm_white=1, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.RGB_COLD_WARM_WHITE, rgb=(1, 1, 1), ) ] @@ -1118,16 +1089,8 @@ async def test_light_rgbww_with_cold_warm_white_support( call( key=1, state=True, - color_brightness=0, - cold_white=0, - warm_white=100, - color_mode=LightColorCapability.RGB - | LightColorCapability.WHITE - | LightColorCapability.COLOR_TEMPERATURE - | LightColorCapability.COLD_WARM_WHITE - | LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, - rgb=(0, 0, 0), + color_temperature=400.0, + color_mode=ESPColorMode.COLOR_TEMPERATURE, ) ] ) @@ -1733,11 +1696,16 @@ async def test_light_effects( max_mireds=400, effects=["effect1", "effect2"], supported_color_modes=[ - LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS, + ESPColorMode.ON_OFF, + ESPColorMode.BRIGHTNESS, ], ) ] - states = [LightState(key=1, state=True, brightness=100)] + states = [ + LightState( + key=1, state=True, brightness=100, color_mode=ESPColorMode.BRIGHTNESS + ) + ] user_service = [] await mock_generic_device_entry( mock_client=mock_client, @@ -1761,8 +1729,7 @@ async def test_light_effects( call( key=1, state=True, - color_mode=LightColorCapability.ON_OFF - | LightColorCapability.BRIGHTNESS, + color_mode=ESPColorMode.BRIGHTNESS, effect="effect1", ) ] diff --git a/tests/components/esphome/test_sensor.py b/tests/components/esphome/test_sensor.py index 0c443dc5941..6763d2ab9a9 100644 --- a/tests/components/esphome/test_sensor.py +++ b/tests/components/esphome/test_sensor.py @@ -203,7 +203,7 @@ async def test_generic_numeric_sensor_legacy_last_reset_convert( key=1, name="my sensor", unique_id="my_sensor", - last_reset_type=LastResetType.AUTO, + legacy_last_reset_type=LastResetType.AUTO, state_class=ESPHomeSensorStateClass.MEASUREMENT, ) ] From f0c5fbfb8a80bd91bd25db4af2365d1a47247ce9 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 14 May 2025 06:04:38 +0200 Subject: [PATCH 0419/1175] Bump pylamarzocco to 2.0.3 (#144825) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 1fbef073394..d948d46ef1f 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.2"] + "requirements": ["pylamarzocco==2.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34dcee1337f..7ce4e6858d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2090,7 +2090,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.2 +pylamarzocco==2.0.3 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45a1775b72b..9fdb52041e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1705,7 +1705,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.2 +pylamarzocco==2.0.3 # homeassistant.components.lastfm pylast==5.1.0 From b1ffcb4245651fbf0d998ea7de52953ea3e4f15c Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 14 May 2025 07:08:47 +0300 Subject: [PATCH 0420/1175] Jewish calendar - Fix Parasha values (#144646) * Fix Parasha values * Fix test * Update sensor.py --- homeassistant/components/jewish_calendar/sensor.py | 4 ++-- tests/components/jewish_calendar/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index deaae64547a..063818aedf3 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -275,9 +275,9 @@ class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): } return after_shkia_date.hdate if self.entity_description.key == "weekly_portion": - self._attr_options = list(Parasha) + self._attr_options = [str(p) for p in Parasha] # Compute the weekly portion based on the upcoming shabbat. - return after_tzais_date.upcoming_shabbat.parasha + return str(after_tzais_date.upcoming_shabbat.parasha) if self.entity_description.key == "holiday": _holidays = after_shkia_date.holidays _id = ", ".join(holiday.name for holiday in _holidays) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index b33d8f3e84b..9364fcda40c 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -96,7 +96,7 @@ TEST_PARAMS = [ "device_class": "enum", "friendly_name": "Jewish Calendar Weekly Torah portion", "icon": "mdi:book-open-variant", - "options": list(Parasha), + "options": [str(p) for p in Parasha], }, }, "he", From 6bc6733c40d8ecd128d5a7166caedda2e988c896 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Wed, 14 May 2025 05:10:47 +0100 Subject: [PATCH 0421/1175] Add config flow data descriptions to Squeezebox (#144619) * add data_descriptions * tweaks * review updates --- homeassistant/components/squeezebox/strings.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 9109a378ea8..8f0d45bd737 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -17,7 +17,14 @@ "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "https": "Connect over https (requires reverse proxy)" + "https": "Connect over HTTPS (requires reverse proxy)" + }, + "data_description": { + "host": "[%key:component::squeezebox::config::step::user::data_description::host%]", + "port": "The web interface port on the LMS. The default is 9000.", + "username": "The username from LMS Advanced Security (if defined).", + "password": "The password from LMS Advanced Security (if defined).", + "https": "Connect to the LMS over HTTPS (requires reverse proxy)." } } }, @@ -29,7 +36,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_server_found": "No LMS server found." + "no_server_found": "No LMS found." } }, "services": { From 9729f1f38b67804f109d60b539df4991dea044bb Mon Sep 17 00:00:00 2001 From: John Hillery <34005807+jrhillery@users.noreply.github.com> Date: Wed, 14 May 2025 00:16:05 -0400 Subject: [PATCH 0422/1175] Provide ability to select nexia RoomIQ sensors (#144278) Co-authored-by: J. Nick Koston --- homeassistant/components/nexia/strings.json | 3 + homeassistant/components/nexia/switch.py | 71 +- .../nexia/fixtures/sensors_xl1050_house.json | 1096 +++++++++++++++++ tests/components/nexia/test_switch.py | 64 +- tests/components/nexia/util.py | 3 +- 5 files changed, 1233 insertions(+), 4 deletions(-) create mode 100644 tests/components/nexia/fixtures/sensors_xl1050_house.json diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index f6b08d5e8e5..6dbfe552e35 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -65,6 +65,9 @@ "hold": { "name": "Hold" }, + "room_iq_sensor": { + "name": "Include {sensor_name}" + }, "emergency_heat": { "name": "Emergency heat" } diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 1897ad67414..bf1495217a7 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -2,14 +2,19 @@ from __future__ import annotations +from collections.abc import Iterable +import functools as ft from typing import Any from nexia.const import OPERATION_MODE_OFF +from nexia.roomiq import NexiaRoomIQHarmonizer +from nexia.sensor import NexiaSensor from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import NexiaDataUpdateCoordinator @@ -17,6 +22,14 @@ from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity from .types import NexiaConfigEntry +async def _stop_harmonizers( + _: Event, harmonizers: Iterable[NexiaRoomIQHarmonizer] +) -> None: + """Run the shutdown methods when preparing to stop.""" + for harmonizer in harmonizers: + await harmonizer.async_shutdown() # Never suspends + + async def async_setup_entry( hass: HomeAssistant, config_entry: NexiaConfigEntry, @@ -25,7 +38,8 @@ async def async_setup_entry( """Set up switches for a Nexia device.""" coordinator = config_entry.runtime_data nexia_home = coordinator.nexia_home - entities: list[NexiaHoldSwitch | NexiaEmergencyHeatSwitch] = [] + entities: list[SwitchEntity] = [] + room_iq_zones: dict[int, NexiaRoomIQHarmonizer] = {} for thermostat_id in nexia_home.get_thermostat_ids(): thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id) if thermostat.has_emergency_heat(): @@ -33,8 +47,18 @@ async def async_setup_entry( for zone_id in thermostat.get_zone_ids(): zone: NexiaThermostatZone = thermostat.get_zone_by_id(zone_id) entities.append(NexiaHoldSwitch(coordinator, zone)) + if len(zone_sensors := zone.get_sensors()) > 1: + entities.extend( + NexiaRoomIQSwitch(coordinator, zone, sensor, room_iq_zones) + for sensor in zone_sensors + ) async_add_entities(entities) + if room_iq_zones: + listener = ft.partial(_stop_harmonizers, harmonizers=room_iq_zones.values()) + config_entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, listener) + ) class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): @@ -68,6 +92,49 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): self._signal_zone_update() +class NexiaRoomIQSwitch(NexiaThermostatZoneEntity, SwitchEntity): + """Provides Nexia RoomIQ sensor switch support.""" + + _attr_translation_key = "room_iq_sensor" + + def __init__( + self, + coordinator: NexiaDataUpdateCoordinator, + zone: NexiaThermostatZone, + sensor: NexiaSensor, + room_iq_zones: dict[int, NexiaRoomIQHarmonizer], + ) -> None: + """Initialize the RoomIQ sensor switch.""" + super().__init__(coordinator, zone, f"{sensor.id}_room_iq_sensor") + self._attr_translation_placeholders = {"sensor_name": sensor.name} + self._sensor_id = sensor.id + if zone.zone_id in room_iq_zones: + self._harmonizer = room_iq_zones[zone.zone_id] + else: + self._harmonizer = NexiaRoomIQHarmonizer( + zone, coordinator.async_refresh, self._signal_zone_update + ) + room_iq_zones[zone.zone_id] = self._harmonizer + + @property + def is_on(self) -> bool: + """Return if the sensor is part of the zone average temperature.""" + if self._harmonizer.request_pending(): + return self._sensor_id in self._harmonizer.selected_sensor_ids + + return self._zone.get_sensor_by_id(self._sensor_id).weight > 0.0 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Include this sensor.""" + self._harmonizer.trigger_add_sensor(self._sensor_id) + self._signal_zone_update() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Remove this sensor.""" + self._harmonizer.trigger_remove_sensor(self._sensor_id) + self._signal_zone_update() + + class NexiaEmergencyHeatSwitch(NexiaThermostatEntity, SwitchEntity): """Provides Nexia emergency heat switch support.""" diff --git a/tests/components/nexia/fixtures/sensors_xl1050_house.json b/tests/components/nexia/fixtures/sensors_xl1050_house.json new file mode 100644 index 00000000000..4293b92c6cf --- /dev/null +++ b/tests/components/nexia/fixtures/sensors_xl1050_house.json @@ -0,0 +1,1096 @@ +{ + "success": true, + "error": null, + "result": { + "id": 123456, + "name": "My Home", + "third_party_integrations": [], + "latitude": null, + "longitude": null, + "time_zone": "America/New_York", + "dealer_opt_in": true, + "room_iq_enabled": true, + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456" + }, + "edit": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/edit", + "method": "GET" + } + ], + "child": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/devices", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 5378307, + "name": "Center", + "name_editable": true, + "features": [ + { + "name": "advanced_info", + "items": [ + { + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, + { + "type": "label_value", + "label": "AUID", + "value": "0295CB84" + }, + { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1726826973" + }, + { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2024-09-20 10:09:33 UTC" + }, + { + "type": "label_value", + "label": "Firmware Version", + "value": "5.11.1" + } + ] + }, + { + "name": "thermostat", + "scale": "f", + "temperature": 69, + "device_identifier": "XxlZone-85034552", + "status": "System Idle", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/setpoints" + } + }, + "setpoint_delta": 3, + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 69, + "system_status": "System Idle", + "operating_state": "idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "dealer_contact_info", + "has_dealer_identifier": true, + "actions": { + "request_current_dealer_info": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/dealers/7043919191" + }, + "request_dealers_by_zip": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/dealers/5378307/search" + } + } + }, + { + "name": "thermostat_mode", + "label": "System Mode", + "value": "HEAT", + "display_value": "Heating", + "options": [ + { + "id": "thermostat_mode", + "label": "System Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "run_schedule", + "display_value": "Run Schedule", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/run_mode" + } + } + }, + { + "name": "room_iq_sensors", + "sensors": [ + { + "id": 17687546, + "name": "Center", + "icon": { + "name": "room_iq_onboard", + "modifiers": [] + }, + "type": "thermostat", + "serial_number": "NativeIDTUniqueID", + "weight": 0.5, + "temperature": 68, + "temperature_valid": true, + "humidity": 32, + "humidity_valid": true, + "has_online": false, + "has_battery": false + }, + { + "id": 17687549, + "name": "Upstairs", + "icon": { + "name": "room_iq_wireless", + "modifiers": [] + }, + "type": "930", + "serial_number": "2410R5C53X", + "weight": 0.5, + "temperature": 69, + "temperature_valid": true, + "humidity": 32, + "humidity_valid": true, + "has_online": true, + "connected": true, + "has_battery": true, + "battery_level": 95, + "battery_low": false, + "battery_valid": true + } + ], + "should_show": true, + "actions": { + "request_current_state": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/request_current_sensor_state" + }, + "update_active_sensors": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/update_active_sensors" + } + } + }, + { + "name": "preset_setpoints", + "presets": { + "1": { + "cool": 78, + "heat": 70 + }, + "2": { + "cool": 85, + "heat": 62 + }, + "3": { + "cool": 82, + "heat": 62 + } + } + }, + { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [ + { + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "value": "circulate", + "display_value": "Circulate", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/fan_mode" + } + } + }, + { + "name": "thermostat_default_fan_mode", + "value": "circulate", + "actions": { + "update_thermostat_default_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/fan_mode" + } + } + }, + { + "name": "gen_2_app", + "is_supported": false, + "validation_failures": [ + "Thermostat has wireless sensors.", + "Unauthorized to use Gen 2 App." + ] + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1.0, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=TraneXl1050-5378307\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=TraneXl1050-5378307", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=TraneXl1050-5378307", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=TraneXl1050-5378307", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }, + { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/TraneXl1050-5378307?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/TraneXl1050-5378307?report_type=monthly" + } + } + } + ], + "icon": [ + { + "name": "thermostat", + "modifiers": ["temperature-69"] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=5378307" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e16684f6-b1e3-4e25-b006-e4d599dab2e9" + } + }, + "last_updated_at": "2025-01-06T17:45:09.000-05:00", + "settings": [ + { + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [ + { + "value": 0, + "label": "None" + }, + { + "value": 1, + "label": "Home" + }, + { + "value": 2, + "label": "Away" + }, + { + "value": 3, + "label": "Sleep" + } + ], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/preset_selected" + } + } + }, + { + "type": "system_mode", + "title": "System Mode", + "current_value": "HEAT", + "options": [ + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/zone_mode" + } + } + }, + { + "type": "run_mode", + "title": "Run Mode", + "current_value": "run_schedule", + "options": [ + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/run_mode" + } + } + }, + { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/scheduling_enabled" + } + } + }, + { + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "circulate", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "on", + "label": "On" + }, + { + "value": "circulate", + "label": "Circulate" + } + ], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/fan_mode" + } + } + }, + { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 10, + "options": [ + { + "value": 10, + "label": "10 minutes" + }, + { + "value": 15, + "label": "15 minutes" + }, + { + "value": 20, + "label": "20 minutes" + }, + { + "value": 25, + "label": "25 minutes" + }, + { + "value": 30, + "label": "30 minutes" + }, + { + "value": 35, + "label": "35 minutes" + }, + { + "value": 40, + "label": "40 minutes" + }, + { + "value": 45, + "label": "45 minutes" + }, + { + "value": 50, + "label": "50 minutes" + }, + { + "value": 55, + "label": "55 minutes" + } + ], + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes" + ], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/fan_circulation_time" + } + } + }, + { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [ + { + "value": "auto", + "label": "Auto" + }, + { + "value": "quick", + "label": "Quick" + }, + { + "value": "allergy", + "label": "Allergy" + } + ], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/air_cleaner_mode" + } + } + }, + { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.5, + "options": [ + { + "value": 0.35, + "label": "35%" + }, + { + "value": 0.4, + "label": "40%" + }, + { + "value": 0.45, + "label": "45%" + }, + { + "value": 0.5, + "label": "50%" + }, + { + "value": 0.55, + "label": "55%" + }, + { + "value": 0.6, + "label": "60%" + }, + { + "value": 0.65, + "label": "65%" + } + ], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/dehumidify" + } + } + }, + { + "type": "emergency_heat", + "title": "Emergency Heat", + "current_value": false, + "options": [ + { + "value": true, + "label": "ON" + }, + { + "value": false, + "label": "OFF" + } + ], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/emergency_heat" + } + } + }, + { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [ + { + "value": "f", + "label": "F" + }, + { + "value": "c", + "label": "C" + } + ], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/5378307/scale" + } + } + } + ], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "39", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "33", + "system_status": "System Idle", + "delta": 3, + "manufacturer": "AmericanStandard", + "country_code": "US", + "state_code": "NC", + "zones": [ + { + "type": "xxl_zone", + "id": 85034552, + "name": "NativeZone", + "current_zone_mode": "HEAT", + "temperature": 69, + "setpoints": { + "heat": 69, + "cool": null + }, + "operating_state": "", + "heating_setpoint": 69, + "cooling_setpoint": null, + "zone_status": "", + "settings": [], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-69"] + }, + "features": [ + { + "name": "thermostat", + "scale": "f", + "temperature": 69, + "device_identifier": "XxlZone-85034552", + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/setpoints" + } + }, + "setpoint_delta": 3, + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 69, + "system_status": "System Idle" + }, + { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, + { + "name": "dealer_contact_info", + "has_dealer_identifier": true, + "actions": { + "request_current_dealer_info": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/dealers/7043919191" + }, + "request_dealers_by_zip": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/dealers/5378307/search" + } + } + }, + { + "name": "thermostat_mode", + "label": "System Mode", + "value": "HEAT", + "display_value": "Heating", + "options": [ + { + "id": "thermostat_mode", + "label": "System Mode", + "value": "thermostat_mode", + "header": true + }, + { + "value": "AUTO", + "label": "Auto" + }, + { + "value": "COOL", + "label": "Cooling" + }, + { + "value": "HEAT", + "label": "Heating" + }, + { + "value": "OFF", + "label": "Off" + } + ], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/zone_mode" + } + } + }, + { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [ + { + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, + { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, + { + "value": "permanent_hold", + "label": "Permanent Hold" + }, + { + "value": "run_schedule", + "label": "Run Schedule" + } + ], + "value": "run_schedule", + "display_value": "Run Schedule", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/run_mode" + } + } + }, + { + "name": "room_iq_sensors", + "sensors": [ + { + "id": 17687546, + "name": "Center", + "icon": { + "name": "room_iq_onboard", + "modifiers": [] + }, + "type": "thermostat", + "serial_number": "NativeIDTUniqueID", + "weight": 0.5, + "temperature": 68, + "temperature_valid": true, + "humidity": 32, + "humidity_valid": true, + "has_online": false, + "has_battery": false + }, + { + "id": 17687549, + "name": "Upstairs", + "icon": { + "name": "room_iq_wireless", + "modifiers": [] + }, + "type": "930", + "serial_number": "2410R5C53X", + "weight": 0.5, + "temperature": 69, + "temperature_valid": true, + "humidity": 32, + "humidity_valid": true, + "has_online": true, + "connected": true, + "has_battery": true, + "battery_level": 95, + "battery_low": false, + "battery_valid": true + } + ], + "should_show": true, + "actions": { + "request_current_state": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/request_current_sensor_state" + }, + "update_active_sensors": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/update_active_sensors" + } + } + }, + { + "name": "preset_setpoints", + "presets": { + "1": { + "cool": 78, + "heat": 70 + }, + "2": { + "cool": 85, + "heat": 62 + }, + "3": { + "cool": 82, + "heat": 62 + } + } + }, + { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1.0, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-85034552\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-85034552", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-85034552", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-85034552", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/85034552" + } + } + } + ], + "generic_input_sensors": [] + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/devices" + }, + "template": { + "data": { + "title": null, + "fields": [], + "_links": { + "child-schema": [ + { + "data": { + "label": "Connect New Device", + "icon": { + "name": "new_device", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/enrollables_schema" + } + } + } + }, + { + "data": { + "label": "Create Group", + "icon": { + "name": "create_group", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/groups/new" + } + } + } + } + ] + } + } + } + }, + "item_type": "application/vnd.nexia.device+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/automations", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 4995413, + "name": "My First Automation", + "enabled": false, + "settings": [], + "triggers": [], + "description": "Click the Edit button to set up automation for your devices.", + "icon": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/4995413" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=4995413", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=4995413" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d827e212-3055-4835-8bda-333d26f05c9d" + } + } + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/automations" + }, + "template": { + "href": "https://www.mynexia.com/mobile/houses/123456/automation_edit_buffers", + "method": "POST" + } + }, + "item_type": "application/vnd.nexia.automation+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/modes", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [ + { + "id": 6631129, + "name": "Day", + "current_mode": false, + "icon": "home.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/6631129" + } + } + }, + { + "id": 6631132, + "name": "Night", + "current_mode": true, + "icon": "home.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/6631132" + } + } + } + ], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/modes" + } + }, + "item_type": "application/vnd.nexia.mode+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.event+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/houses/123456/videos/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.video+json" + } + }, + { + "href": "https://www.mynexia.com/mobile/choices", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/choices" + } + }, + "item_type": "application/vnd.nexia.choice+json" + } + } + ], + "feature_code_url": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/feature_code", + "method": "POST" + } + ], + "remove_zwave_device": [ + { + "href": "https://www.mynexia.com/mobile/houses/123456/remove_zwave_device", + "cancel_href": "https://www.mynexia.com/mobile/houses/123456/cancel_remove_zwave_device", + "method": "POST", + "timeout": 240, + "display": true + } + ] + } + } +} diff --git a/tests/components/nexia/test_switch.py b/tests/components/nexia/test_switch.py index 821d939bac5..e532201f01e 100644 --- a/tests/components/nexia/test_switch.py +++ b/tests/components/nexia/test_switch.py @@ -1,12 +1,74 @@ """The switch tests for the nexia platform.""" -from homeassistant.const import STATE_ON +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) from homeassistant.core import HomeAssistant from .util import async_init_integration +from tests.common import async_fire_time_changed + async def test_hold_switch(hass: HomeAssistant) -> None: """Test creation of the hold switch.""" await async_init_integration(hass) assert hass.states.get("switch.nick_office_hold").state == STATE_ON + + +async def test_nexia_sensor_switch( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test NexiaRoomIQSensorSwitch.""" + await async_init_integration(hass, house_fixture="nexia/sensors_xl1050_house.json") + sw1_id = f"{Platform.SWITCH}.center_nativezone_include_center" + sw1 = {ATTR_ENTITY_ID: sw1_id} + sw2_id = f"{Platform.SWITCH}.center_nativezone_include_upstairs" + sw2 = {ATTR_ENTITY_ID: sw2_id} + + # Switch starts out on. + assert (entity_state := hass.states.get(sw1_id)) is not None + assert entity_state.state == STATE_ON + + # Turn switch off. + await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw1, blocking=True) + assert hass.states.get(sw1_id).state == STATE_OFF + + # Turn switch back on. + await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_ON, sw1, blocking=True) + assert hass.states.get(sw1_id).state == STATE_ON + + # The other switch also starts out on. + assert (entity_state := hass.states.get(sw2_id)) is not None + assert entity_state.state == STATE_ON + + # Turn both switches off, an invalid combination. + await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw1, blocking=True) + await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw2, blocking=True) + assert hass.states.get(sw1_id).state == STATE_OFF + assert hass.states.get(sw2_id).state == STATE_OFF + + # Wait for switches to revert to device status. + freezer.tick(6) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(sw1_id).state == STATE_ON + assert hass.states.get(sw2_id).state == STATE_ON + + # Turn switch off. + await hass.services.async_call(SWITCH_DOMAIN, SERVICE_TURN_OFF, sw2, blocking=True) + assert hass.states.get(sw2_id).state == STATE_OFF + + # Exercise shutdown path. + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert hass.states.get(sw2_id).state == STATE_ON diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 1104ffad63d..d9f0f59b719 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -17,10 +17,11 @@ async def async_init_integration( hass: HomeAssistant, skip_setup: bool = False, exception: Exception | None = None, + *, + house_fixture="nexia/mobile_houses_123456.json", ) -> MockConfigEntry: """Set up the nexia integration in Home Assistant.""" - house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" set_fan_speed_fixture = "nexia/set_fan_speed_2293892.json" From 31847d8cfbd322e64069d254d49724e5f7159acd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 07:57:33 +0200 Subject: [PATCH 0423/1175] Adjust handling of SamsungTV misaligned MAC (#144810) * Cleanup SamsungTV misaligned MAC formatting * Simplify * One more * Revert and add comment * Adjust comment * One more --- .../components/samsungtv/__init__.py | 12 +++---- .../components/samsungtv/config_flow.py | 1 + .../components/samsungtv/test_config_flow.py | 9 ++--- tests/components/samsungtv/test_init.py | 36 ++++++------------- 4 files changed, 21 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 0fbd0f6d1e0..f7af5efc899 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -44,7 +44,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] @callback def _async_get_device_bridge( - hass: HomeAssistant, data: dict[str, Any] + hass: HomeAssistant, data: Mapping[str, Any] ) -> SamsungTVBridge: """Get device bridge.""" return SamsungTVBridge.get_bridge( @@ -171,20 +171,18 @@ async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: SamsungTVConfigEntry ) -> SamsungTVBridge: """Create a bridge object and update any missing data in the config entry.""" - updated_data: dict[str, str | int] = {} + updated_data: dict[str, str] = {} host: str = entry.data[CONF_HOST] method: str = entry.data[CONF_METHOD] - load_info_attempted = False info: dict[str, Any] | None = None - bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) + bridge = _async_get_device_bridge(hass, entry.data) mac: str | None = entry.data.get(CONF_MAC) model: str | None = entry.data.get(CONF_MODEL) + # Incorrect MAC cleanup introduced in #110599, can be removed in 2026.3 mac_is_incorrectly_formatted = mac and dr.format_mac(mac) != mac - if ( - not mac or not model or mac_is_incorrectly_formatted - ) and not load_info_attempted: + if not mac or not model or mac_is_incorrectly_formatted: info = await bridge.async_device_info() if not mac or mac_is_incorrectly_formatted: diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index c7406cc740f..dbde1ee1ef3 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -96,6 +96,7 @@ def _mac_is_same_with_incorrect_formatting( current_unformatted_mac: str, formatted_mac: str ) -> bool: """Check if two macs are the same but formatted incorrectly.""" + # Incorrect MAC cleanup introduced in #110599, can be removed in 2026.3 current_formatted_mac = format_mac(current_unformatted_mac) return ( current_formatted_mac == formatted_mac diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index f62c3cc1ae8..25c8bf9bab9 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1301,14 +1301,15 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures( - "remote_websocket", "rest_api", "remote_encrypted_websocket_failing" -) +@pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_update_missing_mac_unique_id_added_from_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" - entry = MockConfigEntry(domain=DOMAIN, data=ENTRYDATA_LEGACY, unique_id=None) + # Incorrect MAC cleanup introduced in #110599, can be removed in 2026.3 + entry_data = deepcopy(ENTRYDATA_WEBSOCKET) + del entry_data[CONF_MAC] + entry = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id=None) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index abafb1854ba..26bfbf328fa 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,10 +1,9 @@ """Tests for the Samsung TV Integration.""" from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch import pytest -from samsungtvws.async_remote import SamsungTVWSAsyncRemote from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -148,28 +147,13 @@ async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote_websocket", "rest_api") async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None: """Test incorrectly formatted mac is corrected.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" - ) as remote_class: - remote = Mock(SamsungTVWSAsyncRemote) - remote.__aenter__ = AsyncMock(return_value=remote) - remote.__aexit__ = AsyncMock() - remote.token = "123456789" - remote_class.return_value = remote + # Incorrect MAC cleanup introduced in #110599, can be removed in 2026.3 + await setup_samsungtv_entry( + hass, + {**ENTRYDATA_WEBSOCKET, CONF_MAC: "aabbaaaaaaaa"}, + ) + await hass.async_block_till_done() - await setup_samsungtv_entry( - hass, - { - CONF_HOST: "fake_host", - CONF_NAME: "fake", - CONF_PORT: 8001, - CONF_TOKEN: "123456789", - CONF_METHOD: METHOD_WEBSOCKET, - CONF_MAC: "aabbaaaaaaaa", - }, - ) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].data[CONF_MAC] == "aa:bb:aa:aa:aa:aa" From ab5d60e33d1d910d0ae9645e9260357b2ef37584 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Wed, 14 May 2025 07:59:48 +0200 Subject: [PATCH 0424/1175] Make DHCP discovery aware of the network integration (#144767) Co-authored-by: J. Nick Koston --- homeassistant/components/dhcp/__init__.py | 28 +++++++++++- homeassistant/components/dhcp/manifest.json | 1 + tests/components/dhcp/test_init.py | 48 +++++++++++++++++++++ tests/components/dhcp/test_websocket_api.py | 1 + tests/test_requirements.py | 2 +- 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 76d11f22424..70340c81f2f 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -7,6 +7,7 @@ from collections.abc import Callable from datetime import timedelta from fnmatch import translate from functools import lru_cache, partial +from ipaddress import IPv4Address import itertools import logging import re @@ -22,6 +23,7 @@ from aiodiscover.discovery import ( from cached_ipaddress import cached_ip_addresses from homeassistant import config_entries +from homeassistant.components import network from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, ATTR_IP, @@ -421,9 +423,33 @@ class DHCPWatcher(WatcherBase): response.ip_address, response.hostname, response.mac_address ) + async def async_get_adapter_indexes(self) -> list[int] | None: + """Get the adapter indexes.""" + adapters = await network.async_get_adapters(self.hass) + if network.async_only_default_interface_enabled(adapters): + return None + return [ + adapter["index"] + for adapter in adapters + if ( + adapter["enabled"] + and adapter["index"] is not None + and adapter["ipv4"] + and ( + addresses := [IPv4Address(ip["address"]) for ip in adapter["ipv4"]] + ) + and any( + ip for ip in addresses if not ip.is_loopback and not ip.is_global + ) + ) + ] + async def async_start(self) -> None: """Start watching for dhcp packets.""" - self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request) + self._unsub = await aiodhcpwatcher.async_start( + self._async_process_dhcp_request, + await self.async_get_adapter_indexes(), + ) class RediscoveryWatcher(WatcherBase): diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index c3b0121ff2b..ea2a4f4f820 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,6 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "codeowners": ["@bdraco"], + "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/dhcp", "integration_type": "system", "iot_class": "local_push", diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index f036902faed..4f7680ee2ab 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -157,6 +157,7 @@ async def _async_get_handle_dhcp_packet( hass, DHCPData(integration_matchers, set(), address_data), ) + with patch("aiodhcpwatcher.async_start"): await dhcp_watcher.async_start() @@ -171,6 +172,53 @@ async def _async_get_handle_dhcp_packet( return cast("Callable[[Any], Awaitable[None]]", _async_handle_dhcp_packet) +async def test_dhcp_start_using_multiple_interfaces( + hass: HomeAssistant, +) -> None: + """Test start using multiple interfaces.""" + + def _generate_mock_adapters(): + return [ + { + "index": 1, + "auto": False, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.0.1", "network_prefix": 24}], + "ipv6": [], + "name": "eth0", + }, + { + "index": 2, + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.1", "network_prefix": 24}], + "ipv6": [], + "name": "eth1", + }, + ] + + integration_matchers = dhcp.async_index_integration_matchers( + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}] + ) + dhcp_watcher = dhcp.DHCPWatcher( + hass, + DHCPData(integration_matchers, set(), {}), + ) + + with ( + patch("aiodhcpwatcher.async_start") as mock_start, + patch( + "homeassistant.components.dhcp.network.async_get_adapters", + return_value=_generate_mock_adapters(), + ), + ): + await dhcp_watcher.async_start() + + mock_start.assert_called_with(dhcp_watcher._async_process_dhcp_request, [1, 2]) + + async def test_dhcp_match_hostname_and_macaddress(hass: HomeAssistant) -> None: """Test matching based on hostname and macaddress.""" integration_matchers = dhcp.async_index_integration_matchers( diff --git a/tests/components/dhcp/test_websocket_api.py b/tests/components/dhcp/test_websocket_api.py index eb008c49ab1..0b21ef8e856 100644 --- a/tests/components/dhcp/test_websocket_api.py +++ b/tests/components/dhcp/test_websocket_api.py @@ -22,6 +22,7 @@ async def test_subscribe_discovery( async def mock_start( callback: Callable[[aiodhcpwatcher.DHCPRequest], None], + if_indexes: list[int] | None = None, ) -> None: """Mock start.""" nonlocal saved_callback diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 191e1b7368c..9fcb84beec6 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -655,5 +655,5 @@ async def test_discovery_requirements_dhcp(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 1 # dhcp does not depend on http + assert len(mock_process.mock_calls) == 2 # dhcp does not depend on http assert mock_process.mock_calls[0][1][1] == dhcp.requirements From 9aa26641886e4ac31ac9019ff2eef5e7915cccc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 14 May 2025 08:07:38 +0200 Subject: [PATCH 0425/1175] Change unknown to unknown_code for missing Miele codes to avoid confusion (#144699) * Change unknown to unknown_code * Update snapshot * Automatically replace missing codes with None * Update snapshot --- homeassistant/components/miele/const.py | 4 ++-- homeassistant/components/miele/vacuum.py | 2 +- tests/components/miele/snapshots/test_sensor.ambr | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 2b933873da4..e6de990043d 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -339,7 +339,7 @@ class StateProgramType(MieleEnum): automatic_program = 2 cleaning_care_program = 3 maintenance_program = 4 - unknown = -9999 + missing2none = -9999 class StateDryingStep(MieleEnum): @@ -353,7 +353,7 @@ class StateDryingStep(MieleEnum): hand_iron_2 = 5 machine_iron = 6 smoothing = 7 - unknown = -9999 + missing2none = -9999 WASHING_MACHINE_PROGRAM_ID: dict[int, str] = { diff --git a/homeassistant/components/miele/vacuum.py b/homeassistant/components/miele/vacuum.py index 1e14d33f461..66b3788fec5 100644 --- a/homeassistant/components/miele/vacuum.py +++ b/homeassistant/components/miele/vacuum.py @@ -82,7 +82,7 @@ class MieleVacuumStateCode(MieleEnum): blocked_front_wheel = 5900 docked = 5903, 5904 remote_controlled = 5910 - unknown = -9999 + missing2none = -9999 SUPPORTED_FEATURES = ( diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index bd9c305fe18..9cc2aa83b01 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -788,7 +788,6 @@ 'maintenance_program', 'normal_operation_mode', 'own_program', - 'unknown', ]), }), 'config_entry_id': , @@ -830,7 +829,6 @@ 'maintenance_program', 'normal_operation_mode', 'own_program', - 'unknown', ]), }), 'context': , From d2a692393fb32a42a40282ef08d018798e3df400 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 14 May 2025 16:08:24 +1000 Subject: [PATCH 0426/1175] Fix wall connector states in Teslemetry (#144855) * Fix wall connector * Update snapshot --- homeassistant/components/teslemetry/entity.py | 3 ++- homeassistant/components/teslemetry/sensor.py | 3 +-- tests/components/teslemetry/snapshots/test_sensor.ambr | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 170d4e3a3ae..588bf0b1b65 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -9,6 +9,7 @@ from tesla_fleet_api.teslemetry import EnergySite, Vehicle from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -228,7 +229,7 @@ class TeslemetryWallConnectorEntity(TeslemetryPollingEntity): super().__init__(data.live_coordinator, key) @property - def _value(self) -> int: + def _value(self) -> StateType: """Return a specific wall connector value from coordinator data.""" return ( self.coordinator.data.get("wall_connectors", {}) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 52b978dfd21..ee1dddf4774 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -1785,8 +1785,7 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" - if self.exists: - self._attr_native_value = self.entity_description.value_fn(self._value) + self._attr_native_value = self.entity_description.value_fn(self._value) class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity): diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 8e9ce51e297..3b860039b03 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -4978,7 +4978,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle-statealt] @@ -4991,7 +4991,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle_2-entry] @@ -5038,7 +5038,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors[sensor.wall_connector_vehicle_2-statealt] @@ -5051,7 +5051,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'disconnected', }) # --- # name: test_sensors_streaming[sensor.test_battery_level-state] From 67174fb07eb9e5ad50147554084e15764d380913 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 14 May 2025 14:37:51 +0800 Subject: [PATCH 0427/1175] Remove myself as code owner of sun component (#144854) * Remove myself as code owner I'm haven't actively been working on this for some time. Have removed myself as a code owner. * cleanup * cleanup --------- Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 ++-- homeassistant/components/sun/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ed87cfc635c..b8d7ea952ee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1484,8 +1484,8 @@ build.json @home-assistant/supervisor /tests/components/subaru/ @G-Two /homeassistant/components/suez_water/ @ooii @jb101010-2 /tests/components/suez_water/ @ooii @jb101010-2 -/homeassistant/components/sun/ @Swamp-Ig -/tests/components/sun/ @Swamp-Ig +/homeassistant/components/sun/ @home-assistant/core +/tests/components/sun/ @home-assistant/core /homeassistant/components/supla/ @mwegrzynek /homeassistant/components/surepetcare/ @benleb @danielhiversen /tests/components/surepetcare/ @benleb @danielhiversen diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index f6b4ae1976b..b693509b27a 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -1,7 +1,7 @@ { "domain": "sun", "name": "Sun", - "codeowners": ["@Swamp-Ig"], + "codeowners": ["@home-assistant/core"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sun", "iot_class": "calculated", From ac54b81289d5913fb591bfa63de3d7b87c4c6cf4 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Wed, 14 May 2025 10:01:14 +0200 Subject: [PATCH 0428/1175] Fix spelling of "IP address" in `plugwise` (#144861) --- homeassistant/components/plugwise/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index d26e70d1c4f..fdbe8c39015 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -23,7 +23,7 @@ }, "data_description": { "password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.", - "host": "The hostname or IP-address of your Smile. You can find it in your router or the Plugwise app.", + "host": "The hostname or IP address of your Smile. You can find it in your router or the Plugwise app.", "port": "By default your Smile uses port 80, normally you should not have to change this.", "username": "Default is `smile`, or `stretch` for the legacy Stretch." } From 34663e160dc52558a56301f85fd3cc29f286b627 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 14 May 2025 02:42:22 -0700 Subject: [PATCH 0429/1175] Bump ical to 9.2.4 (#144852) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index b43ded01d6e..d6f2ee76615 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"] + "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 2fa603d51ff..07de4a82244 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 735c11e645a..367c75d5755 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==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 33a46ea3dc8..9cf39b7ce45 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.2"] + "requirements": ["ical==9.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7ce4e6858d2..6e0110c16cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1197,7 +1197,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.2 +ical==9.2.4 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fdb52041e0..19b0d23a012 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,7 +1018,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.2 +ical==9.2.4 # homeassistant.components.caldav icalendar==6.1.0 From 577ddd90216855895ebcc4d4301e40906d41d2e4 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 14 May 2025 05:42:43 -0400 Subject: [PATCH 0430/1175] Bump python-snoo to 0.6.6 (#144849) --- homeassistant/components/snoo/manifest.json | 2 +- homeassistant/components/snoo/strings.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/snoo/manifest.json b/homeassistant/components/snoo/manifest.json index 839382b2d84..2afec990e4b 100644 --- a/homeassistant/components/snoo/manifest.json +++ b/homeassistant/components/snoo/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["snoo"], "quality_scale": "bronze", - "requirements": ["python-snoo==0.6.5"] + "requirements": ["python-snoo==0.6.6"] } diff --git a/homeassistant/components/snoo/strings.json b/homeassistant/components/snoo/strings.json index 1c86c066c7f..e4a5c634a68 100644 --- a/homeassistant/components/snoo/strings.json +++ b/homeassistant/components/snoo/strings.json @@ -56,7 +56,8 @@ "power": "Power button pressed", "status_requested": "Status requested", "sticky_white_noise_updated": "Sleepytime sounds updated", - "config_change": "Config changed" + "config_change": "Config changed", + "restart": "Restart" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 6e0110c16cf..83f76330e44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ python-roborock==2.18.2 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.5 +python-snoo==0.6.6 # homeassistant.components.songpal python-songpal==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19b0d23a012..db7005ad57f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2023,7 +2023,7 @@ python-roborock==2.18.2 python-smarttub==0.0.39 # homeassistant.components.snoo -python-snoo==0.6.5 +python-snoo==0.6.6 # homeassistant.components.songpal python-songpal==0.16.2 From 27798a600414dd76d08c029de07fe0721f86da0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 11:43:14 +0200 Subject: [PATCH 0431/1175] Bump actions/dependency-review-action from 4.7.0 to 4.7.1 (#144856) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.7.0 to 4.7.1. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/v4.7.0...v4.7.1) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 497e5b4b149..e0070874882 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -653,7 +653,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Dependency review - uses: actions/dependency-review-action@v4.7.0 + uses: actions/dependency-review-action@v4.7.1 with: license-check: false # We use our own license audit checks From 063deab3cb191c6bab6ca04b72a2bb2a017500a6 Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Wed, 14 May 2025 11:44:59 +0200 Subject: [PATCH 0432/1175] Doorbell Event is fired just once in homematicip_cloud (#144357) * fire event if event type if correct * Fix requested changes --- .../components/homematicip_cloud/event.py | 37 +++++++++++++++++-- .../homematicip_cloud/test_event.py | 29 +++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/event.py b/homeassistant/components/homematicip_cloud/event.py index 47a5ff46224..fc7f43bad1a 100644 --- a/homeassistant/components/homematicip_cloud/event.py +++ b/homeassistant/components/homematicip_cloud/event.py @@ -1,8 +1,11 @@ """Support for HomematicIP Cloud events.""" +from collections.abc import Callable from dataclasses import dataclass from typing import TYPE_CHECKING +from homematicip.base.channel_event import ChannelEvent +from homematicip.base.functionalChannels import FunctionalChannel from homematicip.device import Device from homeassistant.components.event import ( @@ -23,6 +26,9 @@ from .hap import HomematicipHAP class HmipEventEntityDescription(EventEntityDescription): """Description of a HomematicIP Cloud event.""" + channel_event_types: list[str] | None = None + channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None + EVENT_DESCRIPTIONS = { "doorbell": HmipEventEntityDescription( @@ -30,6 +36,8 @@ EVENT_DESCRIPTIONS = { translation_key="doorbell", device_class=EventDeviceClass.DOORBELL, event_types=["ring"], + channel_event_types=["DOOR_BELL_SENSOR_EVENT"], + channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT", ), } @@ -41,24 +49,29 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[DOMAIN][config_entry.unique_id] + entities: list[HomematicipGenericEntity] = [] - async_add_entities( + entities.extend( HomematicipDoorBellEvent( hap, device, channel.index, - EVENT_DESCRIPTIONS["doorbell"], + description, ) + for description in EVENT_DESCRIPTIONS.values() for device in hap.home.devices for channel in device.functionalChannels - if channel.channelRole == "DOOR_BELL_INPUT" + if description.channel_selector_fn and description.channel_selector_fn(channel) ) + async_add_entities(entities) + class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity): """Event class for HomematicIP doorbell events.""" _attr_device_class = EventDeviceClass.DOORBELL + entity_description: HmipEventEntityDescription def __init__( self, @@ -86,9 +99,27 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity): @callback def _async_handle_event(self, *args, **kwargs) -> None: """Handle the event fired by the functional channel.""" + raised_channel_event = self._get_channel_event_from_args(*args) + + if not self._should_raise(raised_channel_event): + return + event_types = self.entity_description.event_types if TYPE_CHECKING: assert event_types is not None self._trigger_event(event_type=event_types[0]) self.async_write_ha_state() + + def _should_raise(self, event_type: str) -> bool: + """Check if the event should be raised.""" + if self.entity_description.channel_event_types is None: + return False + return event_type in self.entity_description.channel_event_types + + def _get_channel_event_from_args(self, *args) -> str: + """Get the channel event.""" + if isinstance(args[0], ChannelEvent): + return args[0].channelEventType + + return "" diff --git a/tests/components/homematicip_cloud/test_event.py b/tests/components/homematicip_cloud/test_event.py index de615b35808..fcd16ca62d5 100644 --- a/tests/components/homematicip_cloud/test_event.py +++ b/tests/components/homematicip_cloud/test_event.py @@ -35,3 +35,32 @@ async def test_door_bell_event( ha_state = hass.states.get(entity_id) assert ha_state.state != STATE_UNKNOWN + + +async def test_door_bell_event_wrong_event_type( + hass: HomeAssistant, + default_mock_hap_factory: HomeFactory, +) -> None: + """Test of door bell event of HmIP-DSD-PCB.""" + entity_id = "event.dsdpcb_klingel_doorbell" + entity_name = "dsdpcb_klingel doorbell" + device_model = "HmIP-DSD-PCB" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["dsdpcb_klingel"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + ch = hmip_device.functionalChannels[1] + channel_event = ChannelEvent( + channelEventType="KEY_PRESS", channelIndex=1, deviceId=ch.device.id + ) + + assert ha_state.state == STATE_UNKNOWN + + ch.fire_channel_event(channel_event) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN From 4287df5f3d45125444206e0fa09a7df71b47639d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 11:51:32 +0200 Subject: [PATCH 0433/1175] Use HassKey in ps4 (#144868) --- homeassistant/components/ps4/__init__.py | 23 +++++++++++++++-------- homeassistant/components/ps4/const.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 2ccf086071a..f863bc6f46c 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -1,9 +1,13 @@ """Support for PlayStation 4 consoles.""" +from __future__ import annotations + +from dataclasses import dataclass import logging import os +from typing import TYPE_CHECKING -from pyps4_2ndscreen.ddp import async_create_ddp_endpoint +from pyps4_2ndscreen.ddp import DDPProtocol, async_create_ddp_endpoint from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol @@ -41,6 +45,9 @@ from .const import ( PS4_DATA, ) +if TYPE_CHECKING: + from .media_player import PS4Device + _LOGGER = logging.getLogger(__name__) SERVICE_COMMAND = "send_command" @@ -57,21 +64,21 @@ PLATFORMS = [Platform.MEDIA_PLAYER] CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) +@dataclass class PS4Data: """Init Data Class.""" - def __init__(self): - """Init Class.""" - self.devices = [] - self.protocol = None + devices: list[PS4Device] + protocol: DDPProtocol async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the PS4 Component.""" - hass.data[PS4_DATA] = PS4Data() - transport, protocol = await async_create_ddp_endpoint() - hass.data[PS4_DATA].protocol = protocol + hass.data[PS4_DATA] = PS4Data( + devices=[], + protocol=protocol, + ) _LOGGER.debug("PS4 DDP endpoint created: %s, %s", transport, protocol) service_handle(hass) return True diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index bd1144c4d98..f552388fe1d 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -1,5 +1,14 @@ """Constants for PlayStation 4.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from . import PS4Data + ATTR_MEDIA_IMAGE_URL = "media_image_url" CONFIG_ENTRY_VERSION = 3 DEFAULT_NAME = "PlayStation 4" @@ -7,7 +16,7 @@ DEFAULT_REGION = "United States" DEFAULT_ALIAS = "Home-Assistant" DOMAIN = "ps4" GAMES_FILE = ".ps4-games.{}.json" -PS4_DATA = "ps4_data" +PS4_DATA: HassKey[PS4Data] = HassKey(DOMAIN) COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps", "ps_hold") From 30ecba9944c8921c43f1ae11c4bf573d8a47dc04 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 11:58:01 +0200 Subject: [PATCH 0434/1175] Finish cleaning up SamsungTV init tests (#144865) FInish cleaning up SamsungTV init tests --- .../samsungtv/snapshots/test_init.ambr | 59 ------------------- tests/components/samsungtv/test_init.py | 45 +++++--------- 2 files changed, 14 insertions(+), 90 deletions(-) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 96dfed3b1ce..b29b824a7dd 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -112,62 +112,3 @@ }), ]) # --- -# name: test_setup_updates_from_ssdp - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'tv', - 'friendly_name': 'Mock Title', - 'is_volume_muted': False, - 'source_list': list([ - 'TV', - 'HDMI', - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'media_player.mock_title', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_setup_updates_from_ssdp.1 - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'source_list': list([ - 'TV', - 'HDMI', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'media_player', - 'entity_category': None, - 'entity_id': 'media_player.mock_title', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': None, - 'platform': 'samsungtv', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': 'sample-entry-id', - 'unit_of_measurement': None, - }) -# --- diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 26bfbf328fa..74af1b72c1c 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -6,7 +6,6 @@ from unittest.mock import Mock, patch import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.samsungtv.const import ( CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, @@ -19,16 +18,9 @@ from homeassistant.components.samsungtv.const import ( UPNP_SVC_RENDERING_CONTROL, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_METHOD, - CONF_NAME, - CONF_PORT, - CONF_TOKEN, -) +from homeassistant.const import CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from . import setup_samsungtv_entry from .const import ( @@ -41,14 +33,6 @@ from .const import ( from tests.common import MockConfigEntry, load_json_object_fixture -ENTITY_ID = f"{MP_DOMAIN}.mock_title" -MOCK_CONFIG = { - CONF_HOST: "fake_host", - CONF_NAME: "fake_name", - CONF_METHOD: METHOD_WEBSOCKET, - CONF_PORT: 8001, -} - @pytest.mark.parametrize( "entry_data", @@ -76,7 +60,7 @@ async def test_setup( assert device_entries == snapshot -@pytest.mark.usefixtures("remote_websocket", "remote_encrypted_websocket_failing") +@pytest.mark.usefixtures("remote_websocket") async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -84,23 +68,26 @@ async def test_setup_h_j_model( rest_api.rest_device_info.return_value = load_json_object_fixture( "device_info_UE48JU6400.json", DOMAIN ) - await setup_samsungtv_entry(hass, MOCK_CONFIG) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) - assert state + entry = await setup_samsungtv_entry( + hass, {**ENTRYDATA_WEBSOCKET, CONF_MODEL: "UE48JU6400"} + ) + + assert entry.state is ConfigEntryState.LOADED + assert "H and J series use an encrypted protocol" in caplog.text -@pytest.mark.usefixtures("remote_websocket", "rest_api") -async def test_setup_updates_from_ssdp( - hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion -) -> None: +@pytest.mark.usefixtures("remote_websocket") +async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None: """Test setting up the entry fetches data from ssdp cache.""" entry = MockConfigEntry( domain="samsungtv", data=ENTRYDATA_WEBSOCKET, entry_id="sample-entry-id" ) entry.add_to_hass(hass) + assert not entry.data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION) + assert not entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + async def _mock_async_get_discovery_info_by_st(hass: HomeAssistant, mock_st: str): if mock_st == UPNP_SVC_RENDERING_CONTROL: return [MOCK_SSDP_DATA_RENDERING_CONTROL_ST] @@ -113,11 +100,7 @@ async def test_setup_updates_from_ssdp( _mock_async_get_discovery_info_by_st, ): await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() - assert hass.states.get("media_player.mock_title") == snapshot - assert entity_registry.async_get("media_player.mock_title") == snapshot assert ( entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] == "http://10.10.12.34:7676/smp_2_" ) From 5acae7f86d1f3cb42f1cadbf44f16e372ee00550 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 14 May 2025 11:58:29 +0200 Subject: [PATCH 0435/1175] Fix Reolink setup when ONVIF push is unsupported (#144869) * Fix setup when ONVIF push is not supported * fix styling --- homeassistant/components/reolink/host.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 378c167d469..c3a8d340501 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -581,7 +581,12 @@ class ReolinkHost: ) return - await self._api.subscribe(self._webhook_url) + try: + await self._api.subscribe(self._webhook_url) + except NotSupportedError as err: + self._onvif_push_supported = False + _LOGGER.debug(err) + return _LOGGER.debug( "Host %s: subscribed successfully to webhook %s", @@ -602,7 +607,11 @@ class ReolinkHost: return # API is shutdown, no need to subscribe try: - if self._onvif_push_supported and not self._api.baichuan.events_active: + if ( + self._onvif_push_supported + and not self._api.baichuan.events_active + and self._cancel_tcp_push_check is None + ): await self._renew(SubType.push) if self._onvif_long_poll_supported and self._long_poll_task is not None: From 1748dbd60f49560634631a57f0c4d7785e84a6c5 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Wed, 14 May 2025 10:59:28 +0100 Subject: [PATCH 0436/1175] Add parallel_updates to new updates platform for Squeezebox (#144864) initial --- homeassistant/components/squeezebox/update.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/squeezebox/update.py b/homeassistant/components/squeezebox/update.py index c37594d346d..900eca97041 100644 --- a/homeassistant/components/squeezebox/update.py +++ b/homeassistant/components/squeezebox/update.py @@ -38,6 +38,9 @@ newplugins = UpdateEntityDescription( POLL_AFTER_INSTALL = 120 +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + _LOGGER = logging.getLogger(__name__) From 91f01d660fc790c5e7ba8a1b8ac90b1800f2f29d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 12:04:43 +0200 Subject: [PATCH 0437/1175] Move ps4 services to separate module (#144870) --- homeassistant/components/ps4/__init__.py | 48 +++--------------------- homeassistant/components/ps4/services.py | 37 ++++++++++++++++++ 2 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/ps4/services.py diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index f863bc6f46c..ddde4620871 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING from pyps4_2ndscreen.ddp import DDPProtocol, async_create_ddp_endpoint from pyps4_2ndscreen.media_art import COUNTRIES -import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.media_player import ( @@ -18,15 +17,8 @@ from homeassistant.components.media_player import ( MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_COMMAND, - ATTR_ENTITY_ID, - ATTR_LOCKED, - CONF_REGION, - CONF_TOKEN, - Platform, -) -from homeassistant.core import HomeAssistant, ServiceCall, split_entity_id +from homeassistant.const import ATTR_LOCKED, CONF_REGION, CONF_TOKEN, Platform +from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -36,28 +28,14 @@ from homeassistant.util import location as location_util from homeassistant.util.json import JsonObjectType, load_json_object from .config_flow import PlayStation4FlowHandler # noqa: F401 -from .const import ( - ATTR_MEDIA_IMAGE_URL, - COMMANDS, - COUNTRYCODE_NAMES, - DOMAIN, - GAMES_FILE, - PS4_DATA, -) +from .const import ATTR_MEDIA_IMAGE_URL, COUNTRYCODE_NAMES, DOMAIN, GAMES_FILE, PS4_DATA +from .services import register_services if TYPE_CHECKING: from .media_player import PS4Device _LOGGER = logging.getLogger(__name__) -SERVICE_COMMAND = "send_command" - -PS4_COMMAND_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS)), - } -) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -80,7 +58,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: protocol=protocol, ) _LOGGER.debug("PS4 DDP endpoint created: %s, %s", transport, protocol) - service_handle(hass) + register_services(hass) return True @@ -223,19 +201,3 @@ def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: if data_reformatted: save_games(hass, games, unique_id) return games - - -def service_handle(hass: HomeAssistant): - """Handle for services.""" - - async def async_service_command(call: ServiceCall) -> None: - """Service for sending commands.""" - entity_ids = call.data[ATTR_ENTITY_ID] - command = call.data[ATTR_COMMAND] - for device in hass.data[PS4_DATA].devices: - if device.entity_id in entity_ids: - await device.async_send_command(command) - - hass.services.async_register( - DOMAIN, SERVICE_COMMAND, async_service_command, schema=PS4_COMMAND_SCHEMA - ) diff --git a/homeassistant/components/ps4/services.py b/homeassistant/components/ps4/services.py new file mode 100644 index 00000000000..7da3cb0ae93 --- /dev/null +++ b/homeassistant/components/ps4/services.py @@ -0,0 +1,37 @@ +"""Support for PlayStation 4 consoles.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv + +from .const import COMMANDS, DOMAIN, PS4_DATA + +SERVICE_COMMAND = "send_command" + +PS4_COMMAND_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS)), + } +) + + +async def async_service_command(call: ServiceCall) -> None: + """Service for sending commands.""" + entity_ids = call.data[ATTR_ENTITY_ID] + command = call.data[ATTR_COMMAND] + for device in call.hass.data[PS4_DATA].devices: + if device.entity_id in entity_ids: + await device.async_send_command(command) + + +def register_services(hass: HomeAssistant) -> None: + """Handle for services.""" + + hass.services.async_register( + DOMAIN, SERVICE_COMMAND, async_service_command, schema=PS4_COMMAND_SCHEMA + ) From a21e586140b4f622f15f730c72c071c62eefb83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20den=20Berg?= Date: Wed, 14 May 2025 12:14:20 +0200 Subject: [PATCH 0438/1175] Show Sonos playlists under favorites (#142357) * Update media_browser.py * Update favorites.py * Update media_player.py * Update media_browser.py * Update media_player.py * Update favorites.py * Update media_browser.py * Update media_player.py * Update favorites.py * Added/fixed testing for showing sonos native playlists in the media browser * Create a DidlFavorite to wrap playlists. * Processed PR feedback --- homeassistant/components/sonos/favorites.py | 13 +++++++++++++ tests/components/sonos/conftest.py | 6 +++++- .../sonos/snapshots/test_media_browser.ambr | 11 +++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/favorites.py b/homeassistant/components/sonos/favorites.py index 333c4809e62..f8b3dbbe492 100644 --- a/homeassistant/components/sonos/favorites.py +++ b/homeassistant/components/sonos/favorites.py @@ -106,6 +106,9 @@ class SonosFavorites(SonosHouseholdCoordinator): def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: """Update cache of known favorites and return if cache has changed.""" new_favorites = soco.music_library.get_sonos_favorites(full_album_art_uri=True) + new_playlists = soco.music_library.get_music_library_information( + "sonos_playlists", full_album_art_uri=True + ) # Polled update_id values do not match event_id values # Each speaker can return a different polled update_id @@ -131,6 +134,16 @@ class SonosFavorites(SonosHouseholdCoordinator): except SoCoException as ex: # Skip unknown types _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) + for playlist in new_playlists: + playlist_reference = DidlFavorite( + title=playlist.title, + parent_id=playlist.parent_id, + item_id=playlist.item_id, + resources=playlist.resources, + desc=playlist.desc, + ) + playlist_reference.reference = playlist + self._favorites.append(playlist_reference) _LOGGER.debug( "Cached %s favorites for household %s using %s", diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index e22f18c6d77..b33151678a5 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -505,7 +505,7 @@ def mock_browse_by_idstring( def mock_get_music_library_information( - search_type: str, search_term: str, full_album_art_uri: bool = True + search_type: str, search_term: str | None = None, 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": @@ -517,6 +517,10 @@ def mock_get_music_library_information( "object.container.album.musicAlbum", ) ] + if search_type == "sonos_playlists": + playlists = load_json_value_fixture("sonos_playlists.json", "sonos") + playlists_list = [DidlPlaylistContainer.from_dict(pl) for pl in playlists] + return SearchResult(playlists_list, "sonos_playlists", 1, 1, 0) return [] diff --git a/tests/components/sonos/snapshots/test_media_browser.ambr b/tests/components/sonos/snapshots/test_media_browser.ambr index faa06a9adc2..07992c4474c 100644 --- a/tests/components/sonos/snapshots/test_media_browser.ambr +++ b/tests/components/sonos/snapshots/test_media_browser.ambr @@ -16,6 +16,17 @@ 'thumbnail': None, 'title': 'Albums', }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'playlist', + 'media_content_id': 'object.container.playlistContainer', + 'media_content_type': 'favorites_folder', + 'thumbnail': None, + 'title': 'Playlists', + }), dict({ 'can_expand': True, 'can_play': False, From 9a06584a1d75a5b178755dc410573f3c9a4b3aad Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Wed, 14 May 2025 12:21:26 +0200 Subject: [PATCH 0439/1175] Bump influxdb-client to 1.48.0 (#144845) * Bump influxdb-client to 1.48.0 * Adjust typing, fix mypy * Update homeassistant/components/influxdb/__init__.py * Update homeassistant/components/influxdb/__init__.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/influxdb/__init__.py | 2 +- homeassistant/components/influxdb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 95a94cf8fa0..d0cf7c3f8c9 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -338,7 +338,7 @@ def get_influx_connection( # noqa: C901 conf, test_write=False, test_read=False ) -> InfluxClient: """Create the correct influx connection for the API version.""" - kwargs = { + kwargs: dict[str, Any] = { CONF_TIMEOUT: TIMEOUT, } precision = conf.get(CONF_PRECISION) diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 55af2b37fb7..fbc6560899a 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["influxdb", "influxdb_client"], "quality_scale": "legacy", - "requirements": ["influxdb==5.3.1", "influxdb-client==1.24.0"] + "requirements": ["influxdb==5.3.1", "influxdb-client==1.48.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 83f76330e44..6eda282e955 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1230,7 +1230,7 @@ imgw_pib==1.0.10 incomfort-client==0.6.8 # homeassistant.components.influxdb -influxdb-client==1.24.0 +influxdb-client==1.48.0 # homeassistant.components.influxdb influxdb==5.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db7005ad57f..cb47548ebba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1045,7 +1045,7 @@ imgw_pib==1.0.10 incomfort-client==0.6.8 # homeassistant.components.influxdb -influxdb-client==1.24.0 +influxdb-client==1.48.0 # homeassistant.components.influxdb influxdb==5.3.1 From 8ccedd4064caa65d85c055c5cbeae4c29560c0bb Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 14 May 2025 20:21:45 +1000 Subject: [PATCH 0440/1175] Add credit balance sensor to Teslemetry (#144365) * Add credits * Credits string and icon * Add test * tests and fixes * Add units * Update homeassistant/components/teslemetry/sensor.py Co-authored-by: Joost Lekkerkerker * Update snapshot with unit --------- Co-authored-by: Joost Lekkerkerker --- .../components/teslemetry/__init__.py | 2 +- .../components/teslemetry/icons.json | 3 + homeassistant/components/teslemetry/models.py | 1 + homeassistant/components/teslemetry/sensor.py | 40 ++++++++++- .../components/teslemetry/strings.json | 4 ++ .../teslemetry/snapshots/test_sensor.ambr | 72 +++++++++++++++++++ tests/components/teslemetry/test_sensor.py | 7 ++ 7 files changed, 126 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 7b46caf2b43..5d9a757b9e6 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -241,7 +241,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) - ) # Setup Platforms - entry.runtime_data = TeslemetryData(vehicles, energysites, scopes) + entry.runtime_data = TeslemetryData(vehicles, energysites, scopes, stream) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if stream: diff --git a/homeassistant/components/teslemetry/icons.json b/homeassistant/components/teslemetry/icons.json index 242dccf90ec..edd5d404499 100644 --- a/homeassistant/components/teslemetry/icons.json +++ b/homeassistant/components/teslemetry/icons.json @@ -415,6 +415,9 @@ "brick_voltage_min": { "default": "mdi:battery-low" }, + "credit_balance": { + "default": "mdi:credit-card" + }, "cruise_follow_distance": { "default": "mdi:car-cruise-control" }, diff --git a/homeassistant/components/teslemetry/models.py b/homeassistant/components/teslemetry/models.py index 4f0d26a1cba..51eed97227e 100644 --- a/homeassistant/components/teslemetry/models.py +++ b/homeassistant/components/teslemetry/models.py @@ -28,6 +28,7 @@ class TeslemetryData: vehicles: list[TeslemetryVehicleData] energysites: list[TeslemetryEnergyData] scopes: list[Scope] + stream: TeslemetryStream @dataclass diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index ee1dddf4774..ab075d18132 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from teslemetry_stream import TeslemetryStreamVehicle +from teslemetry_stream import TeslemetryStream, TeslemetryStreamVehicle from homeassistant.components.sensor import ( RestoreSensor, @@ -45,7 +45,7 @@ from .entity import ( TeslemetryVehicleStreamEntity, TeslemetryWallConnectorEntity, ) -from .models import TeslemetryEnergyData, TeslemetryVehicleData +from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData PARALLEL_UPDATES = 0 @@ -1618,6 +1618,12 @@ async def async_setup_entry( if energysite.history_coordinator is not None ) + entities.append( + TeslemetryCreditBalanceSensor( + entry.unique_id or entry.entry_id, entry.runtime_data + ) + ) + async_add_entities(entities) @@ -1825,3 +1831,33 @@ class TeslemetryEnergyHistorySensorEntity(TeslemetryEnergyHistoryEntity, SensorE def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" self._attr_native_value = self._value + + +class TeslemetryCreditBalanceSensor(RestoreSensor): + """Entity for Teslemetry Credit balance.""" + + _attr_has_entity_name = True + stream: TeslemetryStream + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_suggested_display_precision = 0 + + def __init__(self, uid: str, data: TeslemetryData) -> None: + """Initialize common aspects of a Teslemetry entity.""" + + self._attr_translation_key = "credit_balance" + self._attr_unique_id = f"{uid}_credit_balance" + self.stream = data.stream + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + + if (sensor_data := await self.async_get_last_sensor_data()) is not None: + self._attr_native_value = sensor_data.native_value + + self.async_on_remove(self.stream.listen_Balance(self._async_update)) + + def _async_update(self, value: int) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index cd20cde6293..fb68e045b37 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -490,6 +490,10 @@ "climate_state_passenger_temp_setting": { "name": "Passenger temperature setting" }, + "credit_balance": { + "name": "Teslemetry credits", + "unit_of_measurement": "credits" + }, "drive_state_active_route_destination": { "name": "Destination" }, diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 3b860039b03..13d87dbe88b 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -2424,6 +2424,75 @@ 'state': '0', }) # --- +# name: test_sensors[sensor.teslemetry_credits-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.teslemetry_credits', + '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': 'Teslemetry credits', + 'platform': 'teslemetry', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'credit_balance', + 'unique_id': 'abc-123_credit_balance', + 'unit_of_measurement': 'credits', + }) +# --- +# name: test_sensors[sensor.teslemetry_credits-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Teslemetry credits', + 'state_class': , + 'unit_of_measurement': 'credits', + }), + 'context': , + 'entity_id': 'sensor.teslemetry_credits', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.teslemetry_credits-statealt] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Teslemetry credits', + 'state_class': , + 'unit_of_measurement': 'credits', + }), + 'context': , + 'entity_id': 'sensor.teslemetry_credits', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.test_battery_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5054,6 +5123,9 @@ 'state': 'disconnected', }) # --- +# name: test_sensors_streaming[sensor.teslemetry_credits-state] + '1980' +# --- # name: test_sensors_streaming[sensor.test_battery_level-state] '90' # --- diff --git a/tests/components/teslemetry/test_sensor.py b/tests/components/teslemetry/test_sensor.py index 213811f6ea0..f50dc93bde4 100644 --- a/tests/components/teslemetry/test_sensor.py +++ b/tests/components/teslemetry/test_sensor.py @@ -73,6 +73,12 @@ async def test_sensors_streaming( Signal.TIME_TO_FULL_CHARGE: 0.166666667, Signal.MINUTES_TO_ARRIVAL: None, }, + "credits": { + "type": "wake_up", + "cost": 20, + "name": "wake_up", + "balance": 1980, + }, "createdAt": "2024-10-04T10:45:17.537Z", } ) @@ -91,6 +97,7 @@ async def test_sensors_streaming( "sensor.test_charge_cable", "sensor.test_time_to_full_charge", "sensor.test_time_to_arrival", + "sensor.teslemetry_credits", ): state = hass.states.get(entity_id) assert state.state == snapshot(name=f"{entity_id}-state") From 161b62d8fa496207c0413fce898736479c1409c4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 12:24:46 +0200 Subject: [PATCH 0441/1175] Drop alias from local DOMAIN import (#144867) --- homeassistant/components/axis/config_flow.py | 6 +++--- .../bmw_connected_drive/coordinator.py | 18 ++++++------------ homeassistant/components/cast/media_player.py | 12 ++++++------ homeassistant/components/cloud/alexa_config.py | 4 ++-- .../components/cloud/google_config.py | 4 ++-- homeassistant/components/deconz/hub/hub.py | 9 ++------- homeassistant/components/hue/v1/light.py | 8 ++++---- .../components/hue/v1/sensor_device.py | 10 +++------- homeassistant/components/konnected/switch.py | 8 ++++---- homeassistant/components/ps4/media_player.py | 4 ++-- .../components/rachio/binary_sensor.py | 4 ++-- homeassistant/components/rachio/calendar.py | 4 ++-- homeassistant/components/rachio/switch.py | 8 ++++---- homeassistant/components/sonos/media_player.py | 18 +++++++++--------- homeassistant/components/sonos/switch.py | 4 ++-- homeassistant/components/unifi/config_flow.py | 4 ++-- 16 files changed, 55 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 8b4a1d4f5f5..388e360040e 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -47,7 +47,7 @@ from .const import ( CONF_VIDEO_SOURCE, DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, - DOMAIN as AXIS_DOMAIN, + DOMAIN, ) from .errors import AuthenticationRequired, CannotConnect from .hub import AxisHub, get_axis_api @@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https" PROTOCOL_CHOICES = ["https", "http"] -class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): +class AxisFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" VERSION = 3 @@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): model = self.config[CONF_MODEL] same_model = [ entry.data[CONF_NAME] - for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) + for entry in self.hass.config_entries.async_entries(DOMAIN) if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model ] diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index b54d9245bbd..73e19ca7af5 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -22,13 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.ssl import get_default_context -from .const import ( - CONF_GCID, - CONF_READ_ONLY, - CONF_REFRESH_TOKEN, - DOMAIN as BMW_DOMAIN, - SCAN_INTERVALS, -) +from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS _LOGGER = logging.getLogger(__name__) @@ -63,7 +57,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): hass, _LOGGER, config_entry=config_entry, - name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}", + name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}", update_interval=timedelta( seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]] ), @@ -81,26 +75,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): except MyBMWCaptchaMissingError as err: # If a captcha is required (user/password login flow), always trigger the reauth flow raise ConfigEntryAuthFailed( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="missing_captcha", ) from err except MyBMWAuthError as err: # Allow one retry interval before raising AuthFailed to avoid flaky API issues if self.last_update_success: raise UpdateFailed( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="update_failed", translation_placeholders={"exception": str(err)}, ) from err # Clear refresh token and trigger reauth if previous update failed as well self._update_config_entry_refresh_token(None) raise ConfigEntryAuthFailed( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_auth", ) from err except (MyBMWAPIError, RequestError) as err: raise UpdateFailed( - translation_domain=BMW_DOMAIN, + translation_domain=DOMAIN, translation_key="update_failed", translation_placeholders={"exception": str(err)}, ) from err diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 8ff078dfafd..e17360127b9 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -60,7 +60,7 @@ from .const import ( ADDED_CAST_DEVICES_KEY, CAST_MULTIZONE_MANAGER_KEY, CONF_IGNORE_CEC, - DOMAIN as CAST_DOMAIN, + DOMAIN, SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, @@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self._cast_view_remove_handler: CALLBACK_TYPE | None = None self._attr_unique_id = str(cast_info.uuid) self._attr_device_info = DeviceInfo( - identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, + identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))}, manufacturer=str(cast_info.cast_info.manufacturer), model=cast_info.cast_info.model_name, name=str(cast_info.friendly_name), @@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Generate root node.""" children = [] # Add media browsers - for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): + for platform in self.hass.data[DOMAIN]["cast_platform"].values(): children.extend( await platform.async_get_media_browser_root_object( self.hass, self._chromecast.cast_type @@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): platform: CastProtocol assert media_content_type is not None - for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): + for platform in self.hass.data[DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, media_content_type, @@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) # Handle media supported by a known cast app - if media_type == CAST_DOMAIN: + if media_type == DOMAIN: try: app_data = json.loads(media_id) if metadata := extra.get("metadata"): @@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return # Try the cast platforms - for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): + for platform in self.hass.data[DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, chromecast, media_type, media_id ) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 5b77a02384d..5bd40eb5b83 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, - DOMAIN as CLOUD_DOMAIN, + DOMAIN, PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_SHOULD_EXPOSE, @@ -55,7 +55,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}" +CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}" # Time to wait when entity preferences have changed before syncing it to # the cloud. diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 43dd5279d35..2b6f45ec474 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -41,7 +41,7 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_DISABLE_2FA, - DOMAIN as CLOUD_DOMAIN, + DOMAIN, PREF_DISABLE_2FA, PREF_SHOULD_EXPOSE, ) @@ -52,7 +52,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}" +CLOUD_GOOGLE = f"{DOMAIN}.{GOOGLE_DOMAIN}" SUPPORTED_DOMAINS = { diff --git a/homeassistant/components/deconz/hub/hub.py b/homeassistant/components/deconz/hub/hub.py index 3020d624f97..f82f1d857fd 100644 --- a/homeassistant/components/deconz/hub/hub.py +++ b/homeassistant/components/deconz/hub/hub.py @@ -17,12 +17,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..const import ( - CONF_MASTER_GATEWAY, - DOMAIN as DECONZ_DOMAIN, - HASSIO_CONFIGURATION_URL, - PLATFORMS, -) +from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS from .config import DeconzConfig if TYPE_CHECKING: @@ -193,7 +188,7 @@ class DeconzHub: config_entry_id=self.config_entry.entry_id, configuration_url=configuration_url, entry_type=dr.DeviceEntryType.SERVICE, - identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)}, + identifiers={(DOMAIN, self.api.config.bridge_id)}, manufacturer="Dresden Elektronik", model=self.api.config.model_id, name=self.api.config.name, diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 33b99a7895b..a806572e0f1 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -45,7 +45,7 @@ from ..const import ( CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_UNREACHABLE, - DOMAIN as HUE_DOMAIN, + DOMAIN, GROUP_TYPE_ENTERTAINMENT, GROUP_TYPE_LIGHT_GROUP, GROUP_TYPE_LIGHT_SOURCE, @@ -141,7 +141,7 @@ def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" - bridge: HueBridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) rooms = {} @@ -518,7 +518,7 @@ class HueLight(CoordinatorEntity, LightEntity): suggested_area = self._rooms[self.light.id] return DeviceInfo( - identifiers={(HUE_DOMAIN, self.device_id)}, + identifiers={(DOMAIN, self.device_id)}, manufacturer=self.light.manufacturername, # productname added in Hue Bridge API 1.24 # (published 03/05/2018) @@ -526,7 +526,7 @@ class HueLight(CoordinatorEntity, LightEntity): name=self.name, sw_version=self.light.swversion, suggested_area=suggested_area, - via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), + via_device=(DOMAIN, self.bridge.api.config.bridgeid), ) async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/hue/v1/sensor_device.py b/homeassistant/components/hue/v1/sensor_device.py index cb0a2721334..a18f2176f67 100644 --- a/homeassistant/components/hue/v1/sensor_device.py +++ b/homeassistant/components/hue/v1/sensor_device.py @@ -3,11 +3,7 @@ from homeassistant.helpers import entity from homeassistant.helpers.device_registry import DeviceInfo -from ..const import ( - CONF_ALLOW_UNREACHABLE, - DEFAULT_ALLOW_UNREACHABLE, - DOMAIN as HUE_DOMAIN, -) +from ..const import CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE, DOMAIN class GenericHueDevice(entity.Entity): # pylint: disable=hass-enforce-class-module @@ -55,10 +51,10 @@ class GenericHueDevice(entity.Entity): # pylint: disable=hass-enforce-class-mod Links individual entities together in the hass device registry. """ return DeviceInfo( - identifiers={(HUE_DOMAIN, self.device_id)}, + identifiers={(DOMAIN, self.device_id)}, manufacturer=self.primary_sensor.manufacturername, model=(self.primary_sensor.productname or self.primary_sensor.modelid), name=self.primary_sensor.name, sw_version=self.primary_sensor.swversion, - via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), + via_device=(DOMAIN, self.bridge.api.config.bridgeid), ) diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 58311502cbe..54f74f0d461 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -22,7 +22,7 @@ from .const import ( CONF_ACTIVATION, CONF_MOMENTARY, CONF_PAUSE, - DOMAIN as KONNECTED_DOMAIN, + DOMAIN, STATE_HIGH, STATE_LOW, ) @@ -36,7 +36,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up switches attached to a Konnected device from a config entry.""" - data = hass.data[KONNECTED_DOMAIN] + data = hass.data[DOMAIN] device_id = config_entry.data["id"] switches = [ KonnectedSwitch(device_id, zone_data.get(CONF_ZONE), zone_data) @@ -63,12 +63,12 @@ class KonnectedSwitch(SwitchEntity): f"{device_id}-{self._zone_num}-{self._momentary}-" f"{self._pause}-{self._repeat}" ) - self._attr_device_info = DeviceInfo(identifiers={(KONNECTED_DOMAIN, device_id)}) + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_id)}) @property def panel(self): """Return the Konnected HTTP client.""" - device_data = self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id] + device_data = self.hass.data[DOMAIN][CONF_DEVICES][self._device_id] return device_data.get("panel") @property diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 4de7cbeb463..aaec7cdf105 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -34,7 +34,7 @@ from . import format_unique_id, load_games, save_games from .const import ( ATTR_MEDIA_IMAGE_URL, DEFAULT_ALIAS, - DOMAIN as PS4_DOMAIN, + DOMAIN, PS4_DATA, REGIONS as deprecated_regions, ) @@ -366,7 +366,7 @@ class PS4Device(MediaPlayerEntity): _sw_version = _sw_version[1:4] sw_version = f"{_sw_version[0]}.{_sw_version[1:]}" self._attr_device_info = DeviceInfo( - identifiers={(PS4_DOMAIN, status["host-id"])}, + identifiers={(DOMAIN, status["host-id"])}, manufacturer="Sony Interactive Entertainment Inc.", model="PlayStation 4", name=status["host-name"], diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 3bf0f716c6d..0c502a98c9a 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( - DOMAIN as DOMAIN_RACHIO, + DOMAIN, KEY_BATTERY_STATUS, KEY_DEVICE_ID, KEY_LOW, @@ -55,7 +55,7 @@ async def async_setup_entry( def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: entities: list[Entity] = [] - person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] for controller in person.controllers: entities.append(RachioControllerOnlineBinarySensor(controller)) entities.append(RachioRainSensor(controller)) diff --git a/homeassistant/components/rachio/calendar.py b/homeassistant/components/rachio/calendar.py index 91ad29fac9f..984e5ae8881 100644 --- a/homeassistant/components/rachio/calendar.py +++ b/homeassistant/components/rachio/calendar.py @@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - DOMAIN as DOMAIN_RACHIO, + DOMAIN, KEY_ADDRESS, KEY_DURATION_SECONDS, KEY_ID, @@ -44,7 +44,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up entry for Rachio smart hose timer calendar.""" - person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( RachioCalendarEntity(base_station.schedule_coordinator, base_station) for base_station in person.base_stations diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 25cdeac62f7..0edccf02320 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -23,7 +23,7 @@ from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_ti from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, - DOMAIN as DOMAIN_RACHIO, + DOMAIN, KEY_CURRENT_STATUS, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, @@ -119,7 +119,7 @@ async def async_setup_entry( def start_multiple(service: ServiceCall) -> None: """Service to start multiple zones in sequence.""" zones_list = [] - person = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + person = hass.data[DOMAIN][config_entry.entry_id] entity_id = service.data[ATTR_ENTITY_ID] duration = iter(service.data[ATTR_DURATION]) default_time = service.data[ATTR_DURATION][0] @@ -160,7 +160,7 @@ async def async_setup_entry( return hass.services.async_register( - DOMAIN_RACHIO, + DOMAIN, SERVICE_START_MULTIPLE_ZONES, start_multiple, schema=START_MULTIPLE_ZONES_SCHEMA, @@ -177,7 +177,7 @@ async def async_setup_entry( def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: entities: list[Entity] = [] - person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] # Fetch the schedule once at startup # in order to avoid every zone doing it for controller in person.controllers: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a774de0ae5b..573c28d700a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -52,7 +52,7 @@ from homeassistant.helpers.event import async_call_later from . import UnjoinData, media_browser from .const import ( DATA_SONOS, - DOMAIN as SONOS_DOMAIN, + DOMAIN, MEDIA_TYPES_TO_SONOS, MODELS_LINEIN_AND_TV, MODELS_LINEIN_ONLY, @@ -119,7 +119,7 @@ async def async_setup_entry( _LOGGER.debug("Creating media_player on %s", speaker.zone_name) async_add_entities([SonosMediaPlayerEntity(speaker)]) - @service.verify_domain_control(hass, SONOS_DOMAIN) + @service.verify_domain_control(hass, DOMAIN) async def async_service_handle(service_call: ServiceCall) -> None: """Handle dispatched services.""" assert platform is not None @@ -151,11 +151,11 @@ async def async_setup_entry( ) hass.services.async_register( - SONOS_DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema + DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema ) hass.services.async_register( - SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema + DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema ) platform.async_register_entity_service( @@ -448,7 +448,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if len(fav) != 1: raise ServiceValidationError( - translation_domain=SONOS_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_favorite", translation_placeholders={ "name": name, @@ -577,7 +577,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) else: raise HomeAssistantError( - translation_domain=SONOS_DOMAIN, + translation_domain=DOMAIN, translation_key="announce_media_error", translation_placeholders={ "media_id": media_id, @@ -684,7 +684,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): playlist = next((p for p in playlists if p.title == media_id), None) if not playlist: raise ServiceValidationError( - translation_domain=SONOS_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_sonos_playlist", translation_placeholders={ "name": media_id, @@ -697,7 +697,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): item = media_browser.get_media(self.media.library, media_id, media_type) if not item: raise ServiceValidationError( - translation_domain=SONOS_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_media", translation_placeholders={ "media_id": media_id, @@ -706,7 +706,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self._play_media_queue(soco, item, enqueue) else: raise ServiceValidationError( - translation_domain=SONOS_DOMAIN, + translation_domain=DOMAIN, translation_key="invalid_content_type", translation_placeholders={ "media_type": media_type, diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index ce4774a4138..052dbd990b2 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -20,7 +20,7 @@ from homeassistant.helpers.event import async_track_time_change from .const import ( DATA_SONOS, - DOMAIN as SONOS_DOMAIN, + DOMAIN, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM, SONOS_CREATE_SWITCHES, @@ -276,7 +276,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): new_device = device_registry.async_get_or_create( config_entry_id=cast(str, entity.config_entry_id), - identifiers={(SONOS_DOMAIN, self.soco.uid)}, + identifiers={(DOMAIN, self.soco.uid)}, connections={(dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address)}, ) if ( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 3878e4c60eb..c8c6a54f9fe 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -56,7 +56,7 @@ from .const import ( CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, DEFAULT_DPI_RESTRICTIONS, - DOMAIN as UNIFI_DOMAIN, + DOMAIN, ) from .errors import AuthenticationRequired, CannotConnect from .hub import UnifiHub, get_unifi_api @@ -72,7 +72,7 @@ MODEL_PORTS = { } -class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN): +class UnifiFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a UniFi Network config flow.""" VERSION = 1 From c023f610dd172fbfca18a9051d29365cb0f6165c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Wed, 14 May 2025 12:28:32 +0200 Subject: [PATCH 0442/1175] Introduce recorder.get_statistics service (#142602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: abmantis Co-authored-by: J. Nick Koston Co-authored-by: Abílio Costa --- homeassistant/components/recorder/icons.json | 3 + homeassistant/components/recorder/services.py | 99 +++++- .../components/recorder/services.yaml | 60 ++++ .../components/recorder/strings.json | 30 ++ tests/components/recorder/test_statistics.py | 323 +++++++++++++++++- 5 files changed, 512 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/icons.json b/homeassistant/components/recorder/icons.json index 9e41637184a..87634bedcc8 100644 --- a/homeassistant/components/recorder/icons.json +++ b/homeassistant/components/recorder/icons.json @@ -11,6 +11,9 @@ }, "enable": { "service": "mdi:database" + }, + "get_statistics": { + "service": "mdi:chart-bar" } } } diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py index cc74d7a2376..ba454c59bf3 100644 --- a/homeassistant/components/recorder/services.py +++ b/homeassistant/components/recorder/services.py @@ -8,7 +8,13 @@ from typing import cast import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, + callback, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.service import ( @@ -16,15 +22,18 @@ from homeassistant.helpers.service import ( async_register_admin_service, ) from homeassistant.util import dt as dt_util +from homeassistant.util.json import JsonArrayType, JsonObjectType from .const import ATTR_APPLY_FILTER, ATTR_KEEP_DAYS, ATTR_REPACK, DOMAIN from .core import Recorder +from .statistics import statistics_during_period from .tasks import PurgeEntitiesTask, PurgeTask SERVICE_PURGE = "purge" SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" +SERVICE_GET_STATISTICS = "get_statistics" SERVICE_PURGE_SCHEMA = vol.Schema( { @@ -63,6 +72,20 @@ SERVICE_PURGE_ENTITIES_SCHEMA = vol.All( SERVICE_ENABLE_SCHEMA = vol.Schema({}) SERVICE_DISABLE_SCHEMA = vol.Schema({}) +SERVICE_GET_STATISTICS_SCHEMA = vol.Schema( + { + vol.Required("start_time"): cv.datetime, + vol.Optional("end_time"): cv.datetime, + vol.Required("statistic_ids"): vol.All(cv.ensure_list, [cv.string]), + vol.Required("period"): vol.In(["5minute", "hour", "day", "week", "month"]), + vol.Required("types"): vol.All( + cv.ensure_list, + [vol.In(["change", "last_reset", "max", "mean", "min", "state", "sum"])], + ), + vol.Optional("units"): vol.Schema({cv.string: cv.string}), + } +) + @callback def _async_register_purge_service(hass: HomeAssistant, instance: Recorder) -> None: @@ -135,6 +158,79 @@ def _async_register_disable_service(hass: HomeAssistant, instance: Recorder) -> ) +@callback +def _async_register_get_statistics_service( + hass: HomeAssistant, instance: Recorder +) -> None: + async def async_handle_get_statistics_service( + service: ServiceCall, + ) -> ServiceResponse: + """Handle calls to the get_statistics service.""" + start_time = dt_util.as_utc(service.data["start_time"]) + end_time = ( + dt_util.as_utc(service.data["end_time"]) + if "end_time" in service.data + else None + ) + + statistic_ids = service.data["statistic_ids"] + types = service.data["types"] + period = service.data["period"] + units = service.data.get("units") + + result = await instance.async_add_executor_job( + statistics_during_period, + hass, + start_time, + end_time, + statistic_ids, + period, + units, + types, + ) + + formatted_result: JsonObjectType = {} + for statistic_id, statistic_rows in result.items(): + formatted_statistic_rows: JsonArrayType = [] + + for row in statistic_rows: + formatted_row: JsonObjectType = { + "start": dt_util.utc_from_timestamp(row["start"]).isoformat(), + "end": dt_util.utc_from_timestamp(row["end"]).isoformat(), + } + if (last_reset := row.get("last_reset")) is not None: + formatted_row["last_reset"] = dt_util.utc_from_timestamp( + last_reset + ).isoformat() + if (state := row.get("state")) is not None: + formatted_row["state"] = state + if (sum_value := row.get("sum")) is not None: + formatted_row["sum"] = sum_value + if (min_value := row.get("min")) is not None: + formatted_row["min"] = min_value + if (max_value := row.get("max")) is not None: + formatted_row["max"] = max_value + if (mean := row.get("mean")) is not None: + formatted_row["mean"] = mean + if (change := row.get("change")) is not None: + formatted_row["change"] = change + + formatted_statistic_rows.append(formatted_row) + + formatted_result[statistic_id] = formatted_statistic_rows + + return {"statistics": formatted_result} + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_GET_STATISTICS, + async_handle_get_statistics_service, + schema=SERVICE_GET_STATISTICS_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + + @callback def async_register_services(hass: HomeAssistant, instance: Recorder) -> None: """Register recorder services.""" @@ -142,3 +238,4 @@ def async_register_services(hass: HomeAssistant, instance: Recorder) -> None: _async_register_purge_entities_service(hass, instance) _async_register_enable_service(hass, instance) _async_register_disable_service(hass, instance) + _async_register_get_statistics_service(hass, instance) diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml index 7d7b926548c..65aa797d91b 100644 --- a/homeassistant/components/recorder/services.yaml +++ b/homeassistant/components/recorder/services.yaml @@ -48,3 +48,63 @@ purge_entities: disable: enable: + +get_statistics: + fields: + start_time: + required: true + example: "2025-01-01 00:00:00" + selector: + datetime: + + end_time: + required: false + example: "2025-01-02 00:00:00" + selector: + datetime: + + statistic_ids: + required: true + example: + - sensor.energy_consumption + - sensor.temperature + selector: + entity: + multiple: true + + period: + required: true + example: "hour" + selector: + select: + options: + - "5minute" + - "hour" + - "day" + - "week" + - "month" + + types: + required: true + example: + - "mean" + - "sum" + selector: + select: + options: + - "change" + - "last_reset" + - "max" + - "mean" + - "min" + - "state" + - "sum" + multiple: true + + units: + required: false + example: + energy: "kWh" + temperature: "°C" + selector: + object: diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 0c8d47548bf..eb7e0c8b63d 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -66,6 +66,36 @@ "enable": { "name": "[%key:common::action::enable%]", "description": "Starts the recording of events and state changes." + }, + "get_statistics": { + "name": "Get statistics", + "description": "Retrieves statistics data for entities within a specific time period.", + "fields": { + "end_time": { + "name": "End time", + "description": "The end time for the statistics query. If omitted, returns all statistics from start time onward." + }, + "period": { + "name": "Period", + "description": "The time period to group statistics by." + }, + "start_time": { + "name": "Start time", + "description": "The start time for the statistics query." + }, + "statistic_ids": { + "name": "Statistic IDs", + "description": "The entity IDs or statistic IDs to return statistics for." + }, + "types": { + "name": "Types", + "description": "The types of statistics values to return." + }, + "units": { + "name": "Units", + "description": "Optional unit conversion mapping." + } + } } } } diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index ed754723426..a8d8ed61020 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -2,12 +2,15 @@ from collections.abc import Generator from datetime import timedelta +import re from typing import Any from unittest.mock import ANY, Mock, patch import pytest from sqlalchemy import select +import voluptuous as vol +from homeassistant import exceptions from homeassistant.components import recorder from homeassistant.components.recorder import Recorder, history, statistics from homeassistant.components.recorder.db_schema import StatisticsShortTerm @@ -40,7 +43,7 @@ from homeassistant.components.recorder.table_managers.statistics_meta import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.sensor import UNIT_CONVERTERS -from homeassistant.core import HomeAssistant +from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -56,7 +59,7 @@ from .common import ( statistics_during_period, ) -from tests.common import MockPlatform, mock_platform +from tests.common import MockPlatform, MockUser, mock_platform from tests.typing import RecorderInstanceContextManager, WebSocketGenerator @@ -3421,3 +3424,319 @@ async def test_recorder_platform_with_partial_statistics_support( for meth in supported_methods: getattr(recorder_platform, meth).assert_called_once() + + +@pytest.mark.parametrize( + ("service_args", "expected_result"), + [ + ( + { + "start_time": "2023-05-08 07:00:00Z", + "period": "hour", + "statistic_ids": ["sensor.i_dont_exist"], + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + }, + {"statistics": {}}, + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "period": "hour", + "statistic_ids": [ + "sensor.total_energy_import1", + "sensor.total_energy_import2", + ], + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + }, + { + "statistics": { + "sensor.total_energy_import1": [ + { + "last_reset": "2021-12-31T22:00:00+00:00", + "change": 2.0, + "end": "2023-05-08T08:00:00+00:00", + "start": "2023-05-08T07:00:00+00:00", + "state": 0.0, + "sum": 2.0, + "min": 0.0, + "max": 10.0, + "mean": 1.0, + }, + { + "change": 1.0, + "end": "2023-05-08T09:00:00+00:00", + "start": "2023-05-08T08:00:00+00:00", + "state": 1.0, + "sum": 3.0, + "min": 1.0, + "max": 11.0, + "mean": 1.0, + }, + { + "change": 2.0, + "end": "2023-05-08T10:00:00+00:00", + "start": "2023-05-08T09:00:00+00:00", + "state": 2.0, + "sum": 5.0, + "min": 2.0, + "max": 12.0, + "mean": 1.0, + }, + { + "change": 3.0, + "end": "2023-05-08T11:00:00+00:00", + "start": "2023-05-08T10:00:00+00:00", + "state": 3.0, + "sum": 8.0, + "min": 3.0, + "max": 13.0, + "mean": 1.0, + }, + ], + "sensor.total_energy_import2": [ + { + "last_reset": "2021-12-31T22:00:00+00:00", + "change": 2.0, + "end": "2023-05-08T08:00:00+00:00", + "start": "2023-05-08T07:00:00+00:00", + "state": 0.0, + "sum": 2.0, + "min": 0.0, + "max": 10.0, + "mean": 1.0, + }, + { + "change": 1.0, + "end": "2023-05-08T09:00:00+00:00", + "start": "2023-05-08T08:00:00+00:00", + "state": 1.0, + "sum": 3.0, + "min": 1.0, + "max": 11.0, + "mean": 1.0, + }, + { + "change": 2.0, + "end": "2023-05-08T10:00:00+00:00", + "start": "2023-05-08T09:00:00+00:00", + "state": 2.0, + "sum": 5.0, + "min": 2.0, + "max": 12.0, + "mean": 1.0, + }, + { + "change": 3.0, + "end": "2023-05-08T11:00:00+00:00", + "start": "2023-05-08T10:00:00+00:00", + "state": 3.0, + "sum": 8.0, + "min": 3.0, + "max": 13.0, + "mean": 1.0, + }, + ], + } + }, + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "period": "day", + "statistic_ids": [ + "sensor.total_energy_import1", + "sensor.total_energy_import2", + ], + "types": ["sum"], + }, + { + "statistics": { + "sensor.total_energy_import1": [ + { + "start": "2023-05-08T07:00:00+00:00", + "end": "2023-05-09T07:00:00+00:00", + "sum": 8.0, + } + ], + "sensor.total_energy_import2": [ + { + "start": "2023-05-08T07:00:00+00:00", + "end": "2023-05-09T07:00:00+00:00", + "sum": 8.0, + } + ], + } + }, + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "end_time": "2023-05-08 08:00:00Z", + "period": "hour", + "types": ["change", "sum"], + "statistic_ids": ["sensor.total_energy_import1"], + "units": {"energy": "Wh"}, + }, + { + "statistics": { + "sensor.total_energy_import1": [ + { + "start": "2023-05-08T07:00:00+00:00", + "end": "2023-05-08T08:00:00+00:00", + "change": 2000.0, + "sum": 2000.0, + }, + ], + } + }, + ), + ], +) +@pytest.mark.usefixtures("recorder_mock") +async def test_get_statistics_service( + hass: HomeAssistant, + hass_read_only_user: MockUser, + service_args: dict[str, Any], + expected_result: dict[str, Any], +) -> None: + """Test the get_statistics service.""" + period1 = dt_util.as_utc(dt_util.parse_datetime("2023-05-08 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2023-05-08 01:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2023-05-08 02:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2023-05-08 03:00:00")) + + last_reset = dt_util.parse_datetime("2022-01-01T00:00:00+02:00") + external_statistics = ( + { + "start": period1, + "state": 0, + "sum": 2, + "min": 0, + "max": 10, + "mean": 1, + "last_reset": last_reset, + }, + { + "start": period2, + "state": 1, + "sum": 3, + "min": 1, + "max": 11, + "mean": 1, + "last_reset": None, + }, + { + "start": period3, + "state": 2, + "sum": 5, + "min": 2, + "max": 12, + "mean": 1, + "last_reset": None, + }, + { + "start": period4, + "state": 3, + "sum": 8, + "min": 3, + "max": 13, + "mean": 1, + "last_reset": None, + }, + ) + external_metadata1 = { + "has_mean": True, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.total_energy_import1", + "unit_of_measurement": "kWh", + } + external_metadata2 = { + "has_mean": True, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.total_energy_import2", + "unit_of_measurement": "kWh", + } + async_import_statistics(hass, external_metadata1, external_statistics) + async_import_statistics(hass, external_metadata2, external_statistics) + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + result = await hass.services.async_call( + "recorder", "get_statistics", service_args, return_response=True, blocking=True + ) + assert result == expected_result + + with pytest.raises(exceptions.Unauthorized): + result = await hass.services.async_call( + "recorder", + "get_statistics", + service_args, + return_response=True, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + + +@pytest.mark.parametrize( + ("service_args", "missing_key"), + [ + ( + { + "period": "hour", + "statistic_ids": ["sensor.sensor"], + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + }, + "start_time", + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "period": "hour", + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + }, + "statistic_ids", + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "statistic_ids": ["sensor.sensor"], + "types": ["change", "last_reset", "max", "mean", "min", "state", "sum"], + }, + "period", + ), + ( + { + "start_time": "2023-05-08 07:00:00Z", + "period": "hour", + "statistic_ids": ["sensor.sensor"], + }, + "types", + ), + ], +) +@pytest.mark.usefixtures("recorder_mock") +async def test_get_statistics_service_missing_mandatory_keys( + hass: HomeAssistant, + service_args: dict[str, Any], + missing_key: str, +) -> None: + """Test the get_statistics service with missing mandatory keys.""" + + await async_recorder_block_till_done(hass) + + with pytest.raises( + vol.error.MultipleInvalid, + match=re.escape(f"required key not provided @ data['{missing_key}']"), + ): + await hass.services.async_call( + "recorder", + "get_statistics", + service_args, + return_response=True, + blocking=True, + ) From 2fdda91cb8a16b59bb4367c18281bd5482980013 Mon Sep 17 00:00:00 2001 From: Jeremiah Paige Date: Wed, 14 May 2025 03:34:40 -0700 Subject: [PATCH 0443/1175] Fix pandora.media_player to not sleep during event loop (#141957) * Fix pandora.media_player to not sleep during event loop * factor out pianobar spawn * linting cleanup --------- Co-authored-by: Joost Lekkerkerker --- .../components/pandora/media_player.py | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 0b2f5b7055f..064b2930971 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -94,18 +94,22 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._attr_media_duration = 0 self._pianobar: pexpect.spawn[str] | None = None - def turn_on(self) -> None: - """Turn the media player on.""" - if self.state != MediaPlayerState.OFF: - return - self._pianobar = pexpect.spawn("pianobar", encoding="utf-8") + async def _start_pianobar(self) -> bool: + pianobar = pexpect.spawn("pianobar", encoding="utf-8") + pianobar.delaybeforesend = None + # mypy thinks delayafterread must be a float but that is not what pexpect says + # https://github.com/pexpect/pexpect/blob/4.9/pexpect/expect.py#L170 + pianobar.delayafterread = None # type: ignore[assignment] + pianobar.delayafterclose = 0 + pianobar.delayafterterminate = 0 _LOGGER.debug("Started pianobar subprocess") - mode = self._pianobar.expect( - ["Receiving new playlist", "Select station:", "Email:"] + mode = await pianobar.expect( + ["Receiving new playlist", "Select station:", "Email:"], + async_=True, ) if mode == 1: # station list was presented. dismiss it. - self._pianobar.sendcontrol("m") + pianobar.sendcontrol("m") elif mode == 2: _LOGGER.warning( "The pianobar client is not configured to log in. " @@ -113,16 +117,20 @@ class PandoraMediaPlayer(MediaPlayerEntity): "https://www.home-assistant.io/integrations/pandora/" ) # pass through the email/password prompts to quit cleanly - self._pianobar.sendcontrol("m") - self._pianobar.sendcontrol("m") - self._pianobar.terminate() - self._pianobar = None - return - self._update_stations() - self.update_playing_status() + pianobar.sendcontrol("m") + pianobar.sendcontrol("m") + pianobar.terminate() + return False + self._pianobar = pianobar + return True - self._attr_state = MediaPlayerState.IDLE - self.schedule_update_ha_state() + async def async_turn_on(self) -> None: + """Turn the media player on.""" + if self.state == MediaPlayerState.OFF and await self._start_pianobar(): + await self._update_stations() + await self.update_playing_status() + self._attr_state = MediaPlayerState.IDLE + self.schedule_update_ha_state() def turn_off(self) -> None: """Turn the media player off.""" @@ -142,30 +150,24 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() - def media_play(self) -> None: + async def async_media_play(self) -> None: """Send play command.""" - self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) + await self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() - def media_pause(self) -> None: + async def async_media_pause(self) -> None: """Send pause command.""" - self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) + await self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._attr_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() - def media_next_track(self) -> None: + async def async_media_next_track(self) -> None: """Go to next track.""" - self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) + await self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) self.schedule_update_ha_state() - @property - def media_title(self) -> str | None: - """Title of current playing media.""" - self.update_playing_status() - return self._attr_media_title - - def select_source(self, source: str) -> None: + async def async_select_source(self, source: str) -> None: """Choose a different Pandora station and play it.""" if self.source_list is None: return @@ -176,45 +178,46 @@ class PandoraMediaPlayer(MediaPlayerEntity): return _LOGGER.debug("Setting station %s, %d", source, station_index) assert self._pianobar is not None - self._send_station_list_command() + await self._send_station_list_command() self._pianobar.sendline(f"{station_index}") - self._pianobar.expect("\r\n") + await self._pianobar.expect("\r\n", async_=True) self._attr_state = MediaPlayerState.PLAYING - def _send_station_list_command(self) -> None: + async def _send_station_list_command(self) -> None: """Send a station list command.""" assert self._pianobar is not None self._pianobar.send("s") try: - self._pianobar.expect("Select station:", timeout=1) + await self._pianobar.expect("Select station:", async_=True, timeout=1) except pexpect.exceptions.TIMEOUT: # try again. Buffer was contaminated. - self._clear_buffer() + await self._clear_buffer() self._pianobar.send("s") - self._pianobar.expect("Select station:") + await self._pianobar.expect("Select station:", async_=True) - def update_playing_status(self) -> None: + async def update_playing_status(self) -> None: """Query pianobar for info about current media_title, station.""" - response = self._query_for_playing_status() + response = await self._query_for_playing_status() if not response: return self._update_current_station(response) self._update_current_song(response) self._update_song_position() - def _query_for_playing_status(self) -> str | None: + async def _query_for_playing_status(self) -> str | None: """Query system for info about current track.""" assert self._pianobar is not None - self._clear_buffer() + await self._clear_buffer() self._pianobar.send("i") try: - match_idx = self._pianobar.expect( + match_idx = await self._pianobar.expect( [ r"(\d\d):(\d\d)/(\d\d):(\d\d)", "No song playing", "Select station", "Receiving new playlist", - ] + ], + async_=True, ) except pexpect.exceptions.EOF: _LOGGER.warning("Pianobar process already exited") @@ -229,11 +232,11 @@ class PandoraMediaPlayer(MediaPlayerEntity): _LOGGER.warning("On unexpected station list page") self._pianobar.sendcontrol("m") # press enter self._pianobar.sendcontrol("m") # do it again b/c an 'i' got in - self.update_playing_status() + await self.update_playing_status() return None if match_idx == 3: _LOGGER.debug("Received new playlist list") - self.update_playing_status() + await self.update_playing_status() return None return self._pianobar.before @@ -292,7 +295,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): repr(self._pianobar.after), ) - def _send_pianobar_command(self, service_cmd: str) -> None: + async def _send_pianobar_command(self, service_cmd: str) -> None: """Send a command to Pianobar.""" assert self._pianobar is not None command = CMD_MAP.get(service_cmd) @@ -300,13 +303,13 @@ class PandoraMediaPlayer(MediaPlayerEntity): if command is None: _LOGGER.warning("Command %s not supported yet", service_cmd) return - self._clear_buffer() + await self._clear_buffer() self._pianobar.sendline(command) - def _update_stations(self) -> None: + async def _update_stations(self) -> None: """List defined Pandora stations.""" assert self._pianobar is not None - self._send_station_list_command() + await self._send_station_list_command() station_lines = self._pianobar.before or "" _LOGGER.debug("Getting stations: %s", station_lines) self._attr_source_list = [] @@ -320,7 +323,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._pianobar.sendcontrol("m") # press enter with blank line self._pianobar.sendcontrol("m") # do it twice in case an 'i' got in - def _clear_buffer(self) -> None: + async def _clear_buffer(self) -> None: """Clear buffer from pexpect. This is necessary because there are a bunch of 00:00 in the buffer @@ -328,7 +331,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): """ assert self._pianobar is not None try: - while not self._pianobar.expect(".+", timeout=0.1): + while not await self._pianobar.expect(".+", async_=True, timeout=0.1): pass except pexpect.exceptions.TIMEOUT: pass From 48520d90ef680869160bd8ac5ba75f899c1684ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 14 May 2025 13:02:05 +0200 Subject: [PATCH 0444/1175] Add plate sensors for Miele hobs (#144400) * Add plate sensors for miele hobs * Address review comments * Update snapshot --- homeassistant/components/miele/icons.json | 29 + homeassistant/components/miele/sensor.py | 107 +++- homeassistant/components/miele/strings.json | 29 + tests/components/miele/fixtures/hob.json | 168 +++++ .../miele/snapshots/test_sensor.ambr | 595 ++++++++++++++++++ tests/components/miele/test_sensor.py | 15 + 6 files changed, 939 insertions(+), 4 deletions(-) create mode 100644 tests/components/miele/fixtures/hob.json diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index 48df141ac9b..d38a2862e89 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -53,6 +53,35 @@ "spin_speed": { "default": "mdi:sync" }, + "plate": { + "default": "mdi:circle-outline", + "state": { + "0": "mdi:circle-outline", + "110": "mdi:alpha-w-circle-outline", + "220": "mdi:alpha-w-circle-outline", + "1": "mdi:circle-slice-1", + "2": "mdi:circle-slice-1", + "3": "mdi:circle-slice-2", + "4": "mdi:circle-slice-2", + "5": "mdi:circle-slice-3", + "6": "mdi:circle-slice-3", + "7": "mdi:circle-slice-4", + "8": "mdi:circle-slice-4", + "9": "mdi:circle-slice-5", + "10": "mdi:circle-slice-5", + "11": "mdi:circle-slice-5", + "12": "mdi:circle-slice-6", + "13": "mdi:circle-slice-6", + "14": "mdi:circle-slice-6", + "15": "mdi:circle-slice-7", + "16": "mdi:circle-slice-7", + "17": "mdi:circle-slice-8", + "18": "mdi:circle-slice-8", + "117": "mdi:alpha-b-circle-outline", + "118": "mdi:alpha-b-circle-outline", + "217": "mdi:alpha-b-circle-outline" + } + }, "program_type": { "default": "mdi:state-machine" }, diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 5a0b9212971..d09f16ee9a0 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -45,6 +45,56 @@ _LOGGER = logging.getLogger(__name__) DISABLED_TEMPERATURE = -32768 +PLATE_POWERS = [ + "0", + "110", + "220", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "117", + "118", + "217", +] + + +DEFAULT_PLATE_COUNT = 4 + +PLATE_COUNT = { + "KM7678": 6, + "KM7697": 6, + "KM7878": 6, + "KM7897": 6, + "KMDA7633": 5, + "KMDA7634": 5, + "KMDA7774": 5, + "KMX": 6, +} + + +def _get_plate_count(tech_type: str) -> int: + """Get number of zones for hob.""" + stripped = tech_type.replace(" ", "") + for prefix, plates in PLATE_COUNT.items(): + if stripped.startswith(prefix): + return plates + return DEFAULT_PLATE_COUNT + def _convert_duration(value_list: list[int]) -> int | None: """Convert duration to minutes.""" @@ -56,7 +106,7 @@ class MieleSensorDescription(SensorEntityDescription): """Class describing Miele sensor entities.""" value_fn: Callable[[MieleDevice], StateType] - zone: int | None = None + zone: int = 1 @dataclass @@ -341,7 +391,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( ), description=MieleSensorDescription( key="state_temperature_1", - zone=1, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -389,7 +438,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( description=MieleSensorDescription( key="state_core_target_temperature", translation_key="core_target_temperature", - zone=1, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -433,7 +481,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( description=MieleSensorDescription( key="state_core_temperature", translation_key="core_temperature", - zone=1, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -443,6 +490,25 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( ), ), ), + *( + MieleSensorDefinition( + types=( + MieleAppliance.HOB_HIGHLIGHT, + MieleAppliance.HOB_INDUCT_EXTR, + MieleAppliance.HOB_INDUCTION, + ), + description=MieleSensorDescription( + key="state_plate_step", + translation_key="plate", + translation_placeholders={"plate_no": str(i)}, + zone=i, + device_class=SensorDeviceClass.ENUM, + options=PLATE_POWERS, + value_fn=lambda value: value.state_plate_step[0].value_raw, + ), + ) + for i in range(1, 7) + ), MieleSensorDefinition( types=( MieleAppliance.WASHER_DRYER, @@ -492,6 +558,8 @@ async def async_setup_entry( entity_class = MieleProgramIdSensor case "state_program_phase": entity_class = MielePhaseSensor + case "state_plate_step": + entity_class = MielePlateSensor case _: entity_class = MieleSensor if ( @@ -499,6 +567,10 @@ async def async_setup_entry( == SensorDeviceClass.TEMPERATURE and definition.description.value_fn(device) == DISABLED_TEMPERATURE / 100 + ) or ( + definition.description.key == "state_plate_step" + and definition.description.zone + > _get_plate_count(device.tech_type) ): # Don't create entity if API signals that datapoint is disabled continue @@ -552,6 +624,33 @@ class MieleSensor(MieleEntity, SensorEntity): return self.entity_description.value_fn(self.device) +class MielePlateSensor(MieleSensor): + """Representation of a Sensor.""" + + entity_description: MieleSensorDescription + + def __init__( + self, + coordinator: MieleDataUpdateCoordinator, + device_id: str, + description: MieleSensorDescription, + ) -> None: + """Initialize the plate sensor.""" + super().__init__(coordinator, device_id, description) + self._attr_unique_id = f"{device_id}-{description.key}-{description.zone}" + + @property + def native_value(self) -> StateType: + """Return the state of the plate sensor.""" + # state_plate_step is [] if all zones are off + plate_power = ( + self.device.state_plate_step[self.entity_description.zone - 1].value_raw + if self.device.state_plate_step + else 0 + ) + return str(plate_power) + + class MieleStatusSensor(MieleSensor): """Representation of the status sensor.""" diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index adffe9b378c..7cd4386f77b 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -191,6 +191,35 @@ "energy_consumption": { "name": "Energy consumption" }, + "plate": { + "name": "Plate {plate_no}", + "state": { + "0": "0", + "110": "Warming", + "220": "%key::component::miele::sensor::plate::state::110%", + "1": "1", + "2": "1\u2022", + "3": "2", + "4": "2\u2022", + "5": "3", + "6": "3\u2022", + "7": "4", + "8": "4\u2022", + "9": "5", + "10": "5\u2022", + "11": "6", + "12": "6\u2022", + "13": "7", + "14": "7\u2022", + "15": "8", + "16": "8\u2022", + "17": "9", + "18": "9\u2022", + "117": "Boost", + "118": "%key::component::miele::sensor::plate::state::117%", + "217": "%key::component::miele::sensor::plate::state::117%" + } + }, "drying_step": { "name": "Drying step", "state": { diff --git a/tests/components/miele/fixtures/hob.json b/tests/components/miele/fixtures/hob.json new file mode 100644 index 00000000000..f86c6a0044f --- /dev/null +++ b/tests/components/miele/fixtures/hob.json @@ -0,0 +1,168 @@ +{ + "DummyAppliance_hob_w_extr": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 74, + "value_localized": "Hob with vapour extraction" + }, + "deviceName": "KDMA7774 | APP2-2", + "protocolVersion": 2, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "00", + "techType": "KMDA7774-1 R01", + "matNumber": "10974770", + "swids": [ + "4088", + "20269", + "25122", + "4194", + "20270", + "25077", + "4194", + "20270", + "25077", + "4215", + "20270", + "25134", + "4438", + "20314", + "25128" + ] + }, + "xkmIdentLabel": { + "techType": "EK039W", + "releaseVersion": "02.72" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 5, + "value_localized": "In use", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [ + { + "value_raw": 0, + "value_localized": 0, + "key_localized": "Power level" + }, + { + "value_raw": 110, + "value_localized": 2, + "key_localized": "Power level" + }, + { + "value_raw": 8, + "value_localized": 4, + "key_localized": "Power level" + }, + { + "value_raw": 15, + "value_localized": 8, + "key_localized": "Power level" + }, + { + "value_raw": 117, + "value_localized": 10, + "key_localized": "Power level" + } + ], + "ecoFeedback": null, + "batteryLevel": null + } + } +} diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 9cc2aa83b01..40072a8303a 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1,4 +1,599 @@ # serializer version: 1 +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:pot-steam-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_hob_w_extr-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction', + 'icon': 'mdi:pot-steam-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_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': 'Plate 1', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 1', + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_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': 'Plate 2', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 2', + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '110', + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_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': 'Plate 3', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-3', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 3', + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_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': 'Plate 4', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-4', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 4', + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '15', + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hob_with_extraction_plate_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': 'Plate 5', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plate', + 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-5', + 'unit_of_measurement': None, + }) +# --- +# name: test_hob_sensor_states[platforms0-hob.json][sensor.hob_with_extraction_plate_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hob with extraction Plate 5', + 'options': list([ + '0', + '110', + '220', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '117', + '118', + '217', + ]), + }), + 'context': , + 'entity_id': 'sensor.hob_with_extraction_plate_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '117', + }) +# --- # name: test_sensor_states[platforms0][sensor.freezer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index b87a165735f..f5d579fc963 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -24,3 +24,18 @@ async def test_sensor_states( """Test sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["hob.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_hob_sensor_states( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: None, +) -> None: + """Test sensor state.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) From 4f723232e31d5197157efc8864c46bf627fb8298 Mon Sep 17 00:00:00 2001 From: Dmytro Tkach <106728732+DioSWolF@users.noreply.github.com> Date: Wed, 14 May 2025 14:07:19 +0300 Subject: [PATCH 0445/1175] Add modbus light brightness and color temperature (#139703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/components/modbus/__init__.py | 11 +- homeassistant/components/modbus/const.py | 10 + homeassistant/components/modbus/light.py | 197 ++++++++++++++++- tests/components/modbus/test_light.py | 225 ++++++++++++++++++-- 4 files changed, 419 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 52642cc32e3..ab387030af8 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -62,8 +62,10 @@ from .const import ( CALL_TYPE_X_COILS, CALL_TYPE_X_REGISTER_HOLDINGS, CONF_BAUDRATE, + CONF_BRIGHTNESS_REGISTER, CONF_BYTESIZE, CONF_CLIMATES, + CONF_COLOR_TEMP_REGISTER, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_AUTO, @@ -415,7 +417,14 @@ SWITCH_SCHEMA = BASE_SWITCH_SCHEMA.extend( } ) -LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) +LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend( + { + vol.Optional(CONF_BRIGHTNESS_REGISTER): cv.positive_int, + vol.Optional(CONF_COLOR_TEMP_REGISTER): cv.positive_int, + vol.Optional(CONF_MIN_TEMP): cv.positive_int, + vol.Optional(CONF_MAX_TEMP): cv.positive_int, + } +) FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 634637a6b08..068a46b1f81 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -16,6 +16,8 @@ from homeassistant.const import ( CONF_BAUDRATE = "baudrate" CONF_BYTESIZE = "bytesize" CONF_CLIMATES = "climates" +CONF_BRIGHTNESS_REGISTER = "brightness_address" +CONF_COLOR_TEMP_REGISTER = "color_temp_address" CONF_DATA_TYPE = "data_type" CONF_DEVICE_ADDRESS = "device_address" CONF_FANS = "fans" @@ -167,3 +169,11 @@ PLATFORMS = ( (Platform.SENSOR, CONF_SENSORS), (Platform.SWITCH, CONF_SWITCHES), ) + +LIGHT_DEFAULT_MIN_KELVIN = 2000 +LIGHT_DEFAULT_MAX_KELVIN = 7000 +LIGHT_MIN_BRIGHTNESS = 0 +LIGHT_MAX_BRIGHTNESS = 255 +LIGHT_MODBUS_SCALE_MIN = 0 +LIGHT_MODBUS_SCALE_MAX = 100 +LIGHT_MODBUS_INVALID_VALUE = 0xFFFF diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index ce1c881733e..c025eefe0e4 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -2,18 +2,40 @@ from __future__ import annotations +import logging from typing import Any -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + ColorMode, + LightEntity, +) from homeassistant.const import CONF_LIGHTS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_hub +from .const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_WRITE_REGISTER, + CONF_BRIGHTNESS_REGISTER, + CONF_COLOR_TEMP_REGISTER, + CONF_MAX_TEMP, + CONF_MIN_TEMP, + LIGHT_DEFAULT_MAX_KELVIN, + LIGHT_DEFAULT_MIN_KELVIN, + LIGHT_MAX_BRIGHTNESS, + LIGHT_MODBUS_INVALID_VALUE, + LIGHT_MODBUS_SCALE_MAX, + LIGHT_MODBUS_SCALE_MIN, +) from .entity import BaseSwitch +from .modbus import ModbusHub PARALLEL_UPDATES = 1 +_LOGGER = logging.getLogger(__name__) async def async_setup_platform( @@ -32,9 +54,176 @@ async def async_setup_platform( class ModbusLight(BaseSwitch, LightEntity): """Class representing a Modbus light.""" - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} + def __init__( + self, hass: HomeAssistant, hub: ModbusHub, config: dict[str, Any] + ) -> None: + """Initialize the Modbus light entity.""" + super().__init__(hass, hub, config) + self._brightness_address: int | None = config.get(CONF_BRIGHTNESS_REGISTER) + self._color_temp_address: int | None = config.get(CONF_COLOR_TEMP_REGISTER) + + # Determine color mode dynamically + self._attr_color_mode = self._detect_color_mode(config) + self._attr_supported_color_modes = {self._attr_color_mode} + + # Set min/max kelvin values if the mode is COLOR_TEMP + if self._attr_color_mode == ColorMode.COLOR_TEMP: + self._attr_min_color_temp_kelvin = config.get( + CONF_MIN_TEMP, LIGHT_DEFAULT_MIN_KELVIN + ) + self._attr_max_color_temp_kelvin = config.get( + CONF_MAX_TEMP, LIGHT_DEFAULT_MAX_KELVIN + ) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + if (state := await self.async_get_last_state()) is None: + return + + if (brightness := state.attributes.get(ATTR_BRIGHTNESS)) is not None: + self._attr_brightness = brightness + + if (color_temp := state.attributes.get(ATTR_COLOR_TEMP_KELVIN)) is not None: + self._attr_color_temp_kelvin = color_temp + + @staticmethod + def _detect_color_mode(config: dict[str, Any]) -> ColorMode: + """Determine the appropriate color mode for the light based on configuration.""" + if CONF_COLOR_TEMP_REGISTER in config: + return ColorMode.COLOR_TEMP + if CONF_BRIGHTNESS_REGISTER in config: + return ColorMode.BRIGHTNESS + return ColorMode.ONOFF async def async_turn_on(self, **kwargs: Any) -> None: - """Set light on.""" + """Turn light on and set brightness if provided.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness and isinstance(brightness, int): + await self.async_set_brightness(brightness) + color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) + if color_temp and isinstance(color_temp, int): + await self.async_set_color_temp(color_temp) await self.async_turn(self.command_on) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn light off.""" + await self.async_turn(self._command_off) + + async def async_set_brightness(self, brightness: int) -> None: + """Set the brightness of the light.""" + if not self._brightness_address: + return + + conv_brightness = self._convert_brightness_to_modbus(brightness) + + await self._hub.async_pb_call( + unit=self._slave, + address=self._brightness_address, + value=conv_brightness, + use_call=CALL_TYPE_WRITE_REGISTER, + ) + if not self._verify_active: + self._attr_brightness = brightness + + async def async_set_color_temp(self, color_temp_kelvin: int) -> None: + """Send Modbus command to set color temperature.""" + if not self._color_temp_address: + return + + conv_color_temp_kelvin = self._convert_color_temp_to_modbus(color_temp_kelvin) + + await self._hub.async_pb_call( + unit=self._slave, + address=self._color_temp_address, + value=conv_color_temp_kelvin, + use_call=CALL_TYPE_WRITE_REGISTER, + ) + if not self._verify_active: + self._attr_color_temp_kelvin = color_temp_kelvin + + async def _async_update(self) -> None: + """Update the entity state, including brightness and color temperature.""" + await super()._async_update() + + if not self._verify_active: + return + + if self._brightness_address: + brightness_result = await self._hub.async_pb_call( + unit=self._slave, + value=1, + address=self._brightness_address, + use_call=CALL_TYPE_REGISTER_HOLDING, + ) + + if ( + brightness_result + and brightness_result.registers + and brightness_result.registers[0] != LIGHT_MODBUS_INVALID_VALUE + ): + self._attr_brightness = self._convert_modbus_percent_to_brightness( + brightness_result.registers[0] + ) + + if self._color_temp_address: + color_result = await self._hub.async_pb_call( + unit=self._slave, + value=1, + address=self._color_temp_address, + use_call=CALL_TYPE_REGISTER_HOLDING, + ) + if ( + color_result + and color_result.registers + and color_result.registers[0] != LIGHT_MODBUS_INVALID_VALUE + ): + self._attr_color_temp_kelvin = ( + self._convert_modbus_percent_to_temperature( + color_result.registers[0] + ) + ) + + @staticmethod + def _convert_modbus_percent_to_brightness(percent: int) -> int: + """Convert Modbus scale (0-100) to the brightness (0-255).""" + return round( + percent + / (LIGHT_MODBUS_SCALE_MAX - LIGHT_MODBUS_SCALE_MIN) + * LIGHT_MAX_BRIGHTNESS + ) + + def _convert_modbus_percent_to_temperature(self, percent: int) -> int: + """Convert Modbus scale (0-100) to the color temperature in Kelvin (2000-7000 К).""" + assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance( + self._attr_max_color_temp_kelvin, int + ) + return round( + self._attr_min_color_temp_kelvin + + ( + percent + / (LIGHT_MODBUS_SCALE_MAX - LIGHT_MODBUS_SCALE_MIN) + * (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin) + ) + ) + + @staticmethod + def _convert_brightness_to_modbus(brightness: int) -> int: + """Convert brightness (0-255) to Modbus scale (0-100).""" + return round( + brightness + / LIGHT_MAX_BRIGHTNESS + * (LIGHT_MODBUS_SCALE_MAX - LIGHT_MODBUS_SCALE_MIN) + ) + + def _convert_color_temp_to_modbus(self, kelvin: int) -> int: + """Convert color temperature from Kelvin to the Modbus scale (0-100).""" + assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance( + self._attr_max_color_temp_kelvin, int + ) + return round( + LIGHT_MODBUS_SCALE_MIN + + (kelvin - self._attr_min_color_temp_kelvin) + * (LIGHT_MODBUS_SCALE_MAX - LIGHT_MODBUS_SCALE_MIN) + / (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin) + ) diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 745249ff866..56b6d0ef3b4 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -4,12 +4,18 @@ from pymodbus.exceptions import ModbusException import pytest from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP_KELVIN, + DOMAIN as LIGHT_DOMAIN, +) from homeassistant.components.modbus.const import ( CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, + CONF_BRIGHTNESS_REGISTER, + CONF_COLOR_TEMP_REGISTER, CONF_DEVICE_ADDRESS, CONF_INPUT_TYPE, CONF_STATE_OFF, @@ -217,7 +223,23 @@ async def test_all_light(hass: HomeAssistant, mock_do_cycle, expected) -> None: @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, STATE_ON),)], + [ + ( + State( + ENTITY_ID, + STATE_ON, + { + ATTR_BRIGHTNESS: 128, + ATTR_COLOR_TEMP_KELVIN: 4000, + }, + ), + State( + ENTITY_ID2, + STATE_ON, + {}, + ), + ) + ], indirect=True, ) @pytest.mark.parametrize( @@ -229,16 +251,35 @@ async def test_all_light(hass: HomeAssistant, mock_do_cycle, expected) -> None: CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, CONF_SCAN_INTERVAL: 0, - } + CONF_BRIGHTNESS_REGISTER: 1, + CONF_COLOR_TEMP_REGISTER: 2, + }, + { + CONF_NAME: f"{TEST_ENTITY_NAME} 2", + CONF_ADDRESS: 1235, + CONF_SCAN_INTERVAL: 0, + }, ] - }, + } ], ) async def test_restore_state_light( hass: HomeAssistant, mock_test_state, mock_modbus ) -> None: - """Run test for sensor restore state.""" - assert hass.states.get(ENTITY_ID).state == mock_test_state[0].state + """Test Modbus Light restore state with brightness and color_temp.""" + + state_1 = hass.states.get(ENTITY_ID) + state_2 = hass.states.get(ENTITY_ID2) + + assert state_1.state == STATE_ON + assert state_1.attributes.get(ATTR_BRIGHTNESS) == mock_test_state[0].attributes.get( + ATTR_BRIGHTNESS + ) + assert state_1.attributes.get(ATTR_COLOR_TEMP_KELVIN) == mock_test_state[ + 0 + ].attributes.get(ATTR_COLOR_TEMP_KELVIN) + + assert state_2.state == STATE_ON @pytest.mark.parametrize( @@ -271,7 +312,6 @@ async def test_light_service_turn( """Run test for service turn_on/turn_off.""" assert MODBUS_DOMAIN in hass.config.components - assert hass.states.get(ENTITY_ID).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, service_data={ATTR_ENTITY_ID: ENTITY_ID} @@ -307,21 +347,143 @@ async def test_light_service_turn( @pytest.mark.parametrize( - "do_config", + ("do_config", "service_data", "expected_calls"), [ - { - CONF_LIGHTS: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_ADDRESS: 1234, - CONF_WRITE_TYPE: CALL_TYPE_COIL, - CONF_VERIFY: {}, - } - ] - }, + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, + CONF_BRIGHTNESS_REGISTER: 1, + CONF_COLOR_TEMP_REGISTER: 2, + } + ] + }, + {ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 2000}, + [(1, 50), (2, 0)], + ), + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, + CONF_BRIGHTNESS_REGISTER: 1, + } + ] + }, + {ATTR_BRIGHTNESS: 256}, + [(1, 100)], + ), + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 0, + CONF_COLOR_TEMP_REGISTER: 2, + } + ] + }, + {ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 3000}, + [(2, 20)], + ), ], ) -async def test_service_light_update(hass: HomeAssistant, mock_modbus_ha) -> None: +async def test_color_temp_brightness_light( + hass: HomeAssistant, + mock_modbus_ha, + service_data, + expected_calls, +) -> None: + """Test Modbus Light color temperature and brightness.""" + assert hass.states.get(ENTITY_ID).state == STATE_OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + service_data={ATTR_ENTITY_ID: ENTITY_ID, **service_data}, + blocking=True, + ) + assert hass.states.get(ENTITY_ID).state == STATE_ON + calls = mock_modbus_ha.write_register.call_args_list + for expected_register, expected_value in expected_calls: + assert any( + call.args[0] == expected_register and call.kwargs["value"] == expected_value + for call in calls + ), ( + f"Expected register {expected_register} with value {expected_value} not found in calls {calls}" + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + service_data={ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert hass.states.get(ENTITY_ID).state == STATE_OFF + + +@pytest.mark.parametrize( + ( + "do_config", + "input_output_values", + ), + [ + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_BRIGHTNESS_REGISTER: 1, + CONF_COLOR_TEMP_REGISTER: 2, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + }, + ] + }, + [([100, 0], 255, 7000)], + ), + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_BRIGHTNESS_REGISTER: 1, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + }, + ] + }, + [([100, None], 255, None), ([0, None], 0, None)], + ), + ( + { + CONF_LIGHTS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + }, + ] + }, + [([None, None], None, None)], + ), + ], +) +async def test_service_light_update( + hass: HomeAssistant, + mock_modbus_ha, + input_output_values, +) -> None: """Run test for service homeassistant.update_entity.""" await hass.services.async_call( HOMEASSISTANT_DOMAIN, @@ -338,6 +500,31 @@ async def test_service_light_update(hass: HomeAssistant, mock_modbus_ha) -> None blocking=True, ) assert hass.states.get(ENTITY_ID).state == STATE_ON + for ( + register_values, + expected_brightness, + expected_color_temp, + ) in input_output_values: + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_values) + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + { + ATTR_ENTITY_ID: ENTITY_ID, + }, + blocking=True, + ) + assert ( + expected_brightness is None + or hass.states.get(ENTITY_ID).attributes.get(ATTR_BRIGHTNESS) + == expected_brightness + ) + assert ( + expected_color_temp is None + or hass.states.get(ENTITY_ID).attributes.get(ATTR_COLOR_TEMP_KELVIN) + == expected_color_temp + ) + assert hass async def test_no_discovery_info_light( From e89333811ea289e15c2dc4d5b17bb17b76350d8d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 14 May 2025 13:08:26 +0200 Subject: [PATCH 0446/1175] Improve Z-Wave config flow tests (#144871) * Improve Z-Wave config flow tests * Fix test * Use identify check for result type --- tests/components/zwave_js/test_config_flow.py | 204 +++++++++++------- 1 file changed, 125 insertions(+), 79 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index e651a92339b..509fddb8704 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -924,15 +924,15 @@ async def test_usb_discovery_migration( 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"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 @@ -942,13 +942,13 @@ async def test_usb_discovery_migration( 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"] == "instruct_unplug" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": USB_DISCOVERY_INFO.device}) @@ -960,7 +960,7 @@ async def test_usb_discovery_migration( 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["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -1058,15 +1058,15 @@ async def test_usb_discovery_migration_driver_ready_timeout( 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"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 @@ -1076,13 +1076,13 @@ async def test_usb_discovery_migration_driver_ready_timeout( 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": USB_DISCOVERY_INFO.device}) @@ -1094,7 +1094,7 @@ async def test_usb_discovery_migration_driver_ready_timeout( 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["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3718,22 +3718,22 @@ async def test_reconfigure_migrate_with_addon( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 @@ -3743,13 +3743,13 @@ async def test_reconfigure_migrate_with_addon( 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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"] == "choose_serial_port" assert result["data_schema"].schema[CONF_USB_PATH] @@ -3760,7 +3760,7 @@ async def test_reconfigure_migrate_with_addon( }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": "/test"}) @@ -3772,7 +3772,7 @@ async def test_reconfigure_migrate_with_addon( 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["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3860,22 +3860,22 @@ async def test_reconfigure_migrate_driver_ready_timeout( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 @@ -3885,13 +3885,13 @@ async def test_reconfigure_migrate_driver_ready_timeout( 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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"] == "choose_serial_port" assert result["data_schema"].schema[CONF_USB_PATH] @@ -3902,7 +3902,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( }, ) - assert result["type"] == FlowResultType.SHOW_PROGRESS + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" assert set_addon_options.call_args == call( "core_zwave_js", AddonsOptions(config={"device": "/test"}) @@ -3914,7 +3914,7 @@ async def test_reconfigure_migrate_driver_ready_timeout( 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["step_id"] == "restore_nvm" assert client.connect.call_count == 2 @@ -3950,19 +3950,19 @@ async def test_reconfigure_migrate_backup_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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"] == "backup_failed" @@ -3985,30 +3985,28 @@ async def test_reconfigure_migrate_backup_file_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch( - "pathlib.Path.write_bytes", MagicMock(side_effect=OSError("test_error")) - ): + with patch("pathlib.Path.write_bytes", side_effect=OSError("test_error")): await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 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"] == "backup_failed" @@ -4053,35 +4051,35 @@ async def test_reconfigure_migrate_start_addon_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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"] == "choose_serial_port" result = await hass.config_entries.flow.async_configure( @@ -4096,13 +4094,13 @@ async def test_reconfigure_migrate_start_addon_failure( "core_zwave_js", AddonsOptions(config={"device": "/test"}) ) - 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() 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"] == "addon_start_failed" @@ -4148,35 +4146,35 @@ async def test_reconfigure_migrate_restore_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED 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"] == "choose_serial_port" result = await hass.config_entries.flow.async_configure( @@ -4186,13 +4184,13 @@ async def test_reconfigure_migrate_restore_failure( }, ) - 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() 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["step_id"] == "restore_nvm" await hass.async_block_till_done() @@ -4201,13 +4199,13 @@ async def test_reconfigure_migrate_restore_failure( 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"] == "restore_failed" assert result["description_placeholders"]["file_path"] 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["step_id"] == "restore_nvm" await hass.async_block_till_done() @@ -4216,7 +4214,7 @@ async def test_reconfigure_migrate_restore_failure( 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"] == "restore_failed" hass.config_entries.flow.async_abort(result["flow_id"]) @@ -4224,29 +4222,77 @@ async def test_reconfigure_migrate_restore_failure( assert len(hass.config_entries.flow.async_progress()) == 0 -async def test_get_driver_failure(hass: HomeAssistant, integration, client) -> None: - """Test get driver failure.""" +async def test_get_driver_failure_intent_migrate( + hass: HomeAssistant, + integration: MockConfigEntry, +) -> None: + """Test get driver failure in intent migrate step.""" entry = integration hass.config_entries.async_update_entry( integration, unique_id="1234", data={**integration.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + await hass.config_entries.async_unload(integration.entry_id) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "config_entry_not_loaded" + + +async def test_get_driver_failure_instruct_unplug( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test get driver failure in instruct unplug step.""" + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + entry = integration + hass.config_entries.async_update_entry( + integration, unique_id="1234", data={**integration.data, "use_addon": True} + ) + result = await entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" - await hass.config_entries.async_unload(integration.entry_id) - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "config_entry_not_loaded" + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + with patch("pathlib.Path.write_bytes") as mock_file: + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + + await hass.config_entries.async_unload(integration.entry_id) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reset_failed" async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: @@ -4269,29 +4315,29 @@ async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> N result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 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"] == "reset_failed" @@ -4314,29 +4360,29 @@ async def test_choose_serial_port_usb_ports_failure( result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" 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["step_id"] == "backup_nvm" - with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file: + with patch("pathlib.Path.write_bytes") as mock_file: await hass.async_block_till_done() assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 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"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @@ -4345,7 +4391,7 @@ async def test_choose_serial_port_usb_ports_failure( side_effect=OSError("test_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"] == "usb_ports_failed" @@ -4356,14 +4402,14 @@ async def test_configure_addon_usb_ports_failure( entry = integration result = await entry.start_reconfigure_flow(hass) - assert result["type"] == FlowResultType.MENU + assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_reconfigure"} ) - assert result["type"] == FlowResultType.FORM + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor_reconfigure" with patch( @@ -4373,5 +4419,5 @@ async def test_configure_addon_usb_ports_failure( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"use_addon": True} ) - assert result["type"] == FlowResultType.ABORT + assert result["type"] is FlowResultType.ABORT assert result["reason"] == "usb_ports_failed" From 5c86042b317f1b34c40858dee586e3358371c8aa Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 14 May 2025 13:37:02 +0200 Subject: [PATCH 0447/1175] Add Fronius current and voltage for up to 4 MPP trackers (#140120) Support current and voltage of up to 4 MPP trackers --- homeassistant/components/fronius/icons.json | 4 +- homeassistant/components/fronius/sensor.py | 42 ++++++++++++++++++- homeassistant/components/fronius/strings.json | 8 ++-- .../fronius/snapshots/test_sensor.ambr | 8 ++-- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fronius/icons.json b/homeassistant/components/fronius/icons.json index a84140617dd..59d5a110449 100644 --- a/homeassistant/components/fronius/icons.json +++ b/homeassistant/components/fronius/icons.json @@ -4,13 +4,13 @@ "current_dc": { "default": "mdi:current-dc" }, - "current_dc_2": { + "current_dc_mppt_no": { "default": "mdi:current-dc" }, "voltage_dc": { "default": "mdi:current-dc" }, - "voltage_dc_2": { + "voltage_dc_mppt_no": { "default": "mdi:current-dc" }, "co2_factor": { diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index c65f6072ba6..e287786aaa8 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -168,6 +168,26 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, + translation_key="current_dc_mppt_no", + translation_placeholders={"mppt_no": "2"}, + ), + FroniusSensorEntityDescription( + key="current_dc_3", + default_value=0, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + translation_key="current_dc_mppt_no", + translation_placeholders={"mppt_no": "3"}, + ), + FroniusSensorEntityDescription( + key="current_dc_4", + default_value=0, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + translation_key="current_dc_mppt_no", + translation_placeholders={"mppt_no": "4"}, ), FroniusSensorEntityDescription( key="power_ac", @@ -197,6 +217,26 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [ native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, + translation_key="voltage_dc_mppt_no", + translation_placeholders={"mppt_no": "2"}, + ), + FroniusSensorEntityDescription( + key="voltage_dc_3", + default_value=0, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="voltage_dc_mppt_no", + translation_placeholders={"mppt_no": "3"}, + ), + FroniusSensorEntityDescription( + key="voltage_dc_4", + default_value=0, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + translation_key="voltage_dc_mppt_no", + translation_placeholders={"mppt_no": "4"}, ), # device status entities FroniusSensorEntityDescription( @@ -727,7 +767,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn self.response_key = description.response_key or description.key self.solar_net_id = solar_net_id self._attr_native_value = self._get_entity_value() - self._attr_translation_key = description.key + self._attr_translation_key = description.translation_key or description.key def _device_data(self) -> dict[str, Any]: """Extract information for SolarNet device from coordinator data.""" diff --git a/homeassistant/components/fronius/strings.json b/homeassistant/components/fronius/strings.json index 9cd3b7c8a54..e965e3117c5 100644 --- a/homeassistant/components/fronius/strings.json +++ b/homeassistant/components/fronius/strings.json @@ -52,8 +52,8 @@ "current_dc": { "name": "DC current" }, - "current_dc_2": { - "name": "DC current 2" + "current_dc_mppt_no": { + "name": "DC current {mppt_no}" }, "power_ac": { "name": "AC power" @@ -64,8 +64,8 @@ "voltage_dc": { "name": "DC voltage" }, - "voltage_dc_2": { - "name": "DC voltage 2" + "voltage_dc_mppt_no": { + "name": "DC voltage {mppt_no}" }, "inverter_state": { "name": "Inverter state" diff --git a/tests/components/fronius/snapshots/test_sensor.ambr b/tests/components/fronius/snapshots/test_sensor.ambr index 63d2c85986a..1c718910428 100644 --- a/tests/components/fronius/snapshots/test_sensor.ambr +++ b/tests/components/fronius/snapshots/test_sensor.ambr @@ -238,7 +238,7 @@ 'platform': 'fronius', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'current_dc_2', + 'translation_key': 'current_dc_mppt_no', 'unique_id': '12345678-current_dc_2', 'unit_of_measurement': , }) @@ -342,7 +342,7 @@ 'platform': 'fronius', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'voltage_dc_2', + 'translation_key': 'voltage_dc_mppt_no', 'unique_id': '12345678-voltage_dc_2', 'unit_of_measurement': , }) @@ -3798,7 +3798,7 @@ 'platform': 'fronius', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'current_dc_2', + 'translation_key': 'current_dc_mppt_no', 'unique_id': '12345678-current_dc_2', 'unit_of_measurement': , }) @@ -3902,7 +3902,7 @@ 'platform': 'fronius', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'voltage_dc_2', + 'translation_key': 'voltage_dc_mppt_no', 'unique_id': '12345678-voltage_dc_2', 'unit_of_measurement': , }) From e413e9b93b9eacc34a9e30ff92d9807d6c79c6f7 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 14 May 2025 13:40:38 +0200 Subject: [PATCH 0448/1175] Add mac address to airgradient devices (#144876) --- homeassistant/components/airgradient/entity.py | 2 ++ tests/components/airgradient/snapshots/test_init.ambr | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/airgradient/entity.py b/homeassistant/components/airgradient/entity.py index 51256051259..1d5430e5403 100644 --- a/homeassistant/components/airgradient/entity.py +++ b/homeassistant/components/airgradient/entity.py @@ -6,6 +6,7 @@ from typing import Any, Concatenate from airgradient import AirGradientConnectionError, AirGradientError, get_model_name 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 @@ -29,6 +30,7 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]): model_id=measures.model, serial_number=coordinator.serial_number, sw_version=measures.firmware_version, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)}, ) diff --git a/tests/components/airgradient/snapshots/test_init.ambr b/tests/components/airgradient/snapshots/test_init.ambr index 4e0c8027b43..b3181fddfeb 100644 --- a/tests/components/airgradient/snapshots/test_init.ambr +++ b/tests/components/airgradient/snapshots/test_init.ambr @@ -6,6 +6,10 @@ 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ + tuple( + 'mac', + '84:fc:e6:12:f5:b8', + ), }), 'disabled_by': None, 'entry_type': None, @@ -39,6 +43,10 @@ 'config_entries_subentries': , 'configuration_url': None, 'connections': set({ + tuple( + 'mac', + '84:fc:e6:12:f5:b8', + ), }), 'disabled_by': None, 'entry_type': None, From 67b9904740d7fa030e6db845d326efaace99fe04 Mon Sep 17 00:00:00 2001 From: Maximilian Arzberger Date: Wed, 14 May 2025 14:05:23 +0200 Subject: [PATCH 0449/1175] Add Kostal plenticore Installer login support (#133773) * feat: Add Installer login, Add ManualCharge Switch * remove unnecessary field * replace strings with consts * change to CONF and camel_case * Improve existing code * Add translation string * format code * add service code test * format code * format code * remove manual charge switch * add reconfigure config flow * fix flow * add return type * add reconfigure strings * adjust tests * change string * simlify tests * add reconfigure test * add more tests * Fix --------- Co-authored-by: Joost Lekkerkerker --- .../kostal_plenticore/config_flow.py | 36 +- .../components/kostal_plenticore/const.py | 1 + .../kostal_plenticore/coordinator.py | 7 +- .../components/kostal_plenticore/strings.json | 13 +- .../kostal_plenticore/test_config_flow.py | 307 ++++++++++++++++-- 5 files changed, 335 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index 59c737a0874..cce220006c5 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_BASE, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CONF_SERVICE_CODE, DOMAIN from .helper import get_hostname_id _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,7 @@ DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_SERVICE_CODE): str, } ) @@ -32,8 +33,10 @@ async def test_connection(hass: HomeAssistant, data) -> str: """ session = async_get_clientsession(hass) - async with ApiClient(session, data["host"]) as client: - await client.login(data["password"]) + async with ApiClient(session, data[CONF_HOST]) as client: + await client.login( + data[CONF_PASSWORD], service_code=data.get(CONF_SERVICE_CODE) + ) hostname_id = await get_hostname_id(client) values = await client.get_setting_values("scb:network", hostname_id) @@ -70,3 +73,30 @@ class KostalPlenticoreConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Add reconfigure step to allow to reconfigure a config entry.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + try: + hostname = await test_connection(self.hass, user_input) + except AuthenticationException as ex: + errors[CONF_PASSWORD] = "invalid_auth" + _LOGGER.error("Error response: %s", ex) + except (ClientError, TimeoutError): + errors[CONF_HOST] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors[CONF_BASE] = "unknown" + else: + return self.async_update_reload_and_abort( + entry=self._get_reconfigure_entry(), title=hostname, data=user_input + ) + + return self.async_show_form( + step_id="reconfigure", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 668b10e6971..e67f9298438 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,3 +1,4 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" DOMAIN = "kostal_plenticore" +CONF_SERVICE_CODE = "service_code" diff --git a/homeassistant/components/kostal_plenticore/coordinator.py b/homeassistant/components/kostal_plenticore/coordinator.py index a404a997663..f87f8ca630a 100644 --- a/homeassistant/components/kostal_plenticore/coordinator.py +++ b/homeassistant/components/kostal_plenticore/coordinator.py @@ -25,7 +25,7 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import CONF_SERVICE_CODE, DOMAIN from .helper import get_hostname_id _LOGGER = logging.getLogger(__name__) @@ -60,7 +60,10 @@ class Plenticore: async_get_clientsession(self.hass), host=self.host ) try: - await self._client.login(self.config_entry.data[CONF_PASSWORD]) + await self._client.login( + self.config_entry.data[CONF_PASSWORD], + service_code=self.config_entry.data.get(CONF_SERVICE_CODE), + ) except AuthenticationException as err: _LOGGER.error( "Authentication exception connecting to %s: %s", self.host, err diff --git a/homeassistant/components/kostal_plenticore/strings.json b/homeassistant/components/kostal_plenticore/strings.json index 30ce5af5a6c..80a6748e327 100644 --- a/homeassistant/components/kostal_plenticore/strings.json +++ b/homeassistant/components/kostal_plenticore/strings.json @@ -4,7 +4,15 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "service_code": "Service code" + } + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "service_code": "[%key:component::kostal_plenticore::config::step::user::data::service_code%]" } } }, @@ -14,7 +22,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } } } diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index bd9b9ad278d..b4e7ffc0695 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -74,7 +75,7 @@ async def test_form_g1( return_value={"scb:network": {"Hostname": "scb"}} ) - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -86,15 +87,15 @@ async def test_form_g1( mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") mock_apiclient.__aenter__.assert_called_once() mock_apiclient.__aexit__.assert_called_once() - mock_apiclient.login.assert_called_once_with("test-password") + mock_apiclient.login.assert_called_once_with("test-password", service_code=None) mock_apiclient.get_settings.assert_called_once() mock_apiclient.get_setting_values.assert_called_once_with( "scb:network", "Hostname" ) - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "scb" - assert result2["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "scb" + assert result["data"] == { "host": "1.1.1.1", "password": "test-password", } @@ -140,7 +141,7 @@ async def test_form_g2( return_value={"scb:network": {"Network:Hostname": "scb"}} ) - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -152,21 +153,91 @@ async def test_form_g2( mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") mock_apiclient.__aenter__.assert_called_once() mock_apiclient.__aexit__.assert_called_once() - mock_apiclient.login.assert_called_once_with("test-password") + mock_apiclient.login.assert_called_once_with("test-password", service_code=None) mock_apiclient.get_settings.assert_called_once() mock_apiclient.get_setting_values.assert_called_once_with( "scb:network", "Network:Hostname" ) - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "scb" - assert result2["data"] == { + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "scb" + assert result["data"] == { "host": "1.1.1.1", "password": "test-password", } assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_g2_with_service_code( + hass: HomeAssistant, + mock_apiclient_class: type[ApiClient], + mock_apiclient: ApiClient, +) -> None: + """Test the config flow for G2 models with a Service Code.""" + + 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( + "homeassistant.components.kostal_plenticore.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + # mock of the context manager instance + mock_apiclient.login = AsyncMock() + mock_apiclient.get_settings = AsyncMock( + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Network:Hostname", + type="string", + ), + ] + } + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" + return_value={"scb:network": {"Network:Hostname": "scb"}} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + "service_code": "test-service-code", + }, + ) + await hass.async_block_till_done() + + mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") + mock_apiclient.__aenter__.assert_called_once() + mock_apiclient.__aexit__.assert_called_once() + mock_apiclient.login.assert_called_once_with( + "test-password", service_code="test-service-code" + ) + mock_apiclient.get_settings.assert_called_once() + mock_apiclient.get_setting_values.assert_called_once_with( + "scb:network", "Network:Hostname" + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "scb" + assert result["data"] == { + "host": "1.1.1.1", + "password": "test-password", + "service_code": "test-service-code", + } + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( @@ -189,7 +260,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: mock_api_class.return_value = mock_api - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -197,8 +268,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"password": "invalid_auth"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"password": "invalid_auth"} async def test_form_cannot_connect(hass: HomeAssistant) -> None: @@ -223,7 +294,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: mock_api_class.return_value = mock_api - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -231,8 +302,8 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"host": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"host": "cannot_connect"} async def test_form_unexpected_error(hass: HomeAssistant) -> None: @@ -257,7 +328,7 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: mock_api_class.return_value = mock_api - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -265,8 +336,8 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} async def test_already_configured(hass: HomeAssistant) -> None: @@ -281,7 +352,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], { "host": "1.1.1.1", @@ -289,5 +360,197 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_configured" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reconfigure( + hass: HomeAssistant, + mock_apiclient_class: type[ApiClient], + mock_apiclient: ApiClient, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the config flow for G1 models.""" + + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + assert result["errors"] == {} + + # mock of the context manager instance + mock_apiclient.login = AsyncMock() + mock_apiclient.get_settings = AsyncMock( + return_value={ + "scb:network": [ + SettingsData( + min="1", + max="63", + default=None, + access="readwrite", + unit=None, + id="Hostname", + type="string", + ), + ] + } + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" + return_value={"scb:network": {"Hostname": "scb"}} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") + mock_apiclient.__aenter__.assert_called_once() + mock_apiclient.__aexit__.assert_called_once() + mock_apiclient.login.assert_called_once_with("test-password", service_code=None) + mock_apiclient.get_settings.assert_called_once() + mock_apiclient.get_setting_values.assert_called_once_with("scb:network", "Hostname") + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + # changed entry + assert mock_config_entry.data[CONF_HOST] == "1.1.1.1" + assert mock_config_entry.data[CONF_PASSWORD] == "test-password" + + +async def test_reconfigure_invalid_auth( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we handle invalid auth while reconfiguring.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=AuthenticationException(404, "invalid user"), + ) + + # mock of the return instance of ApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"password": "invalid_auth"} + + +async def test_reconfigure_cannot_connect( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we handle cannot connect error.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=TimeoutError(), + ) + + # mock of the return instance of ApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"host": "cannot_connect"} + + +async def test_reconfigure_unexpected_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we handle unexpected error.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) + + with patch( + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" + ) as mock_api_class: + # mock of the context manager instance + mock_api_ctx = MagicMock() + mock_api_ctx.login = AsyncMock( + side_effect=Exception(), + ) + + # mock of the return instance of ApiClient + mock_api = MagicMock() + mock_api.__aenter__.return_value = mock_api_ctx + mock_api.__aexit__.return_value = None + + mock_api_class.return_value = mock_api + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_reconfigure_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we handle already configured error.""" + mock_config_entry.add_to_hass(hass) + MockConfigEntry( + domain="kostal_plenticore", + data={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "foobar"}, + unique_id="112233445566", + ).add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From 710e18f399144ada710d88f98f03ca5ebb542d05 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 14:06:40 +0200 Subject: [PATCH 0450/1175] Use runtime_data in gree (#144880) --- homeassistant/components/gree/__init__.py | 33 ++++++-------------- homeassistant/components/gree/climate.py | 11 +++---- homeassistant/components/gree/const.py | 6 ---- homeassistant/components/gree/coordinator.py | 24 +++++++++----- homeassistant/components/gree/switch.py | 12 +++---- tests/components/gree/test_bridge.py | 12 +++---- 6 files changed, 40 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 7cb4f0f0921..2b5a38082fc 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,33 +1,29 @@ """The Gree Climate integration.""" +from __future__ import annotations + from datetime import timedelta import logging from homeassistant.components.network import async_get_ipv4_broadcast_addresses -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval -from .const import ( - COORDINATORS, - DATA_DISCOVERY_SERVICE, - DISCOVERY_SCAN_INTERVAL, - DISPATCHERS, - DOMAIN, -) -from .coordinator import DiscoveryService +from .const import DISCOVERY_SCAN_INTERVAL +from .coordinator import DiscoveryService, GreeConfigEntry, GreeRuntimeData _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.CLIMATE, Platform.SWITCH] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool: """Set up Gree Climate from a config entry.""" - hass.data.setdefault(DOMAIN, {}) gree_discovery = DiscoveryService(hass, entry) - hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery + entry.runtime_data = GreeRuntimeData( + discovery_service=gree_discovery, coordinators=[] + ) async def _async_scan_update(_=None): bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) @@ -47,15 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool: """Unload a config entry.""" - if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: - hass.data.pop(DATA_DISCOVERY_SERVICE) - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - hass.data[DOMAIN].pop(COORDINATORS, None) - hass.data[DOMAIN].pop(DISPATCHERS, None) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index f703ded1ea2..e3549973f43 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -36,21 +36,18 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( - COORDINATORS, DISPATCH_DEVICE_DISCOVERED, - DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, TARGET_TEMPERATURE_STEP, ) -from .coordinator import DeviceDataUpdateCoordinator +from .coordinator import DeviceDataUpdateCoordinator, GreeConfigEntry from .entity import GreeEntity _LOGGER = logging.getLogger(__name__) @@ -87,17 +84,17 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH] async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GreeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Gree HVAC device from a config entry.""" @callback - def init_device(coordinator): + def init_device(coordinator: DeviceDataUpdateCoordinator) -> None: """Register the device.""" async_add_entities([GreeClimateEntity(coordinator)]) - for coordinator in hass.data[DOMAIN][COORDINATORS]: + for coordinator in entry.runtime_data.coordinators: init_device(coordinator) entry.async_on_unload( diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 14236f09fa2..6c1f8f954c9 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,16 +1,10 @@ """Constants for the Gree Climate integration.""" -COORDINATORS = "coordinators" - -DATA_DISCOVERY_SERVICE = "gree_discovery" - DISCOVERY_SCAN_INTERVAL = 300 DISCOVERY_TIMEOUT = 8 DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered" -DISPATCHERS = "dispatchers" DOMAIN = "gree" -COORDINATOR = "coordinator" FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_HIGH = "medium high" diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index c8b4e6cff54..0d697398fc0 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any @@ -20,7 +21,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.util.dt import utcnow from .const import ( - COORDINATORS, DISCOVERY_TIMEOUT, DISPATCH_DEVICE_DISCOVERED, DOMAIN, @@ -31,14 +31,24 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type GreeConfigEntry = ConfigEntry[GreeRuntimeData] + + +@dataclass +class GreeRuntimeData: + """RUntime data for Gree Climate integration.""" + + discovery_service: DiscoveryService + coordinators: list[DeviceDataUpdateCoordinator] + class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" - config_entry: ConfigEntry + config_entry: GreeConfigEntry def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device + self, hass: HomeAssistant, config_entry: GreeConfigEntry, device: Device ) -> None: """Initialize the data update coordinator.""" super().__init__( @@ -128,7 +138,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): class DiscoveryService(Listener): """Discovery event handler for gree devices.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: GreeConfigEntry) -> None: """Initialize discovery service.""" super().__init__() self.hass = hass @@ -137,8 +147,6 @@ class DiscoveryService(Listener): self.discovery = Discovery(DISCOVERY_TIMEOUT) self.discovery.add_listener(self) - hass.data[DOMAIN].setdefault(COORDINATORS, []) - async def device_found(self, device_info: DeviceInfo) -> None: """Handle new device found on the network.""" @@ -157,14 +165,14 @@ class DiscoveryService(Listener): device.device_info.port, ) coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device) - self.hass.data[DOMAIN][COORDINATORS].append(coordo) + self.entry.runtime_data.coordinators.append(coordo) await coordo.async_refresh() async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo) async def device_update(self, device_info: DeviceInfo) -> None: """Handle updates in device information, update if ip has changed.""" - for coordinator in self.hass.data[DOMAIN][COORDINATORS]: + for coordinator in self.entry.runtime_data.coordinators: if coordinator.device.device_info.mac == device_info.mac: coordinator.device.device_info.ip = device_info.ip await coordinator.async_refresh() diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 67dc10138d1..ab138ea3be6 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -13,13 +13,13 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -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 AddConfigEntryEntitiesCallback -from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN -from .entity import GreeEntity +from .const import DISPATCH_DEVICE_DISCOVERED +from .coordinator import GreeConfigEntry +from .entity import DeviceDataUpdateCoordinator, GreeEntity @dataclass(kw_only=True, frozen=True) @@ -92,13 +92,13 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GreeConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Gree HVAC device from a config entry.""" @callback - def init_device(coordinator): + def init_device(coordinator: DeviceDataUpdateCoordinator) -> None: """Register the device.""" async_add_entities( @@ -106,7 +106,7 @@ async def async_setup_entry( for description in GREE_SWITCHES ) - for coordinator in hass.data[DOMAIN][COORDINATORS]: + for coordinator in entry.runtime_data.coordinators: init_device(coordinator) entry.async_on_unload( diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index acfa1ba43f5..1c67da1f675 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -6,11 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, HVACMode -from homeassistant.components.gree.const import ( - COORDINATORS, - DOMAIN as GREE, - UPDATE_INTERVAL, -) +from homeassistant.components.gree.const import UPDATE_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -42,13 +38,13 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [mock_device_1, mock_device_2] device.side_effect = [mock_device_1, mock_device_2] - await async_setup_gree(hass) + entry = await async_setup_gree(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 1 assert len(hass.states.async_all(CLIMATE_DOMAIN)) == 2 - device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] + device_infos = [x.device.device_info for x in entry.runtime_data.coordinators] assert device_infos[0].ip == "1.1.1.1" assert device_infos[1].ip == "2.2.2.2" @@ -70,7 +66,7 @@ async def test_discovery_after_setup( assert discovery.return_value.scan_count == 2 assert len(hass.states.async_all(CLIMATE_DOMAIN)) == 2 - device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] + device_infos = [x.device.device_info for x in entry.runtime_data.coordinators] assert device_infos[0].ip == "1.1.1.2" assert device_infos[1].ip == "2.2.2.1" From 3b1a33d60692064e3e44c3abffbea93e01677559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 14 May 2025 14:14:48 +0200 Subject: [PATCH 0451/1175] Fix substitutions in strings.json in Miele integration (#144881) Fix substitutions in strings.json --- homeassistant/components/miele/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 7cd4386f77b..d0d8e14cf10 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -196,7 +196,7 @@ "state": { "0": "0", "110": "Warming", - "220": "%key::component::miele::sensor::plate::state::110%", + "220": "[%key:component::miele::entity::sensor::plate::state::110%]", "1": "1", "2": "1\u2022", "3": "2", @@ -216,8 +216,8 @@ "17": "9", "18": "9\u2022", "117": "Boost", - "118": "%key::component::miele::sensor::plate::state::117%", - "217": "%key::component::miele::sensor::plate::state::117%" + "118": "[%key:component::miele::entity::sensor::plate::state::117%]", + "217": "[%key:component::miele::entity::sensor::plate::state::117%]" } }, "drying_step": { From fb9be3da797439c3141bb1c84618c28acf12e27b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 14:30:02 +0200 Subject: [PATCH 0452/1175] Use entry.async_on_unload in geofency (#144882) --- homeassistant/components/geofency/__init__.py | 2 -- homeassistant/components/geofency/device_tracker.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 46a3482ce1e..0e364f0fac1 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -83,7 +83,6 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: hass.data[DOMAIN] = { "beacons": [slugify(beacon) for beacon in mobile_beacons], "devices": set(), - "unsub_device_tracker": {}, } return True @@ -153,7 +152,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 9e2cad50533..54fd7598b9e 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -30,7 +30,7 @@ async def async_setup_entry( async_add_entities([GeofencyEntity(device, gps, location_name, attributes)]) - hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = ( + config_entry.async_on_unload( async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) ) From 10dd11f2572cb7c1d70dc2a63a255f4e6555d6cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 14:30:45 +0200 Subject: [PATCH 0453/1175] Use HassKey in greeneye_monitor (#144878) --- homeassistant/components/greeneye_monitor/const.py | 11 ++++++++++- homeassistant/components/greeneye_monitor/sensor.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/const.py b/homeassistant/components/greeneye_monitor/const.py index 40236b3219f..02c6d9845b0 100644 --- a/homeassistant/components/greeneye_monitor/const.py +++ b/homeassistant/components/greeneye_monitor/const.py @@ -1,5 +1,14 @@ """Shared constants for the greeneye_monitor integration.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.util.hass_dict import HassKey + +if TYPE_CHECKING: + from greeneye import Monitors + CONF_CHANNELS = "channels" CONF_COUNTED_QUANTITY = "counted_quantity" CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse" @@ -13,8 +22,8 @@ CONF_TEMPERATURE_SENSORS = "temperature_sensors" CONF_TIME_UNIT = "time_unit" CONF_VOLTAGE_SENSORS = "voltage" -DATA_GREENEYE_MONITOR = "greeneye_monitor" DOMAIN = "greeneye_monitor" +DATA_GREENEYE_MONITOR: HassKey[Monitors] = HassKey(DOMAIN) SENSOR_TYPE_CURRENT = "current_sensor" SENSOR_TYPE_PULSE_COUNTER = "pulse_counter" diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 04464fe2567..7cfc0e40fc0 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -109,7 +109,7 @@ async def async_setup_platform( if len(monitor_configs) == 0: monitors.remove_listener(on_new_monitor) - monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR] + monitors = hass.data[DATA_GREENEYE_MONITOR] monitors.add_listener(on_new_monitor) for monitor in monitors.monitors.values(): on_new_monitor(monitor) From 993e98a43f25e596ae538956af873380a0912a8a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 14 May 2025 22:31:41 +1000 Subject: [PATCH 0454/1175] Fix pin strings in Teslemetry (#144873) pinstring --- homeassistant/components/teslemetry/strings.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index fb68e045b37..57b6053bb48 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -2,7 +2,8 @@ "common": { "unavailable": "Unavailable", "abort": "Abort", - "vehicle": "Vehicle" + "vehicle": "Vehicle", + "descr_pin": "4-digit code to enable or disable the setting" }, "config": { "abort": { @@ -1208,8 +1209,8 @@ "name": "[%key:common::action::enable%]" }, "pin": { - "description": "4 digit PIN.", - "name": "PIN" + "description": "[%key:component::teslemetry::common::descr_pin%]", + "name": "[%key:common::config_flow::data::pin%]" } }, "name": "Set speed limit" @@ -1240,8 +1241,8 @@ "name": "[%key:common::action::enable%]" }, "pin": { - "description": "4 digit PIN.", - "name": "PIN" + "description": "[%key:component::teslemetry::common::descr_pin%]", + "name": "[%key:common::config_flow::data::pin%]" } }, "name": "Set valet mode" From a9238c757773f3b062f5263805595e1962b84f5b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 14:31:50 +0200 Subject: [PATCH 0455/1175] Use entry.async_on_unload in gpslogger (#144883) --- homeassistant/components/gpslogger/__init__.py | 3 +-- homeassistant/components/gpslogger/device_tracker.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 7c7612ed201..46843b30f4d 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -90,7 +90,7 @@ async def handle_webhook( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure based on config entry.""" - hass.data.setdefault(DOMAIN, {"devices": set(), "unsub_device_tracker": {}}) + hass.data.setdefault(DOMAIN, {"devices": set()}) webhook.async_register( hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) @@ -103,7 +103,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index cf0515f5c41..515f550e566 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -42,9 +42,7 @@ async def async_setup_entry( async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)]) - hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = ( - async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) - ) + entry.async_on_unload(async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)) # Restore previously loaded devices dev_reg = dr.async_get(hass) From ef9965891986068f1ab87d05e02b5b9b0a977a5b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 14:59:10 +0200 Subject: [PATCH 0456/1175] Use runtime_data in gpslogger (#144884) --- homeassistant/components/gpslogger/__init__.py | 6 ++++-- homeassistant/components/gpslogger/device_tracker.py | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 46843b30f4d..37493ed24fa 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -24,6 +24,8 @@ from .const import ( DOMAIN, ) +type GPSLoggerConfigEntry = ConfigEntry[set[str]] + PLATFORMS = [Platform.DEVICE_TRACKER] TRACKER_UPDATE = f"{DOMAIN}_tracker_update" @@ -88,9 +90,9 @@ async def handle_webhook( return web.Response(text=f"Setting location for {device}") -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) -> bool: """Configure based on config entry.""" - hass.data.setdefault(DOMAIN, {"devices": set()}) + entry.runtime_data = set() webhook.async_register( hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 515f550e566..950aa2a2638 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,7 +1,6 @@ """Support for the GPSLogger device tracking.""" from homeassistant.components.device_tracker import TrackerEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, @@ -15,19 +14,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN, TRACKER_UPDATE +from . import TRACKER_UPDATE, GPSLoggerConfigEntry from .const import ( ATTR_ACTIVITY, ATTR_ALTITUDE, ATTR_DIRECTION, ATTR_PROVIDER, ATTR_SPEED, + DOMAIN, ) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: GPSLoggerConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Configure a dispatcher connection based on a config entry.""" @@ -35,10 +35,10 @@ async def async_setup_entry( @callback def _receive_data(device, gps, battery, accuracy, attrs): """Receive set location.""" - if device in hass.data[DOMAIN]["devices"]: + if device in entry.runtime_data: return - hass.data[DOMAIN]["devices"].add(device) + entry.runtime_data.add(device) async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)]) @@ -56,7 +56,7 @@ async def async_setup_entry( entities = [] for dev_id in dev_ids: - hass.data[DOMAIN]["devices"].add(dev_id) + entry.runtime_data.add(dev_id) entity = GPSLoggerEntity(dev_id, None, None, None, None) entities.append(entity) From b0ff4b58411b7abf37ab13bd44a8eb64e35cb909 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Wed, 14 May 2025 15:01:01 +0200 Subject: [PATCH 0457/1175] Add flow detection to Rachio hose timer (#144075) * flow binary sensor * rename property * Move const and update coordinator reference * update controller descriptions * Address review comments * Use lookup for rain sensor * Update online binary sensor * make it a bit more readable --------- Co-authored-by: J. Nick Koston --- .../components/rachio/binary_sensor.py | 195 ++++++++++-------- homeassistant/components/rachio/const.py | 4 + homeassistant/components/rachio/entity.py | 31 ++- homeassistant/components/rachio/strings.json | 3 + homeassistant/components/rachio/switch.py | 7 +- 5 files changed, 146 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 0c502a98c9a..be379a23cab 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -1,28 +1,31 @@ """Integration with the Rachio Iro sprinkler system controller.""" -from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any 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, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( - DOMAIN, - KEY_BATTERY_STATUS, + DOMAIN as DOMAIN_RACHIO, + KEY_BATTERY, + KEY_DETECT_FLOW, KEY_DEVICE_ID, - KEY_LOW, + KEY_FLOW, + KEY_ONLINE, + KEY_RAIN_SENSOR, KEY_RAIN_SENSOR_TRIPPED, - KEY_REPLACE, - KEY_REPORTED_STATE, - KEY_STATE, KEY_STATUS, KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, @@ -30,7 +33,7 @@ from .const import ( STATUS_ONLINE, ) from .coordinator import RachioUpdateCoordinator -from .device import RachioPerson +from .device import RachioIro, RachioPerson from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_COLD_REBOOT, @@ -43,6 +46,67 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) +@dataclass(frozen=True, kw_only=True) +class RachioControllerBinarySensorDescription(BinarySensorEntityDescription): + """Describe a Rachio controller binary sensor.""" + + update_received: Callable[[str], bool | None] + is_on: Callable[[RachioIro], bool] + signal_string: str + + +CONTROLLER_BINARY_SENSOR_TYPES: tuple[RachioControllerBinarySensorDescription, ...] = ( + RachioControllerBinarySensorDescription( + key=KEY_ONLINE, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + signal_string=SIGNAL_RACHIO_CONTROLLER_UPDATE, + is_on=lambda controller: controller.init_data[KEY_STATUS] == STATUS_ONLINE, + update_received={ + SUBTYPE_ONLINE: True, + SUBTYPE_COLD_REBOOT: True, + SUBTYPE_OFFLINE: False, + }.get, + ), + RachioControllerBinarySensorDescription( + key=KEY_RAIN_SENSOR, + translation_key="rain", + device_class=BinarySensorDeviceClass.MOISTURE, + signal_string=SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, + is_on=lambda controller: controller.init_data[KEY_RAIN_SENSOR_TRIPPED], + update_received={ + SUBTYPE_RAIN_SENSOR_DETECTION_ON: True, + SUBTYPE_RAIN_SENSOR_DETECTION_OFF: False, + }.get, + ), +) + + +@dataclass(frozen=True, kw_only=True) +class RachioHoseTimerBinarySensorDescription(BinarySensorEntityDescription): + """Describe a Rachio hose timer binary sensor.""" + + value_fn: Callable[[RachioHoseTimerEntity], bool] + exists_fn: Callable[[dict[str, Any]], bool] = lambda _: True + + +HOSE_TIMER_BINARY_SENSOR_TYPES: tuple[RachioHoseTimerBinarySensorDescription, ...] = ( + RachioHoseTimerBinarySensorDescription( + key=KEY_BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.BATTERY, + value_fn=lambda device: device.battery, + ), + RachioHoseTimerBinarySensorDescription( + key=KEY_FLOW, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + translation_key="flow", + value_fn=lambda device: device.no_flow_detected, + exists_fn=lambda valve: valve[KEY_DETECT_FLOW], + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -55,23 +119,38 @@ async def async_setup_entry( def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: entities: list[Entity] = [] - person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] - for controller in person.controllers: - entities.append(RachioControllerOnlineBinarySensor(controller)) - entities.append(RachioRainSensor(controller)) + person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] entities.extend( - RachioHoseTimerBattery(valve, base_station.status_coordinator) + RachioControllerBinarySensor(controller, description) + for controller in person.controllers + for description in CONTROLLER_BINARY_SENSOR_TYPES + ) + entities.extend( + RachioHoseTimerBinarySensor(valve, base_station.status_coordinator, description) for base_station in person.base_stations for valve in base_station.status_coordinator.data.values() + for description in HOSE_TIMER_BINARY_SENSOR_TYPES + if description.exists_fn(valve) ) return entities class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): - """Represent a binary sensor that reflects a Rachio state.""" + """Represent a binary sensor that reflects a Rachio controller state.""" + entity_description: RachioControllerBinarySensorDescription _attr_has_entity_name = True + def __init__( + self, + controller: RachioIro, + description: RachioControllerBinarySensorDescription, + ) -> None: + """Initialize a controller binary sensor.""" + super().__init__(controller) + self.entity_description = description + self._attr_unique_id = f"{controller.controller_id}-{description.key}" + @callback def _async_handle_any_update(self, *args, **kwargs) -> None: """Determine whether an update event applies to this device.""" @@ -82,97 +161,49 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): # For this device self._async_handle_update(args, kwargs) - @abstractmethod - def _async_handle_update(self, *args, **kwargs) -> None: - """Handle an update to the state of this sensor.""" - - -class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): - """Represent a binary sensor that reflects if the controller is online.""" - - _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - - @property - def unique_id(self) -> str: - """Return a unique id for this entity.""" - return f"{self._controller.controller_id}-online" - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - if args[0][0][KEY_SUBTYPE] in (SUBTYPE_ONLINE, SUBTYPE_COLD_REBOOT): - self._attr_is_on = True - elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: - self._attr_is_on = False + if ( + updated_state := self.entity_description.update_received( + args[0][0][KEY_SUBTYPE] + ) + ) is not None: + self._attr_is_on = updated_state self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Subscribe to updates.""" - self._attr_is_on = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE + self._attr_is_on = self.entity_description.is_on(self._controller) self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_RACHIO_CONTROLLER_UPDATE, + self.entity_description.signal_string, self._async_handle_any_update, ) ) -class RachioRainSensor(RachioControllerBinarySensor): - """Represent a binary sensor that reflects the status of the rain sensor.""" +class RachioHoseTimerBinarySensor(RachioHoseTimerEntity, BinarySensorEntity): + """Represents a binary sensor for a smart hose timer.""" - _attr_device_class = BinarySensorDeviceClass.MOISTURE - _attr_translation_key = "rain" - - @property - def unique_id(self) -> str: - """Return a unique id for this entity.""" - return f"{self._controller.controller_id}-rain_sensor" - - @callback - def _async_handle_update(self, *args, **kwargs) -> None: - """Handle an update to the state of this sensor.""" - if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON: - self._attr_is_on = True - elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF: - self._attr_is_on = False - - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Subscribe to updates.""" - self._attr_is_on = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED] - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, - self._async_handle_any_update, - ) - ) - - -class RachioHoseTimerBattery(RachioHoseTimerEntity, BinarySensorEntity): - """Represents a battery sensor for a smart hose timer.""" - - _attr_device_class = BinarySensorDeviceClass.BATTERY + entity_description: RachioHoseTimerBinarySensorDescription def __init__( - self, data: dict[str, Any], coordinator: RachioUpdateCoordinator + self, + data: dict[str, Any], + coordinator: RachioUpdateCoordinator, + description: RachioHoseTimerBinarySensorDescription, ) -> None: - """Initialize a smart hose timer battery sensor.""" + """Initialize a smart hose timer binary sensor.""" super().__init__(data, coordinator) - self._attr_unique_id = f"{self.id}-battery" + self.entity_description = description + self._attr_unique_id = f"{self.id}-{description.key}" + self._update_attr() @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] in [ - KEY_LOW, - KEY_REPLACE, - ] + self._attr_is_on = self.entity_description.value_fn(self) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index ad670fc3608..08a09f309f6 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -25,10 +25,12 @@ KEY_ID = "id" KEY_NAME = "name" KEY_MODEL = "model" KEY_ON = "on" +KEY_ONLINE = "online" KEY_DURATION = "totalDuration" KEY_DURATION_MINUTES = "duration" KEY_RAIN_DELAY = "rainDelayExpirationDate" KEY_RAIN_DELAY_END = "endTime" +KEY_RAIN_SENSOR = "rain_sensor" KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped" KEY_STATUS = "status" KEY_SUBTYPE = "subType" @@ -57,6 +59,8 @@ KEY_STATE = "state" KEY_CONNECTED = "connected" KEY_CURRENT_STATUS = "lastWateringAction" KEY_DETECT_FLOW = "detectFlow" +KEY_BATTERY = "battery" +KEY_FLOW = "flow" KEY_BATTERY_STATUS = "batteryStatus" KEY_LOW = "LOW" KEY_REPLACE = "REPLACE" diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index 056abe9145b..10657a1f0e9 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -12,9 +12,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DEFAULT_NAME, DOMAIN, + KEY_BATTERY_STATUS, KEY_CONNECTED, + KEY_CURRENT_STATUS, + KEY_FLOW_DETECTED, KEY_ID, + KEY_LOW, KEY_NAME, + KEY_REPLACE, KEY_REPORTED_STATE, KEY_STATE, ) @@ -70,17 +75,29 @@ class RachioHoseTimerEntity(CoordinatorEntity[RachioUpdateCoordinator]): manufacturer=DEFAULT_NAME, configuration_url="https://app.rach.io", ) - self._update_attr() + + @property + def reported_state(self) -> dict[str, Any]: + """Return the reported state.""" + return self.coordinator.data[self.id][KEY_STATE][KEY_REPORTED_STATE] @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 - ] - ) + return super().available and self.reported_state[KEY_CONNECTED] + + @property + def battery(self) -> bool: + """Return the battery status.""" + return self.reported_state[KEY_BATTERY_STATUS] in [KEY_LOW, KEY_REPLACE] + + @property + def no_flow_detected(self) -> bool: + """Return true if valve is on and flow is not detected.""" + if status := self.reported_state.get(KEY_CURRENT_STATUS): + # Since this is a problem indicator we need the opposite of the API state + return not status.get(KEY_FLOW_DETECTED, True) + return False @abstractmethod def _update_attr(self) -> None: diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json index d51a1d5f920..ea3c8911463 100644 --- a/homeassistant/components/rachio/strings.json +++ b/homeassistant/components/rachio/strings.json @@ -31,6 +31,9 @@ "binary_sensor": { "rain": { "name": "Rain" + }, + "flow": { + "name": "Flow" } }, "calendar": { diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 0edccf02320..e2c5d66b967 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -37,9 +37,7 @@ from .const import ( KEY_ON, KEY_RAIN_DELAY, KEY_RAIN_DELAY_END, - KEY_REPORTED_STATE, KEY_SCHEDULE_ID, - KEY_STATE, KEY_SUBTYPE, KEY_SUMMARY, KEY_TYPE, @@ -548,6 +546,7 @@ class RachioValve(RachioHoseTimerEntity, SwitchEntity): self._person = person self._base = base self._attr_unique_id = f"{self.id}-valve" + self._update_attr() def turn_on(self, **kwargs: Any) -> None: """Turn on this valve.""" @@ -575,7 +574,5 @@ class RachioValve(RachioHoseTimerEntity, SwitchEntity): @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._static_attrs = self.reported_state self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs From d273a92a19e8a242d573105a8bf851d9e65b7265 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:40 -0400 Subject: [PATCH 0458/1175] Refactor template optional configuration attributes (#144887) --- .../components/template/template_entity.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 88708278758..41ebf5bc1be 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -76,23 +76,35 @@ TEMPLATE_ENTITY_ICON_SCHEMA = vol.Schema( } ) -TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( +TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA = vol.Schema( { vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), - vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, } -).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) +) + +TEMPLATE_ENTITY_COMMON_SCHEMA = ( + vol.Schema( + { + vol.Optional(CONF_AVAILABILITY): cv.template, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + } + ) + .extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema) +) def make_template_entity_common_schema(default_name: str) -> vol.Schema: """Return a schema with default name.""" - return vol.Schema( - { - vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), - vol.Optional(CONF_AVAILABILITY): cv.template, - } - ).extend(make_template_entity_base_schema(default_name).schema) + return ( + vol.Schema( + { + vol.Optional(CONF_AVAILABILITY): cv.template, + } + ) + .extend(make_template_entity_base_schema(default_name).schema) + .extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema) + ) TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY = vol.Schema( From 11644d48ee930426c82c4f8d6977b43e87448d67 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Wed, 14 May 2025 10:04:07 -0400 Subject: [PATCH 0459/1175] Use snapshot testing for APCUPSD integration (#130770) * First try to use snapshot testing for sensors * Use snapshot testing * Add ambr files * Update comment * Address review comments * Remove duplicate async init integration call * Add device test for cases w/o SERIALNO * Use friendlier snapshot names * Use * to mandate keyed argument for async_init_integration * Always pass mock config entry ID * Fix incorrect ID --- tests/components/apcupsd/__init__.py | 7 +- .../apcupsd/snapshots/test_binary_sensor.ambr | 48 + .../apcupsd/snapshots/test_init.ambr | 133 ++ .../apcupsd/snapshots/test_sensor.ambr | 1992 +++++++++++++++++ .../components/apcupsd/test_binary_sensor.py | 26 +- tests/components/apcupsd/test_init.py | 98 +- tests/components/apcupsd/test_sensor.py | 136 +- 7 files changed, 2244 insertions(+), 196 deletions(-) create mode 100644 tests/components/apcupsd/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/apcupsd/snapshots/test_init.ambr create mode 100644 tests/components/apcupsd/snapshots/test_sensor.ambr diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index 5994a7f4c17..2a786925e70 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -82,13 +82,18 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict( async def async_init_integration( - hass: HomeAssistant, host: str = "test", status: dict[str, str] | None = None + hass: HomeAssistant, + *, + host: str = "test", + status: dict[str, str] | None = None, + entry_id: str = "mocked-config-entry-id", ) -> MockConfigEntry: """Set up the APC UPS Daemon integration in HomeAssistant.""" if status is None: status = MOCK_STATUS entry = MockConfigEntry( + entry_id=entry_id, version=1, domain=DOMAIN, title="APCUPSd", diff --git a/tests/components/apcupsd/snapshots/test_binary_sensor.ambr b/tests/components/apcupsd/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..0ab9dfb047e --- /dev/null +++ b/tests/components/apcupsd/snapshots/test_binary_sensor.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.myups_online_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.myups_online_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': 'Online status', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'online_status', + 'unique_id': 'XXXXXXXXXXXX_statflag', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.myups_online_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Online status', + }), + 'context': , + 'entity_id': 'binary_sensor.myups_online_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr new file mode 100644 index 00000000000..39f28b528fc --- /dev/null +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -0,0 +1,133 @@ +# serializer version: 1 +# name: test_async_setup_entry[status0][device_MyUPS_XXXXXXXXXXXX] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '928.a8 .D USB FW:a8', + 'id': , + 'identifiers': set({ + tuple( + 'apcupsd', + 'XXXXXXXXXXXX', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'APC', + 'model': 'Back-UPS ES 600', + 'model_id': None, + 'name': 'MyUPS', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '3.14.14 (31 May 2016) unknown', + 'via_device_id': None, + }) +# --- +# name: test_async_setup_entry[status1][device_APC UPS_XXXX] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'apcupsd', + 'XXXX', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'APC', + 'model': None, + 'model_id': None, + 'name': 'APC UPS', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_async_setup_entry[status2][device_APC UPS_] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'apcupsd', + 'mocked-config-entry-id', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'APC', + 'model': None, + 'model_id': None, + 'name': 'APC UPS', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_async_setup_entry[status3][device_APC UPS_Blank] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'apcupsd', + 'mocked-config-entry-id', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'APC', + 'model': None, + 'model_id': None, + 'name': 'APC UPS', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..6409f205d4f --- /dev/null +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -0,0 +1,1992 @@ +# serializer version: 1 +# name: test_sensor[sensor.myups_alarm_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_alarm_delay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarm delay', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm_delay', + 'unique_id': 'XXXXXXXXXXXX_alarmdel', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_alarm_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Alarm delay', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_alarm_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_sensor[sensor.myups_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_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': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'XXXXXXXXXXXX_bcharge', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.myups_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'MyUPS Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.myups_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_sensor[sensor.myups_battery_nominal_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_battery_nominal_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery nominal voltage', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_nominal_voltage', + 'unique_id': 'XXXXXXXXXXXX_nombattv', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_battery_nominal_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Battery nominal voltage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_battery_nominal_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.0', + }) +# --- +# name: test_sensor[sensor.myups_battery_replaced-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_battery_replaced', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery replaced', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_replacement_date', + 'unique_id': 'XXXXXXXXXXXX_battdate', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_battery_replaced-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Battery replaced', + }), + 'context': , + 'entity_id': 'sensor.myups_battery_replaced', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01', + }) +# --- +# name: test_sensor[sensor.myups_battery_shutdown-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_battery_shutdown', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery shutdown', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_battery_charge', + 'unique_id': 'XXXXXXXXXXXX_mbattchg', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.myups_battery_shutdown-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Battery shutdown', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.myups_battery_shutdown', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_sensor[sensor.myups_battery_timeout-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_battery_timeout', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Battery timeout', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_time', + 'unique_id': 'XXXXXXXXXXXX_maxtime', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_battery_timeout-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Battery timeout', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_battery_timeout', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.myups_battery_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_battery_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery voltage', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'battery_voltage', + 'unique_id': 'XXXXXXXXXXXX_battv', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_battery_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Battery voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_battery_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '13.7', + }) +# --- +# name: test_sensor[sensor.myups_cable_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_cable_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cable type', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cable_type', + 'unique_id': 'XXXXXXXXXXXX_cable', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_cable_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Cable type', + }), + 'context': , + 'entity_id': 'sensor.myups_cable_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'USB Cable', + }) +# --- +# name: test_sensor[sensor.myups_daemon_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_daemon_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Daemon version', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'version', + 'unique_id': 'XXXXXXXXXXXX_version', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_daemon_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Daemon version', + }), + 'context': , + 'entity_id': 'sensor.myups_daemon_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.14.14 (31 May 2016) unknown', + }) +# --- +# name: test_sensor[sensor.myups_date_and_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_date_and_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Date and time', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'date_and_time', + 'unique_id': 'XXXXXXXXXXXX_end apc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_date_and_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Date and time', + }), + 'context': , + 'entity_id': 'sensor.myups_date_and_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01 00:00:00 0000', + }) +# --- +# name: test_sensor[sensor.myups_driver-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_driver', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Driver', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'driver', + 'unique_id': 'XXXXXXXXXXXX_driver', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_driver-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Driver', + }), + 'context': , + 'entity_id': 'sensor.myups_driver', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'USB UPS Driver', + }) +# --- +# name: test_sensor[sensor.myups_firmware_version-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_firmware_version', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Firmware version', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'firmware_version', + 'unique_id': 'XXXXXXXXXXXX_firmware', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_firmware_version-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Firmware version', + }), + 'context': , + 'entity_id': 'sensor.myups_firmware_version', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '928.a8 .D USB FW:a8', + }) +# --- +# name: test_sensor[sensor.myups_input_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_input_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Input voltage', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'line_voltage', + 'unique_id': 'XXXXXXXXXXXX_linev', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_input_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Input voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_input_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '124.0', + }) +# --- +# name: test_sensor[sensor.myups_internal_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_internal_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': 'Internal temperature', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'internal_temperature', + 'unique_id': 'XXXXXXXXXXXX_itemp', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_internal_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'MyUPS Internal temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_internal_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '34.6', + }) +# --- +# name: test_sensor[sensor.myups_last_self_test-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_last_self_test', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last self test', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_self_test', + 'unique_id': 'XXXXXXXXXXXX_laststest', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_last_self_test-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Last self test', + }), + 'context': , + 'entity_id': 'sensor.myups_last_self_test', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01 00:00:00 0000', + }) +# --- +# name: test_sensor[sensor.myups_last_transfer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_last_transfer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Last transfer', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_transfer', + 'unique_id': 'XXXXXXXXXXXX_lastxfer', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_last_transfer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Last transfer', + }), + 'context': , + 'entity_id': 'sensor.myups_last_transfer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Automatic or explicit self test', + }) +# --- +# name: test_sensor[sensor.myups_load-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_load', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Load', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'load_capacity', + 'unique_id': 'XXXXXXXXXXXX_loadpct', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[sensor.myups_load-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Load', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.myups_load', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '14.0', + }) +# --- +# name: test_sensor[sensor.myups_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_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': 'Mode', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ups_mode', + 'unique_id': 'XXXXXXXXXXXX_upsmode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Mode', + }), + 'context': , + 'entity_id': 'sensor.myups_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Stand Alone', + }) +# --- +# name: test_sensor[sensor.myups_model-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_model', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Model', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'model', + 'unique_id': 'XXXXXXXXXXXX_model', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_model-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Model', + }), + 'context': , + 'entity_id': 'sensor.myups_model', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Back-UPS ES 600', + }) +# --- +# name: test_sensor[sensor.myups_name-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_name', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Name', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ups_name', + 'unique_id': 'XXXXXXXXXXXX_upsname', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_name-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Name', + }), + 'context': , + 'entity_id': 'sensor.myups_name', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'MyUPS', + }) +# --- +# name: test_sensor[sensor.myups_nominal_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_nominal_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Nominal apparent power', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nominal_apparent_power', + 'unique_id': 'XXXXXXXXXXXX_nomapnt', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_nominal_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'MyUPS Nominal apparent power', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_nominal_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.0', + }) +# --- +# name: test_sensor[sensor.myups_nominal_input_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_nominal_input_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Nominal input voltage', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nominal_input_voltage', + 'unique_id': 'XXXXXXXXXXXX_nominv', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_nominal_input_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Nominal input voltage', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_nominal_input_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120', + }) +# --- +# name: test_sensor[sensor.myups_nominal_output_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_nominal_output_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Nominal output power', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'nominal_output_power', + 'unique_id': 'XXXXXXXXXXXX_nompower', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_nominal_output_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'MyUPS Nominal output power', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_nominal_output_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '330', + }) +# --- +# name: test_sensor[sensor.myups_output_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_output_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Output current', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'output_current', + 'unique_id': 'XXXXXXXXXXXX_outcurnt', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_output_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'MyUPS Output current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_output_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.88', + }) +# --- +# name: test_sensor[sensor.myups_self_test_interval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_self_test_interval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self test interval', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'self_test_interval', + 'unique_id': 'XXXXXXXXXXXX_stesti', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_self_test_interval-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Self test interval', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_self_test_interval', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_sensor[sensor.myups_self_test_result-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_self_test_result', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self test result', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'self_test_result', + 'unique_id': 'XXXXXXXXXXXX_selftest', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_self_test_result-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Self test result', + }), + 'context': , + 'entity_id': 'sensor.myups_self_test_result', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'NO', + }) +# --- +# name: test_sensor[sensor.myups_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_sensitivity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensitivity', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sensitivity', + 'unique_id': 'XXXXXXXXXXXX_sense', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Sensitivity', + }), + 'context': , + 'entity_id': 'sensor.myups_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Medium', + }) +# --- +# name: test_sensor[sensor.myups_serial_number-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_serial_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': 'Serial number', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'serial_number', + 'unique_id': 'XXXXXXXXXXXX_serialno', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_serial_number-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Serial number', + }), + 'context': , + 'entity_id': 'sensor.myups_serial_number', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'XXXXXXXXXXXX', + }) +# --- +# name: test_sensor[sensor.myups_shutdown_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_shutdown_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Shutdown time', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'min_time', + 'unique_id': 'XXXXXXXXXXXX_mintimel', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_shutdown_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Shutdown time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_shutdown_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor[sensor.myups_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_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': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'XXXXXXXXXXXX_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Status', + }), + 'context': , + 'entity_id': 'sensor.myups_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ONLINE', + }) +# --- +# name: test_sensor[sensor.myups_status_data-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_status_data', + 'has_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 data', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'apc_status', + 'unique_id': 'XXXXXXXXXXXX_apc', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_status_data-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Status data', + }), + 'context': , + 'entity_id': 'sensor.myups_status_data', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '001,038,0985', + }) +# --- +# name: test_sensor[sensor.myups_status_date-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_status_date', + 'has_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 date', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'date', + 'unique_id': 'XXXXXXXXXXXX_date', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_status_date-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Status date', + }), + 'context': , + 'entity_id': 'sensor.myups_status_date', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01 00:00:00 0000', + }) +# --- +# name: test_sensor[sensor.myups_status_flag-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_status_flag', + 'has_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 flag', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'online_status', + 'unique_id': 'XXXXXXXXXXXX_statflag', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_status_flag-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Status flag', + }), + 'context': , + 'entity_id': 'sensor.myups_status_flag', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0x05000008', + }) +# --- +# name: test_sensor[sensor.myups_time_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_time_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Time left', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'time_left', + 'unique_id': 'XXXXXXXXXXXX_timeleft', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_time_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MyUPS Time left', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_time_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '51.0', + }) +# --- +# name: test_sensor[sensor.myups_time_on_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_time_on_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': 'Time on battery', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'time_on_battery', + 'unique_id': 'XXXXXXXXXXXX_tonbatt', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_time_on_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MyUPS Time on battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_time_on_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[sensor.myups_total_time_on_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_total_time_on_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': 'Total time on battery', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'total_time_on_battery', + 'unique_id': 'XXXXXXXXXXXX_cumonbatt', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_total_time_on_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'MyUPS Total time on battery', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_total_time_on_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensor[sensor.myups_transfer_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_transfer_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Transfer count', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfer_count', + 'unique_id': 'XXXXXXXXXXXX_numxfers', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_transfer_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Transfer count', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.myups_transfer_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_sensor[sensor.myups_transfer_from_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_transfer_from_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Transfer from battery', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfer_from_battery', + 'unique_id': 'XXXXXXXXXXXX_xoffbatt', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_transfer_from_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Transfer from battery', + }), + 'context': , + 'entity_id': 'sensor.myups_transfer_from_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01 00:00:00 0000', + }) +# --- +# name: test_sensor[sensor.myups_transfer_high-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_transfer_high', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Transfer high', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfer_high', + 'unique_id': 'XXXXXXXXXXXX_hitrans', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_transfer_high-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Transfer high', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_transfer_high', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '139.0', + }) +# --- +# name: test_sensor[sensor.myups_transfer_low-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_transfer_low', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Transfer low', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfer_low', + 'unique_id': 'XXXXXXXXXXXX_lotrans', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.myups_transfer_low-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'MyUPS Transfer low', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.myups_transfer_low', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92.0', + }) +# --- +# name: test_sensor[sensor.myups_transfer_to_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.myups_transfer_to_battery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Transfer to battery', + 'platform': 'apcupsd', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'transfer_to_battery', + 'unique_id': 'XXXXXXXXXXXX_xonbatt', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.myups_transfer_to_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'MyUPS Transfer to battery', + }), + 'context': , + 'entity_id': 'sensor.myups_transfer_to_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1970-01-01 00:00:00 0000', + }) +# --- diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 02351109603..d9d45830024 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,27 +1,29 @@ """Test binary sensors of APCUPSd integration.""" -import pytest +from unittest.mock import 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 homeassistant.util import slugify from . import MOCK_STATUS, async_init_integration +from tests.common import snapshot_platform + async def test_binary_sensor( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: - """Test states of binary sensor.""" - await async_init_integration(hass, status=MOCK_STATUS) - - device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] - state = hass.states.get(f"binary_sensor.{device_slug}_online_status") - assert state - assert state.state == "on" - entry = entity_registry.async_get(f"binary_sensor.{device_slug}_online_status") - assert entry - assert entry.unique_id == f"{serialno}_statflag" + """Test states of binary sensors.""" + with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.BINARY_SENSOR]): + config_entry = await async_init_integration(hass, status=MOCK_STATUS) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_no_binary_sensor(hass: HomeAssistant) -> None: diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index 9edf4d8282f..e5c295ae1bf 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -5,6 +5,7 @@ from collections import OrderedDict from unittest.mock import patch import pytest +from syrupy import SnapshotAssertion from homeassistant.components.apcupsd.const import DOMAIN from homeassistant.components.apcupsd.coordinator import UPDATE_INTERVAL @@ -12,6 +13,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.util import slugify, utcnow from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration @@ -28,71 +30,31 @@ from tests.common import MockConfigEntry, async_fire_time_changed # Contains "SERIALNO" but no "UPSNAME" field. # We should create devices for the entities and prefix their IDs with default "APC UPS". MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, - # Does not contain either "SERIALNO" field or "UPSNAME" field. Our integration should work - # fine without it by falling back to config entry ID as unique ID and "APC UPS" as default name. + # Does not contain either "SERIALNO" field or "UPSNAME" field. + # Our integration should work fine without it by falling back to config entry ID as unique + # ID and "APC UPS" as default name. MOCK_MINIMAL_STATUS, # Some models report "Blank" as SERIALNO, but we should treat it as not reported. MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"}, ], ) -async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> None: - """Test a successful setup entry.""" - await async_init_integration(hass, status=status) - - prefix = slugify(status.get("UPSNAME", "APC UPS")) + "_" - - # Verify successful setup by querying the status sensor. - state = hass.states.get(f"binary_sensor.{prefix}online_status") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "on" - - -@pytest.mark.parametrize( - "status", - [ - # We should not create device entries if SERIALNO is not reported. - MOCK_MINIMAL_STATUS, - # Some models report "Blank" as SERIALNO, but we should treat it as not reported. - MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"}, - # We should set the device name to be the friendly UPSNAME field if available. - MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX", "UPSNAME": "MyUPS"}, - # Otherwise, we should fall back to default device name --- "APC UPS". - MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, - # We should create all fields of the device entry if they are available. - MOCK_STATUS, - ], -) -async def test_device_entry( - hass: HomeAssistant, status: OrderedDict, device_registry: dr.DeviceRegistry +async def test_async_setup_entry( + hass: HomeAssistant, + status: OrderedDict, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, ) -> None: - """Test successful setup of device entries.""" + """Test a successful setup entry.""" config_entry = await async_init_integration(hass, status=status) - - # Verify device info is properly set up. - assert len(device_registry.devices) == 1 - entry = device_registry.async_get_device( - {(DOMAIN, config_entry.unique_id or config_entry.entry_id)} + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, config_entry.unique_id or config_entry.entry_id)} ) - assert entry is not None - # Specify the mapping between field name and the expected fields in device entry. - fields = { - "UPSNAME": entry.name, - "MODEL": entry.model, - "VERSION": entry.sw_version, - "FIRMWARE": entry.hw_version, - } + name = f"device_{device_entry.name}_{status.get('SERIALNO', '')}" + assert device_entry == snapshot(name=name) - for field, entry_value in fields.items(): - if field in status: - assert entry_value == status[field] - # Even if UPSNAME is not available, we must fall back to default "APC UPS". - elif field == "UPSNAME": - assert entry_value == "APC UPS" - else: - assert not entry_value - - assert entry.manufacturer == "APC" + platforms = async_get_platforms(hass, DOMAIN) + assert len(platforms) > 0 + assert all(len(p.entities) > 0 for p in platforms) async def test_multiple_integrations(hass: HomeAssistant) -> None: @@ -101,8 +63,12 @@ async def test_multiple_integrations(hass: HomeAssistant) -> None: status1 = MOCK_STATUS | {"LOADPCT": "15.0 Percent", "SERIALNO": "XXXXX1"} status2 = MOCK_STATUS | {"LOADPCT": "16.0 Percent", "SERIALNO": "XXXXX2"} entries = ( - await async_init_integration(hass, host="test1", status=status1), - await async_init_integration(hass, host="test2", status=status2), + await async_init_integration( + hass, host="test1", status=status1, entry_id="entry-id-1" + ), + await async_init_integration( + hass, host="test2", status=status2, entry_id="entry-id-2" + ), ) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -121,8 +87,12 @@ async def test_multiple_integrations_different_devices(hass: HomeAssistant) -> N status1 = MOCK_STATUS | {"SERIALNO": "XXXXX1", "UPSNAME": "MyUPS1"} status2 = MOCK_STATUS | {"SERIALNO": "XXXXX2", "UPSNAME": "MyUPS2"} entries = ( - await async_init_integration(hass, host="test1", status=status1), - await async_init_integration(hass, host="test2", status=status2), + await async_init_integration( + hass, host="test1", status=status1, entry_id="entry-id-1" + ), + await async_init_integration( + hass, host="test2", status=status2, entry_id="entry-id-2" + ), ) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -159,8 +129,12 @@ async def test_unload_remove_entry(hass: HomeAssistant) -> None: """Test successful unload and removal of an entry.""" # Load two integrations from two mock hosts. entries = ( - await async_init_integration(hass, host="test1", status=MOCK_STATUS), - await async_init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), + await async_init_integration( + hass, host="test1", status=MOCK_STATUS, entry_id="entry-id-1" + ), + await async_init_integration( + hass, host="test2", status=MOCK_MINIMAL_STATUS, entry_id="entry-id-2" + ), ) # Assert they are loaded. diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index f36421c4183..b14db49970b 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -3,22 +3,15 @@ from datetime import timedelta from unittest.mock import patch +import pytest +from syrupy import SnapshotAssertion + from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfElectricPotential, - UnitOfPower, - UnitOfTime, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -28,118 +21,19 @@ from homeassistant.util.dt import utcnow from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, snapshot_platform -async def test_sensor(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None: - """Test states of sensor.""" - await async_init_integration(hass, status=MOCK_STATUS) - device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] - - # Test a representative string sensor. - state = hass.states.get(f"sensor.{device_slug}_mode") - assert state - assert state.state == "Stand Alone" - entry = entity_registry.async_get(f"sensor.{device_slug}_mode") - assert entry - assert entry.unique_id == f"{serialno}_upsmode" - - # Test two representative voltage sensors. - state = hass.states.get(f"sensor.{device_slug}_input_voltage") - assert state - assert state.state == "124.0" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - entry = entity_registry.async_get(f"sensor.{device_slug}_input_voltage") - assert entry - assert entry.unique_id == f"{serialno}_linev" - - state = hass.states.get(f"sensor.{device_slug}_battery_voltage") - assert state - assert state.state == "13.7" - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT - ) - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - entry = entity_registry.async_get(f"sensor.{device_slug}_battery_voltage") - assert entry - assert entry.unique_id == f"{serialno}_battv" - - # Test a representative time sensor. - state = hass.states.get(f"sensor.{device_slug}_self_test_interval") - assert state - assert state.state == "7" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.DAYS - entry = entity_registry.async_get(f"sensor.{device_slug}_self_test_interval") - assert entry - assert entry.unique_id == f"{serialno}_stesti" - - # Test a representative percentage sensor. - state = hass.states.get(f"sensor.{device_slug}_load") - assert state - assert state.state == "14.0" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - entry = entity_registry.async_get(f"sensor.{device_slug}_load") - assert entry - assert entry.unique_id == f"{serialno}_loadpct" - - # Test a representative wattage sensor. - state = hass.states.get(f"sensor.{device_slug}_nominal_output_power") - assert state - assert state.state == "330" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - entry = entity_registry.async_get(f"sensor.{device_slug}_nominal_output_power") - assert entry - assert entry.unique_id == f"{serialno}_nompower" - - -async def test_sensor_name(hass: HomeAssistant) -> None: - """Test if sensor name follows the recommended entity naming scheme. - - See https://developers.home-assistant.io/docs/core/entity/#entity-naming for more details. - """ - await async_init_integration(hass, status=MOCK_STATUS) - - all_states = hass.states.async_all() - assert len(all_states) != 0 - - device_name = MOCK_STATUS["UPSNAME"] - for state in all_states: - # Friendly name must start with the device name. - friendly_name = state.name - assert friendly_name.startswith(device_name) - - # Entity names should start with a capital letter, the rest of the words are lower case. - entity_name = friendly_name.removeprefix(device_name).strip() - assert entity_name == entity_name.capitalize() - - -async def test_sensor_disabled( - hass: HomeAssistant, entity_registry: er.EntityRegistry +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, ) -> None: - """Test sensor disabled by default.""" - await async_init_integration(hass) - - device_slug, serialno = slugify(MOCK_STATUS["UPSNAME"]), MOCK_STATUS["SERIALNO"] - # Test a representative integration-disabled sensor. - entry = entity_registry.async_get(f"sensor.{device_slug}_model") - assert entry.disabled - assert entry.unique_id == f"{serialno}_model" - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test enabling entity. - updated_entry = entity_registry.async_update_entity( - entry.entity_id, disabled_by=None - ) - - assert updated_entry != entry - assert updated_entry.disabled is False + """Test states of sensor.""" + with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.SENSOR]): + config_entry = await async_init_integration(hass, status=MOCK_STATUS) + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) async def test_state_update(hass: HomeAssistant) -> None: @@ -241,7 +135,7 @@ async def test_multiple_manual_update_entity(hass: HomeAssistant) -> None: async def test_sensor_unknown(hass: HomeAssistant) -> None: - """Test if our integration can properly certain sensors as unknown when it becomes so.""" + """Test if our integration can properly mark certain sensors as unknown when it becomes so.""" await async_init_integration(hass, status=MOCK_MINIMAL_STATUS) ups_mode_id = "sensor.apc_ups_mode" From 4bc5987f36d50aa52b7073a00756787c50f864b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 16:46:36 +0200 Subject: [PATCH 0460/1175] Use runtime_data in rachio (#144896) --- homeassistant/components/rachio/__init__.py | 14 ++++++-------- homeassistant/components/rachio/binary_sensor.py | 12 ++++++------ homeassistant/components/rachio/calendar.py | 8 +++----- homeassistant/components/rachio/device.py | 4 +++- homeassistant/components/rachio/switch.py | 13 +++++++------ homeassistant/components/rachio/webhooks.py | 11 +++++------ 6 files changed, 30 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index d6cdd2701b6..ab0886096cc 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -7,13 +7,12 @@ from rachiopy import Rachio from requests.exceptions import ConnectTimeout from homeassistant.components import cloud -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, DOMAIN -from .device import RachioPerson +from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS +from .device import RachioConfigEntry, RachioPerson from .webhooks import ( async_get_or_create_registered_webhook_id_and_url, async_register_webhook, @@ -25,21 +24,20 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.CALENDAR, Platform.SWITCH] -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: RachioConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): async_unregister_webhook(hass, entry) - hass.data[DOMAIN].pop(entry.entry_id) return unload_ok -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_remove_entry(hass: HomeAssistant, entry: RachioConfigEntry) -> None: """Remove a rachio config entry.""" if CONF_CLOUDHOOK_URL in entry.data: await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: RachioConfigEntry) -> bool: """Set up the Rachio config entry.""" config = entry.data @@ -97,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await base.schedule_coordinator.async_config_entry_first_refresh() # Enable platform - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person + entry.runtime_data = person async_register_webhook(hass, entry) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index be379a23cab..dbe41de2c4c 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -18,7 +17,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( - DOMAIN as DOMAIN_RACHIO, KEY_BATTERY, KEY_DETECT_FLOW, KEY_DEVICE_ID, @@ -33,7 +31,7 @@ from .const import ( STATUS_ONLINE, ) from .coordinator import RachioUpdateCoordinator -from .device import RachioIro, RachioPerson +from .device import RachioConfigEntry, RachioIro from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_COLD_REBOOT, @@ -109,7 +107,7 @@ HOSE_TIMER_BINARY_SENSOR_TYPES: tuple[RachioHoseTimerBinarySensorDescription, .. async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RachioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Rachio binary sensors.""" @@ -117,9 +115,11 @@ async def async_setup_entry( async_add_entities(entities) -def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: +def _create_entities( + hass: HomeAssistant, config_entry: RachioConfigEntry +) -> list[Entity]: entities: list[Entity] = [] - person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + person = config_entry.runtime_data entities.extend( RachioControllerBinarySensor(controller, description) for controller in person.controllers diff --git a/homeassistant/components/rachio/calendar.py b/homeassistant/components/rachio/calendar.py index 984e5ae8881..18b1b6a4d8f 100644 --- a/homeassistant/components/rachio/calendar.py +++ b/homeassistant/components/rachio/calendar.py @@ -9,7 +9,6 @@ from homeassistant.components.calendar import ( CalendarEntityFeature, CalendarEvent, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -17,7 +16,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - DOMAIN, KEY_ADDRESS, KEY_DURATION_SECONDS, KEY_ID, @@ -33,18 +31,18 @@ from .const import ( KEY_VALVE_NAME, ) from .coordinator import RachioScheduleUpdateCoordinator -from .device import RachioPerson +from .device import RachioConfigEntry _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RachioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up entry for Rachio smart hose timer calendar.""" - person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] + person = config_entry.runtime_data async_add_entities( RachioCalendarEntity(base_station.schedule_coordinator, base_station) for base_station in person.base_stations diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 179e5f5ec0d..a5dd3dba054 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -57,11 +57,13 @@ RESUME_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) STOP_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) +type RachioConfigEntry = ConfigEntry[RachioPerson] + class RachioPerson: """Represent a Rachio user.""" - def __init__(self, rachio: Rachio, config_entry: ConfigEntry) -> None: + def __init__(self, rachio: Rachio, config_entry: RachioConfigEntry) -> None: """Create an object from the provided API instance.""" # Use API token to get user ID self.rachio = rachio diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index e2c5d66b967..bfd75ad7e8b 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -9,7 +9,6 @@ from typing import Any import voluptuous as vol from homeassistant.components.switch import SwitchEntity -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 @@ -57,7 +56,7 @@ from .const import ( SLOPE_SLIGHT, SLOPE_STEEP, ) -from .device import RachioPerson +from .device import RachioConfigEntry from .entity import RachioDevice, RachioHoseTimerEntity from .webhooks import ( SUBTYPE_RAIN_DELAY_OFF, @@ -99,7 +98,7 @@ START_MULTIPLE_ZONES_SCHEMA = vol.Schema( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: RachioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Rachio switches.""" @@ -117,7 +116,7 @@ async def async_setup_entry( def start_multiple(service: ServiceCall) -> None: """Service to start multiple zones in sequence.""" zones_list = [] - person = hass.data[DOMAIN][config_entry.entry_id] + person = config_entry.runtime_data entity_id = service.data[ATTR_ENTITY_ID] duration = iter(service.data[ATTR_DURATION]) default_time = service.data[ATTR_DURATION][0] @@ -173,9 +172,11 @@ async def async_setup_entry( ) -def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: +def _create_entities( + hass: HomeAssistant, config_entry: RachioConfigEntry +) -> list[Entity]: entities: list[Entity] = [] - person: RachioPerson = hass.data[DOMAIN][config_entry.entry_id] + person = config_entry.runtime_data # Fetch the schedule once at startup # in order to avoid every zone doing it for controller in person.controllers: diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 06cd0941dcc..a88df37cb7d 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -5,7 +5,6 @@ from __future__ import annotations from aiohttp import web from homeassistant.components import cloud, webhook -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_WEBHOOK_ID, URL_API from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -21,7 +20,7 @@ from .const import ( SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) -from .device import RachioPerson +from .device import RachioConfigEntry # Device webhook values TYPE_CONTROLLER_STATUS = "DEVICE_STATUS" @@ -83,7 +82,7 @@ SIGNAL_MAP = { @callback -def async_register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: +def async_register_webhook(hass: HomeAssistant, entry: RachioConfigEntry) -> None: """Register a webhook.""" webhook_id: str = entry.data[CONF_WEBHOOK_ID] @@ -91,7 +90,7 @@ def async_register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: hass: HomeAssistant, webhook_id: str, request: web.Request ) -> web.Response: """Handle webhook calls from the server.""" - person: RachioPerson = hass.data[DOMAIN][entry.entry_id] + person = entry.runtime_data data = await request.json() try: @@ -114,14 +113,14 @@ def async_register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: @callback -def async_unregister_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: +def async_unregister_webhook(hass: HomeAssistant, entry: RachioConfigEntry) -> None: """Unregister a webhook.""" webhook_id: str = entry.data[CONF_WEBHOOK_ID] webhook.async_unregister(hass, webhook_id) async def async_get_or_create_registered_webhook_id_and_url( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: RachioConfigEntry ) -> str: """Generate webhook url.""" config = entry.data.copy() From a0f35a84ae1281147c454c5071b474562f0e8841 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 14 May 2025 16:49:30 +0200 Subject: [PATCH 0461/1175] Positioning for LCN covers (#143588) * Fix motor control function names * Add position logic for BS4 * Use helper methods from pypck * Add motor positioning to domain_data schema * Fix tests * Add motor positioning via module * Invert motor cover positions * Merge relay cover classes back into one class * Update snapshot for covers * Revert bump lcn-frontend to 0.2.4 --- homeassistant/components/lcn/const.py | 5 +- homeassistant/components/lcn/cover.py | 107 ++++++--- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lcn/schemas.py | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../lcn/fixtures/config_entry_pchk.json | 25 ++- .../components/lcn/snapshots/test_cover.ambr | 98 ++++++++ tests/components/lcn/test_cover.py | 209 +++++++++++++----- 9 files changed, 370 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index b443e05def7..d67c02ed56a 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -56,6 +56,7 @@ CONF_SCENES = "scenes" CONF_REGISTER = "register" CONF_OUTPUTS = "outputs" CONF_REVERSE_TIME = "reverse_time" +CONF_POSITIONING_MODE = "positioning_mode" DIM_MODES = ["STEPS50", "STEPS200"] @@ -235,4 +236,6 @@ TIME_UNITS = [ "D", ] -MOTOR_REVERSE_TIME = ["RT70", "RT600", "RT1200"] +MOTOR_REVERSE_TIMES = ["RT70", "RT600", "RT1200"] + +MOTOR_POSITIONING_MODES = ["NONE", "BS4", "MODULE"] diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index be713871aae..068d8f5ba11 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -6,7 +6,12 @@ from typing import Any import pypck -from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverEntity +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as DOMAIN_COVER, + CoverEntity, + CoverEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES from homeassistant.core import HomeAssistant @@ -17,6 +22,7 @@ from .const import ( ADD_ENTITIES_CALLBACKS, CONF_DOMAIN_DATA, CONF_MOTOR, + CONF_POSITIONING_MODE, CONF_REVERSE_TIME, DOMAIN, ) @@ -115,7 +121,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" state = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.device_connection.control_motors_outputs( + if not await self.device_connection.control_motor_outputs( state, self.reverse_time ): return @@ -126,7 +132,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" state = pypck.lcn_defs.MotorStateModifier.UP - if not await self.device_connection.control_motors_outputs( + if not await self.device_connection.control_motor_outputs( state, self.reverse_time ): return @@ -138,7 +144,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" state = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.device_connection.control_motors_outputs(state): + if not await self.device_connection.control_motor_outputs(state): return self._attr_is_closing = False self._attr_is_opening = False @@ -176,11 +182,25 @@ class LcnRelayCover(LcnEntity, CoverEntity): _attr_is_closing = False _attr_is_opening = False _attr_assumed_state = True + _attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + positioning_mode: pypck.lcn_defs.MotorPositioningMode def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: """Initialize the LCN cover.""" super().__init__(config, config_entry) + self.positioning_mode = pypck.lcn_defs.MotorPositioningMode( + config[CONF_DOMAIN_DATA].get( + CONF_POSITIONING_MODE, pypck.lcn_defs.MotorPositioningMode.NONE.value + ) + ) + + if self.positioning_mode != pypck.lcn_defs.MotorPositioningMode.NONE: + self._attr_supported_features |= CoverEntityFeature.SET_POSITION + self.motor = pypck.lcn_defs.MotorPort[config[CONF_DOMAIN_DATA][CONF_MOTOR]] self.motor_port_onoff = self.motor.value * 2 self.motor_port_updown = self.motor_port_onoff + 1 @@ -193,7 +213,9 @@ class LcnRelayCover(LcnEntity, CoverEntity): """Run when entity about to be added to hass.""" await super().async_added_to_hass() if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.motor) + await self.device_connection.activate_status_request_handler( + self.motor, self.positioning_mode + ) async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" @@ -203,9 +225,11 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.device_connection.control_motors_relays(states): + if not await self.device_connection.control_motor_relays( + self.motor.value, + pypck.lcn_defs.MotorStateModifier.DOWN, + self.positioning_mode, + ): return self._attr_is_opening = False self._attr_is_closing = True @@ -213,9 +237,11 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP - if not await self.device_connection.control_motors_relays(states): + if not await self.device_connection.control_motor_relays( + self.motor.value, + pypck.lcn_defs.MotorStateModifier.UP, + self.positioning_mode, + ): return self._attr_is_closed = False self._attr_is_opening = True @@ -224,26 +250,55 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 - states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.device_connection.control_motors_relays(states): + if not await self.device_connection.control_motor_relays( + self.motor.value, + pypck.lcn_defs.MotorStateModifier.STOP, + self.positioning_mode, + ): return self._attr_is_closing = False self._attr_is_opening = False self.async_write_ha_state() - def input_received(self, input_obj: InputType) -> None: - """Set cover states when LCN input object (command) is received.""" - if not isinstance(input_obj, pypck.inputs.ModStatusRelays): + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + if not await self.device_connection.control_motor_relays_position( + self.motor.value, position, mode=self.positioning_mode + ): return - - states = input_obj.states # list of boolean values (relay on/off) - if states[self.motor_port_onoff]: # motor is on - self._attr_is_opening = not states[self.motor_port_updown] # set direction - self._attr_is_closing = states[self.motor_port_updown] # set direction - else: # motor is off - self._attr_is_opening = False - self._attr_is_closing = False - self._attr_is_closed = states[self.motor_port_updown] + self._attr_is_closed = (self._attr_current_cover_position == 0) & ( + position == 0 + ) + if self._attr_current_cover_position is not None: + self._attr_is_closing = self._attr_current_cover_position > position + self._attr_is_opening = self._attr_current_cover_position < position + self._attr_current_cover_position = position self.async_write_ha_state() + + def input_received(self, input_obj: InputType) -> None: + """Set cover states when LCN input object (command) is received.""" + if isinstance(input_obj, pypck.inputs.ModStatusRelays): + self._attr_is_opening = input_obj.is_opening(self.motor.value) + self._attr_is_closing = input_obj.is_closing(self.motor.value) + + if self.positioning_mode == pypck.lcn_defs.MotorPositioningMode.NONE: + self._attr_is_closed = input_obj.is_assumed_closed(self.motor.value) + self.async_write_ha_state() + elif ( + isinstance( + input_obj, + ( + pypck.inputs.ModStatusMotorPositionBS4, + pypck.inputs.ModStatusMotorPositionModule, + ), + ) + and input_obj.motor == self.motor.value + ): + self._attr_current_cover_position = input_obj.position + if self._attr_current_cover_position in [0, 100]: + self._attr_is_opening = False + self._attr_is_closing = False + self._attr_is_closed = self._attr_current_cover_position == 0 + self.async_write_ha_state() diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index e5313eee4f3..0031cbcc947 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.8.5", "lcn-frontend==0.2.4"] + "requirements": ["pypck==0.8.6", "lcn-frontend==0.2.4"] } diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py index d90e264692c..fcc6044dd77 100644 --- a/homeassistant/components/lcn/schemas.py +++ b/homeassistant/components/lcn/schemas.py @@ -21,6 +21,7 @@ from .const import ( CONF_MOTOR, CONF_OUTPUT, CONF_OUTPUTS, + CONF_POSITIONING_MODE, CONF_REGISTER, CONF_REVERSE_TIME, CONF_SETPOINT, @@ -30,7 +31,8 @@ from .const import ( LED_PORTS, LOGICOP_PORTS, MOTOR_PORTS, - MOTOR_REVERSE_TIME, + MOTOR_POSITIONING_MODES, + MOTOR_REVERSE_TIMES, OUTPUT_PORTS, RELAY_PORTS, S0_INPUTS, @@ -68,8 +70,11 @@ DOMAIN_DATA_CLIMATE: VolDictType = { DOMAIN_DATA_COVER: VolDictType = { vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), + vol.Optional(CONF_POSITIONING_MODE, default="none"): vol.All( + vol.Upper, vol.In(MOTOR_POSITIONING_MODES) + ), vol.Optional(CONF_REVERSE_TIME, default="rt1200"): vol.All( - vol.Upper, vol.In(MOTOR_REVERSE_TIME) + vol.Upper, vol.In(MOTOR_REVERSE_TIMES) ), } diff --git a/requirements_all.txt b/requirements_all.txt index 6eda282e955..787e1831914 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2227,7 +2227,7 @@ pypalazzetti==0.1.19 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.8.5 +pypck==0.8.6 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb47548ebba..2fe65d0a93d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1821,7 +1821,7 @@ pyownet==0.10.0.post1 pypalazzetti==0.1.19 # homeassistant.components.lcn -pypck==0.8.5 +pypck==0.8.6 # homeassistant.components.pglab pypglab==0.0.5 diff --git a/tests/components/lcn/fixtures/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json index f319e37b265..5ded11d619a 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk.json +++ b/tests/components/lcn/fixtures/config_entry_pchk.json @@ -125,7 +125,30 @@ "domain": "cover", "domain_data": { "motor": "MOTOR1", - "reverse_time": "RT1200" + "reverse_time": "RT1200", + "positioning_mode": "NONE" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Relays_BS4", + "resource": "motor2", + "domain": "cover", + "domain_data": { + "motor": "MOTOR2", + "reverse_time": "RT1200", + "positioning_mode": "BS4" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Relays_Module", + "resource": "motor3", + "domain": "cover", + "domain_data": { + "motor": "MOTOR3", + "reverse_time": "RT1200", + "positioning_mode": "MODULE" } }, { diff --git a/tests/components/lcn/snapshots/test_cover.ambr b/tests/components/lcn/snapshots/test_cover.ambr index 3e9c4ee72eb..4d1356e3c92 100644 --- a/tests/components/lcn/snapshots/test_cover.ambr +++ b/tests/components/lcn/snapshots/test_cover.ambr @@ -97,3 +97,101 @@ 'state': 'open', }) # --- +# name: test_setup_lcn_cover[cover.cover_relays_bs4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.cover_relays_bs4', + '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': 'Cover_Relays_BS4', + 'platform': 'lcn', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'lcn/config_entry_pchk_json-m000007-motor2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_lcn_cover[cover.cover_relays_bs4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'friendly_name': 'Cover_Relays_BS4', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.cover_relays_bs4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- +# name: test_setup_lcn_cover[cover.cover_relays_module-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.cover_relays_module', + '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': 'Cover_Relays_Module', + 'platform': 'lcn', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'lcn/config_entry_pchk_json-m000007-motor3', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup_lcn_cover[cover.cover_relays_module-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'friendly_name': 'Cover_Relays_Module', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.cover_relays_module', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index ff4311b6687..f2dd71757c9 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -2,17 +2,29 @@ from unittest.mock import patch -from pypck.inputs import ModStatusOutput, ModStatusRelays +from pypck.inputs import ( + ModStatusMotorPositionBS4, + ModStatusMotorPositionModule, + ModStatusOutput, + ModStatusRelays, +) from pypck.lcn_addr import LcnAddr -from pypck.lcn_defs import MotorReverseTime, MotorStateModifier +from pypck.lcn_defs import MotorPositioningMode, MotorReverseTime, MotorStateModifier +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.cover import DOMAIN as DOMAIN_COVER, CoverState +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as DOMAIN_COVER, + CoverState, +) from homeassistant.components.lcn.helpers import get_device_connection from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, STATE_UNAVAILABLE, Platform, @@ -26,6 +38,8 @@ from tests.common import snapshot_platform COVER_OUTPUTS = "cover.cover_outputs" COVER_RELAYS = "cover.cover_relays" +COVER_RELAYS_BS4 = "cover.cover_relays_bs4" +COVER_RELAYS_MODULE = "cover.cover_relays_MODULE" async def test_setup_lcn_cover( @@ -46,13 +60,13 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_outputs" - ) as control_motors_outputs: + MockModuleConnection, "control_motor_outputs" + ) as control_motor_outputs: state = hass.states.get(COVER_OUTPUTS) state.state = CoverState.CLOSED # command failed - control_motors_outputs.return_value = False + control_motor_outputs.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -61,7 +75,7 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_outputs.assert_awaited_with( + control_motor_outputs.assert_awaited_with( MotorStateModifier.UP, MotorReverseTime.RT1200 ) @@ -70,8 +84,8 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None assert state.state != CoverState.OPENING # command success - control_motors_outputs.reset_mock(return_value=True) - control_motors_outputs.return_value = True + control_motor_outputs.reset_mock(return_value=True) + control_motor_outputs.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -80,7 +94,7 @@ async def test_outputs_open(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_outputs.assert_awaited_with( + control_motor_outputs.assert_awaited_with( MotorStateModifier.UP, MotorReverseTime.RT1200 ) @@ -94,13 +108,13 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_outputs" - ) as control_motors_outputs: + MockModuleConnection, "control_motor_outputs" + ) as control_motor_outputs: state = hass.states.get(COVER_OUTPUTS) state.state = CoverState.OPEN # command failed - control_motors_outputs.return_value = False + control_motor_outputs.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -109,7 +123,7 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non blocking=True, ) - control_motors_outputs.assert_awaited_with( + control_motor_outputs.assert_awaited_with( MotorStateModifier.DOWN, MotorReverseTime.RT1200 ) @@ -118,8 +132,8 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non assert state.state != CoverState.CLOSING # command success - control_motors_outputs.reset_mock(return_value=True) - control_motors_outputs.return_value = True + control_motor_outputs.reset_mock(return_value=True) + control_motor_outputs.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -128,7 +142,7 @@ async def test_outputs_close(hass: HomeAssistant, entry: MockConfigEntry) -> Non blocking=True, ) - control_motors_outputs.assert_awaited_with( + control_motor_outputs.assert_awaited_with( MotorStateModifier.DOWN, MotorReverseTime.RT1200 ) @@ -142,13 +156,13 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_outputs" - ) as control_motors_outputs: + MockModuleConnection, "control_motor_outputs" + ) as control_motor_outputs: state = hass.states.get(COVER_OUTPUTS) state.state = CoverState.CLOSING # command failed - control_motors_outputs.return_value = False + control_motor_outputs.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -157,15 +171,15 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + control_motor_outputs.assert_awaited_with(MotorStateModifier.STOP) state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == CoverState.CLOSING # command success - control_motors_outputs.reset_mock(return_value=True) - control_motors_outputs.return_value = True + control_motor_outputs.reset_mock(return_value=True) + control_motor_outputs.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -174,7 +188,7 @@ async def test_outputs_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + control_motor_outputs.assert_awaited_with(MotorStateModifier.STOP) state = hass.states.get(COVER_OUTPUTS) assert state is not None @@ -186,16 +200,13 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_relays" - ) as control_motors_relays: - states = [MotorStateModifier.NOCHANGE] * 4 - states[0] = MotorStateModifier.UP - + MockModuleConnection, "control_motor_relays" + ) as control_motor_relays: state = hass.states.get(COVER_RELAYS) state.state = CoverState.CLOSED # command failed - control_motors_relays.return_value = False + control_motor_relays.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -204,15 +215,17 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.UP, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state != CoverState.OPENING # command success - control_motors_relays.reset_mock(return_value=True) - control_motors_relays.return_value = True + control_motor_relays.reset_mock(return_value=True) + control_motor_relays.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -221,7 +234,9 @@ async def test_relays_open(hass: HomeAssistant, entry: MockConfigEntry) -> None: blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.UP, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None @@ -233,16 +248,13 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_relays" - ) as control_motors_relays: - states = [MotorStateModifier.NOCHANGE] * 4 - states[0] = MotorStateModifier.DOWN - + MockModuleConnection, "control_motor_relays" + ) as control_motor_relays: state = hass.states.get(COVER_RELAYS) state.state = CoverState.OPEN # command failed - control_motors_relays.return_value = False + control_motor_relays.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -251,15 +263,17 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.DOWN, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state != CoverState.CLOSING # command success - control_motors_relays.reset_mock(return_value=True) - control_motors_relays.return_value = True + control_motor_relays.reset_mock(return_value=True) + control_motor_relays.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -268,7 +282,9 @@ async def test_relays_close(hass: HomeAssistant, entry: MockConfigEntry) -> None blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.DOWN, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None @@ -280,16 +296,13 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: await init_integration(hass, entry) with patch.object( - MockModuleConnection, "control_motors_relays" - ) as control_motors_relays: - states = [MotorStateModifier.NOCHANGE] * 4 - states[0] = MotorStateModifier.STOP - + MockModuleConnection, "control_motor_relays" + ) as control_motor_relays: state = hass.states.get(COVER_RELAYS) state.state = CoverState.CLOSING # command failed - control_motors_relays.return_value = False + control_motor_relays.return_value = False await hass.services.async_call( DOMAIN_COVER, @@ -298,15 +311,17 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.STOP, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == CoverState.CLOSING # command success - control_motors_relays.reset_mock(return_value=True) - control_motors_relays.return_value = True + control_motor_relays.reset_mock(return_value=True) + control_motor_relays.return_value = True await hass.services.async_call( DOMAIN_COVER, @@ -315,13 +330,74 @@ async def test_relays_stop(hass: HomeAssistant, entry: MockConfigEntry) -> None: blocking=True, ) - control_motors_relays.assert_awaited_with(states) + control_motor_relays.assert_awaited_with( + 0, MotorStateModifier.STOP, MotorPositioningMode.NONE + ) state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state not in (CoverState.CLOSING, CoverState.OPENING) +@pytest.mark.parametrize( + ("entity_id", "motor", "positioning_mode"), + [ + (COVER_RELAYS_BS4, 1, MotorPositioningMode.BS4), + (COVER_RELAYS_MODULE, 2, MotorPositioningMode.MODULE), + ], +) +async def test_relays_set_position( + hass: HomeAssistant, + entry: MockConfigEntry, + entity_id: str, + motor: int, + positioning_mode: MotorPositioningMode, +) -> None: + """Test the relays cover moves to position.""" + await init_integration(hass, entry) + + with patch.object( + MockModuleConnection, "control_motor_relays_position" + ) as control_motor_relays_position: + state = hass.states.get(entity_id) + state.state = CoverState.CLOSED + + # command failed + control_motor_relays_position.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, + blocking=True, + ) + + control_motor_relays_position.assert_awaited_with( + motor, 50, mode=positioning_mode + ) + + state = hass.states.get(entity_id) + assert state.state == CoverState.CLOSED + + # command success + control_motor_relays_position.reset_mock(return_value=True) + control_motor_relays_position.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50}, + blocking=True, + ) + + control_motor_relays_position.assert_awaited_with( + motor, 50, mode=positioning_mode + ) + + state = hass.states.get(entity_id) + assert state.state == CoverState.OPEN + + async def test_pushed_outputs_status_change( hass: HomeAssistant, entry: MockConfigEntry ) -> None: @@ -372,8 +448,9 @@ async def test_pushed_relays_status_change( address = LcnAddr(0, 7, False) states = [False] * 8 - state = hass.states.get(COVER_RELAYS) - state.state = CoverState.CLOSED + for entity_id in (COVER_RELAYS, COVER_RELAYS_BS4, COVER_RELAYS_MODULE): + state = hass.states.get(entity_id) + state.state = CoverState.CLOSED # push status "open" states[0:2] = [True, False] @@ -405,6 +482,26 @@ async def test_pushed_relays_status_change( assert state is not None assert state.state == CoverState.CLOSING + # push status "set position" via BS4 + inp = ModStatusMotorPositionBS4(address, 1, 50) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(COVER_RELAYS_BS4) + assert state is not None + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + # push status "set position" via MODULE + inp = ModStatusMotorPositionModule(address, 2, 75) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(COVER_RELAYS_MODULE) + assert state is not None + assert state.state == CoverState.OPEN + assert state.attributes[ATTR_CURRENT_POSITION] == 75 + async def test_unload_config_entry(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test the cover is removed when the config entry is unloaded.""" From 2d0c1fac24ff46f1cebf0460330f2a06f85675f6 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 14 May 2025 17:05:45 +0200 Subject: [PATCH 0462/1175] Fix "tunneling" spelling in KNX (#144895) --- homeassistant/components/knx/config_flow.py | 20 ++++++++++---------- homeassistant/components/knx/const.py | 6 +++--- homeassistant/components/knx/strings.json | 4 ++-- tests/components/knx/test_config_flow.py | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index eda160cd1a6..14a9016bcb9 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -84,9 +84,9 @@ CONF_KEYRING_FILE: Final = "knxkeys_file" CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type" CONF_KNX_TUNNELING_TYPE_LABELS: Final = { - CONF_KNX_TUNNELING: "UDP (Tunnelling v1)", - CONF_KNX_TUNNELING_TCP: "TCP (Tunnelling v2)", - CONF_KNX_TUNNELING_TCP_SECURE: "Secure Tunnelling (TCP)", + CONF_KNX_TUNNELING: "UDP (Tunneling v1)", + CONF_KNX_TUNNELING_TCP: "TCP (Tunneling v2)", + CONF_KNX_TUNNELING_TCP_SECURE: "Secure Tunneling (TCP)", } OPTION_MANUAL_TUNNEL: Final = "Manual" @@ -393,7 +393,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): except (vol.Invalid, XKNXException): errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" - selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE] + selected_tunneling_type = user_input[CONF_KNX_TUNNELING_TYPE] if not errors: try: self._selected_tunnel = await request_description( @@ -406,16 +406,16 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): errors["base"] = "cannot_connect" else: if bool(self._selected_tunnel.tunnelling_requires_secure) is not ( - selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE + selected_tunneling_type == CONF_KNX_TUNNELING_TCP_SECURE ) or ( - selected_tunnelling_type == CONF_KNX_TUNNELING_TCP + selected_tunneling_type == CONF_KNX_TUNNELING_TCP and not self._selected_tunnel.supports_tunnelling_tcp ): errors[CONF_KNX_TUNNELING_TYPE] = "unsupported_tunnel_type" if not errors: self.new_entry_data = KNXConfigEntryData( - connection_type=selected_tunnelling_type, + connection_type=selected_tunneling_type, host=_host, port=user_input[CONF_PORT], route_back=user_input[CONF_KNX_ROUTE_BACK], @@ -426,11 +426,11 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): tunnel_endpoint_ia=None, ) - if selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE: + if selected_tunneling_type == CONF_KNX_TUNNELING_TCP_SECURE: return await self.async_step_secure_key_source_menu_tunnel() self.new_title = ( "Tunneling " - f"{'UDP' if selected_tunnelling_type == CONF_KNX_TUNNELING else 'TCP'} " + f"{'UDP' if selected_tunneling_type == CONF_KNX_TUNNELING else 'TCP'} " f"@ {_host}" ) return self.finish_flow() @@ -497,7 +497,7 @@ class KNXCommonFlow(ABC, ConfigEntryBaseFlow): async def async_step_secure_tunnel_manual( self, user_input: dict | None = None ) -> ConfigFlowResult: - """Configure ip secure tunnelling manually.""" + """Configure ip secure tunneling manually.""" errors: dict = {} if user_input is not None: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index c0c3b9ec2e6..3ce79b4ca7a 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -104,9 +104,9 @@ class KNXConfigEntryData(TypedDict, total=False): multicast_group: str multicast_port: int route_back: bool # not required - host: str # only required for tunnelling - port: int # only required for tunnelling - tunnel_endpoint_ia: str | None # tunnelling only - not required (use get()) + host: str # only required for tunneling + port: int # only required for tunneling + tunnel_endpoint_ia: str | None # tunneling only - not required (use get()) # KNX secure user_id: int | None # not required user_password: str | None # not required diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 737cc2d8b2d..77228ea34d9 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -85,7 +85,7 @@ } }, "secure_tunnel_manual": { - "title": "Secure tunnelling", + "title": "Secure tunneling", "description": "Please enter your IP Secure information.", "data": { "user_id": "User ID", @@ -140,7 +140,7 @@ "keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "no_router_discovered": "No KNXnet/IP router was discovered on the network.", "no_tunnel_discovered": "Could not find a KNX tunneling server on your network.", - "unsupported_tunnel_type": "Selected tunnelling type not supported by gateway." + "unsupported_tunnel_type": "Selected tunneling type not supported by gateway." } }, "options": { diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 3e4c9408542..6ebe8192f69 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1033,7 +1033,7 @@ async def test_form_with_automatic_connection_handling( async def _get_menu_step_secure_tunnel(hass: HomeAssistant) -> FlowResult: - """Return flow in secure_tunnelling menu step.""" + """Return flow in secure_tunnel menu step.""" gateway = _gateway_descriptor( "192.168.0.1", 3675, @@ -1082,7 +1082,7 @@ async def test_get_secure_menu_step_manual_tunnelling( request_description_mock: MagicMock, hass: HomeAssistant, ) -> None: - """Test flow reaches secure_tunnellinn menu step from manual tunnelling configuration.""" + """Test flow reaches secure_tunnellinn menu step from manual tunneling configuration.""" gateway = _gateway_descriptor( "192.168.0.1", 3675, @@ -1129,7 +1129,7 @@ async def test_get_secure_menu_step_manual_tunnelling( async def test_configure_secure_tunnel_manual(hass: HomeAssistant, knx_setup) -> None: - """Test configure tunnelling secure keys manually.""" + """Test configure tunneling secure keys manually.""" menu_step = await _get_menu_step_secure_tunnel(hass) result = await hass.config_entries.flow.async_configure( From 43b1dd64a73e83a60102c9e8143d75ed30be7e95 Mon Sep 17 00:00:00 2001 From: "Glenn Vandeuren (aka Iondependent)" Date: Wed, 14 May 2025 17:13:06 +0200 Subject: [PATCH 0463/1175] Handle unit conversion in lib for niko_home_control (#141837) * handle unit conversion in lib * bump lib * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/niko_home_control/light.py | 6 +++--- homeassistant/components/niko_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/niko_home_control/conftest.py | 2 +- tests/components/niko_home_control/test_light.py | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index b0a2d12b004..853fae342f4 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -110,11 +110,11 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity): if action.is_dimmable: self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_brightness = round(action.state * 2.55) + self._attr_brightness = action.state async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" - await self._action.turn_on(round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55)) + await self._action.turn_on(kwargs.get(ATTR_BRIGHTNESS, 255)) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" @@ -125,4 +125,4 @@ class NikoHomeControlLight(NikoHomeControlEntity, LightEntity): state = self._action.state self._attr_is_on = state > 0 if brightness_supported(self.supported_color_modes): - self._attr_brightness = round(state * 2.55) + self._attr_brightness = state diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 83fca0ca2d6..1193d33d435 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "iot_class": "local_push", "loggers": ["nikohomecontrol"], - "requirements": ["nhc==0.4.10"] + "requirements": ["nhc==0.4.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 787e1831914..d6fb1ae7bfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1505,7 +1505,7 @@ nextcord==2.6.0 nextdns==4.0.0 # homeassistant.components.niko_home_control -nhc==0.4.10 +nhc==0.4.12 # homeassistant.components.nibe_heatpump nibe==2.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fe65d0a93d..a4e35701a74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ nextcord==2.6.0 nextdns==4.0.0 # homeassistant.components.niko_home_control -nhc==0.4.10 +nhc==0.4.12 # homeassistant.components.nibe_heatpump nibe==2.17.0 diff --git a/tests/components/niko_home_control/conftest.py b/tests/components/niko_home_control/conftest.py index 130baf72228..35260b387de 100644 --- a/tests/components/niko_home_control/conftest.py +++ b/tests/components/niko_home_control/conftest.py @@ -45,7 +45,7 @@ def dimmable_light() -> NHCLight: mock.is_dimmable = True mock.name = "dimmable light" mock.suggested_area = "room" - mock.state = 100 + mock.state = 255 return mock diff --git a/tests/components/niko_home_control/test_light.py b/tests/components/niko_home_control/test_light.py index 865e1303cb0..a11f846bba6 100644 --- a/tests/components/niko_home_control/test_light.py +++ b/tests/components/niko_home_control/test_light.py @@ -42,11 +42,11 @@ async def test_entities( @pytest.mark.parametrize( ("light_id", "data", "set_brightness"), [ - (0, {ATTR_ENTITY_ID: "light.light"}, 100), + (0, {ATTR_ENTITY_ID: "light.light"}, 255), ( 1, {ATTR_ENTITY_ID: "light.dimmable_light", ATTR_BRIGHTNESS: 50}, - 20, + 50, ), ], ) @@ -121,8 +121,8 @@ async def test_updating( assert hass.states.get("light.dimmable_light").state == STATE_ON assert hass.states.get("light.dimmable_light").attributes[ATTR_BRIGHTNESS] == 255 - dimmable_light.state = 80 - await find_update_callback(mock_niko_home_control_connection, 2)(80) + dimmable_light.state = 204 + await find_update_callback(mock_niko_home_control_connection, 2)(204) await hass.async_block_till_done() assert hass.states.get("light.dimmable_light").state == STATE_ON From 49b7559b1ffd85f9be0a288603eb5e4230d8d1ba Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 14 May 2025 17:14:57 +0200 Subject: [PATCH 0464/1175] Fix snapshots in APC (#144901) --- tests/components/apcupsd/snapshots/test_sensor.ambr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr index 6409f205d4f..1be83198dcc 100644 --- a/tests/components/apcupsd/snapshots/test_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -707,7 +707,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Last self test', + 'original_name': 'Last self-test', 'platform': 'apcupsd', 'previous_unique_id': None, 'supported_features': 0, @@ -719,7 +719,7 @@ # name: test_sensor[sensor.myups_last_self_test-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'MyUPS Last self test', + 'friendly_name': 'MyUPS Last self-test', }), 'context': , 'entity_id': 'sensor.myups_last_self_test', @@ -1192,7 +1192,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Self test interval', + 'original_name': 'Self-test interval', 'platform': 'apcupsd', 'previous_unique_id': None, 'supported_features': 0, @@ -1204,7 +1204,7 @@ # name: test_sensor[sensor.myups_self_test_interval-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'MyUPS Self test interval', + 'friendly_name': 'MyUPS Self-test interval', 'unit_of_measurement': , }), 'context': , @@ -1240,7 +1240,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Self test result', + 'original_name': 'Self-test result', 'platform': 'apcupsd', 'previous_unique_id': None, 'supported_features': 0, @@ -1252,7 +1252,7 @@ # name: test_sensor[sensor.myups_self_test_result-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'MyUPS Self test result', + 'friendly_name': 'MyUPS Self-test result', }), 'context': , 'entity_id': 'sensor.myups_self_test_result', From d44a34ce1ebf0f1d298675ebcee5c61a83d6de22 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 14 May 2025 17:24:19 +0200 Subject: [PATCH 0465/1175] Refactor DeviceAutomationTriggerProtocol (#144888) --- .../components/device_automation/trigger.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index cc8c4d4d52e..071b8236086 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -8,11 +8,7 @@ import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers.trigger import ( - TriggerActionType, - TriggerInfo, - TriggerProtocol, -) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ( @@ -25,13 +21,28 @@ from .helpers import async_validate_device_automation_config TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol): +class DeviceAutomationTriggerProtocol(Protocol): """Define the format of device_trigger modules. - Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config - from TriggerProtocol. + Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config. """ + TRIGGER_SCHEMA: vol.Schema + + async def async_validate_trigger_config( + self, hass: HomeAssistant, config: ConfigType + ) -> ConfigType: + """Validate config.""" + + async def async_attach_trigger( + self, + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, + ) -> CALLBACK_TYPE: + """Attach a trigger.""" + async def async_get_trigger_capabilities( self, hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: From 7963665c40aef60f10e99a80a868077fb9321917 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Thu, 15 May 2025 00:58:25 +0900 Subject: [PATCH 0466/1175] Add fan for ventilator (#142444) Add ventilator device Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/fan.py | 121 +++++++++++++++++------ 1 file changed, 91 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/lg_thinq/fan.py b/homeassistant/components/lg_thinq/fan.py index 6d07c98744a..7d20be68b01 100644 --- a/homeassistant/components/lg_thinq/fan.py +++ b/homeassistant/components/lg_thinq/fan.py @@ -2,11 +2,13 @@ from __future__ import annotations +from dataclasses import dataclass import logging from typing import Any from thinqconnect import DeviceType -from thinqconnect.integration import ExtendedProperty +from thinqconnect.devices.const import Property as ThinQProperty +from thinqconnect.integration import ActiveMode from homeassistant.components.fan import ( FanEntity, @@ -24,16 +26,35 @@ from . import ThinqConfigEntry from .coordinator import DeviceDataUpdateCoordinator from .entity import ThinQEntity -DEVICE_TYPE_FAN_MAP: dict[DeviceType, tuple[FanEntityDescription, ...]] = { + +@dataclass(frozen=True, kw_only=True) +class ThinQFanEntityDescription(FanEntityDescription): + """Describes ThinQ fan entity.""" + + operation_key: str + preset_modes: list[str] | None = None + + +DEVICE_TYPE_FAN_MAP: dict[DeviceType, tuple[ThinQFanEntityDescription, ...]] = { DeviceType.CEILING_FAN: ( - FanEntityDescription( - key=ExtendedProperty.FAN, + ThinQFanEntityDescription( + key=ThinQProperty.WIND_STRENGTH, name=None, + operation_key=ThinQProperty.CEILING_FAN_OPERATION_MODE, + ), + ), + DeviceType.VENTILATOR: ( + ThinQFanEntityDescription( + key=ThinQProperty.WIND_STRENGTH, + name=None, + translation_key=ThinQProperty.WIND_STRENGTH, + operation_key=ThinQProperty.VENTILATOR_OPERATION_MODE, + preset_modes=["auto"], ), ), } -FOUR_STEP_SPEEDS = ["low", "mid", "high", "turbo"] +ORDERED_NAMED_FAN_SPEEDS = ["low", "mid", "high", "turbo", "power"] _LOGGER = logging.getLogger(__name__) @@ -52,7 +73,9 @@ async def async_setup_entry( for description in descriptions: entities.extend( ThinQFanEntity(coordinator, description, property_id) - for property_id in coordinator.api.get_active_idx(description.key) + for property_id in coordinator.api.get_active_idx( + description.key, ActiveMode.READ_WRITE + ) ) if entities: @@ -65,48 +88,76 @@ class ThinQFanEntity(ThinQEntity, FanEntity): def __init__( self, coordinator: DeviceDataUpdateCoordinator, - entity_description: FanEntityDescription, + entity_description: ThinQFanEntityDescription, property_id: str, ) -> None: """Initialize fan platform.""" super().__init__(coordinator, entity_description, property_id) - self._ordered_named_fan_speeds = [] + self._ordered_named_fan_speeds = ORDERED_NAMED_FAN_SPEEDS.copy() self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF ) - if (fan_modes := self.data.fan_modes) is not None: - self._attr_speed_count = len(fan_modes) - if self.speed_count == 4: - self._ordered_named_fan_speeds = FOUR_STEP_SPEEDS + self._attr_preset_modes = [] + for option in self.data.options: + if ( + entity_description.preset_modes is not None + and option in entity_description.preset_modes + ): + self._attr_supported_features |= FanEntityFeature.PRESET_MODE + self._attr_preset_modes.append(option) + else: + for ordered_step in ORDERED_NAMED_FAN_SPEEDS: + if ( + ordered_step in self._ordered_named_fan_speeds + and ordered_step not in self.data.options + ): + self._ordered_named_fan_speeds.remove(ordered_step) + self._attr_speed_count = len(self._ordered_named_fan_speeds) + self._operation_id = entity_description.operation_key def _update_status(self) -> None: """Update status itself.""" super()._update_status() # Update power on state. - self._attr_is_on = self.data.is_on + self._attr_is_on = _is_on = self.coordinator.data[self._operation_id].is_on # Update fan speed. - if ( - self.data.is_on - and (mode := self.data.fan_mode) in self._ordered_named_fan_speeds - ): - self._attr_percentage = ordered_list_item_to_percentage( - self._ordered_named_fan_speeds, mode - ) + if _is_on and (mode := self.data.value) is not None: + if self.preset_modes is not None and mode in self.preset_modes: + self._attr_preset_mode = mode + self._attr_percentage = 0 + elif mode in self._ordered_named_fan_speeds: + self._attr_percentage = ordered_list_item_to_percentage( + self._ordered_named_fan_speeds, mode + ) + self._attr_preset_mode = None else: + self._attr_preset_mode = None self._attr_percentage = 0 _LOGGER.debug( - "[%s:%s] update status: %s -> %s (percentage=%s)", + "[%s:%s] update status: is_on=%s, percentage=%s, preset_mode=%s", self.coordinator.device_name, self.property_id, - self.data.is_on, - self.is_on, + _is_on, self.percentage, + self.preset_mode, + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + _LOGGER.debug( + "[%s:%s] async_set_preset_mode. preset_mode=%s", + self.coordinator.device_name, + self.property_id, + preset_mode, + ) + await self.async_call_api( + self.coordinator.api.post(self.property_id, preset_mode) ) async def async_set_percentage(self, percentage: int) -> None: @@ -129,9 +180,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity): percentage, value, ) - await self.async_call_api( - self.coordinator.api.async_set_fan_mode(self.property_id, value) - ) + await self.async_call_api(self.coordinator.api.post(self.property_id, value)) async def async_turn_on( self, @@ -141,13 +190,25 @@ class ThinQFanEntity(ThinQEntity, FanEntity): ) -> None: """Turn on the fan.""" _LOGGER.debug( - "[%s:%s] async_turn_on", self.coordinator.device_name, self.property_id + "[%s:%s] async_turn_on percentage=%s, preset_mode=%s, kwargs=%s", + self.coordinator.device_name, + self._operation_id, + percentage, + preset_mode, + kwargs, + ) + await self.async_call_api( + self.coordinator.api.async_turn_on(self._operation_id) ) - await self.async_call_api(self.coordinator.api.async_turn_on(self.property_id)) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" _LOGGER.debug( - "[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id + "[%s:%s] async_turn_off kwargs=%s", + self.coordinator.device_name, + self._operation_id, + kwargs, + ) + await self.async_call_api( + self.coordinator.api.async_turn_off(self._operation_id) ) - await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id)) From 9d451b63585d748730c89968a4d8bde4e6254a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Wed, 14 May 2025 18:06:21 +0200 Subject: [PATCH 0467/1175] Add support for identify buttons to WMS WebControl pro (#143339) * Remove _attr_name = None from generic base class * Add support for identify buttons to WMS WebControl pro * Fix PERF401 as suggested by joostlek * Fix fixture name after rebase * Split test --------- Co-authored-by: Joostlek --- homeassistant/components/wmspro/__init__.py | 7 +- homeassistant/components/wmspro/button.py | 40 +++++++++++ homeassistant/components/wmspro/cover.py | 1 + homeassistant/components/wmspro/entity.py | 1 - homeassistant/components/wmspro/light.py | 1 + .../wmspro/snapshots/test_button.ambr | 16 +++++ tests/components/wmspro/test_button.py | 66 +++++++++++++++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/wmspro/button.py create mode 100644 tests/components/wmspro/snapshots/test_button.ambr create mode 100644 tests/components/wmspro/test_button.py diff --git a/homeassistant/components/wmspro/__init__.py b/homeassistant/components/wmspro/__init__.py index 37bf1495a56..ebfdf5b8b34 100644 --- a/homeassistant/components/wmspro/__init__.py +++ b/homeassistant/components/wmspro/__init__.py @@ -15,7 +15,12 @@ from homeassistant.helpers.typing import UNDEFINED from .const import DOMAIN, MANUFACTURER -PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE] +PLATFORMS: list[Platform] = [ + Platform.BUTTON, + Platform.COVER, + Platform.LIGHT, + Platform.SCENE, +] type WebControlProConfigEntry = ConfigEntry[WebControlPro] diff --git a/homeassistant/components/wmspro/button.py b/homeassistant/components/wmspro/button.py new file mode 100644 index 00000000000..f1ab0489b86 --- /dev/null +++ b/homeassistant/components/wmspro/button.py @@ -0,0 +1,40 @@ +"""Identify support for WMS WebControl pro.""" + +from __future__ import annotations + +from wmspro.const import WMS_WebControl_pro_API_actionDescription + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import WebControlProConfigEntry +from .entity import WebControlProGenericEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: WebControlProConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the WMS based identify buttons from a config entry.""" + hub = config_entry.runtime_data + + entities: list[WebControlProGenericEntity] = [ + WebControlProIdentifyButton(config_entry.entry_id, dest) + for dest in hub.dests.values() + if dest.action(WMS_WebControl_pro_API_actionDescription.Identify) + ] + + async_add_entities(entities) + + +class WebControlProIdentifyButton(WebControlProGenericEntity, ButtonEntity): + """Representation of a WMS based identify button.""" + + _attr_device_class = ButtonDeviceClass.IDENTIFY + + async def async_press(self) -> None: + """Handle the button press.""" + action = self._dest.action(WMS_WebControl_pro_API_actionDescription.Identify) + await action() diff --git a/homeassistant/components/wmspro/cover.py b/homeassistant/components/wmspro/cover.py index d46ffa6dab6..97ce540dc0b 100644 --- a/homeassistant/components/wmspro/cover.py +++ b/homeassistant/components/wmspro/cover.py @@ -45,6 +45,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): """Base representation of a WMS based cover.""" _drive_action_desc: WMS_WebControl_pro_API_actionDescription + _attr_name = None @property def current_cover_position(self) -> int | None: diff --git a/homeassistant/components/wmspro/entity.py b/homeassistant/components/wmspro/entity.py index 0bbbc69a294..758a89b7ed8 100644 --- a/homeassistant/components/wmspro/entity.py +++ b/homeassistant/components/wmspro/entity.py @@ -15,7 +15,6 @@ class WebControlProGenericEntity(Entity): _attr_attribution = ATTRIBUTION _attr_has_entity_name = True - _attr_name = None def __init__(self, config_entry_id: str, dest: Destination) -> None: """Initialize the entity with destination channel.""" diff --git a/homeassistant/components/wmspro/light.py b/homeassistant/components/wmspro/light.py index d181beb1eaa..754e537c34a 100644 --- a/homeassistant/components/wmspro/light.py +++ b/homeassistant/components/wmspro/light.py @@ -42,6 +42,7 @@ class WebControlProLight(WebControlProGenericEntity, LightEntity): """Representation of a WMS based light.""" _attr_color_mode = ColorMode.ONOFF + _attr_name = None _attr_supported_color_modes = {ColorMode.ONOFF} @property diff --git a/tests/components/wmspro/snapshots/test_button.ambr b/tests/components/wmspro/snapshots/test_button.ambr new file mode 100644 index 00000000000..431a92c26d6 --- /dev/null +++ b/tests/components/wmspro/snapshots/test_button.ambr @@ -0,0 +1,16 @@ +# serializer version: 1 +# name: test_button_update + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by WMS WebControl pro API', + 'device_class': 'identify', + 'friendly_name': 'Markise Identify', + }), + 'context': , + 'entity_id': 'button.markise_identify', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/wmspro/test_button.py b/tests/components/wmspro/test_button.py new file mode 100644 index 00000000000..2894399f9f9 --- /dev/null +++ b/tests/components/wmspro/test_button.py @@ -0,0 +1,66 @@ +"""Test the wmspro button support.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import setup_config_entry + +from tests.common import MockConfigEntry + + +async def test_button_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hub_ping: AsyncMock, + mock_hub_configuration_prod_awning_dimmer: AsyncMock, + mock_hub_status_prod_awning: AsyncMock, + mock_action_call: AsyncMock, + snapshot: SnapshotAssertion, +) -> None: + """Test that a button entity is created and updated correctly.""" + assert await setup_config_entry(hass, mock_config_entry) + assert len(mock_hub_ping.mock_calls) == 1 + assert len(mock_hub_configuration_prod_awning_dimmer.mock_calls) == 1 + assert len(mock_hub_status_prod_awning.mock_calls) == 2 + + entity = hass.states.get("button.markise_identify") + assert entity is not None + assert entity == snapshot + + +async def test_button_press( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hub_ping: AsyncMock, + mock_hub_configuration_prod_awning_dimmer: AsyncMock, + mock_hub_status_prod_awning: AsyncMock, + mock_action_call: AsyncMock, +) -> None: + """Test that a button entity is pressed correctly.""" + + assert await setup_config_entry(hass, mock_config_entry) + + with patch( + "wmspro.destination.Destination.refresh", + return_value=True, + ): + before = len(mock_hub_status_prod_awning.mock_calls) + entity = hass.states.get("button.markise_identify") + before_state = entity.state + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity.entity_id}, + blocking=True, + ) + + entity = hass.states.get("button.markise_identify") + assert entity is not None + assert entity.state != before_state + assert len(mock_hub_status_prod_awning.mock_calls) == before From 8004c6605b301f5c3e8b745bcf65712d88ffffb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 14 May 2025 19:25:01 +0200 Subject: [PATCH 0468/1175] Update Tibber lib 0.31.2 (#144908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 3a3a772a934..43cbd79afef 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tibber", "iot_class": "cloud_polling", "loggers": ["tibber"], - "requirements": ["pyTibber==0.30.8"] + "requirements": ["pyTibber==0.31.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index d6fb1ae7bfa..c801d6b137e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1801,7 +1801,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.31.2 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4e35701a74..a3b594404bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1488,7 +1488,7 @@ pyHomee==1.2.8 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.30.8 +pyTibber==0.31.2 # homeassistant.components.dlink pyW215==0.7.0 From 4b7650f2d237a91f20bc365f2e1c25c67e1aa3a7 Mon Sep 17 00:00:00 2001 From: Nick Kuiper <65495045+NickKoepr@users.noreply.github.com> Date: Wed, 14 May 2025 19:37:16 +0200 Subject: [PATCH 0469/1175] Add buttons to Blue current integration (#143964) * Add buttons to Blue current integration * Apply feedback * Changed configEntry to use the BlueCurrentConfigEntry. The connector is now accessed via the entry instead of hass.data. * Changed test_buttons_created test to use the snapshot_platform function. Also removed the entry.unique_id check in the test_charge_point_buttons function because this is not needed anymore, according to https://github.com/home-assistant/core/pull/114000#discussion_r1627201872 * Applied requested changes. Changes requested by joostlek. * Moved has_value from BlueCurrentEntity to class level. This value was still inside the __init__ function, so the value was not overwritten by the ChargePointButton. --------- Co-authored-by: Floris272 --- .../components/blue_current/__init__.py | 2 +- .../components/blue_current/button.py | 89 +++++++++++ .../components/blue_current/entity.py | 5 +- .../components/blue_current/icons.json | 11 ++ .../components/blue_current/strings.json | 11 ++ .../blue_current/snapshots/test_button.ambr | 144 ++++++++++++++++++ tests/components/blue_current/test_button.py | 51 +++++++ 7 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/blue_current/button.py create mode 100644 tests/components/blue_current/snapshots/test_button.ambr create mode 100644 tests/components/blue_current/test_button.py diff --git a/homeassistant/components/blue_current/__init__.py b/homeassistant/components/blue_current/__init__.py index 6d0ccd7b6db..775ca16a12a 100644 --- a/homeassistant/components/blue_current/__init__.py +++ b/homeassistant/components/blue_current/__init__.py @@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE type BlueCurrentConfigEntry = ConfigEntry[Connector] -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR] CHARGE_POINTS = "CHARGE_POINTS" DATA = "data" DELAY = 5 diff --git a/homeassistant/components/blue_current/button.py b/homeassistant/components/blue_current/button.py new file mode 100644 index 00000000000..9d2cde547ca --- /dev/null +++ b/homeassistant/components/blue_current/button.py @@ -0,0 +1,89 @@ +"""Support for Blue Current buttons.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from bluecurrent_api.client import Client + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import BlueCurrentConfigEntry, Connector +from .entity import ChargepointEntity + + +@dataclass(kw_only=True, frozen=True) +class ChargePointButtonEntityDescription(ButtonEntityDescription): + """Describes a Blue Current button entity.""" + + function: Callable[[Client, str], Coroutine[Any, Any, None]] + + +CHARGE_POINT_BUTTONS = ( + ChargePointButtonEntityDescription( + key="reset", + translation_key="reset", + function=lambda client, evse_id: client.reset(evse_id), + device_class=ButtonDeviceClass.RESTART, + ), + ChargePointButtonEntityDescription( + key="reboot", + translation_key="reboot", + function=lambda client, evse_id: client.reboot(evse_id), + device_class=ButtonDeviceClass.RESTART, + ), + ChargePointButtonEntityDescription( + key="stop_charge_session", + translation_key="stop_charge_session", + function=lambda client, evse_id: client.stop_session(evse_id), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: BlueCurrentConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Blue Current buttons.""" + connector: Connector = entry.runtime_data + async_add_entities( + ChargePointButton( + connector, + button, + evse_id, + ) + for evse_id in connector.charge_points + for button in CHARGE_POINT_BUTTONS + ) + + +class ChargePointButton(ChargepointEntity, ButtonEntity): + """Define a charge point button.""" + + has_value = True + entity_description: ChargePointButtonEntityDescription + + def __init__( + self, + connector: Connector, + description: ChargePointButtonEntityDescription, + evse_id: str, + ) -> None: + """Initialize the button.""" + super().__init__(connector, evse_id) + + self.entity_description = description + self._attr_unique_id = f"{description.key}_{evse_id}" + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.function(self.connector.client, self.evse_id) diff --git a/homeassistant/components/blue_current/entity.py b/homeassistant/components/blue_current/entity.py index cae7d420c99..426b7c06845 100644 --- a/homeassistant/components/blue_current/entity.py +++ b/homeassistant/components/blue_current/entity.py @@ -1,7 +1,5 @@ """Entity representing a Blue Current charge point.""" -from abc import abstractmethod - from homeassistant.const import ATTR_NAME from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo @@ -17,12 +15,12 @@ class BlueCurrentEntity(Entity): _attr_has_entity_name = True _attr_should_poll = False + has_value = False def __init__(self, connector: Connector, signal: str) -> None: """Initialize the entity.""" self.connector = connector self.signal = signal - self.has_value = False async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -43,7 +41,6 @@ class BlueCurrentEntity(Entity): return self.connector.connected and self.has_value @callback - @abstractmethod def update_from_latest_data(self) -> None: """Update the entity from the latest data.""" diff --git a/homeassistant/components/blue_current/icons.json b/homeassistant/components/blue_current/icons.json index b5a5f2be81e..ce936902e91 100644 --- a/homeassistant/components/blue_current/icons.json +++ b/homeassistant/components/blue_current/icons.json @@ -19,6 +19,17 @@ "current_left": { "default": "mdi:gauge" } + }, + "button": { + "reset": { + "default": "mdi:restart" + }, + "reboot": { + "default": "mdi:restart-alert" + }, + "stop_charge_session": { + "default": "mdi:stop" + } } } } diff --git a/homeassistant/components/blue_current/strings.json b/homeassistant/components/blue_current/strings.json index a8a9aff7f08..28eb20fa912 100644 --- a/homeassistant/components/blue_current/strings.json +++ b/homeassistant/components/blue_current/strings.json @@ -113,6 +113,17 @@ "grid_max_current": { "name": "Max grid current" } + }, + "button": { + "stop_charge_session": { + "name": "Stop charge session" + }, + "reboot": { + "name": "Reboot" + }, + "reset": { + "name": "Reset" + } } } } diff --git a/tests/components/blue_current/snapshots/test_button.ambr b/tests/components/blue_current/snapshots/test_button.ambr new file mode 100644 index 00000000000..0dc27892ceb --- /dev/null +++ b/tests/components/blue_current/snapshots/test_button.ambr @@ -0,0 +1,144 @@ +# serializer version: 1 +# name: test_buttons_created[button.101_reboot-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.101_reboot', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reboot', + 'platform': 'blue_current', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reboot', + 'unique_id': 'reboot_101', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons_created[button.101_reboot-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': '101 Reboot', + }), + 'context': , + 'entity_id': 'button.101_reboot', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons_created[button.101_reset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.101_reset', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reset', + 'platform': 'blue_current', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'reset', + 'unique_id': 'reset_101', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons_created[button.101_reset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'restart', + 'friendly_name': '101 Reset', + }), + 'context': , + 'entity_id': 'button.101_reset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons_created[button.101_stop_charge_session-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.101_stop_charge_session', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop charge session', + 'platform': 'blue_current', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop_charge_session', + 'unique_id': 'stop_charge_session_101', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons_created[button.101_stop_charge_session-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '101 Stop charge session', + }), + 'context': , + 'entity_id': 'button.101_stop_charge_session', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/blue_current/test_button.py b/tests/components/blue_current/test_button.py new file mode 100644 index 00000000000..7b9e7a7e7ce --- /dev/null +++ b/tests/components/blue_current/test_button.py @@ -0,0 +1,51 @@ +"""The tests for Blue Current buttons.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + +charge_point_buttons = ["stop_charge_session", "reset", "reboot"] + + +async def test_buttons_created( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test if all buttons are created.""" + await init_integration(hass, config_entry, Platform.BUTTON) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.freeze_time("2023-01-13 12:00:00+00:00") +async def test_charge_point_buttons( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test the underlying charge point buttons.""" + await init_integration(hass, config_entry, Platform.BUTTON) + + for button in charge_point_buttons: + state = hass.states.get(f"button.101_{button}") + assert state is not None + assert state.state == STATE_UNKNOWN + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.101_{button}"}, + blocking=True, + ) + + state = hass.states.get(f"button.101_{button}") + assert state + assert state.state == "2023-01-13T12:00:00+00:00" From 0eb6c88bc59ac20f5be18b1ce81fb1b2be01bc84 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 14 May 2025 20:45:58 +0200 Subject: [PATCH 0470/1175] Add system LED brightness to eheimdigital (#144915) --- .../components/eheimdigital/icons.json | 6 + .../components/eheimdigital/number.py | 18 ++ .../components/eheimdigital/strings.json | 3 + tests/components/eheimdigital/conftest.py | 2 + .../eheimdigital/snapshots/test_number.ambr | 171 ++++++++++++++++++ tests/components/eheimdigital/test_number.py | 22 +++ 6 files changed, 222 insertions(+) diff --git a/homeassistant/components/eheimdigital/icons.json b/homeassistant/components/eheimdigital/icons.json index 41a362c757c..cbe2613dd97 100644 --- a/homeassistant/components/eheimdigital/icons.json +++ b/homeassistant/components/eheimdigital/icons.json @@ -15,6 +15,12 @@ }, "night_temperature_offset": { "default": "mdi:thermometer" + }, + "system_led": { + "default": "mdi:led-on", + "state": { + "0": "mdi:led-off" + } } }, "sensor": { diff --git a/homeassistant/components/eheimdigital/number.py b/homeassistant/components/eheimdigital/number.py index f4504be624c..7fd0c6b6de7 100644 --- a/homeassistant/components/eheimdigital/number.py +++ b/homeassistant/components/eheimdigital/number.py @@ -109,6 +109,20 @@ HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], .. ), ) +GENERAL_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalDevice], ...] = ( + EheimDigitalNumberDescription[EheimDigitalDevice]( + key="system_led", + translation_key="system_led", + entity_category=EntityCategory.CONFIG, + native_min_value=0, + native_max_value=100, + native_step=PRECISION_WHOLE, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.sys_led, + set_value_fn=lambda device, value: device.set_sys_led(int(value)), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -138,6 +152,10 @@ async def async_setup_entry( ) for description in HEATER_DESCRIPTIONS ) + entities.extend( + EheimDigitalNumber[EheimDigitalDevice](coordinator, device, description) + for description in GENERAL_DESCRIPTIONS + ) async_add_entities(entities) diff --git a/homeassistant/components/eheimdigital/strings.json b/homeassistant/components/eheimdigital/strings.json index 97a3fbe4e0d..f6f6b74a72e 100644 --- a/homeassistant/components/eheimdigital/strings.json +++ b/homeassistant/components/eheimdigital/strings.json @@ -62,6 +62,9 @@ }, "night_temperature_offset": { "name": "Night temperature offset" + }, + "system_led": { + "name": "System LED brightness" } }, "sensor": { diff --git a/tests/components/eheimdigital/conftest.py b/tests/components/eheimdigital/conftest.py index 654028c7c11..5b828f830a4 100644 --- a/tests/components/eheimdigital/conftest.py +++ b/tests/components/eheimdigital/conftest.py @@ -47,6 +47,7 @@ def classic_led_ctrl_mock(): classic_led_ctrl_mock.sw_version = "1.0.0_1.0.0" classic_led_ctrl_mock.light_mode = LightMode.DAYCL_MODE classic_led_ctrl_mock.light_level = (10, 39) + classic_led_ctrl_mock.sys_led = 20 return classic_led_ctrl_mock @@ -69,6 +70,7 @@ def heater_mock(): heater_mock.operation_mode = HeaterMode.MANUAL heater_mock.day_start_time = time(8, 0, tzinfo=timezone(timedelta(hours=1))) heater_mock.night_start_time = time(20, 0, tzinfo=timezone(timedelta(hours=1))) + heater_mock.sys_led = 20 return heater_mock diff --git a/tests/components/eheimdigital/snapshots/test_number.ambr b/tests/components/eheimdigital/snapshots/test_number.ambr index d647b16bf49..554e7c9c3a3 100644 --- a/tests/components/eheimdigital/snapshots/test_number.ambr +++ b/tests/components/eheimdigital/snapshots/test_number.ambr @@ -1,4 +1,61 @@ # serializer version: 1 +# name: test_setup[number.mock_classicledcontrol_e_system_led_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_classicledcontrol_e_system_led_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'System LED brightness', + 'platform': 'eheimdigital', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'system_led', + 'unique_id': '00:00:00:00:00:01_system_led', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[number.mock_classicledcontrol_e_system_led_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock classicLEDcontrol+e System LED brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.mock_classicledcontrol_e_system_led_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[number.mock_classicvario_day_speed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -170,6 +227,63 @@ 'state': 'unknown', }) # --- +# name: test_setup[number.mock_classicvario_system_led_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_classicvario_system_led_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'System LED brightness', + 'platform': 'eheimdigital', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'system_led', + 'unique_id': '00:00:00:00:00:03_system_led', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[number.mock_classicvario_system_led_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock classicVARIO System LED brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.mock_classicvario_system_led_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[number.mock_heater_night_temperature_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -227,6 +341,63 @@ 'state': 'unknown', }) # --- +# name: test_setup[number.mock_heater_system_led_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_heater_system_led_brightness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'System LED brightness', + 'platform': 'eheimdigital', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'system_led', + 'unique_id': '00:00:00:00:00:02_system_led', + 'unit_of_measurement': '%', + }) +# --- +# name: test_setup[number.mock_heater_system_led_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Heater System LED brightness', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.mock_heater_system_led_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_setup[number.mock_heater_temperature_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/eheimdigital/test_number.py b/tests/components/eheimdigital/test_number.py index d84c14f95a5..a23f461744a 100644 --- a/tests/components/eheimdigital/test_number.py +++ b/tests/components/eheimdigital/test_number.py @@ -67,6 +67,12 @@ async def test_setup( "set_night_temperature_offset", (0.4,), ), + ( + "number.mock_heater_system_led_brightness", + 20, + "set_sys_led", + (20,), + ), ], ), ( @@ -90,6 +96,12 @@ async def test_setup( "set_night_speed", (int(72.1),), ), + ( + "number.mock_classicvario_system_led_brightness", + 20, + "set_sys_led", + (20,), + ), ], ), ], @@ -140,6 +152,11 @@ async def test_set_value( "night_temperature_offset", 2.3, ), + ( + "number.mock_heater_system_led_brightness", + "sys_led", + 87, + ), ], ), ( @@ -160,6 +177,11 @@ async def test_set_value( "night_speed", 12, ), + ( + "number.mock_classicvario_system_led_brightness", + "sys_led", + 35, + ), ], ), ], From 460f02ede52f09c3385777615108caa7de833e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 14 May 2025 20:46:28 +0200 Subject: [PATCH 0471/1175] Update mill library 0.12.5 (#144911) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update mill library 0.12.5 Signed-off-by: Daniel Hjelseth Høyer * Update mill library 0.12.5 Signed-off-by: Daniel Hjelseth Høyer --------- Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/mill/coordinator.py | 4 ++-- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mill/coordinator.py b/homeassistant/components/mill/coordinator.py index 288b341b0f9..a701acb8ddb 100644 --- a/homeassistant/components/mill/coordinator.py +++ b/homeassistant/components/mill/coordinator.py @@ -26,7 +26,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -TWO_YEARS = 2 * 365 * 24 +TWO_YEARS_DAYS = 2 * 365 class MillDataUpdateCoordinator(DataUpdateCoordinator): @@ -91,7 +91,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator): if not last_stats or not last_stats.get(statistic_id): hourly_data = ( await self.mill_data_connection.fetch_historic_energy_usage( - dev_id, n_days=TWO_YEARS + dev_id, n_days=TWO_YEARS_DAYS ) ) hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0])) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index bfad9b48cb9..c5cc94ead30 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/mill", "iot_class": "local_polling", "loggers": ["mill", "mill_local"], - "requirements": ["millheater==0.12.3", "mill-local==0.3.0"] + "requirements": ["millheater==0.12.5", "mill-local==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index c801d6b137e..9c1a484ef2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1424,7 +1424,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.3 +millheater==0.12.5 # homeassistant.components.minio minio==7.1.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3b594404bb..7a417bb1ee8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1197,7 +1197,7 @@ microBeesPy==0.3.5 mill-local==0.3.0 # homeassistant.components.mill -millheater==0.12.3 +millheater==0.12.5 # homeassistant.components.minio minio==7.1.12 From dbdffbba233c743d32239aefc3dea0da1be1b2c9 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Thu, 15 May 2025 06:56:08 +1200 Subject: [PATCH 0472/1175] Add binary sensors to bosch_alarm (#142147) * Add binary sensors to bosch_alarm * make one device per sensor, remove device class guessing * fix tests * update tests * Apply suggested changes * add binary sensors * make fault sensors diagnostic * update tests * update binary sensors to use base entity * fix strings * fix icons * add state translations for area ready sensors * use constants in tests * apply changes from review * remove fault prefix, use default translation for battery low * update tests --- .../components/bosch_alarm/__init__.py | 1 + .../components/bosch_alarm/binary_sensor.py | 220 ++ .../components/bosch_alarm/entity.py | 37 +- .../components/bosch_alarm/icons.json | 38 + .../components/bosch_alarm/strings.json | 46 + tests/components/bosch_alarm/conftest.py | 1 + .../snapshots/test_binary_sensor.ambr | 2995 +++++++++++++++++ .../bosch_alarm/test_binary_sensor.py | 78 + 8 files changed, 3415 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/bosch_alarm/binary_sensor.py create mode 100644 tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/bosch_alarm/test_binary_sensor.py diff --git a/homeassistant/components/bosch_alarm/__init__.py b/homeassistant/components/bosch_alarm/__init__.py index 19debe10549..06ec98e91ba 100644 --- a/homeassistant/components/bosch_alarm/__init__.py +++ b/homeassistant/components/bosch_alarm/__init__.py @@ -16,6 +16,7 @@ from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN PLATFORMS: list[Platform] = [ Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/bosch_alarm/binary_sensor.py b/homeassistant/components/bosch_alarm/binary_sensor.py new file mode 100644 index 00000000000..ced97f04686 --- /dev/null +++ b/homeassistant/components/bosch_alarm/binary_sensor.py @@ -0,0 +1,220 @@ +"""Support for Bosch Alarm Panel binary sensors.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from bosch_alarm_mode2 import Panel +from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import BoschAlarmConfigEntry +from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity + + +@dataclass(kw_only=True, frozen=True) +class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription): + """Describes Bosch Alarm sensor entity.""" + + fault: int + + +FAULT_TYPES = [ + BoschAlarmFaultEntityDescription( + key="panel_fault_battery_low", + entity_registry_enabled_default=True, + device_class=BinarySensorDeviceClass.BATTERY, + fault=ALARM_PANEL_FAULTS.BATTERY_LOW, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_battery_mising", + translation_key="panel_fault_battery_mising", + entity_registry_enabled_default=True, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.BATTERY_MISING, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_ac_fail", + translation_key="panel_fault_ac_fail", + entity_registry_enabled_default=True, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.AC_FAIL, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_phone_line_failure", + translation_key="panel_fault_phone_line_failure", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_parameter_crc_fail_in_pif", + translation_key="panel_fault_parameter_crc_fail_in_pif", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_communication_fail_since_rps_hang_up", + translation_key="panel_fault_communication_fail_since_rps_hang_up", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_sdi_fail_since_rps_hang_up", + translation_key="panel_fault_sdi_fail_since_rps_hang_up", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_user_code_tamper_since_rps_hang_up", + translation_key="panel_fault_user_code_tamper_since_rps_hang_up", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_fail_to_call_rps_since_rps_hang_up", + translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up", + entity_registry_enabled_default=False, + fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_point_bus_fail_since_rps_hang_up", + translation_key="panel_fault_point_bus_fail_since_rps_hang_up", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_log_overflow", + translation_key="panel_fault_log_overflow", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW, + ), + BoschAlarmFaultEntityDescription( + key="panel_fault_log_threshold", + translation_key="panel_fault_log_threshold", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.PROBLEM, + fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BoschAlarmConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up binary sensors for alarm points and the connection status.""" + panel = config_entry.runtime_data + + entities: list[BinarySensorEntity] = [ + PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id) + for point_id in panel.points + ] + + entities.extend( + PanelFaultsSensor( + panel, + config_entry.unique_id or config_entry.entry_id, + fault_type, + ) + for fault_type in FAULT_TYPES + ) + + entities.extend( + AreaReadyToArmSensor( + panel, area_id, config_entry.unique_id or config_entry.entry_id, "away" + ) + for area_id in panel.areas + ) + + entities.extend( + AreaReadyToArmSensor( + panel, area_id, config_entry.unique_id or config_entry.entry_id, "home" + ) + for area_id in panel.areas + ) + + async_add_entities(entities) + + +PARALLEL_UPDATES = 0 + + +class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity): + """A binary sensor entity for each fault type in a bosch alarm panel.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + entity_description: BoschAlarmFaultEntityDescription + + def __init__( + self, + panel: Panel, + unique_id: str, + entity_description: BoschAlarmFaultEntityDescription, + ) -> None: + """Set up a binary sensor entity for each fault type in a bosch alarm panel.""" + super().__init__(panel, unique_id, True) + self.entity_description = entity_description + self._fault_type = entity_description.fault + self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return if this fault has occurred.""" + return self._fault_type in self.panel.panel_faults_ids + + +class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity): + """A binary sensor entity showing if a panel is ready to arm.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, panel: Panel, area_id: int, unique_id: str, arm_type: str + ) -> None: + """Set up a binary sensor entity for the arming status in a bosch alarm panel.""" + super().__init__(panel, area_id, unique_id, False, False, True) + self.panel = panel + self._arm_type = arm_type + self._attr_translation_key = f"area_ready_to_arm_{arm_type}" + self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}" + + @property + def is_on(self) -> bool: + """Return if this panel is ready to arm.""" + if self._arm_type == "away": + return self._area.all_ready + if self._arm_type == "home": + return self._area.all_ready or self._area.part_ready + return False + + +class PointSensor(BoschAlarmPointEntity, BinarySensorEntity): + """A binary sensor entity for a point in a bosch alarm panel.""" + + _attr_name = None + + def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None: + """Set up a binary sensor entity for a point in a bosch alarm panel.""" + super().__init__(panel, point_id, unique_id) + self._attr_unique_id = self._point_unique_id + + @property + def is_on(self) -> bool: + """Return if this point sensor is on.""" + return self._point.is_open() diff --git a/homeassistant/components/bosch_alarm/entity.py b/homeassistant/components/bosch_alarm/entity.py index e9223b729c4..537ee412e47 100644 --- a/homeassistant/components/bosch_alarm/entity.py +++ b/homeassistant/components/bosch_alarm/entity.py @@ -17,9 +17,13 @@ class BoschAlarmEntity(Entity): _attr_has_entity_name = True - def __init__(self, panel: Panel, unique_id: str) -> None: + def __init__( + self, panel: Panel, unique_id: str, observe_faults: bool = False + ) -> None: """Set up a entity for a bosch alarm panel.""" self.panel = panel + self._observe_faults = observe_faults + self._attr_should_poll = False self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=f"Bosch {panel.model}", @@ -34,10 +38,14 @@ class BoschAlarmEntity(Entity): async def async_added_to_hass(self) -> None: """Observe state changes.""" self.panel.connection_status_observer.attach(self.schedule_update_ha_state) + if self._observe_faults: + self.panel.faults_observer.attach(self.schedule_update_ha_state) async def async_will_remove_from_hass(self) -> None: """Stop observing state changes.""" self.panel.connection_status_observer.detach(self.schedule_update_ha_state) + if self._observe_faults: + self.panel.faults_observer.attach(self.schedule_update_ha_state) class BoschAlarmAreaEntity(BoschAlarmEntity): @@ -88,6 +96,33 @@ class BoschAlarmAreaEntity(BoschAlarmEntity): self._area.status_observer.detach(self.schedule_update_ha_state) +class BoschAlarmPointEntity(BoschAlarmEntity): + """A base entity for point related entities within a bosch alarm panel.""" + + def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None: + """Set up a area related entity for a bosch alarm panel.""" + super().__init__(panel, unique_id) + self._point_id = point_id + self._point_unique_id = f"{unique_id}_point_{point_id}" + self._point = panel.points[point_id] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._point_unique_id)}, + name=self._point.name, + manufacturer="Bosch Security Systems", + via_device=(DOMAIN, unique_id), + ) + + async def async_added_to_hass(self) -> None: + """Observe state changes.""" + await super().async_added_to_hass() + self._point.status_observer.attach(self.schedule_update_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Stop observing state changes.""" + await super().async_added_to_hass() + self._point.status_observer.detach(self.schedule_update_ha_state) + + class BoschAlarmDoorEntity(BoschAlarmEntity): """A base entity for area related entities within a bosch alarm panel.""" diff --git a/homeassistant/components/bosch_alarm/icons.json b/homeassistant/components/bosch_alarm/icons.json index 44a94fdc570..43f6f33e066 100644 --- a/homeassistant/components/bosch_alarm/icons.json +++ b/homeassistant/components/bosch_alarm/icons.json @@ -24,6 +24,44 @@ "on": "mdi:lock-open" } } + }, + "binary_sensor": { + "panel_fault_parameter_crc_fail_in_pif": { + "default": "mdi:alert-circle" + }, + "panel_fault_phone_line_failure": { + "default": "mdi:alert-circle" + }, + "panel_fault_sdi_fail_since_rps_hang_up": { + "default": "mdi:alert-circle" + }, + "panel_fault_user_code_tamper_since_rps_hang_up": { + "default": "mdi:alert-circle" + }, + "panel_fault_fail_to_call_rps_since_rps_hang_up": { + "default": "mdi:alert-circle" + }, + "panel_fault_point_bus_fail_since_rps_hang_up": { + "default": "mdi:alert-circle" + }, + "panel_fault_log_overflow": { + "default": "mdi:alert-circle" + }, + "panel_fault_log_threshold": { + "default": "mdi:alert-circle" + }, + "area_ready_to_arm_away": { + "default": "mdi:shield", + "state": { + "on": "mdi:shield-lock" + } + }, + "area_ready_to_arm_home": { + "default": "mdi:shield", + "state": { + "on": "mdi:shield-home" + } + } } } } diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 4e71d14fe4a..3a6604c2634 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -60,6 +60,52 @@ } }, "entity": { + "binary_sensor": { + "panel_fault_battery_mising": { + "name": "Battery missing" + }, + "panel_fault_ac_fail": { + "name": "AC Failure" + }, + "panel_fault_parameter_crc_fail_in_pif": { + "name": "CRC failure in panel configuration" + }, + "panel_fault_phone_line_failure": { + "name": "Phone line failure" + }, + "panel_fault_sdi_fail_since_rps_hang_up": { + "name": "SDI failure since RPS hang up" + }, + "panel_fault_user_code_tamper_since_rps_hang_up": { + "name": "User code tamper since RPS hang up" + }, + "panel_fault_fail_to_call_rps_since_rps_hang_up": { + "name": "Failure to call RPS since RPS hang up" + }, + "panel_fault_point_bus_fail_since_rps_hang_up": { + "name": "Point bus failure since RPS hang up" + }, + "panel_fault_log_overflow": { + "name": "Log overflow" + }, + "panel_fault_log_threshold": { + "name": "Log threshold reached" + }, + "area_ready_to_arm_away": { + "name": "Area ready to arm away", + "state": { + "on": "Ready", + "off": "Not ready" + } + }, + "area_ready_to_arm_home": { + "name": "Area ready to arm home", + "state": { + "on": "Ready", + "off": "Not ready" + } + } + }, "switch": { "secured": { "name": "Secured" diff --git a/tests/components/bosch_alarm/conftest.py b/tests/components/bosch_alarm/conftest.py index 76bb896daf5..3be4ba2c816 100644 --- a/tests/components/bosch_alarm/conftest.py +++ b/tests/components/bosch_alarm/conftest.py @@ -171,6 +171,7 @@ def mock_panel( client.model = model_name client.faults = [] client.events = [] + client.panel_faults_ids = [] client.firmware_version = "1.0.0" client.protocol_version = "1.0.0" client.serial_number = serial_number diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..e5396b662f3 --- /dev/null +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -0,0 +1,2995 @@ +# serializer version: 1 +# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_away-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'has_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 ready to arm away', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_away', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_away-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm away', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_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': 'Area ready to arm home', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_home', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm home', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_ac_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_ac_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_ac_fail', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_ac_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 AC Failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_ac_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bosch AMAX 3000 Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery_missing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_battery_missing', + '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 missing', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_battery_mising', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery_missing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 Battery missing', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_battery_missing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CRC failure in panel configuration', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 CRC failure in panel configuration', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Failure to call RPS since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bosch AMAX 3000 Failure to call RPS since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_overflow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_log_overflow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log overflow', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_overflow', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_overflow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 Log overflow', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_log_overflow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_log_threshold_reached', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log threshold reached', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_threshold', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 Log threshold reached', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_log_threshold_reached', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_phone_line_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phone line failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_phone_line_failure', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Bosch AMAX 3000 Phone line failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_phone_line_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Point bus failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 Point bus failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SDI failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 SDI failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'User code tamper since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch AMAX 3000 User code tamper since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.co_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.co_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.co_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CO Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.co_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.door', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Door', + }), + 'context': , + 'entity_id': 'binary_sensor.door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.glassbreak_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.glassbreak_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Glassbreak Sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.motion_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.motion_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Motion Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.smoke_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.smoke_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[amax_3000][binary_sensor.window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Window', + }), + 'context': , + 'entity_id': 'binary_sensor.window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_away-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'has_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 ready to arm away', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_away', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_away-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm away', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_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': 'Area ready to arm home', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_home', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm home', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_ac_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_ac_fail', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_ac_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) AC Failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bosch B5512 (US1B) Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery_missing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', + '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 missing', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_battery_mising', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery_missing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) Battery missing', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CRC failure in panel configuration', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) CRC failure in panel configuration', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Failure to call RPS since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_overflow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log overflow', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_overflow', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_overflow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) Log overflow', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log threshold reached', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_threshold', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) Log threshold reached', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phone line failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_phone_line_failure', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Bosch B5512 (US1B) Phone line failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Point bus failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) Point bus failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SDI failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) SDI failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'User code tamper since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch B5512 (US1B) User code tamper since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.co_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.co_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.co_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CO Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.co_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.door', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Door', + }), + 'context': , + 'entity_id': 'binary_sensor.door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.glassbreak_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.glassbreak_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Glassbreak Sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.motion_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.motion_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Motion Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.smoke_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.smoke_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[b5512][binary_sensor.window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Window', + }), + 'context': , + 'entity_id': 'binary_sensor.window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_away-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'has_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 ready to arm away', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_away', + 'unique_id': '1234567890_area_1_ready_to_arm_away', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_away-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm away', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_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': 'Area ready to arm home', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_home', + 'unique_id': '1234567890_area_1_ready_to_arm_home', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm home', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_ac_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_ac_fail', + 'unique_id': '1234567890_fault_panel_fault_ac_fail', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_ac_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 AC Failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_fault_panel_fault_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bosch Solution 3000 Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery_missing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + '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 missing', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_battery_mising', + 'unique_id': '1234567890_fault_panel_fault_battery_mising', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery_missing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Battery missing', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CRC failure in panel configuration', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', + 'unique_id': '1234567890_fault_panel_fault_parameter_crc_fail_in_pif', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 CRC failure in panel configuration', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Failure to call RPS since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unique_id': '1234567890_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_overflow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log overflow', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_overflow', + 'unique_id': '1234567890_fault_panel_fault_log_overflow', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_overflow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Log overflow', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log threshold reached', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_threshold', + 'unique_id': '1234567890_fault_panel_fault_log_threshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Log threshold reached', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phone line failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_phone_line_failure', + 'unique_id': '1234567890_fault_panel_fault_phone_line_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Bosch Solution 3000 Phone line failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Point bus failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', + 'unique_id': '1234567890_fault_panel_fault_point_bus_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Point bus failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', + 'unique_id': '1234567890_fault_panel_fault_communication_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SDI failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', + 'unique_id': '1234567890_fault_panel_fault_sdi_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 SDI failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'User code tamper since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', + 'unique_id': '1234567890_fault_panel_fault_user_code_tamper_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 User code tamper since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.co_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.co_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.co_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CO Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.co_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.door', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Door', + }), + 'context': , + 'entity_id': 'binary_sensor.door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.glassbreak_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.glassbreak_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Glassbreak Sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.motion_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.motion_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Motion Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.smoke_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.smoke_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '1234567890_point_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[solution_3000][binary_sensor.window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Window', + }), + 'context': , + 'entity_id': 'binary_sensor.window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/bosch_alarm/test_binary_sensor.py b/tests/components/bosch_alarm/test_binary_sensor.py new file mode 100644 index 00000000000..e788d7c5eda --- /dev/null +++ b/tests/components/bosch_alarm/test_binary_sensor.py @@ -0,0 +1,78 @@ +"""Tests for Bosch Alarm component.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import call_observable, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[None]: + """Return the platforms to be loaded for this test.""" + with patch( + "homeassistant.components.bosch_alarm.PLATFORMS", [Platform.BINARY_SENSOR] + ): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_panel: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the binary sensor state.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("model", ["b5512"]) +async def test_panel_faults( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that fault sensor state changes after inducing a fault.""" + await setup_integration(hass, mock_config_entry) + entity_id = "binary_sensor.bosch_b5512_us1b_battery" + assert hass.states.get(entity_id).state == STATE_OFF + mock_panel.panel_faults_ids = [ALARM_PANEL_FAULTS.BATTERY_LOW] + await call_observable(hass, mock_panel.faults_observer) + assert hass.states.get(entity_id).state == STATE_ON + + +@pytest.mark.parametrize("model", ["b5512"]) +async def test_area_ready_to_arm( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that fault sensor state changes after inducing a fault.""" + await setup_integration(hass, mock_config_entry) + entity_id = "binary_sensor.area1_area_ready_to_arm_away" + entity_id_2 = "binary_sensor.area1_area_ready_to_arm_home" + assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(entity_id_2).state == STATE_ON + area.all_ready = False + await call_observable(hass, area.status_observer) + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id_2).state == STATE_ON + area.part_ready = False + await call_observable(hass, area.status_observer) + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id_2).state == STATE_OFF From 1e8843947c55b66a4bd844944b233bdb16d422d8 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Thu, 15 May 2025 07:00:41 +1200 Subject: [PATCH 0473/1175] Add sensor for alarm status in bosch_alarm (#142564) * Add sensor for alarm status * style fixes * fix icons * style fixes * update tests * apply change from code review * add alarm to alarm sensor state * Apply changes from review --- .../components/bosch_alarm/icons.json | 9 + .../components/bosch_alarm/sensor.py | 40 +- .../components/bosch_alarm/strings.json | 27 ++ .../bosch_alarm/snapshots/test_sensor.ambr | 423 ++++++++++++++++++ tests/components/bosch_alarm/test_sensor.py | 19 +- 5 files changed, 515 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_alarm/icons.json b/homeassistant/components/bosch_alarm/icons.json index 43f6f33e066..b13822fa711 100644 --- a/homeassistant/components/bosch_alarm/icons.json +++ b/homeassistant/components/bosch_alarm/icons.json @@ -1,6 +1,15 @@ { "entity": { "sensor": { + "alarms_gas": { + "default": "mdi:alert-circle" + }, + "alarms_fire": { + "default": "mdi:alert-circle" + }, + "alarms_burglary": { + "default": "mdi:alert-circle" + }, "faulting_points": { "default": "mdi:alert-circle" } diff --git a/homeassistant/components/bosch_alarm/sensor.py b/homeassistant/components/bosch_alarm/sensor.py index 3d61c72a883..479aaa03049 100644 --- a/homeassistant/components/bosch_alarm/sensor.py +++ b/homeassistant/components/bosch_alarm/sensor.py @@ -6,6 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from bosch_alarm_mode2 import Panel +from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES from bosch_alarm_mode2.panel import Area from homeassistant.components.sensor import SensorEntity, SensorEntityDescription @@ -15,18 +16,53 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import BoschAlarmConfigEntry from .entity import BoschAlarmAreaEntity +ALARM_TYPES = { + "burglary": { + ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory", + ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble", + ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm", + }, + "gas": { + ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory", + ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble", + ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm", + }, + "fire": { + ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory", + ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble", + ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm", + }, +} + @dataclass(kw_only=True, frozen=True) class BoschAlarmSensorEntityDescription(SensorEntityDescription): """Describes Bosch Alarm sensor entity.""" - value_fn: Callable[[Area], int] + value_fn: Callable[[Area], str | int] observe_alarms: bool = False observe_ready: bool = False observe_status: bool = False +def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]: + """Build a value_fn for a given priority type.""" + return lambda area: next( + (key for priority, key in priority_info.items() if priority in area.alarms_ids), + "no_issues", + ) + + SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [ + *[ + BoschAlarmSensorEntityDescription( + key=f"alarms_{key}", + translation_key=f"alarms_{key}", + value_fn=priority_value_fn(priority_type), + observe_alarms=True, + ) + for key, priority_type in ALARM_TYPES.items() + ], BoschAlarmSensorEntityDescription( key="faulting_points", translation_key="faulting_points", @@ -81,6 +117,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity): self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}" @property - def native_value(self) -> int: + def native_value(self) -> str | int: """Return the state of the sensor.""" return self.entity_description.value_fn(self._area) diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 3a6604c2634..b9176c41a08 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -118,6 +118,33 @@ } }, "sensor": { + "alarms_gas": { + "name": "Gas alarm issues", + "state": { + "supervisory": "Supervisory", + "trouble": "Trouble", + "alarm": "Alarm", + "no_issues": "No issues" + } + }, + "alarms_fire": { + "name": "Fire alarm issues", + "state": { + "supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]", + "trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]", + "alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]", + "no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]" + } + }, + "alarms_burglary": { + "name": "Burglary alarm issues", + "state": { + "supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]", + "trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]", + "alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]", + "no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]" + } + }, "faulting_points": { "name": "Faulting points", "unit_of_measurement": "points" diff --git a/tests/components/bosch_alarm/snapshots/test_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_sensor.ambr index def2c503a6a..64a02e730f6 100644 --- a/tests/components/bosch_alarm/snapshots/test_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_sensor.ambr @@ -1,4 +1,51 @@ # serializer version: 1 +# name: test_sensor[amax_3000][sensor.area1_burglary_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burglary alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_burglary', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[amax_3000][sensor.area1_burglary_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Burglary alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- # name: test_sensor[amax_3000][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -47,6 +94,147 @@ 'state': '0', }) # --- +# name: test_sensor[amax_3000][sensor.area1_fire_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fire alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_fire', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[amax_3000][sensor.area1_fire_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Fire alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[amax_3000][sensor.area1_gas_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gas alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_gas', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[amax_3000][sensor.area1_gas_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Gas alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[b5512][sensor.area1_burglary_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burglary alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_burglary', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[b5512][sensor.area1_burglary_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Burglary alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- # name: test_sensor[b5512][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -95,6 +283,147 @@ 'state': '0', }) # --- +# name: test_sensor[b5512][sensor.area1_fire_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fire alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_fire', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[b5512][sensor.area1_fire_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Fire alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[b5512][sensor.area1_gas_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gas alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_gas', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[b5512][sensor.area1_gas_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Gas alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[solution_3000][sensor.area1_burglary_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burglary alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_burglary', + 'unique_id': '1234567890_area_1_alarms_burglary', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[solution_3000][sensor.area1_burglary_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Burglary alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- # name: test_sensor[solution_3000][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -143,3 +472,97 @@ 'state': '0', }) # --- +# name: test_sensor[solution_3000][sensor.area1_fire_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fire alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_fire', + 'unique_id': '1234567890_area_1_alarms_fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[solution_3000][sensor.area1_fire_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Fire alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[solution_3000][sensor.area1_gas_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gas alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_gas', + 'unique_id': '1234567890_area_1_alarms_gas', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[solution_3000][sensor.area1_gas_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Gas alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- diff --git a/tests/components/bosch_alarm/test_sensor.py b/tests/components/bosch_alarm/test_sensor.py index 02153a9656e..c986fdab733 100644 --- a/tests/components/bosch_alarm/test_sensor.py +++ b/tests/components/bosch_alarm/test_sensor.py @@ -3,6 +3,7 @@ from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, patch +from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES import pytest from syrupy.assertion import SnapshotAssertion @@ -48,5 +49,21 @@ async def test_faulting_points( area.faults = 1 await call_observable(hass, area.ready_observer) - assert hass.states.get(entity_id).state == "1" + + +async def test_alarm_faults( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that alarm state changes after arming the panel.""" + await setup_integration(hass, mock_config_entry) + entity_id = "sensor.area1_fire_alarm_issues" + assert hass.states.get(entity_id).state == "no_issues" + + area.alarms_ids = [ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE] + await call_observable(hass, area.alarm_observer) + + assert hass.states.get(entity_id).state == "trouble" From 9428127021325b9f7500e03a9627929840bfa2e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 May 2025 15:45:40 -0400 Subject: [PATCH 0474/1175] Add media search and play intent (#144269) * Add media search intent * Add PLAY_MEDIA as required feature and remove explicit responses --------- Co-authored-by: Michael Hansen --- homeassistant/components/demo/media_player.py | 19 +++ .../components/media_player/intent.py | 136 ++++++++++++++- tests/components/media_player/test_intent.py | 158 ++++++++++++++++++ 3 files changed, 311 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 5cd83722742..ad7ddcba285 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -6,12 +6,16 @@ from datetime import datetime from typing import Any from homeassistant.components.media_player import ( + BrowseMedia, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, RepeatMode, + SearchMedia, + SearchMediaQuery, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -407,3 +411,18 @@ class DemoSearchPlayer(AbstractDemoPlayer): """A Demo media player that supports searching.""" _attr_supported_features = SEARCH_PLAYER_SUPPORT + + async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia: + """Demo implementation of search media.""" + return SearchMedia( + result=[ + BrowseMedia( + title="Search result", + media_class=MediaClass.MOVIE, + media_content_type=MediaType.MOVIE, + media_content_id="search_result_id", + can_play=True, + can_expand=False, + ) + ] + ) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index 4349362b13a..85f0598695b 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -2,7 +2,9 @@ from collections.abc import Iterable from dataclasses import dataclass, field +import logging import time +from typing import cast import voluptuous as vol @@ -14,9 +16,17 @@ from homeassistant.const import ( SERVICE_VOLUME_SET, ) from homeassistant.core import Context, HomeAssistant, State -from homeassistant.helpers import intent +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, intent -from . import ATTR_MEDIA_VOLUME_LEVEL, DOMAIN, MediaPlayerDeviceClass +from . import ( + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN, + SERVICE_PLAY_MEDIA, + SERVICE_SEARCH_MEDIA, + MediaPlayerDeviceClass, + SearchMedia, +) from .const import MediaPlayerEntityFeature, MediaPlayerState INTENT_MEDIA_PAUSE = "HassMediaPause" @@ -24,6 +34,9 @@ INTENT_MEDIA_UNPAUSE = "HassMediaUnpause" INTENT_MEDIA_NEXT = "HassMediaNext" INTENT_MEDIA_PREVIOUS = "HassMediaPrevious" INTENT_SET_VOLUME = "HassSetVolume" +INTENT_MEDIA_SEARCH_AND_PLAY = "HassMediaSearchAndPlay" + +_LOGGER = logging.getLogger(__name__) @dataclass @@ -109,6 +122,7 @@ async def async_setup_intents(hass: HomeAssistant) -> None: device_classes={MediaPlayerDeviceClass}, ), ) + intent.async_register(hass, MediaSearchAndPlayHandler()) class MediaPauseHandler(intent.ServiceIntentHandler): @@ -207,3 +221,121 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler): return await super().async_handle_states( intent_obj, match_result, match_constraints ) + + +class MediaSearchAndPlayHandler(intent.IntentHandler): + """Handle HassMediaSearchAndPlay intents.""" + + description = "Searches for media and plays the first result" + + intent_type = INTENT_MEDIA_SEARCH_AND_PLAY + slot_schema = { + vol.Required("search_query"): cv.string, + # Optional name/area/floor slots handled by intent matcher + vol.Optional("name"): cv.string, + vol.Optional("area"): cv.string, + vol.Optional("floor"): cv.string, + vol.Optional("preferred_area_id"): cv.string, + vol.Optional("preferred_floor_id"): cv.string, + } + platforms = {DOMAIN} + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the intent.""" + hass = intent_obj.hass + slots = self.async_validate_slots(intent_obj.slots) + search_query = slots["search_query"]["value"] + + # Entity name to match + name_slot = slots.get("name", {}) + entity_name: str | None = name_slot.get("value") + + # Get area/floor info + area_slot = slots.get("area", {}) + area_id = area_slot.get("value") + + floor_slot = slots.get("floor", {}) + floor_id = floor_slot.get("value") + + # Find matching entities + match_constraints = intent.MatchTargetsConstraints( + name=entity_name, + area_name=area_id, + floor_name=floor_id, + domains={DOMAIN}, + assistant=intent_obj.assistant, + features=MediaPlayerEntityFeature.SEARCH_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA, + single_target=True, + ) + match_result = intent.async_match_targets( + hass, + match_constraints, + intent.MatchTargetsPreferences( + area_id=slots.get("preferred_area_id", {}).get("value"), + floor_id=slots.get("preferred_floor_id", {}).get("value"), + ), + ) + + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) + + target_entity = match_result.states[0] + target_entity_id = target_entity.entity_id + + # 1. Search Media + try: + search_response = await hass.services.async_call( + DOMAIN, + SERVICE_SEARCH_MEDIA, + { + "search_query": search_query, + }, + target={ + "entity_id": target_entity_id, + }, + blocking=True, + context=intent_obj.context, + return_response=True, + ) + except HomeAssistantError as err: + _LOGGER.error("Error calling search_media: %s", err) + raise intent.IntentHandleError(f"Error searching media: {err}") from err + + if ( + not search_response + or not ( + entity_response := cast( + SearchMedia, search_response.get(target_entity_id) + ) + ) + or not (results := entity_response.result) + ): + # No results found + return intent_obj.create_response() + + # 2. Play Media (first result) + first_result = results[0] + try: + await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": target_entity_id, + "media_content_id": first_result.media_content_id, + "media_content_type": first_result.media_content_type, + }, + blocking=True, + context=intent_obj.context, + ) + except HomeAssistantError as err: + _LOGGER.error("Error calling play_media: %s", err) + raise intent.IntentHandleError(f"Error playing media: {err}") from err + + # Success + response = intent_obj.create_response() + response.async_set_speech_slots({"media": first_result}) + response.response_type = intent.IntentResponseType.ACTION_DONE + return response diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index 8e7211183e7..6429d6889c0 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -8,7 +8,13 @@ from homeassistant.components.media_player import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_PLAY_MEDIA, + SERVICE_SEARCH_MEDIA, SERVICE_VOLUME_SET, + BrowseMedia, + MediaClass, + MediaType, + SearchMedia, intent as media_player_intent, ) from homeassistant.components.media_player.const import MediaPlayerEntityFeature @@ -19,6 +25,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( area_registry as ar, entity_registry as er, @@ -635,3 +642,154 @@ async def test_manual_pause_unpause( assert response.response_type == intent.IntentResponseType.ACTION_DONE assert len(calls) == 1 assert calls[0].data == {"entity_id": device_2.entity_id} + + +async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None: + """Test HassMediaSearchAndPlay intent for media players.""" + await media_player_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_media_player" + attributes = { + ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.SEARCH_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA + } + hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes) + + # Test successful search and play + search_result_item = BrowseMedia( + title="Test Track", + media_class=MediaClass.MUSIC, + media_content_type=MediaType.MUSIC, + media_content_id="library/artist/123/album/456/track/789", + can_play=True, + can_expand=False, + ) + + # Mock service calls + search_results = [search_result_item] + search_calls = async_mock_service( + hass, + DOMAIN, + SERVICE_SEARCH_MEDIA, + response={entity_id: SearchMedia(result=search_results)}, + ) + play_calls = async_mock_service(hass, DOMAIN, SERVICE_PLAY_MEDIA) + + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "test query"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + + # Response should contain a "media" slot with the matched item. + assert not response.speech + media = response.speech_slots.get("media") + assert isinstance(media, BrowseMedia) + assert media.title == "Test Track" + + assert len(search_calls) == 1 + search_call = search_calls[0] + assert search_call.domain == DOMAIN + assert search_call.service == SERVICE_SEARCH_MEDIA + assert search_call.data == { + "entity_id": entity_id, + "search_query": "test query", + } + + assert len(play_calls) == 1 + play_call = play_calls[0] + assert play_call.domain == DOMAIN + assert play_call.service == SERVICE_PLAY_MEDIA + assert play_call.data == { + "entity_id": entity_id, + "media_content_id": search_result_item.media_content_id, + "media_content_type": search_result_item.media_content_type, + } + + # Test no search results + search_results.clear() + response = await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "another query"}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + + # A search failure is indicated by no "media" slot in the response. + assert not response.speech + assert "media" not in response.speech_slots + assert len(search_calls) == 2 # Search was called again + assert len(play_calls) == 1 # Play was not called again + + # Test feature not supported + hass.states.async_set( + entity_id, + STATE_IDLE, + attributes={}, + ) + with pytest.raises(intent.MatchFailedError): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "test query"}}, + ) + + # Test feature not supported (missing SEARCH_MEDIA) + hass.states.async_set( + entity_id, + STATE_IDLE, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.PLAY_MEDIA}, + ) + with pytest.raises(intent.MatchFailedError): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "test query"}}, + ) + + # Test play media service errors + search_results.append(search_result_item) + hass.states.async_set( + entity_id, + STATE_IDLE, + attributes={ATTR_SUPPORTED_FEATURES: MediaPlayerEntityFeature.SEARCH_MEDIA}, + ) + + async_mock_service( + hass, + DOMAIN, + SERVICE_PLAY_MEDIA, + raise_exception=HomeAssistantError("Play failed"), + ) + with pytest.raises(intent.MatchFailedError): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "play error query"}}, + ) + + # Test search service error + hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes) + async_mock_service( + hass, + DOMAIN, + SERVICE_SEARCH_MEDIA, + raise_exception=HomeAssistantError("Search failed"), + ) + with pytest.raises(intent.IntentHandleError, match="Error searching media"): + await intent.async_handle( + hass, + "test", + media_player_intent.INTENT_MEDIA_SEARCH_AND_PLAY, + {"search_query": {"value": "error query"}}, + ) From 6b35b069b26496a01e03b738e4d37a845ae4e983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 14 May 2025 22:05:29 +0100 Subject: [PATCH 0475/1175] Remove duplicated code in unit conversion util (#144912) --- homeassistant/util/unit_conversion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index f559512c1a7..e4312a7865f 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -151,8 +151,8 @@ class BaseUnitConverter: cls, from_unit: str | None, to_unit: str | None ) -> float: """Get floored base10 log ratio between units of measurement.""" - from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit) - return floor(max(0, log10(from_ratio / to_ratio))) + ratio = cls.get_unit_ratio(from_unit, to_unit) + return floor(max(0, log10(ratio))) @classmethod @lru_cache From 3b9d8e00bca62fbf99300742765209fabad06b4c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 23:13:37 +0200 Subject: [PATCH 0476/1175] Use runtime_data and HassKey in geofency (#144886) --- homeassistant/components/geofency/__init__.py | 20 ++++++++-------- .../components/geofency/device_tracker.py | 23 +++++++++++-------- tests/components/geofency/test_init.py | 3 +-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 0e364f0fac1..6ced8af8bc6 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -20,9 +20,12 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify +from homeassistant.util.hass_dict import HassKey from .const import DOMAIN +type GeofencyConfigEntry = ConfigEntry[set[str]] + PLATFORMS = [Platform.DEVICE_TRACKER] CONF_MOBILE_BEACONS = "mobile_beacons" @@ -75,15 +78,13 @@ WEBHOOK_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +_DATA_GEOFENCY: HassKey[list[str]] = HassKey(DOMAIN) + async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Set up the Geofency component.""" - config = hass_config.get(DOMAIN, {}) - mobile_beacons = config.get(CONF_MOBILE_BEACONS, []) - hass.data[DOMAIN] = { - "beacons": [slugify(beacon) for beacon in mobile_beacons], - "devices": set(), - } + mobile_beacons = hass_config.get(DOMAIN, {}).get(CONF_MOBILE_BEACONS, []) + hass.data[_DATA_GEOFENCY] = [slugify(beacon) for beacon in mobile_beacons] return True @@ -98,7 +99,7 @@ async def handle_webhook( text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY ) - if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]): + if _is_mobile_beacon(data, hass.data[_DATA_GEOFENCY]): return _set_location(hass, data, None) if data["entry"] == LOCATION_ENTRY: location_name = data["name"] @@ -139,8 +140,9 @@ def _set_location(hass, data, location_name): return web.Response(text=f"Setting location for {device}") -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool: """Configure based on config entry.""" + entry.runtime_data = set() webhook.async_register( hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) @@ -149,7 +151,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 54fd7598b9e..4a57eaab2f5 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,7 +1,6 @@ """Support for the Geofency device tracker platform.""" from homeassistant.components.device_tracker import TrackerEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -10,12 +9,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from . import DOMAIN, TRACKER_UPDATE +from . import TRACKER_UPDATE, GeofencyConfigEntry +from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: GeofencyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Geofency config entry.""" @@ -23,12 +23,14 @@ async def async_setup_entry( @callback def _receive_data(device, gps, location_name, attributes): """Fire HA event to set location.""" - if device in hass.data[DOMAIN]["devices"]: + if device in config_entry.runtime_data: return - hass.data[DOMAIN]["devices"].add(device) + config_entry.runtime_data.add(device) - async_add_entities([GeofencyEntity(device, gps, location_name, attributes)]) + async_add_entities( + [GeofencyEntity(config_entry, device, gps, location_name, attributes)] + ) config_entry.async_on_unload( async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) @@ -45,8 +47,8 @@ async def async_setup_entry( } if dev_ids: - hass.data[DOMAIN]["devices"].update(dev_ids) - async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids) + config_entry.runtime_data.update(dev_ids) + async_add_entities(GeofencyEntity(config_entry, dev_id) for dev_id in dev_ids) class GeofencyEntity(TrackerEntity, RestoreEntity): @@ -55,8 +57,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): _attr_has_entity_name = True _attr_name = None - def __init__(self, device, gps=None, location_name=None, attributes=None): + def __init__(self, entry, device, gps=None, location_name=None, attributes=None): """Set up Geofency entity.""" + self._entry = entry self._attr_extra_state_attributes = attributes or {} self._name = device self._attr_location_name = location_name @@ -93,7 +96,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): """Clean up after entity before removal.""" await super().async_will_remove_from_hass() self._unsub_dispatcher() - self.hass.data[DOMAIN]["devices"].remove(self.unique_id) + self._entry.runtime_data.remove(self.unique_id) @callback def _async_receive_data(self, device, gps, location_name, attributes): diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 33740397868..0e8752c97ec 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -318,12 +318,11 @@ async def test_load_unload_entry( state_1 = hass.states.get(f"device_tracker.{device_name}") assert state_1.state == STATE_HOME - assert len(hass.data[DOMAIN]["devices"]) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] + assert len(entry.runtime_data) == 1 assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert len(hass.data[DOMAIN]["devices"]) == 0 assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From 34c7c3f384deb58ea8d5674256d918f9102f2b18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 14 May 2025 23:14:02 +0200 Subject: [PATCH 0477/1175] Use runtime_data in homematicip_cloud (#144892) --- .../components/homematicip_cloud/__init__.py | 18 +++--- .../homematicip_cloud/alarm_control_panel.py | 7 +-- .../homematicip_cloud/binary_sensor.py | 7 +-- .../components/homematicip_cloud/button.py | 8 +-- .../components/homematicip_cloud/climate.py | 7 +-- .../components/homematicip_cloud/cover.py | 8 +-- .../components/homematicip_cloud/event.py | 8 +-- .../components/homematicip_cloud/hap.py | 6 +- .../components/homematicip_cloud/light.py | 8 +-- .../components/homematicip_cloud/lock.py | 7 +-- .../components/homematicip_cloud/sensor.py | 8 +-- .../components/homematicip_cloud/services.py | 61 +++++++++++-------- .../components/homematicip_cloud/switch.py | 8 +-- .../components/homematicip_cloud/weather.py | 8 +-- .../components/homematicip_cloud/conftest.py | 3 +- tests/components/homematicip_cloud/helper.py | 2 +- .../test_alarm_control_panel.py | 18 +----- .../homematicip_cloud/test_binary_sensor.py | 13 ---- .../homematicip_cloud/test_climate.py | 10 --- .../homematicip_cloud/test_cover.py | 11 ---- .../homematicip_cloud/test_device.py | 10 +-- .../components/homematicip_cloud/test_hap.py | 3 +- .../components/homematicip_cloud/test_init.py | 12 ++-- .../homematicip_cloud/test_light.py | 11 ---- .../components/homematicip_cloud/test_lock.py | 16 +---- .../homematicip_cloud/test_sensor.py | 16 +---- .../homematicip_cloud/test_switch.py | 11 ---- .../homematicip_cloud/test_weather.py | 11 ---- 28 files changed, 97 insertions(+), 219 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index c59a9d788b3..e460c162398 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -3,7 +3,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -21,7 +20,7 @@ from .const import ( HMIPC_HAPID, HMIPC_NAME, ) -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( @@ -45,8 +44,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" - hass.data[DOMAIN] = {} - accesspoints = config.get(DOMAIN, []) for conf in accesspoints: @@ -69,7 +66,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry) -> bool: """Set up an access point from a config entry.""" # 0.104 introduced config entry unique id, this makes upgrading possible @@ -81,8 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hap = HomematicipHAP(hass, entry) - hass.data[DOMAIN][entry.unique_id] = hap + entry.runtime_data = hap if not await hap.async_setup(): return False @@ -110,9 +107,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: HomematicIPConfigEntry +) -> bool: """Unload a config entry.""" - hap = hass.data[DOMAIN].pop(entry.unique_id) + hap = entry.runtime_data + assert hap.reset_connection_listener is not None hap.reset_connection_listener() await async_unload_services(hass) @@ -122,7 +122,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def _async_remove_obsolete_entities( - hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP + hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP ): """Remove obsolete entities from entity registry.""" diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index af57d8b0cd0..ddfe10fba54 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -11,13 +11,12 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, AlarmControlPanelState, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN -from .hap import AsyncHome, HomematicipHAP +from .hap import AsyncHome, HomematicIPConfigEntry, HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -26,11 +25,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data async_add_entities([HomematicipAlarmControlPanelEntity(hap)]) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index e135e95634d..9c0e5620022 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -34,14 +34,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" @@ -75,11 +74,11 @@ SAM_DEVICE_ATTRIBUTES = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [HomematicipCloudConnectionSensor(hap)] for device in hap.home.devices: if isinstance(device, AccelerationSensor): diff --git a/homeassistant/components/homematicip_cloud/button.py b/homeassistant/components/homematicip_cloud/button.py index 0d70ad53d54..31fa2c889ac 100644 --- a/homeassistant/components/homematicip_cloud/button.py +++ b/homeassistant/components/homematicip_cloud/button.py @@ -5,22 +5,20 @@ from __future__ import annotations from homematicip.device import WallMountedGarageDoorController from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP button from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data async_add_entities( HomematicipGarageDoorControllerButton(hap, device) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 0952f17d3ec..7f393cf52bd 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -24,7 +24,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo @@ -32,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} @@ -55,11 +54,11 @@ HMIP_ECO_CM = "ECO" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP climate from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data async_add_entities( HomematicipHeatingGroup(hap, device) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 317024658e1..f9986e0c526 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -21,13 +21,11 @@ from homeassistant.components.cover import ( CoverDeviceClass, CoverEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP HMIP_COVER_OPEN = 0 HMIP_COVER_CLOSED = 1 @@ -37,11 +35,11 @@ HMIP_SLATS_CLOSED = 1 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP cover from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [ HomematicipCoverShutterGroup(hap, group) for group in hap.home.groups diff --git a/homeassistant/components/homematicip_cloud/event.py b/homeassistant/components/homematicip_cloud/event.py index fc7f43bad1a..101c3e3015a 100644 --- a/homeassistant/components/homematicip_cloud/event.py +++ b/homeassistant/components/homematicip_cloud/event.py @@ -13,13 +13,11 @@ from homeassistant.components.event import ( EventEntity, EventEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP @dataclass(frozen=True, kw_only=True) @@ -44,11 +42,11 @@ EVENT_DESCRIPTIONS = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP cover from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] entities.extend( diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 6f98836a1ff..86630c2896c 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -25,6 +25,8 @@ from .errors import HmipcConnectionError _LOGGER = logging.getLogger(__name__) +type HomematicIPConfigEntry = ConfigEntry[HomematicipHAP] + async def build_context_async( hass: HomeAssistant, hapid: str | None, authtoken: str | None @@ -102,7 +104,9 @@ class HomematicipHAP: home: AsyncHome - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: HomematicIPConfigEntry + ) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 338599b9a14..855f5851d73 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -28,22 +28,20 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] for device in hap.home.devices: if isinstance(device, BrandSwitchMeasuring): diff --git a/homeassistant/components/homematicip_cloud/lock.py b/homeassistant/components/homematicip_cloud/lock.py index 04461682f8d..bae075e1a17 100644 --- a/homeassistant/components/homematicip_cloud/lock.py +++ b/homeassistant/components/homematicip_cloud/lock.py @@ -9,12 +9,11 @@ from homematicip.base.enums import LockState, MotorState from homematicip.device import DoorLockDrive from homeassistant.components.lock import LockEntity, LockEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity +from .hap import HomematicIPConfigEntry from .helpers import handle_errors _LOGGER = logging.getLogger(__name__) @@ -36,11 +35,11 @@ DEVICE_DLD_ATTRIBUTES = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP locks from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data async_add_entities( HomematicipDoorLockDrive(hap, device) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ba739273788..4f43e6d6ca7 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -44,7 +44,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, LIGHT_LUX, @@ -61,9 +60,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP from .helpers import get_channels_from_device ATTR_CURRENT_ILLUMINATION = "current_illumination" @@ -96,11 +94,11 @@ ILLUMINATION_DEVICE_ATTRIBUTES = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] for device in hap.home.devices: if isinstance(device, HomeControlAccessPoint): diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 4518c7736eb..2e76a0b7aac 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -22,6 +22,7 @@ from homeassistant.helpers.service import ( ) from .const import DOMAIN +from .hap import HomematicIPConfigEntry _LOGGER = logging.getLogger(__name__) @@ -218,7 +219,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: async def async_unload_services(hass: HomeAssistant): """Unload HomematicIP Cloud services.""" - if hass.data[DOMAIN]: + if hass.config_entries.async_loaded_entries(DOMAIN): return for hmipc_service in HMIPC_SERVICES: @@ -235,8 +236,9 @@ async def _async_activate_eco_mode_with_duration( if home := _get_home(hass, hapid): await home.activate_absence_with_duration_async(duration) else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_duration_async(duration) + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.activate_absence_with_duration_async(duration) async def _async_activate_eco_mode_with_period( @@ -249,8 +251,9 @@ async def _async_activate_eco_mode_with_period( if home := _get_home(hass, hapid): await home.activate_absence_with_period_async(endtime) else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_period_async(endtime) + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.activate_absence_with_period_async(endtime) async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: @@ -262,8 +265,9 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> if home := _get_home(hass, hapid): await home.activate_vacation_async(endtime, temperature) else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_vacation_async(endtime, temperature) + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.activate_vacation_async(endtime, temperature) async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: @@ -272,8 +276,9 @@ async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) if home := _get_home(hass, hapid): await home.deactivate_absence_async() else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_absence_async() + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.deactivate_absence_async() async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: @@ -282,8 +287,9 @@ async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) if home := _get_home(hass, hapid): await home.deactivate_vacation_async() else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_vacation_async() + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.deactivate_vacation_async() async def _set_active_climate_profile( @@ -293,14 +299,15 @@ async def _set_active_climate_profile( entity_id_list = service.data[ATTR_ENTITY_ID] climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 - for hap in hass.data[DOMAIN].values(): + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): if entity_id_list != "all": for entity_id in entity_id_list: - group = hap.hmip_device_by_entity_id.get(entity_id) + group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id) if group and isinstance(group, HeatingGroup): await group.set_active_profile_async(climate_profile_index) else: - for group in hap.home.groups: + for group in entry.runtime_data.home.groups: if isinstance(group, HeatingGroup): await group.set_active_profile_async(climate_profile_index) @@ -313,8 +320,10 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] anonymize = service.data[ATTR_ANONYMIZE] - for hap in hass.data[DOMAIN].values(): - hap_sgtin = hap.config_entry.unique_id + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + hap_sgtin = entry.unique_id + assert hap_sgtin is not None if anonymize: hap_sgtin = hap_sgtin[-4:] @@ -323,7 +332,7 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N path = Path(config_path) config_file = path / file_name - json_state = await hap.home.download_configuration_async() + json_state = await entry.runtime_data.home.download_configuration_async() json_state = handle_config(json_state, anonymize) config_file.write_text(json_state, encoding="utf8") @@ -333,14 +342,15 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall) """Service to reset the energy counter.""" entity_id_list = service.data[ATTR_ENTITY_ID] - for hap in hass.data[DOMAIN].values(): + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): if entity_id_list != "all": for entity_id in entity_id_list: - device = hap.hmip_device_by_entity_id.get(entity_id) + device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id) if device and isinstance(device, SwitchMeasuring): await device.reset_energy_counter_async() else: - for device in hap.home.devices: + for device in entry.runtime_data.home.devices: if isinstance(device, SwitchMeasuring): await device.reset_energy_counter_async() @@ -353,14 +363,17 @@ async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall if home := _get_home(hass, hapid): await home.set_cooling_async(cooling) else: - for hap in hass.data[DOMAIN].values(): - await hap.home.set_cooling_async(cooling) + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + await entry.runtime_data.home.set_cooling_async(cooling) def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" - if hap := hass.data[DOMAIN].get(hapid): - return hap.home + entry: HomematicIPConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.unique_id == hapid: + return entry.runtime_data.home raise ServiceValidationError( translation_domain=DOMAIN, diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 2de02fb22a5..4927d9a32df 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -23,22 +23,20 @@ from homematicip.device import ( from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import ATTR_GROUP_MEMBER_UNREACHABLE, HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP switch from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [ HomematicipGroupSwitch(hap, group) for group in hap.home.groups diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 78e86ec652c..061f6642bb2 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -18,14 +18,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, WeatherEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfSpeed, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import HomematicipGenericEntity -from .hap import HomematicipHAP +from .hap import HomematicIPConfigEntry, HomematicipHAP HOME_WEATHER_CONDITION = { WeatherCondition.CLEAR: ATTR_CONDITION_SUNNY, @@ -48,11 +46,11 @@ HOME_WEATHER_CONDITION = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HomematicIPConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" - hap = hass.data[DOMAIN][config_entry.unique_id] + hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] for device in hap.home.devices: if isinstance(device, WeatherSensorPro): diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 8672dfedd13..bcadf407950 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -97,7 +97,8 @@ async def mock_hap_with_service_fixture( mock_hap = await default_mock_hap_factory.async_get_mock_hap() await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() - hass.data[HMIPC_DOMAIN] = {HAPID: mock_hap} + entry = hass.config_entries.async_entries(HMIPC_DOMAIN)[0] + entry.runtime_data = mock_hap return mock_hap diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 78c03c6847c..946ccc569a4 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -120,7 +120,7 @@ class HomeFactory: await self.hass.async_block_till_done() - hap = self.hass.data[HMIPC_DOMAIN][HAPID] + hap = self.hmip_config_entry.runtime_data mock_home.on_update(hap.async_update) mock_home.on_create(hap.async_create_entity) return hap diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 853660ceac6..df83560b893 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -2,13 +2,8 @@ from homematicip.async_home import AsyncHome -from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, - AlarmControlPanelState, -) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.alarm_control_panel import AlarmControlPanelState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, get_and_check_entity_basics @@ -39,17 +34,6 @@ async def _async_manipulate_security_zones( await hass.async_block_till_done() -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, - ALARM_CONTROL_PANEL_DOMAIN, - {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}}, - ) - - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_alarm_control_panel( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 02e96b10fe8..4f6913cc8e8 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -2,8 +2,6 @@ from homematicip.base.enums import SmokeDetectorAlarmType, WindowState -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_ACCELERATION_SENSOR_MODE, ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, @@ -25,21 +23,10 @@ from homeassistant.components.homematicip_cloud.entity import ( ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, - BINARY_SENSOR_DOMAIN, - {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_home_cloud_connection_sensor( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index c39d4fa2d99..28d0fca0d80 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate import ( ATTR_HVAC_ACTION, ATTR_PRESET_MODE, ATTR_PRESET_MODES, - DOMAIN as CLIMATE_DOMAIN, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -26,7 +25,6 @@ from homeassistant.components.homematicip_cloud.climate import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from homeassistant.setup import async_setup_component from .helper import ( HAPID, @@ -36,14 +34,6 @@ from .helper import ( ) -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_heating_group_heat( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index aa104da0546..b005090309b 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -5,25 +5,14 @@ from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, - DOMAIN as COVER_DOMAIN, CoverState, ) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_cover_shutter( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index fd72f275489..abd0e18b368 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -4,18 +4,12 @@ from unittest.mock import patch from homematicip.base.enums import EventType -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from .helper import ( - HAPID, - HomeFactory, - async_manipulate_test_data, - get_and_check_entity_basics, -) +from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics from tests.common import MockConfigEntry @@ -115,7 +109,7 @@ async def test_hmip_add_device( assert len(device_registry.devices) == pre_device_count assert len(entity_registry.entities) == pre_entity_count - new_hap = hass.data[HMIPC_DOMAIN][HAPID] + new_hap = hmip_config_entry.runtime_data assert len(new_hap.hmip_device_by_entity_id) == pre_mapping_count diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index e34424d3439..13aaa4d83ba 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -119,14 +119,13 @@ async def test_hap_reset_unloads_entry_if_setup( ) -> None: """Test calling reset while the entry has been setup.""" mock_hap = await default_mock_hap_factory.async_get_mock_hap() - assert hass.data[HMIPC_DOMAIN][HAPID] == mock_hap config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 + assert config_entries[0].runtime_data == mock_hap # hap_reset is called during unload await hass.config_entries.async_unload(config_entries[0].entry_id) # entry is unloaded assert config_entries[0].state is ConfigEntryState.NOT_LOADED - assert hass.data[HMIPC_DOMAIN] == {} async def test_hap_create( diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index f28b3870705..172119a556c 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -34,8 +34,6 @@ async def test_config_with_accesspoint_passed_to_config_entry( } # no config_entry exists assert len(hass.config_entries.async_entries(HMIPC_DOMAIN)) == 0 - # no acccesspoint exists - assert not hass.data.get(HMIPC_DOMAIN) with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", @@ -53,7 +51,7 @@ async def test_config_with_accesspoint_passed_to_config_entry( "name": "name", } # defined access_point created for config_entry - assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP) + assert isinstance(config_entries[0].runtime_data, HomematicipHAP) async def test_config_already_registered_not_passed_to_config_entry( @@ -118,7 +116,7 @@ async def test_load_entry_fails_due_to_connection_error( ): assert await async_setup_component(hass, HMIPC_DOMAIN, {}) - assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] + assert hmip_config_entry.runtime_data assert hmip_config_entry.state is ConfigEntryState.SETUP_RETRY @@ -136,7 +134,7 @@ async def test_load_entry_fails_due_to_generic_exception( ): assert await async_setup_component(hass, HMIPC_DOMAIN, {}) - assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] + assert hmip_config_entry.runtime_data assert hmip_config_entry.state is ConfigEntryState.SETUP_ERROR @@ -159,14 +157,12 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert mock_hap.return_value.mock_calls[0][0] == "async_setup" - assert hass.data[HMIPC_DOMAIN]["ABC123"] config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 + assert config_entries[0].runtime_data assert config_entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entries[0].entry_id) assert config_entries[0].state is ConfigEntryState.NOT_LOADED - # entry is unloaded - assert hass.data[HMIPC_DOMAIN] == {} async def test_hmip_dump_hap_config_services( diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 48d9beccacc..b929bd337cc 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -2,7 +2,6 @@ from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -10,25 +9,15 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, - DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntityFeature, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_light( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_lock.py b/tests/components/homematicip_cloud/test_lock.py index dd581cce044..3805f0f08de 100644 --- a/tests/components/homematicip_cloud/test_lock.py +++ b/tests/components/homematicip_cloud/test_lock.py @@ -5,28 +5,14 @@ from unittest.mock import patch from homematicip.base.enums import LockState as HomematicLockState, MotorState import pytest -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.lock import ( - DOMAIN as LOCK_DOMAIN, - LockEntityFeature, - LockState, -) +from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ATTR_SUPPORTED_FEATURES from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, LOCK_DOMAIN, {LOCK_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_doorlockdrive( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index eebee050d51..3b5773cfa4d 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -2,7 +2,6 @@ from homematicip.base.enums import ValveState -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.entity import ( ATTR_CONFIG_PENDING, ATTR_DEVICE_OVERHEATED, @@ -23,11 +22,7 @@ from homeassistant.components.homematicip_cloud.sensor import ( ATTR_WIND_DIRECTION, ATTR_WIND_DIRECTION_VARIATION, ) -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - DOMAIN as SENSOR_DOMAIN, - SensorStateClass, -) +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, LIGHT_LUX, @@ -39,19 +34,10 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_accesspoint_status( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index bd7952025bc..1a728bfecd4 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -1,25 +1,14 @@ """Tests for HomematicIP Cloud switch.""" -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.entity import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_switch( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index 44df907fcc5..ad97baf485b 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -1,28 +1,17 @@ """Tests for HomematicIP Cloud weather.""" -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.weather import ( ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .helper import HomeFactory, async_manipulate_test_data, get_and_check_entity_basics -async def test_manually_configured_platform(hass: HomeAssistant) -> None: - """Test that we do not set up an access point.""" - assert await async_setup_component( - hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}} - ) - assert not hass.data.get(HMIPC_DOMAIN) - - async def test_hmip_weather_sensor( hass: HomeAssistant, default_mock_hap_factory: HomeFactory ) -> None: From 2050b0b37515bb503b01eee4fff09ea6062ea820 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 14 May 2025 23:23:18 +0200 Subject: [PATCH 0478/1175] Add another EHS SmartThings fixture (#144920) * Add another EHS SmartThings fixture * Add another EHS --- tests/components/smartthings/conftest.py | 2 + .../device_status/da_ac_ehs_01001.json | 744 +++++++++++++++ .../device_status/da_sac_ehs_000002_sub.json | 868 ++++++++++++++++++ .../fixtures/devices/da_ac_ehs_01001.json | 229 +++++ .../devices/da_sac_ehs_000002_sub.json | 308 +++++++ .../smartthings/snapshots/test_init.ambr | 66 ++ .../smartthings/snapshots/test_sensor.ambr | 756 +++++++++++++++ .../smartthings/snapshots/test_switch.ambr | 94 ++ 8 files changed, 3067 insertions(+) create mode 100644 tests/components/smartthings/fixtures/device_status/da_ac_ehs_01001.json create mode 100644 tests/components/smartthings/fixtures/device_status/da_sac_ehs_000002_sub.json create mode 100644 tests/components/smartthings/fixtures/devices/da_ac_ehs_01001.json create mode 100644 tests/components/smartthings/fixtures/devices/da_sac_ehs_000002_sub.json diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b3a58b17637..be744ef7c33 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -118,6 +118,8 @@ def mock_smartthings() -> Generator[AsyncMock]: "vd_sensor_light_2023", "iphone", "da_sac_ehs_000001_sub", + "da_sac_ehs_000002_sub", + "da_ac_ehs_01001", "da_wm_dw_000001", "da_wm_wd_000001", "da_wm_wd_000001_1", diff --git a/tests/components/smartthings/fixtures/device_status/da_ac_ehs_01001.json b/tests/components/smartthings/fixtures/device_status/da_ac_ehs_01001.json new file mode 100644 index 00000000000..2214ed3c3e6 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_ac_ehs_01001.json @@ -0,0 +1,744 @@ +{ + "components": { + "main": { + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 38, + "unit": "C", + "timestamp": "2025-05-14T19:29:59.586Z" + }, + "maximumSetpoint": { + "value": 69, + "unit": "C", + "timestamp": "2025-05-14T19:29:59.586Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["eco", "std", "power", "force"], + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "airConditionerMode": { + "value": "std", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.deviceIdentification": { + "micomAssayCode": { + "value": null + }, + "modelName": { + "value": null + }, + "serialNumber": { + "value": null + }, + "serialNumberExtra": { + "value": null + }, + "modelClassificationCode": { + "value": null + }, + "description": { + "value": null + }, + "releaseYear": { + "value": null + }, + "binaryId": { + "value": "TP1X_DA_AC_EHS_01001_0000", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "switch": { + "switch": { + "value": "off", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "ocf": { + "st": { + "value": null + }, + "mndt": { + "value": null + }, + "mnfv": { + "value": "AEH-WW-TP1-22-AE6000_17240903", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnhw": { + "value": "Realtek", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "di": { + "value": "4165c51e-bf6b-c5b6-fd53-127d6248754b", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnsl": { + "value": "http://www.samsung.com", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "dmv": { + "value": "1.2.1", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "n": { + "value": "Samsung EHS", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnmo": { + "value": "TP1X_DA_AC_EHS_01001_0000|10250141|60070110001711034A00010000002000", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "vid": { + "value": "DA-AC-EHS-01001", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnml": { + "value": "http://www.samsung.com", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnpv": { + "value": "DAWIT 2.0", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "mnos": { + "value": "TizenRT 3.1", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "pi": { + "value": "4165c51e-bf6b-c5b6-fd53-127d6248754b", + "timestamp": "2025-04-13T13:07:05.925Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-04-13T13:07:05.925Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "off", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "samsungce.alwaysOnSensing", + "samsungce.sacDisplayCondition" + ], + "timestamp": "2025-04-13T13:07:09.182Z" + } + }, + "samsungce.sensingOnSuspendMode": { + "sensingOnSuspendMode": { + "value": "unavailable", + "timestamp": "2025-04-13T13:00:53.287Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 25010101, + "timestamp": "2025-04-13T13:00:53.287Z" + } + }, + "sec.diagnosticsInformation": { + "logType": { + "value": ["errCode", "dump"], + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "endpoint": { + "value": "SSM", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "minVersion": { + "value": "3.0", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "signinPermission": { + "value": null + }, + "setupId": { + "value": "AE0", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "protocolType": { + "value": "ble_ocf", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "tsId": { + "value": "DA01", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "mnId": { + "value": "0AJT", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "dumpType": { + "value": "file", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 57, + "unit": "C", + "timestamp": "2025-05-14T19:51:09.752Z" + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": "enabled", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 56, + "unit": "C", + "timestamp": "2025-05-14T19:29:59.586Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": 0, + "duration": 0, + "override": false + }, + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 4053792, + "deltaEnergy": 0, + "power": 0, + "powerEnergy": 0.0, + "persistedEnergy": 0, + "energySaved": 0, + "persistedSavedEnergy": 0, + "start": "2025-05-13T23:00:23Z", + "end": "2025-05-14T13:26:17Z" + }, + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.ehsCycleData": { + "outdoor": { + "value": [ + { + "timestamp": "2025-05-13T22:45:05Z", + "data": "0000000050624249410207D002580000FFFF00350032A05A00000000" + }, + { + "timestamp": "2025-05-13T22:50:07Z", + "data": "001400145B683E414102015A02120002FFFF002F007CA06200000000" + }, + { + "timestamp": "2025-05-13T22:55:06Z", + "data": "00000000586643494102000000000000FFFF003D003BA06200000000" + } + ], + "unit": "C", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "indoor": { + "value": [ + { + "timestamp": "2025-05-13T22:45:05Z", + "data": "4B0559590505014264000000000000000001000000021F1C0000007505054B" + }, + { + "timestamp": "2025-05-13T22:50:07Z", + "data": "5C055D5E0505013A64000000000000000001000000021F210000007505054B" + }, + { + "timestamp": "2025-05-13T22:55:06Z", + "data": "49055D5D0505000000000000000000000000000000021F260000007505054B" + } + ], + "unit": "C", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "custom.outingMode": { + "outingMode": { + "value": "off", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": "disconnected", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.individualControlLock": { + "lockState": { + "value": "unlocked", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.alwaysOnSensing": { + "origins": { + "value": null + }, + "alwaysOn": { + "value": null + } + }, + "refresh": {}, + "samsungce.ehsFsvSettings": { + "fsvSettings": { + "value": [ + { + "id": "1031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 37, + "maxValue": 75, + "value": 65, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 15, + "maxValue": 37, + "value": 26, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1051", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 50, + "maxValue": 70, + "value": 69, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1052", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 30, + "maxValue": 40, + "value": 38, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2011", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -20, + "maxValue": 5, + "value": -5, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 10, + "maxValue": 20, + "value": 10, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2021", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 70, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2022", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 45, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 70, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2091", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 0, + "isValid": true + }, + { + "id": "2092", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 0, + "isValid": true + }, + { + "id": "2093", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 1, + "maxValue": 4, + "value": 2, + "isValid": true + }, + { + "id": "3011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 1, + "isValid": true + }, + { + "id": "3071", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + }, + { + "id": "4011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 1, + "isValid": true + }, + { + "id": "4012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -15, + "maxValue": 20, + "value": 0, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4021", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 0, + "isValid": true + }, + { + "id": "4042", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 5, + "maxValue": 15, + "value": 10, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4061", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + } + ], + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "execute": { + "data": { + "value": null + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": null + } + }, + "sec.wifiConfiguration": { + "autoReconnection": { + "value": true, + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "minVersion": { + "value": "1.0", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "supportedWiFiFreq": { + "value": ["2.4G"], + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "supportedAuthType": { + "value": ["OPEN", "WEP", "WPA-PSK", "WPA2-PSK", "SAE"], + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "protocolType": { + "value": ["helper_hotspot", "ble_ocf"], + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.softwareVersion": { + "versions": { + "value": [ + { + "id": "0", + "swType": "Software", + "versionNumber": "02504A240903", + "description": "Version" + }, + { + "id": "1", + "swType": "Firmware", + "versionNumber": "02501A24062401,FFFFFFFFFFFFFF", + "description": "Version" + }, + { + "id": "2", + "swType": "Outdoor", + "versionNumber": "02572A23081000,02549A10000800", + "description": "Version" + } + ], + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "custom.energyType": { + "energyType": { + "value": null + }, + "energySavingSupport": { + "value": true, + "timestamp": "2025-04-13T13:00:53.287Z" + }, + "drMaxDuration": { + "value": 99999999, + "unit": "min", + "timestamp": "2025-04-13T13:00:53.287Z" + }, + "energySavingLevel": { + "value": null + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": null + }, + "energySavingOperation": { + "value": false, + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": true, + "timestamp": "2025-04-13T13:00:53.287Z" + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": { + "newVersion": "00000000", + "currentVersion": "00000000", + "moduleType": "mainController" + }, + "timestamp": "2025-05-11T20:13:06.918Z" + }, + "otnDUID": { + "value": "7XCFUCFWT6VB4", + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-04-13T13:00:53.287Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "operatingState": { + "value": "none", + "timestamp": "2025-05-11T20:13:06.918Z" + }, + "progress": { + "value": null + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-05-14T19:29:59.586Z" + } + } + }, + "INDOOR1": { + "samsungce.ehsThermostat": { + "connectionState": { + "value": "disconnected", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "off", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 18.5, + "unit": "C", + "timestamp": "2025-05-14T19:54:55.948Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 26, + "unit": "C", + "timestamp": "2025-05-14T19:54:55.948Z" + }, + "maximumSetpoint": { + "value": 65, + "unit": "C", + "timestamp": "2025-05-14T19:54:55.948Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["cool", "heat", "auto"], + "timestamp": "2025-05-14T13:26:17.184Z" + }, + "airConditionerMode": { + "value": "heat", + "timestamp": "2025-05-14T13:26:17.184Z" + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-05-14T19:54:55.948Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 35, + "unit": "C", + "timestamp": "2025-05-14T19:54:55.948Z" + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": null + } + }, + "switch": { + "switch": { + "value": "off", + "timestamp": "2025-05-14T13:26:17.184Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000002_sub.json b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000002_sub.json new file mode 100644 index 00000000000..06f91fbe8b3 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000002_sub.json @@ -0,0 +1,868 @@ +{ + "components": { + "main": { + "samsungce.ehsBoosterHeater": { + "status": { + "value": "off", + "timestamp": "2025-05-08T10:20:02.885Z" + } + }, + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 40, + "unit": "C", + "timestamp": "2025-05-05T03:39:24.310Z" + }, + "maximumSetpoint": { + "value": 57, + "unit": "C", + "timestamp": "2025-05-05T03:39:24.310Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["eco", "std", "power", "force"], + "timestamp": "2025-01-16T18:03:09.830Z" + }, + "airConditionerMode": { + "value": "std", + "timestamp": "2025-05-09T02:59:47.311Z" + } + }, + "samsungce.deviceIdentification": { + "micomAssayCode": { + "value": null + }, + "modelName": { + "value": null + }, + "serialNumber": { + "value": null + }, + "serialNumberExtra": { + "value": null + }, + "modelClassificationCode": { + "value": null + }, + "description": { + "value": null + }, + "releaseYear": { + "value": 22, + "timestamp": "2025-03-31T04:25:24.686Z" + }, + "binaryId": { + "value": "SAC_EHS_SPLIT", + "timestamp": "2025-05-08T18:03:08.376Z" + } + }, + "switch": { + "switch": { + "value": "on", + "timestamp": "2025-05-09T04:25:00.539Z" + } + }, + "ocf": { + "st": { + "value": "2025-05-04T18:37:15Z", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mndt": { + "value": "", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnfv": { + "value": "20250317.1", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnhw": { + "value": "", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "di": { + "value": "3810e5ad-5351-d9f9-12ff-000001200000", + "timestamp": "2025-05-08T18:03:08.220Z" + }, + "mnsl": { + "value": "", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2025-05-08T18:03:08.220Z" + }, + "n": { + "value": "Eco Heating System", + "timestamp": "2025-05-08T18:03:08.220Z" + }, + "mnmo": { + "value": "SAC_EHS_SPLIT|220614|61007300001600000400000000000000", + "timestamp": "2025-05-08T18:03:08.376Z" + }, + "vid": { + "value": "DA-SAC-EHS-000002-SUB", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnml": { + "value": "", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnpv": { + "value": "4.0", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "mnos": { + "value": "Tizen", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "pi": { + "value": "3810e5ad-5351-d9f9-12ff-000001200000", + "timestamp": "2025-05-08T18:03:08.223Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-05-08T18:03:08.220Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "on", + "timestamp": "2025-01-18T15:00:57.101Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "remoteControlStatus", + "thermostatHeatingSetpoint", + "samsungce.systemAirConditionerReservation", + "demandResponseLoadControl" + ], + "timestamp": "2025-04-01T04:45:26.332Z" + } + }, + "samsungce.sensingOnSuspendMode": { + "sensingOnSuspendMode": { + "value": "available", + "timestamp": "2025-03-31T04:25:24.686Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 25010101, + "timestamp": "2025-03-31T05:10:13.818Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 49.6, + "unit": "C", + "timestamp": "2025-05-09T04:55:51.712Z" + } + }, + "thermostatHeatingSetpoint": { + "heatingSetpoint": { + "value": null + }, + "heatingSetpointRange": { + "value": null + } + }, + "samsungce.ehsDiverterValve": { + "position": { + "value": "room", + "timestamp": "2025-05-09T03:33:56.476Z" + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": "enabled", + "timestamp": "2025-01-16T11:17:32.484Z" + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2025-05-08T20:17:09.388Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-01-16T11:17:32.484Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 52, + "unit": "C", + "timestamp": "2025-05-05T03:39:24.310Z" + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": -1, + "start": "1970-01-01T00:00:00Z", + "duration": 0, + "override": false + }, + "timestamp": "2025-01-16T18:03:09.830Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 9575308.0, + "deltaEnergy": 45.0, + "power": 0.015, + "powerEnergy": 0.22207609332044917, + "persistedEnergy": 9575308.0, + "energySaved": 0, + "start": "2025-05-09T04:39:01Z", + "end": "2025-05-09T05:02:01Z" + }, + "timestamp": "2025-05-09T05:02:01.788Z" + } + }, + "samsungce.ehsCycleData": { + "outdoor": { + "value": [ + { + "timestamp": "2025-05-08T19:43:06Z", + "data": "0000000063753CFF3C020050027600000000" + }, + { + "timestamp": "2025-05-08T19:48:06Z", + "data": "000000005A7442FF3F0201E0000000000000" + }, + { + "timestamp": "2025-05-08T19:53:06Z", + "data": "00000000577441FF3E0201E0000000000000" + } + ], + "unit": "C", + "timestamp": "2025-05-09T04:57:00.361Z" + }, + "indoor": { + "value": [ + { + "timestamp": "2025-05-08T19:43:06Z", + "data": "565856575805002B640000000101000000000000000E0BB2" + }, + { + "timestamp": "2025-05-08T19:48:06Z", + "data": "5155575757050000000000000101000000000000000E0BB7" + }, + { + "timestamp": "2025-05-08T19:53:06Z", + "data": "535556565705002B640000000101000000000000000E0BBA" + } + ], + "unit": "C", + "timestamp": "2025-05-09T04:57:00.361Z" + } + }, + "custom.outingMode": { + "outingMode": { + "value": "off", + "timestamp": "2025-01-16T11:17:32.257Z" + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": "disconnected", + "timestamp": "2025-01-16T11:17:32.210Z" + } + }, + "refresh": {}, + "samsungce.ehsFsvSettings": { + "fsvSettings": { + "value": [ + { + "id": "1031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 37, + "maxValue": 65, + "value": 43, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 15, + "maxValue": 37, + "value": 25, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1051", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 50, + "maxValue": 70, + "value": 57, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1052", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 30, + "maxValue": 40, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2011", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -20, + "maxValue": 5, + "value": -10, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 10, + "maxValue": 20, + "value": 20, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2021", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 65, + "value": 37, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2022", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 65, + "value": 25, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 65, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 65, + "value": 25, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2091", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 0, + "isValid": true + }, + { + "id": "2092", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 0, + "isValid": true + }, + { + "id": "2093", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 1, + "maxValue": 4, + "value": 4, + "isValid": true + }, + { + "id": "3011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 1, + "isValid": true + }, + { + "id": "3071", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + }, + { + "id": "4011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + }, + { + "id": "4012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -15, + "maxValue": 20, + "value": 0, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4021", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 0, + "isValid": true + }, + { + "id": "4042", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 5, + "maxValue": 15, + "value": 10, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4061", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 1, + "isValid": true + } + ], + "timestamp": "2025-04-25T02:52:46.974Z" + } + }, + "execute": { + "data": { + "value": { + "payload": { + "rt": ["x.com.samsung.da.information"], + "if": ["oic.if.a"], + "x.com.samsung.da.modelNum": "SAC_EHS_SPLIT|220614|61007300001600000400000000000000", + "x.com.samsung.da.description": "EHS_TANK", + "x.com.samsung.da.serialNum": "0TYZPAOTC00301P", + "x.com.samsung.da.versionId": "Samsung Electronics", + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.number": "DB91-02102A 2023-09-14", + "x.com.samsung.da.type": "Software", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Version" + }, + { + "x.com.samsung.da.number": "DB91-02100A 2020-07-10", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Version" + }, + { + "x.com.samsung.da.number": "DB91-02103B 2022-06-14", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.id": "2", + "x.com.samsung.da.description": "" + }, + { + "x.com.samsung.da.number": "DB91-02091B 2022-08-02", + "x.com.samsung.da.type": "Firmware", + "x.com.samsung.da.newVersionAvailable": "false", + "x.com.samsung.da.id": "3", + "x.com.samsung.da.description": "EHS SPLIT" + } + ] + } + }, + "data": { + "href": "/information/vs/0" + }, + "timestamp": "2024-03-25T19:40:05.820Z" + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": "enabled", + "timestamp": "2025-01-16T11:17:32.301Z" + } + }, + "samsungce.softwareVersion": { + "versions": { + "value": [ + { + "id": "0", + "swType": "Software", + "versionNumber": "DB91-02102A 2025-03-17", + "description": "Version" + }, + { + "id": "1", + "swType": "Firmware", + "versionNumber": "DB91-02100A 2020-07-10", + "description": "Version" + }, + { + "id": "2", + "swType": "Firmware", + "versionNumber": "DB91-02103B 2022-06-14", + "description": "" + }, + { + "id": "3", + "swType": "Firmware", + "versionNumber": "DB91-02091B 2022-08-02", + "description": "EHS SPLIT" + } + ], + "timestamp": "2025-04-28T03:40:34.481Z" + } + }, + "remoteControlStatus": { + "remoteControlEnabled": { + "value": "true", + "timestamp": "2025-01-16T11:17:32.469Z" + } + }, + "custom.energyType": { + "energyType": { + "value": "2.0", + "timestamp": "2025-01-16T18:03:09.830Z" + }, + "energySavingSupport": { + "value": false, + "timestamp": "2023-10-05T18:12:48.916Z" + }, + "drMaxDuration": { + "value": null + }, + "energySavingLevel": { + "value": null + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": null + }, + "energySavingOperation": { + "value": null + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": null + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": null + }, + "otnDUID": { + "value": null + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-01-16T11:17:32.328Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-01-16T11:17:32.328Z" + }, + "operatingState": { + "value": null + }, + "progress": { + "value": null + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-01-16T11:17:32.266Z" + } + } + }, + "INDOOR1": { + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": "disconnected", + "timestamp": "2025-01-16T11:17:32.378Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "on", + "timestamp": "2025-01-16T11:17:32.176Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.systemAirConditionerReservation"], + "timestamp": "2025-03-31T04:25:24.686Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 31.2, + "unit": "C", + "timestamp": "2025-05-09T04:57:52.869Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": -1000, + "unit": "C", + "timestamp": "2025-01-22T11:43:49.976Z" + }, + "maximumSetpoint": { + "value": -1000, + "unit": "C", + "timestamp": "2025-01-22T11:43:49.976Z" + } + }, + "samsungce.ehsDefrostMode": { + "status": { + "value": "off", + "timestamp": "2025-05-07T01:00:50.612Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["auto", "cool", "heat"], + "timestamp": "2025-01-16T11:17:32.378Z" + }, + "airConditionerMode": { + "value": "auto", + "timestamp": "2025-01-22T11:43:43.266Z" + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-01-16T11:17:32.225Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 24, + "unit": "C", + "timestamp": "2025-01-22T11:43:49.976Z" + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": "enabled", + "timestamp": "2025-01-16T11:17:32.176Z" + } + }, + "switch": { + "switch": { + "value": "on", + "timestamp": "2025-05-08T18:03:08.376Z" + } + } + }, + "INDOOR2": { + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": "disconnected", + "timestamp": "2025-01-16T11:17:32.378Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "off", + "timestamp": "2025-01-16T11:17:32.247Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.systemAirConditionerReservation"], + "timestamp": "2025-03-31T04:25:24.686Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 29.1, + "unit": "C", + "timestamp": "2025-05-09T04:47:04.597Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": -1000, + "unit": "C", + "timestamp": "2025-01-22T11:43:54.947Z" + }, + "maximumSetpoint": { + "value": -1000, + "unit": "C", + "timestamp": "2025-01-22T11:43:54.947Z" + } + }, + "samsungce.ehsDefrostMode": { + "status": { + "value": "off", + "timestamp": "2025-05-07T01:00:50.612Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["auto", "cool", "heat"], + "timestamp": "2025-01-16T11:17:32.378Z" + }, + "airConditionerMode": { + "value": "auto", + "timestamp": "2025-01-22T11:43:43.266Z" + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-01-16T11:17:32.413Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 24, + "unit": "C", + "timestamp": "2025-01-22T11:43:54.947Z" + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": "enabled", + "timestamp": "2025-01-16T11:17:32.247Z" + } + }, + "switch": { + "switch": { + "value": "off", + "timestamp": "2025-05-08T18:03:08.376Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_ac_ehs_01001.json b/tests/components/smartthings/fixtures/devices/da_ac_ehs_01001.json new file mode 100644 index 00000000000..61313aac1ca --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_ac_ehs_01001.json @@ -0,0 +1,229 @@ +{ + "items": [ + { + "deviceId": "4165c51e-bf6b-c5b6-fd53-127d6248754b", + "name": "Samsung EHS", + "label": "Heat pump", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-AC-EHS-01001", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "23dad822-0b66-4821-af2d-79ef502f5231", + "ownerId": "9dd8c4fa-c07c-f66d-ccdb-20eca3411b12", + "roomId": "a2d70c20-12aa-48bc-958b-3d47c9b6cffc", + "deviceTypeName": "oic.d.thermostat", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.outingMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "samsungce.alwaysOnSensing", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.sensingOnSuspendMode", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.softwareVersion", + "version": 1 + }, + { + "id": "samsungce.ehsFsvSettings", + "version": 1 + }, + { + "id": "samsungce.ehsCycleData", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.individualControlLock", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "sec.diagnosticsInformation", + "version": 1 + }, + { + "id": "sec.wifiConfiguration", + "version": 1 + } + ], + "categories": [ + { + "name": "Thermostat", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "INDOOR1", + "label": "INDOOR1", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-04-13T13:00:48.941Z", + "profile": { + "id": "e6f1cf68-e4bf-3e35-9f17-288a4e5ee0cb" + }, + "ocf": { + "ocfDeviceType": "oic.d.thermostat", + "name": "Samsung EHS", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "TP1X_DA_AC_EHS_01001_0000|10250141|60070110001711034A00010000002000", + "platformVersion": "DAWIT 2.0", + "platformOS": "TizenRT 3.1", + "hwVersion": "Realtek", + "firmwareVersion": "AEH-WW-TP1-22-AE6000_17240903", + "vendorId": "DA-AC-EHS-01001", + "vendorResourceClientServerVersion": "Realtek Release 3.1.240221", + "lastSignupTime": "2025-04-13T13:00:48.876846635Z", + "transferCandidate": false, + "additionalAuthCodeRequired": false + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "indoorMap": { + "coordinates": [0.0, 0.0, 0.0], + "rotation": [0.0, 0.0, 0.0], + "visible": false, + "data": null + }, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/fixtures/devices/da_sac_ehs_000002_sub.json b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000002_sub.json new file mode 100644 index 00000000000..9722c860519 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000002_sub.json @@ -0,0 +1,308 @@ +{ + "items": [ + { + "deviceId": "3810e5ad-5351-d9f9-12ff-000001200000", + "name": "Eco Heating System", + "label": "W\u00e4rmepumpe", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-SAC-EHS-000002-SUB", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "705633c1-64a2-4d54-9205-bbbd4f843d95", + "ownerId": "312d0773-efec-21c8-279f-5b8724f3ae57", + "roomId": "f9fef09a-b829-4eda-897b-dbaf6eebcac3", + "deviceTypeName": "Samsung OCF Air Conditioner", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "thermostatHeatingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "remoteControlStatus", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.outingMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.sensingOnSuspendMode", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.softwareVersion", + "version": 1 + }, + { + "id": "samsungce.ehsBoosterHeater", + "version": 1 + }, + { + "id": "samsungce.ehsDiverterValve", + "version": 1 + }, + { + "id": "samsungce.ehsFsvSettings", + "version": 1 + }, + { + "id": "samsungce.ehsCycleData", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 + } + ], + "categories": [ + { + "name": "AirConditioner", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "INDOOR1", + "label": "INDOOR1", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.ehsDefrostMode", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "INDOOR2", + "label": "INDOOR2", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.ehsDefrostMode", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2023-10-05T18:12:48.587Z", + "parentDeviceId": "3810e5ad-5351-d9f9-12ff-ed7c35d51a0c", + "profile": { + "id": "5dd2a4b2-981d-3571-96bb-eef6dc19d036" + }, + "ocf": { + "ocfDeviceType": "oic.d.airconditioner", + "name": "Eco Heating System", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "SAC_EHS_SPLIT|220614|61007300001600000400000000000000", + "platformVersion": "4.0", + "platformOS": "Tizen", + "hwVersion": "", + "firmwareVersion": "20250317.1", + "vendorId": "DA-SAC-EHS-000002-SUB", + "vendorResourceClientServerVersion": "4.0.54", + "lastSignupTime": "2023-10-05T18:12:47.561228Z", + "transferCandidate": true, + "additionalAuthCodeRequired": false, + "modelCode": "" + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "indoorMap": { + "coordinates": [142.0, 36.0, 22.0], + "rotation": [270.0, 0.0, 0.0], + "visible": true, + "data": null + }, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index d70d9a1dcfc..46c92bd2388 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -332,6 +332,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_ac_ehs_01001] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': 'Realtek', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '4165c51e-bf6b-c5b6-fd53-127d6248754b', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'TP1X_DA_AC_EHS_01001_0000', + 'model_id': None, + 'name': 'Heat pump', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': 'AEH-WW-TP1-22-AE6000_17240903', + 'via_device_id': None, + }) +# --- # name: test_devices[da_ac_rac_000001] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', @@ -761,6 +794,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_sac_ehs_000002_sub] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '3810e5ad-5351-d9f9-12ff-000001200000', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'SAC_EHS_SPLIT', + 'model_id': None, + 'name': 'Wärmepumpe', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '20250317.1', + 'via_device_id': None, + }) +# --- # name: test_devices[da_wm_dw_000001] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index df943079fe2..6f31a875d5c 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -1084,6 +1084,384 @@ 'state': '23.0', }) # --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_cooling_set_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_cooling_set_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': 'Cooling set point', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thermostat_cooling_setpoint', + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_cooling_set_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Heat pump Cooling set point', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_cooling_set_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat pump Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4053.792', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_energy_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy difference', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_difference', + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat pump Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy_saved-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_energy_saved', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy saved', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saved', + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat pump Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Heat pump Power', + 'power_consumption_end': '2025-05-14T13:26:17Z', + 'power_consumption_start': '2025-05-13T23:00:23Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_power_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_power_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_energy', + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat pump Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Heat pump Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '57', + }) +# --- # name: test_all_entities[da_ac_rac_000001][sensor.ac_office_granit_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5769,6 +6147,384 @@ 'state': '54.3', }) # --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_cooling_set_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_cooling_set_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': 'Cooling set point', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thermostat_cooling_setpoint', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_cooling_set_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Wärmepumpe Cooling set point', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_cooling_set_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wärmepumpe Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9575.308', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_energy_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy difference', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_difference', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wärmepumpe Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.045', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy_saved-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_energy_saved', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy saved', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saved', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wärmepumpe Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wärmepumpe Power', + 'power_consumption_end': '2025-05-09T05:02:01Z', + 'power_consumption_start': '2025-05-09T04:39:01Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.015', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_power_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_power_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_energy', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wärmepumpe Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.000222076093320449', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Wärmepumpe Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49.6', + }) +# --- # name: test_all_entities[da_wm_dw_000001][sensor.dishwasher_completion_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index e1b68971fb8..d43fa207ddf 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -46,6 +46,53 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_ac_ehs_01001][switch.heat_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.heat_pump', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_switch_switch_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][switch.heat_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Heat pump', + }), + 'context': , + 'entity_id': 'switch.heat_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ref_normal_000001][switch.refrigerator_ice_maker-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -281,6 +328,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_sac_ehs_000002_sub][switch.warmepumpe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.warmepumpe', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_switch_switch_switch', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][switch.warmepumpe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wärmepumpe', + }), + 'context': , + 'entity_id': 'switch.warmepumpe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 9a0fed89bd9635b387e56adc289754f8d205f2b9 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Thu, 15 May 2025 00:39:00 +0100 Subject: [PATCH 0479/1175] Translate raised exceptions for Squeezebox (#144842) * initial * tweak * review updates --- .../components/squeezebox/browse_media.py | 28 ++++++++++++++--- .../components/squeezebox/coordinator.py | 6 +++- .../components/squeezebox/media_player.py | 30 +++++++++++++++---- .../components/squeezebox/strings.json | 29 ++++++++++++++++++ homeassistant/components/squeezebox/update.py | 4 ++- 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 3f4af99fffd..6e1ec8b37c4 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -19,7 +19,7 @@ from homeassistant.components.media_player import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request -from .const import UNPLAYABLE_TYPES +from .const import DOMAIN, UNPLAYABLE_TYPES LIBRARY = [ "favorites", @@ -315,7 +315,14 @@ async def build_item_response( children.append(child_media) if children is None: - raise BrowseError(f"Media not found: {search_type} / {search_id}") + raise BrowseError( + translation_domain=DOMAIN, + translation_key="browse_media_not_found", + translation_placeholders={ + "type": str(search_type), + "id": str(search_id), + }, + ) assert media_class["item"] is not None if not search_id: @@ -398,7 +405,13 @@ async def generate_playlist( media_id = payload["search_id"] if media_type not in browse_media.squeezebox_id_by_type: - raise BrowseError(f"Media type not supported: {media_type}") + raise BrowseError( + translation_domain=DOMAIN, + translation_key="browse_media_type_not_supported", + translation_placeholders={ + "media_type": str(media_type), + }, + ) browse_id = (browse_media.squeezebox_id_by_type[media_type], media_id) if media_type.startswith("app-"): @@ -412,4 +425,11 @@ async def generate_playlist( if result and "items" in result: items: list = result["items"] return items - raise BrowseError(f"Media not found: {media_type} / {media_id}") + raise BrowseError( + translation_domain=DOMAIN, + translation_key="browse_media_not_found", + translation_placeholders={ + "type": str(media_type), + "id": str(media_id), + }, + ) diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py index e5d78024ef0..9c7d00eae58 100644 --- a/homeassistant/components/squeezebox/coordinator.py +++ b/homeassistant/components/squeezebox/coordinator.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from . import SqueezeboxConfigEntry from .const import ( + DOMAIN, PLAYER_UPDATE_INTERVAL, SENSOR_UPDATE_INTERVAL, SIGNAL_PLAYER_REDISCOVERED, @@ -65,7 +66,10 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator): data: dict | None = await self.lms.async_prepared_status() if not data: - raise UpdateFailed("No data from status poll") + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="coordinator_no_data", + ) _LOGGER.debug("Raw serverstatus %s=%s", self.lms.name, data) return data diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 315ea46c811..c7c7b79fa89 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -471,7 +471,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): if announce: if media_type not in MediaType.MUSIC: raise ServiceValidationError( - "Announcements must have media type of 'music'. Playlists are not supported" + translation_domain=DOMAIN, + translation_key="invalid_announce_media_type", + translation_placeholders={ + "media_type": str(media_type), + }, ) extra = kwargs.get(ATTR_MEDIA_EXTRA, {}) @@ -480,7 +484,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): announce_volume = get_announce_volume(extra) except ValueError: raise ServiceValidationError( - f"{ATTR_ANNOUNCE_VOLUME} must be a number greater than 0 and less than or equal to 1" + translation_domain=DOMAIN, + translation_key="invalid_announce_volume", + translation_placeholders={ + "announce_volume": ATTR_ANNOUNCE_VOLUME, + }, ) from None else: self._player.set_announce_volume(announce_volume) @@ -489,7 +497,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): announce_timeout = get_announce_timeout(extra) except ValueError: raise ServiceValidationError( - f"{ATTR_ANNOUNCE_TIMEOUT} must be a whole number greater than 0" + translation_domain=DOMAIN, + translation_key="invalid_announce_timeout", + translation_placeholders={ + "announce_timeout": ATTR_ANNOUNCE_TIMEOUT, + }, ) from None else: self._player.set_announce_timeout(announce_timeout) @@ -595,13 +607,21 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): other_player = ent_reg.async_get(other_player_entity_id) if other_player is None: raise ServiceValidationError( - f"Could not find player with entity_id {other_player_entity_id}" + translation_domain=DOMAIN, + translation_key="join_cannot_find_other_player", + translation_placeholders={ + "other_player_entity_id": str(other_player_entity_id) + }, ) if other_player_id := other_player.unique_id: await self._player.async_sync(other_player_id) else: raise ServiceValidationError( - f"Could not join unknown player {other_player_entity_id}" + translation_domain=DOMAIN, + translation_key="join_cannot_join_unknown_player", + translation_placeholders={ + "other_player_entity_id": str(other_player_entity_id) + }, ) async def async_unjoin_player(self) -> None: diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 8f0d45bd737..6a4e30119a0 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -156,5 +156,34 @@ } } } + }, + "exceptions": { + "invalid_announce_media_type": { + "message": "Only type 'music' can be played as announcement (received type {media_type})." + }, + "invalid_announce_volume": { + "message": "{announce_volume} must be a number greater than 0 and less than or equal to 1." + }, + "invalid_announce_timeout": { + "message": "{announce_timeout} must be a number greater than 0." + }, + "join_cannot_find_other_player": { + "message": "Could not find player with entity_id {other_player_entity_id}." + }, + "join_cannot_join_unknown_player": { + "message": "Could not join unknown player {other_player_entity_id}." + }, + "coordinator_no_data": { + "message": "No data from status poll." + }, + "browse_media_not_found": { + "message": "Media not found: {type} / {id}." + }, + "browse_media_type_not_supported": { + "message": "Media type not supported: {media_type}." + }, + "update_restart_failed": { + "message": "Error trying to update LMS Plugins: Restart failed." + } } } diff --git a/homeassistant/components/squeezebox/update.py b/homeassistant/components/squeezebox/update.py index 900eca97041..62579424d25 100644 --- a/homeassistant/components/squeezebox/update.py +++ b/homeassistant/components/squeezebox/update.py @@ -19,6 +19,7 @@ from homeassistant.helpers.event import async_call_later from . import SqueezeboxConfigEntry from .const import ( + DOMAIN, SERVER_MODEL, STATUS_QUERY_VERSION, STATUS_UPDATE_NEWPLUGINS, @@ -161,7 +162,8 @@ class ServerStatusUpdatePlugins(ServerStatusUpdate): self.restart_triggered = False self.async_write_ha_state() raise HomeAssistantError( - "Error trying to update LMS Plugins: Restart failed" + translation_domain=DOMAIN, + translation_key="update_restart_failed", ) async def _async_update_catchall(self, now: datetime | None = None) -> None: From 301ca88f419ddccc3f77a7ef0005609702c56b2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 14 May 2025 22:27:25 -0500 Subject: [PATCH 0480/1175] Bump aioesphomeapi to 31.0.1 (#144939) --- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_light.py | 49 +++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d1fb3a49166..833fa47337f 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==31.0.0", + "aioesphomeapi==31.0.1", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.15.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 9c1a484ef2e..9a5807d7b40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.0.0 +aioesphomeapi==31.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a417bb1ee8..7491190cb92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -229,7 +229,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.0.0 +aioesphomeapi==31.0.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index 0d2e8338c06..0cf3e10f11e 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -204,6 +204,55 @@ async def test_light_brightness( mock_client.light_command.reset_mock() +async def test_light_legacy_brightness( + hass: HomeAssistant, + mock_client: APIClient, + mock_generic_device_entry: MockGenericDeviceEntryType, +) -> None: + """Test a generic light entity that only supports legacy brightness.""" + mock_client.api_version = APIVersion(1, 7) + entity_info = [ + LightInfo( + object_id="mylight", + key=1, + name="my light", + unique_id="my_light", + min_mireds=153, + max_mireds=400, + supported_color_modes=[LightColorCapability.BRIGHTNESS, 2], + ) + ] + states = [ + LightState( + key=1, state=True, brightness=100, color_mode=ESPColorMode.LEGACY_BRIGHTNESS + ) + ] + 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("light.test_mylight") + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_mylight"}, + blocking=True, + ) + mock_client.light_command.assert_has_calls( + [call(key=1, state=True, color_mode=LightColorCapability.BRIGHTNESS)] + ) + mock_client.light_command.reset_mock() + + async def test_light_brightness_on_off( hass: HomeAssistant, mock_client: APIClient, From c7cf9585aedcd9daca06e8334f9aa9d9c9c7889f Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 15 May 2025 02:18:37 -0400 Subject: [PATCH 0481/1175] Add modern style configuration for template fan (#144751) * add modern template fan * address comments and add tests for coverage --- homeassistant/components/template/config.py | 9 +- homeassistant/components/template/fan.py | 153 +- tests/components/template/test_fan.py | 1886 +++++++++++-------- 3 files changed, 1218 insertions(+), 830 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index ca643653cec..5e7425f13d7 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -14,6 +14,7 @@ from homeassistant.components.blueprint import ( ) from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN @@ -46,6 +47,7 @@ from . import ( binary_sensor as binary_sensor_platform, button as button_platform, cover as cover_platform, + fan as fan_platform, image as image_platform, light as light_platform, number as number_platform, @@ -131,9 +133,14 @@ CONFIG_SECTION_SCHEMA = vol.All( vol.Optional(COVER_DOMAIN): vol.All( cv.ensure_list, [cover_platform.COVER_SCHEMA] ), + vol.Optional(FAN_DOMAIN): vol.All( + cv.ensure_list, [fan_platform.FAN_SCHEMA] + ), }, ), - ensure_domains_do_not_have_trigger_or_action(BUTTON_DOMAIN, COVER_DOMAIN), + ensure_domains_do_not_have_trigger_or_action( + BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN + ), ) TEMPLATE_BLUEPRINT_SCHEMA = vol.All( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7ec62891784..32e6b06d108 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -21,6 +21,8 @@ from homeassistant.components.fan import ( from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_NAME, + CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ON, @@ -29,14 +31,17 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN from .template_entity import ( + LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_ICON_SCHEMA, TemplateEntity, rewrite_common_legacy_to_modern_conf, ) @@ -59,54 +64,121 @@ CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] +CONF_DIRECTION = "direction" +CONF_OSCILLATING = "oscillating" +CONF_PERCENTAGE = "percentage" +CONF_PRESET_MODE = "preset_mode" + +LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | { + CONF_DIRECTION_TEMPLATE: CONF_DIRECTION, + CONF_OSCILLATING_TEMPLATE: CONF_OSCILLATING, + CONF_PERCENTAGE_TEMPLATE: CONF_PERCENTAGE, + CONF_PRESET_MODE_TEMPLATE: CONF_PRESET_MODE, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + +DEFAULT_NAME = "Template Fan" + FAN_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_DIRECTION): cv.template, + vol.Optional(CONF_NAME): cv.template, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_OSCILLATING): cv.template, + vol.Optional(CONF_PERCENTAGE): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_PRESET_MODE): cv.template, + vol.Optional(CONF_PRESET_MODES): cv.ensure_list, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema), +) + +LEGACY_FAN_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), vol.Schema( { + vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_TEMPLATE): cv.template, vol.Optional(CONF_PRESET_MODE_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, - vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, - vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_PRESET_MODES): cv.ensure_list, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PERCENTAGE_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_PRESET_MODE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SPEED_COUNT): vol.Coerce(int), - vol.Optional(CONF_PRESET_MODES): cv.ensure_list, - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - {vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_SCHEMA)} + {vol.Required(CONF_FANS): cv.schema_with_slug_keys(LEGACY_FAN_SCHEMA)} ) -async def _async_create_entities(hass: HomeAssistant, config): - """Create the Template Fans.""" +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, config: dict[str, dict] +) -> list[dict]: + """Rewrite legacy fan configuration definitions to modern ones.""" fans = [] - for object_id, entity_config in config[CONF_FANS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) + for object_id, entity_conf in config.items(): + entity_conf = {**entity_conf, CONF_OBJECT_ID: object_id} - unique_id = entity_config.get(CONF_UNIQUE_ID) + entity_conf = rewrite_common_legacy_to_modern_conf( + hass, entity_conf, LEGACY_FIELDS + ) + + if CONF_NAME not in entity_conf: + entity_conf[CONF_NAME] = template.Template(object_id, hass) + + fans.append(entity_conf) + + return fans + + +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the template fans.""" + fans = [] + + for entity_conf in definitions: + unique_id = entity_conf.get(CONF_UNIQUE_ID) + + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" fans.append( TemplateFan( hass, - object_id, - entity_config, + entity_conf, unique_id, ) ) - return fans + async_add_entities(fans) async def async_setup_platform( @@ -116,7 +188,21 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the template fans.""" - async_add_entities(await _async_create_entities(hass, config)) + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(hass, config[CONF_FANS]), + None, + ) + return + + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], + ) class TemplateFan(TemplateEntity, FanEntity): @@ -127,27 +213,24 @@ class TemplateFan(TemplateEntity, FanEntity): def __init__( self, hass: HomeAssistant, - object_id, config: dict[str, Any], unique_id, ) -> None: """Initialize the fan.""" - super().__init__( - hass, config=config, fallback_name=object_id, unique_id=unique_id - ) - self.hass = hass - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) + super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) name = self._attr_name if TYPE_CHECKING: assert name is not None - self._template = config.get(CONF_VALUE_TEMPLATE) - self._percentage_template = config.get(CONF_PERCENTAGE_TEMPLATE) - self._preset_mode_template = config.get(CONF_PRESET_MODE_TEMPLATE) - self._oscillating_template = config.get(CONF_OSCILLATING_TEMPLATE) - self._direction_template = config.get(CONF_DIRECTION_TEMPLATE) + self._template = config.get(CONF_STATE) + self._percentage_template = config.get(CONF_PERCENTAGE) + self._preset_mode_template = config.get(CONF_PRESET_MODE) + self._oscillating_template = config.get(CONF_OSCILLATING) + self._direction_template = config.get(CONF_DIRECTION) self._attr_supported_features |= ( FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON @@ -231,7 +314,7 @@ class TemplateFan(TemplateEntity, FanEntity): if preset_mode is not None: await self.async_set_preset_mode(preset_mode) - elif percentage is not None: + if percentage is not None: await self.async_set_percentage(percentage) if self._template is None: diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index dac97931fa7..a061ce86256 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,8 +5,7 @@ from typing import Any import pytest import voluptuous as vol -from homeassistant import setup -from homeassistant.components import fan +from homeassistant.components import fan, template from homeassistant.components.fan import ( ATTR_DIRECTION, ATTR_OSCILLATING, @@ -14,12 +13,12 @@ from homeassistant.components.fan import ( ATTR_PRESET_MODE, DIRECTION_FORWARD, DIRECTION_REVERSE, - DOMAIN as FAN_DOMAIN, FanEntityFeature, NotValidPresetModeError, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .conftest import ConfigurationStyle @@ -27,23 +26,14 @@ from .conftest import ConfigurationStyle from tests.common import assert_setup_component from tests.components.fan import common -_TEST_OBJECT_ID = "test_fan" -_TEST_FAN = f"fan.{_TEST_OBJECT_ID}" +TEST_OBJECT_ID = "test_fan" +TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}" # Represent for fan's state _STATE_INPUT_BOOLEAN = "input_boolean.state" # Represent for fan's state _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" -# Represent for fan's preset mode -_PRESET_MODE_INPUT_SELECT = "input_select.preset_mode" -# Represent for fan's speed percentage -_PERCENTAGE_INPUT_NUMBER = "input_number.percentage" -# Represent for fan's oscillating -_OSC_INPUT = "input_select.osc" -# Represent for fan's direction -_DIRECTION_INPUT_SELECT = "input_select.direction" - -OPTIMISTIC_ON_OFF_CONFIG = { +OPTIMISTIC_ON_OFF_ACTIONS = { "turn_on": { "service": "test.automation", "data": { @@ -59,7 +49,10 @@ OPTIMISTIC_ON_OFF_CONFIG = { }, }, } - +NAMED_ON_OFF_ACTIONS = { + **OPTIMISTIC_ON_OFF_ACTIONS, + "name": TEST_OBJECT_ID, +} PERCENTAGE_ACTION = { "set_percentage": { @@ -72,7 +65,7 @@ PERCENTAGE_ACTION = { }, } OPTIMISTIC_PERCENTAGE_CONFIG = { - **OPTIMISTIC_ON_OFF_CONFIG, + **OPTIMISTIC_ON_OFF_ACTIONS, **PERCENTAGE_ACTION, } @@ -87,7 +80,7 @@ PRESET_MODE_ACTION = { }, } OPTIMISTIC_PRESET_MODE_CONFIG = { - **OPTIMISTIC_ON_OFF_CONFIG, + **OPTIMISTIC_ON_OFF_ACTIONS, **PRESET_MODE_ACTION, } OPTIMISTIC_PRESET_MODE_CONFIG2 = { @@ -106,7 +99,7 @@ OSCILLATE_ACTION = { }, } OPTIMISTIC_OSCILLATE_CONFIG = { - **OPTIMISTIC_ON_OFF_CONFIG, + **OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION, } @@ -121,16 +114,38 @@ DIRECTION_ACTION = { }, } OPTIMISTIC_DIRECTION_CONFIG = { - **OPTIMISTIC_ON_OFF_CONFIG, + **OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION, } +UNIQUE_ID_CONFIG = { + **OPTIMISTIC_ON_OFF_ACTIONS, + "unique_id": "not-so-unique-anymore", +} + + +def _verify( + hass: HomeAssistant, + expected_state: str, + expected_percentage: int | None = None, + expected_oscillating: bool | None = None, + expected_direction: str | None = None, + expected_preset_mode: str | None = None, +) -> None: + """Verify fan's state, speed and osc.""" + state = hass.states.get(TEST_ENTITY_ID) + attributes = state.attributes + assert state.state == str(expected_state) + assert attributes.get(ATTR_PERCENTAGE) == expected_percentage + assert attributes.get(ATTR_OSCILLATING) == expected_oscillating + assert attributes.get(ATTR_DIRECTION) == expected_direction + assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode async def async_setup_legacy_format( - hass: HomeAssistant, count: int, light_config: dict[str, Any] + hass: HomeAssistant, count: int, fan_config: dict[str, Any] ) -> None: """Do setup of fan integration via legacy format.""" - config = {"fan": {"platform": "template", "fans": light_config}} + config = {"fan": {"platform": "template", "fans": fan_config}} with assert_setup_component(count, fan.DOMAIN): assert await async_setup_component( @@ -144,6 +159,38 @@ async def async_setup_legacy_format( await hass.async_block_till_done() +async def async_setup_modern_format( + hass: HomeAssistant, count: int, fan_config: dict[str, Any] +) -> None: + """Do setup of fan integration via modern format.""" + config = {"template": {"fan": fan_config}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_legacy_named_fan( + hass: HomeAssistant, count: int, fan_config: dict[str, Any] +): + """Do setup of a named fan via legacy format.""" + await async_setup_legacy_format(hass, count, {TEST_OBJECT_ID: fan_config}) + + +async def async_setup_modern_named_fan( + hass: HomeAssistant, count: int, fan_config: dict[str, Any] +): + """Do setup of a named fan via legacy format.""" + await async_setup_modern_format(hass, count, {"name": TEST_OBJECT_ID, **fan_config}) + + async def async_setup_legacy_format_with_attribute( hass: HomeAssistant, count: int, @@ -157,7 +204,7 @@ async def async_setup_legacy_format_with_attribute( hass, count, { - _TEST_OBJECT_ID: { + TEST_OBJECT_ID: { **extra_config, "value_template": "{{ 1 == 1 }}", **extra, @@ -166,16 +213,83 @@ async def async_setup_legacy_format_with_attribute( ) +async def async_setup_modern_format_with_attribute( + hass: HomeAssistant, + count: int, + attribute: str, + attribute_template: str, + extra_config: dict, +) -> None: + """Do setup of a modern fan that has a single templated attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + **extra_config, + "state": "{{ 1 == 1 }}", + **extra, + }, + ) + + @pytest.fixture async def setup_fan( hass: HomeAssistant, count: int, style: ConfigurationStyle, - light_config: dict[str, Any], + fan_config: dict[str, Any], ) -> None: """Do setup of fan integration.""" if style == ConfigurationStyle.LEGACY: - await async_setup_legacy_format(hass, count, light_config) + await async_setup_legacy_format(hass, count, fan_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, fan_config) + + +@pytest.fixture +async def setup_named_fan( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + fan_config: dict[str, Any], +) -> None: + """Do setup of fan integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_named_fan(hass, count, fan_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_named_fan(hass, count, fan_config) + + +@pytest.fixture +async def setup_state_fan( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of fan integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **OPTIMISTIC_ON_OFF_ACTIONS, + "value_template": state_template, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **NAMED_ON_OFF_ACTIONS, + "state": state_template, + }, + ) @pytest.fixture @@ -187,9 +301,14 @@ async def setup_test_fan_with_extra_config( extra_config: dict[str, Any], ) -> None: """Do setup of fan integration.""" - config = {_TEST_OBJECT_ID: {**fan_config, **extra_config}} if style == ConfigurationStyle.LEGACY: - await async_setup_legacy_format(hass, count, config) + await async_setup_legacy_format( + hass, count, {TEST_OBJECT_ID: {**fan_config, **extra_config}} + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": TEST_OBJECT_ID, **fan_config, **extra_config} + ) @pytest.fixture @@ -204,344 +323,507 @@ async def setup_optimistic_fan_attribute( await async_setup_legacy_format_with_attribute( hass, count, "", "", extra_config ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format_with_attribute( + hass, count, "", "", extra_config + ) -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) +@pytest.fixture +async def setup_single_attribute_state_fan( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + attribute: str, + attribute_template: str, + state_template: str, + extra_config: dict, +) -> None: + """Do setup of fan integration testing a single attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **OPTIMISTIC_ON_OFF_ACTIONS, + "value_template": state_template, + **extra, + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **NAMED_ON_OFF_ACTIONS, + "state": state_template, + **extra, + **extra_config, + }, + ) + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 'on' }}")]) @pytest.mark.parametrize( - "config", - [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "turn_on": {"service": "script.fan_on"}, - "turn_off": {"service": "script.fan_off"}, - } - }, - } - }, - ], + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_fan") async def test_missing_optional_config(hass: HomeAssistant) -> None: """Test: missing optional template is ok.""" _verify(hass, STATE_ON, None, None, None, None) -@pytest.mark.parametrize(("count", "domain"), [(0, FAN_DOMAIN)]) +@pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( - "config", + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + "fan_config", [ { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "platform": "template", - "fans": { - "test_fan": { - "turn_on": {"service": "script.fan_on"}, - "turn_off": {"service": "script.fan_off"}, - } - }, - }, - } + "value_template": "{{ 'on' }}", + "turn_off": {"service": "script.fan_off"}, }, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "turn_off": {"service": "script.fan_off"}, - } - }, - }, - } - }, - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "turn_on": {"service": "script.fan_on"}, - } - }, - }, - } + "value_template": "{{ 'on' }}", + "turn_on": {"service": "script.fan_on"}, }, ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_fan") async def test_wrong_template_config(hass: HomeAssistant) -> None: - """Test: missing 'value_template' will fail.""" + """Test: missing 'turn_on' or 'turn_off' will fail.""" assert hass.states.async_all("fan") == [] -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ is_state('input_boolean.state', 'True') }}", - "percentage_template": ( - "{{ states('input_number.percentage') }}" - ), - **OPTIMISTIC_ON_OFF_CONFIG, - **PERCENTAGE_ACTION, - "preset_mode_template": ( - "{{ states('input_select.preset_mode') }}" - ), - **PRESET_MODE_ACTION, - "oscillating_template": "{{ states('input_select.osc') }}", - **OSCILLATE_ACTION, - "direction_template": "{{ states('input_select.direction') }}", - **DIRECTION_ACTION, - "speed_count": "3", - } - }, - } - }, - ], + ("count", "state_template"), [(1, "{{ is_state('input_boolean.state', 'on') }}")] ) -@pytest.mark.usefixtures("start_ha") -async def test_templates_with_entities(hass: HomeAssistant) -> None: - """Test tempalates with values from other entities.""" - _verify(hass, STATE_OFF, 0, None, None, None) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_fan") +async def test_state_template(hass: HomeAssistant) -> None: + """Test state template.""" + _verify(hass, STATE_OFF, None, None, None, None) - hass.states.async_set(_STATE_INPUT_BOOLEAN, True) - hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) - hass.states.async_set(_OSC_INPUT, "True") - - for set_state, set_value, value in ( - (_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD, 66), - (_PERCENTAGE_INPUT_NUMBER, 33, 33), - (_PERCENTAGE_INPUT_NUMBER, 66, 66), - (_PERCENTAGE_INPUT_NUMBER, 100, 100), - (_PERCENTAGE_INPUT_NUMBER, "dog", 0), - ): - hass.states.async_set(set_state, set_value) - await hass.async_block_till_done() - _verify(hass, STATE_ON, value, True, DIRECTION_FORWARD, None) - - hass.states.async_set(_STATE_INPUT_BOOLEAN, False) + hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON) await hass.async_block_till_done() - _verify(hass, STATE_OFF, 0, True, DIRECTION_FORWARD, None) + + _verify(hass, STATE_ON, None, None, None, None) + + hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + _verify(hass, STATE_OFF, None, None, None, None) -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "entity", "tests"), + ("state_template", "expected"), + [ + ("{{ True }}", STATE_ON), + ("{{ False }}", STATE_OFF), + ("{{ x - 1 }}", STATE_UNAVAILABLE), + ("{{ 7.45 }}", STATE_OFF), + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_fan") +async def test_state_template_states(hass: HomeAssistant, expected: str) -> None: + """Test state template.""" + _verify(hass, expected, None, None, None, None) + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), [ ( + 1, + "{{ 1 == 1}}", + "{% if states.input_boolean.state.state %}/local/switch.png{% endif %}", + {}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "picture"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_picture_template(hass: HomeAssistant) -> None: + """Test picture template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("entity_picture") in ("", None) + + hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["entity_picture"] == "/local/switch.png" + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 1 == 1}}", + "{% if states.input_boolean.state.state %}mdi:eye{% endif %}", + {}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "icon"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_icon_template(hass: HomeAssistant) -> None: + """Test icon template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") in ("", None) + + hass.states.async_set(_STATE_INPUT_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["icon"] == "mdi:eye" + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 1 == 1 }}", + "{{ states('sensor.percentage') }}", + PERCENTAGE_ACTION, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "percentage_template"), + (ConfigurationStyle.MODERN, "percentage"), + ], +) +@pytest.mark.parametrize( + ("percent", "expected"), + [ + ("0", 0), + ("33", 33), + ("invalid", 0), + ("5000", 0), + ("100", 100), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_percentage_template( + hass: HomeAssistant, percent: str, expected: int, calls: list[ServiceCall] +) -> None: + """Test templates with fan percentages from other entities.""" + hass.states.async_set("sensor.percentage", percent) + await hass.async_block_till_done() + _verify(hass, STATE_ON, expected, None, None, None) + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 1 == 1 }}", + "{{ states('sensor.preset_mode') }}", + {"preset_modes": ["auto", "smart"], **PRESET_MODE_ACTION}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "preset_mode_template"), + (ConfigurationStyle.MODERN, "preset_mode"), + ], +) +@pytest.mark.parametrize( + ("preset_mode", "expected"), + [ + ("0", None), + ("invalid", None), + ("auto", "auto"), + ("smart", "smart"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_preset_mode_template( + hass: HomeAssistant, preset_mode: str, expected: int +) -> None: + """Test preset_mode template.""" + hass.states.async_set("sensor.preset_mode", preset_mode) + await hass.async_block_till_done() + _verify(hass, STATE_ON, None, None, None, expected) + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 1 == 1 }}", + "{{ is_state('binary_sensor.oscillating', 'on') }}", + OSCILLATE_ACTION, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "oscillating_template"), + (ConfigurationStyle.MODERN, "oscillating"), + ], +) +@pytest.mark.parametrize( + ("oscillating", "expected"), + [ + (STATE_ON, True), + (STATE_OFF, False), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_oscillating_template( + hass: HomeAssistant, oscillating: str, expected: bool | None +) -> None: + """Test oscillating template.""" + hass.states.async_set("binary_sensor.oscillating", oscillating) + await hass.async_block_till_done() + _verify(hass, STATE_ON, None, expected, None, None) + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 1 == 1 }}", + "{{ states('sensor.direction') }}", + DIRECTION_ACTION, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "direction_template"), + (ConfigurationStyle.MODERN, "direction"), + ], +) +@pytest.mark.parametrize( + ("direction", "expected"), + [ + (DIRECTION_FORWARD, DIRECTION_FORWARD), + (DIRECTION_REVERSE, DIRECTION_REVERSE), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_fan") +async def test_direction_template( + hass: HomeAssistant, direction: str, expected: bool | None +) -> None: + """Test direction template.""" + hass.states.async_set("sensor.direction", direction) + await hass.async_block_till_done() + _verify(hass, STATE_ON, None, None, expected, None) + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "percentage_template": "{{ states('sensor.percentage') }}", - **OPTIMISTIC_PERCENTAGE_CONFIG, - }, - }, - } + "availability_template": ( + "{{ is_state('availability_boolean.state', 'on') }}" + ), + "value_template": "{{ 'on' }}", + "oscillating_template": "{{ 1 == 1 }}", + "direction_template": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, }, - "sensor.percentage", - [ - ("0", 0, None), - ("33", 33, None), - ("invalid", 0, None), - ("5000", 0, None), - ("100", 100, None), - ("0", 0, None), - ], ), ( + ConfigurationStyle.MODERN, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "preset_modes": ["auto", "smart"], - "preset_mode_template": ( - "{{ states('sensor.preset_mode') }}" - ), - **OPTIMISTIC_PRESET_MODE_CONFIG, - }, - }, - } + "availability": ("{{ is_state('availability_boolean.state', 'on') }}"), + "state": "{{ 'on' }}", + "oscillating": "{{ 1 == 1 }}", + "direction": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, }, - "sensor.preset_mode", - [ - ("0", None, None), - ("invalid", None, None), - ("auto", None, "auto"), - ("smart", None, "smart"), - ("invalid", None, None), - ], ), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_templates_with_entities2(hass: HomeAssistant, entity, tests) -> None: - """Test templates with values from other entities.""" - for set_percentage, test_percentage, test_type in tests: - hass.states.async_set(entity, set_percentage) - await hass.async_block_till_done() - _verify(hass, STATE_ON, test_percentage, None, None, test_type) - - -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) -@pytest.mark.parametrize( - "config", - [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "availability_template": ( - "{{ is_state('availability_boolean.state', 'on') }}" - ), - "value_template": "{{ 'on' }}", - "oscillating_template": "{{ 1 == 1 }}", - "direction_template": "{{ 'forward' }}", - "turn_on": {"service": "script.fan_on"}, - "turn_off": {"service": "script.fan_off"}, - } - }, - } - }, - ], -) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_named_fan") async def test_availability_template_with_entities(hass: HomeAssistant) -> None: """Test availability tempalates with values from other entities.""" for state, test_assert in ((STATE_ON, True), (STATE_OFF, False)): hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, state) await hass.async_block_till_done() - assert (hass.states.get(_TEST_FAN).state != STATE_UNAVAILABLE) == test_assert + assert ( + hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE + ) == test_assert -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("config", "states"), + ("style", "fan_config", "states"), [ ( + ConfigurationStyle.LEGACY, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'unavailable' }}", - **OPTIMISTIC_ON_OFF_CONFIG, - } - }, - } + "value_template": "{{ 'unavailable' }}", + **OPTIMISTIC_ON_OFF_ACTIONS, }, [STATE_OFF, None, None, None], ), ( + ConfigurationStyle.MODERN, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "percentage_template": "{{ 0 }}", - **OPTIMISTIC_PERCENTAGE_CONFIG, - "oscillating_template": "{{ 'unavailable' }}", - **OSCILLATE_ACTION, - "direction_template": "{{ 'unavailable' }}", - **DIRECTION_ACTION, - } - }, - } + "state": "{{ 'unavailable' }}", + **OPTIMISTIC_ON_OFF_ACTIONS, + }, + [STATE_OFF, None, None, None], + ), + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + "percentage_template": "{{ 0 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating_template": "{{ 'unavailable' }}", + **OSCILLATE_ACTION, + "direction_template": "{{ 'unavailable' }}", + **DIRECTION_ACTION, }, [STATE_ON, 0, None, None], ), ( + ConfigurationStyle.MODERN, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "percentage_template": "{{ 66 }}", - **OPTIMISTIC_PERCENTAGE_CONFIG, - "oscillating_template": "{{ 1 == 1 }}", - **OSCILLATE_ACTION, - "direction_template": "{{ 'forward' }}", - **DIRECTION_ACTION, - } - }, - } + "state": "{{ 'on' }}", + "percentage": "{{ 0 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating": "{{ 'unavailable' }}", + **OSCILLATE_ACTION, + "direction": "{{ 'unavailable' }}", + **DIRECTION_ACTION, + }, + [STATE_ON, 0, None, None], + ), + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + "percentage_template": "{{ 66 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating_template": "{{ 1 == 1 }}", + **OSCILLATE_ACTION, + "direction_template": "{{ 'forward' }}", + **DIRECTION_ACTION, }, [STATE_ON, 66, True, DIRECTION_FORWARD], ), ( + ConfigurationStyle.MODERN, { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'abc' }}", - "percentage_template": "{{ 0 }}", - **OPTIMISTIC_PERCENTAGE_CONFIG, - "oscillating_template": "{{ 'xyz' }}", - **OSCILLATE_ACTION, - "direction_template": "{{ 'right' }}", - **DIRECTION_ACTION, - } - }, - } + "state": "{{ 'on' }}", + "percentage": "{{ 66 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating": "{{ 1 == 1 }}", + **OSCILLATE_ACTION, + "direction": "{{ 'forward' }}", + **DIRECTION_ACTION, + }, + [STATE_ON, 66, True, DIRECTION_FORWARD], + ), + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'abc' }}", + "percentage_template": "{{ 0 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating_template": "{{ 'xyz' }}", + **OSCILLATE_ACTION, + "direction_template": "{{ 'right' }}", + **DIRECTION_ACTION, + }, + [STATE_OFF, 0, None, None], + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'abc' }}", + "percentage": "{{ 0 }}", + **OPTIMISTIC_PERCENTAGE_CONFIG, + "oscillating": "{{ 'xyz' }}", + **OSCILLATE_ACTION, + "direction": "{{ 'right' }}", + **DIRECTION_ACTION, }, [STATE_OFF, 0, None, None], ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_named_fan") async def test_template_with_unavailable_entities(hass: HomeAssistant, states) -> None: """Test unavailability with value_template.""" _verify(hass, states[0], states[1], states[2], states[3], None) -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("style", "fan_config"), [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_fan": { - "value_template": "{{ 'on' }}", - "availability_template": "{{ x - 12 }}", - "preset_mode_template": ( - "{{ states('input_select.preset_mode') }}" - ), - "oscillating_template": "{{ states('input_select.osc') }}", - "direction_template": "{{ states('input_select.direction') }}", - "turn_on": {"service": "script.fan_on"}, - "turn_off": {"service": "script.fan_off"}, - } - }, - } - }, + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + "availability_template": "{{ x - 12 }}", + "preset_mode_template": ("{{ states('input_select.preset_mode') }}"), + "oscillating_template": "{{ states('input_select.osc') }}", + "direction_template": "{{ states('input_select.direction') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + "availability": "{{ x - 12 }}", + "preset_mode": ("{{ states('input_select.preset_mode') }}"), + "oscillating": "{{ states('input_select.osc') }}", + "direction": "{{ states('input_select.direction') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + }, + ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_named_fan") async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text ) -> None: @@ -551,147 +833,380 @@ async def test_invalid_availability_template_keeps_component_available( assert "x" in caplog_setup_text +@pytest.mark.parametrize(("count", "extra_config"), [(1, OPTIMISTIC_ON_OFF_ACTIONS)]) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'off' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'off' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_on_off(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test turn on and turn off.""" - await _register_components(hass) - for expected_calls, (func, state, action) in enumerate( + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + for expected_calls, (func, action) in enumerate( [ - (common.async_turn_on, STATE_ON, "turn_on"), - (common.async_turn_off, STATE_OFF, "turn_off"), + (common.async_turn_on, "turn_on"), + (common.async_turn_off, "turn_off"), ] ): - await func(hass, _TEST_FAN) - assert hass.states.get(_STATE_INPUT_BOOLEAN).state == state - _verify(hass, state, 0, None, None, None) + await func(hass, TEST_ENTITY_ID) + assert len(calls) == expected_calls + 1 assert calls[-1].data["action"] == action - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID -async def test_set_invalid_direction_from_initial_stage( +@pytest.mark.parametrize( + ("count", "extra_config"), + [ + ( + 1, + { + **OPTIMISTIC_ON_OFF_ACTIONS, + **OPTIMISTIC_PRESET_MODE_CONFIG2, + **OPTIMISTIC_PERCENTAGE_CONFIG, + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'off' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'off' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") +async def test_on_with_extra_attributes( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: + """Test turn on and turn off.""" + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + await common.async_turn_on(hass, TEST_ENTITY_ID, 100) + + assert len(calls) == 2 + assert calls[-2].data["action"] == "turn_on" + assert calls[-2].data["caller"] == TEST_ENTITY_ID + + assert calls[-1].data["action"] == "set_percentage" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["percentage"] == 100 + + await common.async_turn_off(hass, TEST_ENTITY_ID) + + assert len(calls) == 3 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + await common.async_turn_on(hass, TEST_ENTITY_ID, None, "auto") + + assert len(calls) == 5 + assert calls[-2].data["action"] == "turn_on" + assert calls[-2].data["caller"] == TEST_ENTITY_ID + + assert calls[-1].data["action"] == "set_preset_mode" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["preset_mode"] == "auto" + + await common.async_turn_off(hass, TEST_ENTITY_ID) + + assert len(calls) == 6 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + await common.async_turn_on(hass, TEST_ENTITY_ID, 50, "high") + + assert len(calls) == 9 + assert calls[-3].data["action"] == "turn_on" + assert calls[-3].data["caller"] == TEST_ENTITY_ID + + assert calls[-2].data["action"] == "set_preset_mode" + assert calls[-2].data["caller"] == TEST_ENTITY_ID + assert calls[-2].data["preset_mode"] == "high" + + assert calls[-1].data["action"] == "set_percentage" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["percentage"] == 50 + + await common.async_turn_off(hass, TEST_ENTITY_ID) + + assert len(calls) == 10 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") +async def test_set_invalid_direction_from_initial_stage(hass: HomeAssistant) -> None: """Test set invalid direction when fan is in initial state.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) - - await common.async_set_direction(hass, _TEST_FAN, "invalid") - - assert hass.states.get(_DIRECTION_INPUT_SELECT).state == "" - _verify(hass, STATE_ON, 0, None, None, None) + await common.async_set_direction(hass, TEST_ENTITY_ID, "invalid") + _verify(hass, STATE_ON, None, None, None, None) +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_set_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test set oscillating.""" - await _register_components(hass) expected_calls = 0 - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) expected_calls += 1 for state in (True, False): - await common.async_oscillate(hass, _TEST_FAN, state) - assert hass.states.get(_OSC_INPUT).state == str(state) - _verify(hass, STATE_ON, 0, state, None, None) + await common.async_oscillate(hass, TEST_ENTITY_ID, state) + _verify(hass, STATE_ON, None, state, None, None) expected_calls += 1 assert len(calls) == expected_calls assert calls[-1].data["action"] == "set_oscillating" - assert calls[-1].data["caller"] == _TEST_FAN - assert calls[-1].data["option"] == state + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["oscillating"] == state +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_set_direction(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test set valid direction.""" - await _register_components(hass) expected_calls = 0 - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) expected_calls += 1 - for cmd in (DIRECTION_FORWARD, DIRECTION_REVERSE): - await common.async_set_direction(hass, _TEST_FAN, cmd) - assert hass.states.get(_DIRECTION_INPUT_SELECT).state == cmd - _verify(hass, STATE_ON, 0, None, cmd, None) + for direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): + await common.async_set_direction(hass, TEST_ENTITY_ID, direction) + _verify(hass, STATE_ON, None, None, direction, None) expected_calls += 1 assert len(calls) == expected_calls assert calls[-1].data["action"] == "set_direction" - assert calls[-1].data["caller"] == _TEST_FAN - assert calls[-1].data["option"] == cmd + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["direction"] == direction +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **DIRECTION_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_set_invalid_direction( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test set invalid direction when fan has valid direction.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) - for cmd in (DIRECTION_FORWARD, "invalid"): - await common.async_set_direction(hass, _TEST_FAN, cmd) - assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD - _verify(hass, STATE_ON, 0, None, DIRECTION_FORWARD, None) + expected_calls = 1 + for direction in (DIRECTION_FORWARD, "invalid"): + await common.async_set_direction(hass, TEST_ENTITY_ID, direction) + _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD, None) + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_direction" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["direction"] == DIRECTION_FORWARD +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, OPTIMISTIC_PRESET_MODE_CONFIG2)] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_preset_modes(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test preset_modes.""" - await _register_components( - hass, ["off", "low", "medium", "high", "auto", "smart"], ["auto", "smart"] - ) - - await common.async_turn_on(hass, _TEST_FAN) - for extra, state, expected_calls in ( - ("auto", "auto", 2), - ("smart", "smart", 3), - ("invalid", "smart", 3), - ): - if extra != state: + expected_calls = 0 + valid_modes = OPTIMISTIC_PRESET_MODE_CONFIG2["preset_modes"] + for mode in ("auto", "low", "medium", "high", "invalid", "smart"): + if mode not in valid_modes: with pytest.raises(NotValidPresetModeError): - await common.async_set_preset_mode(hass, _TEST_FAN, extra) + await common.async_set_preset_mode(hass, TEST_ENTITY_ID, mode) else: - await common.async_set_preset_mode(hass, _TEST_FAN, extra) - assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == state - assert len(calls) == expected_calls - assert calls[-1].data["action"] == "set_preset_mode" - assert calls[-1].data["caller"] == _TEST_FAN - assert calls[-1].data["option"] == state + await common.async_set_preset_mode(hass, TEST_ENTITY_ID, mode) + expected_calls += 1 - await common.async_turn_on(hass, _TEST_FAN, preset_mode="auto") - assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "auto" + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_preset_mode" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["preset_mode"] == mode +@pytest.mark.parametrize(("count", "extra_config"), [(1, OPTIMISTIC_PERCENTAGE_CONFIG)]) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_set_percentage(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test set valid speed percentage.""" - await _register_components(hass) expected_calls = 0 - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) expected_calls += 1 for state, value in ( (STATE_ON, 100), (STATE_ON, 66), (STATE_ON, 0), ): - await common.async_set_percentage(hass, _TEST_FAN, value) - assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value + await common.async_set_percentage(hass, TEST_ENTITY_ID, value) _verify(hass, state, value, None, None, None) expected_calls += 1 assert len(calls) == expected_calls - assert calls[-1].data["action"] == "set_value" - assert calls[-1].data["caller"] == _TEST_FAN - assert calls[-1].data["value"] == value + assert calls[-1].data["action"] == "set_percentage" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["percentage"] == value - await common.async_turn_on(hass, _TEST_FAN, percentage=50) - assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 50 + await common.async_turn_on(hass, TEST_ENTITY_ID, percentage=50) _verify(hass, STATE_ON, 50, None, None, None) +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {"speed_count": 3, **OPTIMISTIC_PERCENTAGE_CONFIG})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_increase_decrease_speed( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test set valid increase and decrease speed.""" - await _register_components(hass, speed_count=3) - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) for func, extra, state, value in ( (common.async_set_percentage, 100, STATE_ON, 100), (common.async_decrease_speed, None, STATE_ON, 66), @@ -699,100 +1214,101 @@ async def test_increase_decrease_speed( (common.async_decrease_speed, None, STATE_ON, 0), (common.async_increase_speed, None, STATE_ON, 33), ): - await func(hass, _TEST_FAN, extra) - assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value + await func(hass, TEST_ENTITY_ID, extra) _verify(hass, state, value, None, None, None) +@pytest.mark.parametrize( + ("count", "fan_config"), + [ + ( + 1, + { + **OPTIMISTIC_ON_OFF_ACTIONS, + "preset_modes": ["auto"], + **PRESET_MODE_ACTION, + **PERCENTAGE_ACTION, + **OSCILLATE_ACTION, + **DIRECTION_ACTION, + }, + ) + ], +) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], +) +@pytest.mark.usefixtures("setup_named_fan") async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test a fan without a value_template.""" - await _register_fan_sources(hass) - with assert_setup_component(1, "fan"): - test_fan_config = { - **OPTIMISTIC_ON_OFF_CONFIG, - "preset_modes": ["auto"], - **PRESET_MODE_ACTION, - **PERCENTAGE_ACTION, - **OSCILLATE_ACTION, - **DIRECTION_ACTION, - } - assert await setup.async_setup_component( - hass, - "fan", - {"fan": {"platform": "template", "fans": {"test_fan": test_fan_config}}}, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) _verify(hass, STATE_ON) assert len(calls) == 1 assert calls[-1].data["action"] == "turn_on" - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID - await common.async_turn_off(hass, _TEST_FAN) + await common.async_turn_off(hass, TEST_ENTITY_ID) _verify(hass, STATE_OFF) assert len(calls) == 2 assert calls[-1].data["action"] == "turn_off" - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID percent = 100 - await common.async_set_percentage(hass, _TEST_FAN, percent) + await common.async_set_percentage(hass, TEST_ENTITY_ID, percent) _verify(hass, STATE_ON, percent) assert len(calls) == 3 assert calls[-1].data["action"] == "set_percentage" assert calls[-1].data["percentage"] == 100 - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID - await common.async_turn_off(hass, _TEST_FAN) + await common.async_turn_off(hass, TEST_ENTITY_ID) _verify(hass, STATE_OFF, percent) assert len(calls) == 4 assert calls[-1].data["action"] == "turn_off" - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID preset = "auto" - await common.async_set_preset_mode(hass, _TEST_FAN, preset) - assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == preset + await common.async_set_preset_mode(hass, TEST_ENTITY_ID, preset) _verify(hass, STATE_ON, percent, None, None, preset) assert len(calls) == 5 assert calls[-1].data["action"] == "set_preset_mode" assert calls[-1].data["preset_mode"] == preset - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID - await common.async_turn_off(hass, _TEST_FAN) + await common.async_turn_off(hass, TEST_ENTITY_ID) _verify(hass, STATE_OFF, percent, None, None, preset) assert len(calls) == 6 assert calls[-1].data["action"] == "turn_off" - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID - await common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD) + await common.async_set_direction(hass, TEST_ENTITY_ID, DIRECTION_FORWARD) _verify(hass, STATE_OFF, percent, None, DIRECTION_FORWARD, preset) assert len(calls) == 7 assert calls[-1].data["action"] == "set_direction" assert calls[-1].data["direction"] == DIRECTION_FORWARD - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID - await common.async_oscillate(hass, _TEST_FAN, True) + await common.async_oscillate(hass, TEST_ENTITY_ID, True) _verify(hass, STATE_OFF, percent, True, DIRECTION_FORWARD, preset) assert len(calls) == 8 assert calls[-1].data["action"] == "set_oscillating" assert calls[-1].data["oscillating"] is True - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID @pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize("style", [ConfigurationStyle.LEGACY]) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) @pytest.mark.parametrize( ("extra_config", "attribute", "action", "verify_attr", "coro", "value"), [ @@ -830,6 +1346,7 @@ async def test_optimistic_state(hass: HomeAssistant, calls: list[ServiceCall]) - ), ], ) +@pytest.mark.usefixtures("setup_optimistic_fan_attribute") async def test_optimistic_attributes( hass: HomeAssistant, attribute: str, @@ -837,27 +1354,43 @@ async def test_optimistic_attributes( verify_attr: str, coro, value: Any, - setup_optimistic_fan_attribute, calls: list[ServiceCall], ) -> None: """Test setting percentage with optimistic template.""" - await coro(hass, _TEST_FAN, value) + await coro(hass, TEST_ENTITY_ID, value) _verify(hass, STATE_ON, **{verify_attr: value}) assert len(calls) == 1 assert calls[-1].data["action"] == action assert calls[-1].data[attribute] == value - assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["caller"] == TEST_ENTITY_ID +@pytest.mark.parametrize(("count", "extra_config"), [(1, OPTIMISTIC_PERCENTAGE_CONFIG)]) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_increase_decrease_speed_default_speed_count( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test set valid increase and decrease speed.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) for func, extra, state, value in ( (common.async_set_percentage, 100, STATE_ON, 100), (common.async_decrease_speed, None, STATE_ON, 99), @@ -865,432 +1398,146 @@ async def test_increase_decrease_speed_default_speed_count( (common.async_decrease_speed, 31, STATE_ON, 67), (common.async_decrease_speed, None, STATE_ON, 66), ): - await func(hass, _TEST_FAN, extra) - assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value + await func(hass, TEST_ENTITY_ID, extra) _verify(hass, state, value, None, None, None) +@pytest.mark.parametrize( + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), + [ + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") async def test_set_invalid_osc_from_initial_state( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test set invalid oscillating when fan is in initial state.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) + await common.async_turn_on(hass, TEST_ENTITY_ID) with pytest.raises(vol.Invalid): - await common.async_oscillate(hass, _TEST_FAN, "invalid") - assert hass.states.get(_OSC_INPUT).state == "" - _verify(hass, STATE_ON, 0, None, None, None) + await common.async_oscillate(hass, TEST_ENTITY_ID, "invalid") + _verify(hass, STATE_ON, None, None, None, None) -async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test set invalid oscillating when fan has valid osc.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) - await common.async_oscillate(hass, _TEST_FAN, True) - assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, 0, True, None, None) - - with pytest.raises(vol.Invalid): - await common.async_oscillate(hass, _TEST_FAN, None) - assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, 0, True, None, None) - - -def _verify( - hass: HomeAssistant, - expected_state: str, - expected_percentage: int | None = None, - expected_oscillating: bool | None = None, - expected_direction: str | None = None, - expected_preset_mode: str | None = None, -) -> None: - """Verify fan's state, speed and osc.""" - state = hass.states.get(_TEST_FAN) - attributes = state.attributes - assert state.state == str(expected_state) - assert attributes.get(ATTR_PERCENTAGE) == expected_percentage - assert attributes.get(ATTR_OSCILLATING) == expected_oscillating - assert attributes.get(ATTR_DIRECTION) == expected_direction - assert attributes.get(ATTR_PRESET_MODE) == expected_preset_mode - - -async def _register_fan_sources(hass: HomeAssistant) -> None: - with assert_setup_component(1, "input_boolean"): - assert await setup.async_setup_component( - hass, "input_boolean", {"input_boolean": {"state": None}} - ) - - with assert_setup_component(1, "input_number"): - assert await setup.async_setup_component( - hass, - "input_number", - { - "input_number": { - "percentage": { - "min": 0.0, - "max": 100.0, - "name": "Percentage", - "step": 1.0, - "mode": "slider", - } - } - }, - ) - - with assert_setup_component(3, "input_select"): - assert await setup.async_setup_component( - hass, - "input_select", - { - "input_select": { - "preset_mode": { - "name": "Preset Mode", - "options": ["auto", "smart"], - }, - "osc": {"name": "oscillating", "options": ["", "True", "False"]}, - "direction": { - "name": "Direction", - "options": ["", DIRECTION_FORWARD, DIRECTION_REVERSE], - }, - } - }, - ) - - -async def _register_components( - hass: HomeAssistant, - speed_list: list[str] | None = None, - preset_modes: list[str] | None = None, - speed_count: int | None = None, -) -> None: - """Register basic components for testing.""" - await _register_fan_sources(hass) - - with assert_setup_component(1, "fan"): - value_template = """ - {% if is_state('input_boolean.state', 'on') %} - {{ 'on' }} - {% else %} - {{ 'off' }} - {% endif %} - """ - - test_fan_config = { - "value_template": value_template, - "preset_mode_template": "{{ states('input_select.preset_mode') }}", - "percentage_template": "{{ states('input_number.percentage') }}", - "oscillating_template": "{{ states('input_select.osc') }}", - "direction_template": "{{ states('input_select.direction') }}", - "turn_on": [ - { - "service": "input_boolean.turn_on", - "entity_id": _STATE_INPUT_BOOLEAN, - }, - { - "service": "test.automation", - "data_template": { - "action": "turn_on", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "turn_off": [ - { - "service": "input_boolean.turn_off", - "entity_id": _STATE_INPUT_BOOLEAN, - }, - { - "service": "input_number.set_value", - "data_template": { - "entity_id": _PERCENTAGE_INPUT_NUMBER, - "value": 0, - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "turn_off", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "set_preset_mode": [ - { - "service": "input_select.select_option", - "data_template": { - "entity_id": _PRESET_MODE_INPUT_SELECT, - "option": "{{ preset_mode }}", - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "set_preset_mode", - "caller": "{{ this.entity_id }}", - "option": "{{ preset_mode }}", - }, - }, - ], - "set_percentage": [ - { - "service": "input_number.set_value", - "data_template": { - "entity_id": _PERCENTAGE_INPUT_NUMBER, - "value": "{{ percentage }}", - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "set_value", - "caller": "{{ this.entity_id }}", - "value": "{{ percentage }}", - }, - }, - ], - "set_oscillating": [ - { - "service": "input_select.select_option", - "data_template": { - "entity_id": _OSC_INPUT, - "option": "{{ oscillating }}", - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "set_oscillating", - "caller": "{{ this.entity_id }}", - "option": "{{ oscillating }}", - }, - }, - ], - "set_direction": [ - { - "service": "input_select.select_option", - "data_template": { - "entity_id": _DIRECTION_INPUT_SELECT, - "option": "{{ direction }}", - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "set_direction", - "caller": "{{ this.entity_id }}", - "option": "{{ direction }}", - }, - }, - ], - } - - if preset_modes: - test_fan_config["preset_modes"] = preset_modes - - if speed_count: - test_fan_config["speed_count"] = speed_count - - assert await setup.async_setup_component( - hass, - "fan", - {"fan": {"platform": "template", "fans": {"test_fan": test_fan_config}}}, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "extra_config"), [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OSCILLATE_ACTION})] +) +@pytest.mark.parametrize( + ("style", "fan_config"), [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "test_template_fan_01": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ true }}", - "turn_on": { - "service": "fan.turn_on", - "entity_id": "fan.test_state", - }, - "turn_off": { - "service": "fan.turn_off", - "entity_id": "fan.test_state", - }, - }, - "test_template_fan_02": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ false }}", - "turn_on": { - "service": "fan.turn_on", - "entity_id": "fan.test_state", - }, - "turn_off": { - "service": "fan.turn_off", - "entity_id": "fan.test_state", - }, - }, - }, - } - }, + ( + ConfigurationStyle.LEGACY, + { + "value_template": "{{ 'on' }}", + }, + ), + ( + ConfigurationStyle.MODERN, + { + "state": "{{ 'on' }}", + }, + ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") +async def test_set_invalid_osc(hass: HomeAssistant, calls: list[ServiceCall]) -> None: + """Test set invalid oscillating when fan has valid osc.""" + await common.async_turn_on(hass, TEST_ENTITY_ID) + await common.async_oscillate(hass, TEST_ENTITY_ID, True) + _verify(hass, STATE_ON, None, True, None, None) + + await common.async_oscillate(hass, TEST_ENTITY_ID, False) + _verify(hass, STATE_ON, None, False, None, None) + + with pytest.raises(vol.Invalid): + await common.async_oscillate(hass, TEST_ENTITY_ID, None) + _verify(hass, STATE_ON, None, False, None, None) + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("fan_config", "style"), + [ + ( + { + "test_template_cover_01": UNIQUE_ID_CONFIG, + "test_template_cover_02": UNIQUE_ID_CONFIG, + }, + ConfigurationStyle.LEGACY, + ), + ( + [ + { + "name": "test_template_cover_01", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_cover_02", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), + ], +) +@pytest.mark.usefixtures("setup_fan") async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one fan per id.""" assert len(hass.states.async_all()) == 1 @pytest.mark.parametrize( - ("speed_count", "percentage_step"), [(0, 1), (100, 1), (3, 100 / 3)] + ("count", "extra_config"), + [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OPTIMISTIC_PERCENTAGE_CONFIG})], ) -async def test_implemented_percentage( - hass: HomeAssistant, speed_count, percentage_step -) -> None: +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], +) +@pytest.mark.parametrize( + ("fan_config", "percentage_step"), + [({"speed_count": 0}, 1), ({"speed_count": 100}, 1), ({"speed_count": 3}, 100 / 3)], +) +@pytest.mark.usefixtures("setup_test_fan_with_extra_config") +async def test_speed_percentage_step(hass: HomeAssistant, percentage_step) -> None: """Test a fan that implements percentage.""" - await setup.async_setup_component( - hass, - "fan", - { - "fan": { - "platform": "template", - "fans": { - "mechanical_ventilation": { - "friendly_name": "Mechanische ventilatie", - "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72", - "value_template": "{{ states('light.mv_snelheid') }}", - "percentage_template": ( - "{{ (state_attr('light.mv_snelheid','brightness') | int /" - " 255 * 100) | int }}" - ), - "turn_on": [ - { - "service": "switch.turn_off", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": 40}, - }, - ], - "turn_off": [ - { - "service": "light.turn_off", - "target": { - "entity_id": "light.mv_snelheid", - }, - }, - { - "service": "switch.turn_on", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - ], - "set_percentage": [ - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": "{{ percentage }}"}, - } - ], - "speed_count": speed_count, - }, - }, - }, - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 - state = hass.states.get("fan.mechanical_ventilation") + state = hass.states.get(TEST_ENTITY_ID) attributes = state.attributes assert attributes["percentage_step"] == percentage_step assert attributes.get("supported_features") & FanEntityFeature.SET_SPEED -@pytest.mark.parametrize(("count", "domain"), [(1, FAN_DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - FAN_DOMAIN: { - "platform": "template", - "fans": { - "mechanical_ventilation": { - "friendly_name": "Mechanische ventilatie", - "unique_id": "a2fd2e38-674b-4b47-b5ef-cc2362211a72", - "value_template": "{{ states('light.mv_snelheid') }}", - "preset_mode_template": "{{ 'any' }}", - "preset_modes": ["any"], - "set_preset_mode": [ - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": "{{ percentage }}"}, - } - ], - "turn_on": [ - { - "service": "switch.turn_off", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - { - "service": "light.turn_on", - "target": { - "entity_id": "light.mv_snelheid", - }, - "data": {"brightness_pct": 40}, - }, - ], - "turn_off": [ - { - "service": "light.turn_off", - "target": { - "entity_id": "light.mv_snelheid", - }, - }, - { - "service": "switch.turn_on", - "target": { - "entity_id": "switch.mv_automatisch", - }, - }, - ], - }, - }, - } - }, - ], + ("count", "fan_config"), + [(1, {**OPTIMISTIC_ON_OFF_ACTIONS, **OPTIMISTIC_PRESET_MODE_CONFIG2})], ) -@pytest.mark.usefixtures("start_ha") -async def test_implemented_preset_mode(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], +) +@pytest.mark.usefixtures("setup_named_fan") +async def test_preset_mode_supported_features(hass: HomeAssistant) -> None: """Test a fan that implements preset_mode.""" assert len(hass.states.async_all()) == 1 - state = hass.states.get("fan.mechanical_ventilation") + state = hass.states.get(TEST_ENTITY_ID) attributes = state.attributes - assert attributes.get("percentage") is None assert attributes.get("supported_features") & FanEntityFeature.PRESET_MODE @@ -1305,6 +1552,13 @@ async def test_implemented_preset_mode(hass: HomeAssistant) -> None: "turn_off": [], }, ), + ( + ConfigurationStyle.MODERN, + { + "turn_on": [], + "turn_off": [], + }, + ), ], ) @pytest.mark.parametrize( @@ -1342,7 +1596,51 @@ async def test_empty_action_config( setup_test_fan_with_extra_config, ) -> None: """Test configuration with empty script.""" - state = hass.states.get(_TEST_FAN) + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["supported_features"] == ( FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON | supported_features ) + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a template unique_id propagates to switch unique_ids.""" + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "fan": [ + { + **OPTIMISTIC_ON_OFF_ACTIONS, + "name": "test_a", + "unique_id": "a", + "state": "{{ true }}", + }, + { + **OPTIMISTIC_ON_OFF_ACTIONS, + "name": "test_b", + "unique_id": "b", + "state": "{{ true }}", + }, + ], + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("fan")) == 2 + + entry = entity_registry.async_get("fan.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("fan.test_b") + assert entry + assert entry.unique_id == "x-b" From 9c4733595af69072d03bd814d10099640ca483c6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 15 May 2025 09:27:48 +0200 Subject: [PATCH 0482/1175] Fix unknown Pure AQI in Sensibo (#144924) * Fix unknown Pure AQI in Sensibo * Fix mypy --- homeassistant/components/sensibo/climate.py | 2 +- homeassistant/components/sensibo/manifest.json | 2 +- homeassistant/components/sensibo/sensor.py | 16 ++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/test_sensor.py | 13 ++++++++++++- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 906c4259ce5..a40cb110f66 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -252,7 +252,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): return features @property - def current_humidity(self) -> int | None: + def current_humidity(self) -> float | None: """Return the current humidity.""" return self.device_data.humidity diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 610695aaf7b..4cadd3f8692 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -15,5 +15,5 @@ "iot_class": "cloud_polling", "loggers": ["pysensibo"], "quality_scale": "platinum", - "requirements": ["pysensibo==1.1.0"] + "requirements": ["pysensibo==1.2.1"] } diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 09f095bfaec..bab85eb2294 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -101,14 +101,25 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( value_fn=lambda data: data.temperature, ), ) + + +def _pure_aqi(pm25_pure: PureAQI | None) -> str | None: + """Return the Pure aqi name or None if unknown.""" + if pm25_pure: + aqi_name = pm25_pure.name.lower() + if aqi_name != "unknown": + return aqi_name + return None + + PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="pm25", translation_key="pm25_pure", device_class=SensorDeviceClass.ENUM, - value_fn=lambda data: data.pm25_pure.name.lower() if data.pm25_pure else None, + value_fn=lambda data: _pure_aqi(data.pm25_pure), extra_fn=None, - options=[aqi.name.lower() for aqi in PureAQI], + options=[aqi.name.lower() for aqi in PureAQI if aqi.name != "UNKNOWN"], ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", @@ -119,6 +130,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( FILTER_LAST_RESET_DESCRIPTION, ) + DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", diff --git a/requirements_all.txt b/requirements_all.txt index 9a5807d7b40..42c54364162 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2293,7 +2293,7 @@ pysaj==0.0.16 pyschlage==2025.4.0 # homeassistant.components.sensibo -pysensibo==1.1.0 +pysensibo==1.2.1 # homeassistant.components.serial pyserial-asyncio-fast==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7491190cb92..aba6bd90c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1875,7 +1875,7 @@ pysabnzbd==1.1.1 pyschlage==2025.4.0 # homeassistant.components.sensibo -pysensibo==1.1.0 +pysensibo==1.2.1 # homeassistant.components.acer_projector # homeassistant.components.crownstone diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 8ea76036123..7b7450b97a4 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -11,7 +11,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry -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 @@ -45,3 +45,14 @@ async def test_sensor( state = hass.states.get("sensor.kitchen_pure_aqi") assert state.state == "moderate" + + mock_client.async_get_devices_data.return_value.parsed[ + "AAZZAAZZ" + ].pm25_pure = PureAQI(0) + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.kitchen_pure_aqi") + assert state.state == STATE_UNKNOWN From 7c306acd5d935e012143376dc6d8f002b14bb2f4 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Thu, 15 May 2025 09:48:01 +0200 Subject: [PATCH 0483/1175] Emoncms remove useless var in tests (#144942) --- tests/components/emoncms/conftest.py | 39 +--------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/tests/components/emoncms/conftest.py b/tests/components/emoncms/conftest.py index 4bd1d68217a..100fb2bd879 100644 --- a/tests/components/emoncms/conftest.py +++ b/tests/components/emoncms/conftest.py @@ -7,14 +7,7 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.emoncms.const import CONF_ONLY_INCLUDE_FEEDID, DOMAIN -from homeassistant.const import ( - CONF_API_KEY, - CONF_ID, - CONF_PLATFORM, - CONF_URL, - CONF_VALUE_TEMPLATE, -) -from homeassistant.helpers.typing import ConfigType +from homeassistant.const import CONF_API_KEY, CONF_URL from tests.common import MockConfigEntry @@ -50,36 +43,6 @@ FLOW_RESULT = { SENSOR_NAME = "emoncms@1.1.1.1" -YAML_BASE = { - CONF_PLATFORM: "emoncms", - CONF_API_KEY: "my_api_key", - CONF_ID: 1, - CONF_URL: "http://1.1.1.1", -} - -YAML = { - **YAML_BASE, - CONF_ONLY_INCLUDE_FEEDID: [1], -} - - -@pytest.fixture -def emoncms_yaml_config() -> ConfigType: - """Mock emoncms yaml configuration.""" - return {"sensor": YAML} - - -@pytest.fixture -def emoncms_yaml_config_with_template() -> ConfigType: - """Mock emoncms yaml conf with template parameter.""" - return {"sensor": {**YAML, CONF_VALUE_TEMPLATE: "{{ value | float + 1500 }}"}} - - -@pytest.fixture -def emoncms_yaml_config_no_include_only_feed_id() -> ConfigType: - """Mock emoncms yaml configuration without include_only_feed_id parameter.""" - return {"sensor": YAML_BASE} - @pytest.fixture def config_entry() -> MockConfigEntry: From fd09476b28fb2739f5c62b879ad734b89752a95e Mon Sep 17 00:00:00 2001 From: markhannon Date: Thu, 15 May 2025 18:12:18 +1000 Subject: [PATCH 0484/1175] Add sensor entity to Zimi integration (#144329) * Import sensor.py * Light design alignment * Fix merge error * Refactor with extend * Update homeassistant/components/zimi/sensor.py Co-authored-by: Josef Zweck * value_fn and inline refactoring * strings.json and translation_key * Add sensor_name * Revert "Add sensor_name" This reverts commit ad3da048e9c5a6ecdb15052c253de7dc46b1120f. * Default naming for sensors * Remove uneeded 'garage' and use default battery name * Bump to zcc-helper 3.5.2 which maps "Garage Controller" tp "Garage" in device.name * Update homeassistant/components/zimi/sensor.py Co-authored-by: Josef Zweck * Update homeassistant/components/zimi/sensor.py Co-authored-by: Josef Zweck * Update strings.json * Revert "Bump to zcc-helper 3.5.2 which maps "Garage Controller" tp "Garage" in device.name" This reverts commit 345ef8a4859c8d0e188462c9a69a4fab8f284b69. * Update homeassistant/components/zimi/sensor.py --------- Co-authored-by: Josef Zweck --- homeassistant/components/zimi/__init__.py | 2 +- homeassistant/components/zimi/entity.py | 7 +- homeassistant/components/zimi/sensor.py | 103 +++++++++++++++++++++ homeassistant/components/zimi/strings.json | 7 ++ 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zimi/sensor.py diff --git a/homeassistant/components/zimi/__init__.py b/homeassistant/components/zimi/__init__.py index ab52c1491e1..a184ba71a52 100644 --- a/homeassistant/components/zimi/__init__.py +++ b/homeassistant/components/zimi/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN from .helpers import async_connect_to_controller -PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SWITCH] +PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zimi/entity.py b/homeassistant/components/zimi/entity.py index 68911992014..12d8f336bf0 100644 --- a/homeassistant/components/zimi/entity.py +++ b/homeassistant/components/zimi/entity.py @@ -21,7 +21,9 @@ class ZimiEntity(Entity): _attr_should_poll = False _attr_has_entity_name = True - def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None: + def __init__( + self, device: ControlPointDevice, api: ControlPoint, use_device_name=True + ) -> None: """Initialize an HA Entity which is a ZimiDevice.""" self._device = device @@ -36,7 +38,8 @@ class ZimiEntity(Entity): suggested_area=device.room, via_device=(DOMAIN, api.mac), ) - self._attr_name = device.name.strip() + if use_device_name: + self._attr_name = device.name.strip() self._attr_suggested_area = device.room @property diff --git a/homeassistant/components/zimi/sensor.py b/homeassistant/components/zimi/sensor.py new file mode 100644 index 00000000000..2c681f8e69e --- /dev/null +++ b/homeassistant/components/zimi/sensor.py @@ -0,0 +1,103 @@ +"""Platform for sensor integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging + +from zcc import ControlPoint +from zcc.device import ControlPointDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import ZimiConfigEntry +from .entity import ZimiEntity + + +@dataclass(frozen=True, kw_only=True) +class ZimiSensorEntityDescription(SensorEntityDescription): + """Class describing Zimi sensor entities.""" + + value_fn: Callable[[ControlPointDevice], StateType] + + +GARAGE_SENSOR_DESCRIPTIONS: tuple[ZimiSensorEntityDescription, ...] = ( + ZimiSensorEntityDescription( + key="door_temperature", + translation_key="door_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda device: device.door_temp, + ), + ZimiSensorEntityDescription( + key="garage_battery", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.BATTERY, + value_fn=lambda device: device.battery_level, + ), + ZimiSensorEntityDescription( + key="garage_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + value_fn=lambda device: device.garage_temp, + ), + ZimiSensorEntityDescription( + key="garage_humidty", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + value_fn=lambda device: device.garage_humidity, + ), +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ZimiConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Zimi Sensor platform.""" + + api = config_entry.runtime_data + + async_add_entities( + ZimiSensor(device, description, api) + for device in api.sensors + for description in GARAGE_SENSOR_DESCRIPTIONS + ) + + +class ZimiSensor(ZimiEntity, SensorEntity): + """Representation of a Zimi sensor.""" + + entity_description: ZimiSensorEntityDescription + + def __init__( + self, + device: ControlPointDevice, + description: ZimiSensorEntityDescription, + api: ControlPoint, + ) -> None: + """Initialize an ZimiSensor with specified type.""" + + super().__init__(device, api, use_device_name=False) + + self.entity_description = description + self._attr_unique_id = device.identifier + "." + self.entity_description.key + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + + return self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/zimi/strings.json b/homeassistant/components/zimi/strings.json index 530eb86ef05..e1c7944b25a 100644 --- a/homeassistant/components/zimi/strings.json +++ b/homeassistant/components/zimi/strings.json @@ -42,5 +42,12 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "door_temperature": { + "name": "Outside temperature" + } + } } } From ea046f32beb53806b544f59b0cef1a05ccbce677 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 15 May 2025 04:43:56 -0400 Subject: [PATCH 0485/1175] Add modern style template lock (#144756) * Add modern style lock * add tests * Add tests and address comments * Update homeassistant/components/template/lock.py --------- Co-authored-by: Erik Montnemery --- homeassistant/components/template/config.py | 7 +- homeassistant/components/template/lock.py | 94 ++- tests/components/template/test_lock.py | 889 +++++++++++++------- 3 files changed, 652 insertions(+), 338 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 5e7425f13d7..1dc20d07c0e 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -17,6 +17,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -50,6 +51,7 @@ from . import ( fan as fan_platform, image as image_platform, light as light_platform, + lock as lock_platform, number as number_platform, select as select_platform, sensor as sensor_platform, @@ -124,6 +126,9 @@ CONFIG_SECTION_SCHEMA = vol.All( vol.Optional(LIGHT_DOMAIN): vol.All( cv.ensure_list, [light_platform.LIGHT_SCHEMA] ), + vol.Optional(LOCK_DOMAIN): vol.All( + cv.ensure_list, [lock_platform.LOCK_SCHEMA] + ), vol.Optional(WEATHER_DOMAIN): vol.All( cv.ensure_list, [weather_platform.WEATHER_SCHEMA] ), @@ -139,7 +144,7 @@ CONFIG_SECTION_SCHEMA = vol.All( }, ), ensure_domains_do_not_have_trigger_or_action( - BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN + BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN ), ) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 12a3e66cb5e..c858325e0ea 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ATTR_CODE, CONF_NAME, CONF_OPTIMISTIC, + CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) @@ -25,14 +26,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_PICTURE, DOMAIN from .template_entity import ( + LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_ICON_SCHEMA, TemplateEntity, rewrite_common_legacy_to_modern_conf, ) CONF_CODE_FORMAT_TEMPLATE = "code_format_template" +CONF_CODE_FORMAT = "code_format" CONF_LOCK = "lock" CONF_UNLOCK = "unlock" CONF_OPEN = "open" @@ -40,26 +45,69 @@ CONF_OPEN = "open" DEFAULT_NAME = "Template Lock" DEFAULT_OPTIMISTIC = False +LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | { + CONF_CODE_FORMAT_TEMPLATE: CONF_CODE_FORMAT, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + +LOCK_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_CODE_FORMAT): cv.template, + vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_NAME): cv.template, + vol.Optional(CONF_OPEN): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PICTURE): cv.template, + vol.Required(CONF_STATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema), +) + + PLATFORM_SCHEMA = LOCK_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, - vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_OPEN): cv.SCRIPT_SCHEMA, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_CODE_FORMAT_TEMPLATE): cv.template, + vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OPEN): cv.SCRIPT_SCHEMA, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, } ).extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY.schema) -async def _async_create_entities( - hass: HomeAssistant, config: dict[str, Any] -) -> list[TemplateLock]: - """Create the Template lock.""" - config = rewrite_common_legacy_to_modern_conf(hass, config) - return [TemplateLock(hass, config, config.get(CONF_UNIQUE_ID))] +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the template fans.""" + fans = [] + + for entity_conf in definitions: + unique_id = entity_conf.get(CONF_UNIQUE_ID) + + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" + + fans.append( + TemplateLock( + hass, + entity_conf, + unique_id, + ) + ) + + async_add_entities(fans) async def async_setup_platform( @@ -68,8 +116,22 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the template lock.""" - async_add_entities(await _async_create_entities(hass, config)) + """Set up the template fans.""" + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + [rewrite_common_legacy_to_modern_conf(hass, config, LEGACY_FIELDS)], + None, + ) + return + + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], + ) class TemplateLock(TemplateEntity, LockEntity): @@ -92,7 +154,7 @@ class TemplateLock(TemplateEntity, LockEntity): if TYPE_CHECKING: assert name is not None - self._state_template = config.get(CONF_VALUE_TEMPLATE) + self._state_template = config.get(CONF_STATE) for action_id, supported_feature in ( (CONF_LOCK, 0), (CONF_UNLOCK, 0), @@ -102,7 +164,7 @@ class TemplateLock(TemplateEntity, LockEntity): if (action_config := config.get(action_id)) is not None: self.add_script(action_id, action_config, name, DOMAIN) self._attr_supported_features |= supported_feature - self._code_format_template = config.get(CONF_CODE_FORMAT_TEMPLATE) + self._code_format_template = config.get(CONF_CODE_FORMAT) self._code_format: str | None = None self._code_format_template_error: TemplateError | None = None self._optimistic = config.get(CONF_OPTIMISTIC) diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 50baa11b2d0..4435e4a2404 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1,9 +1,11 @@ """The tests for the Template lock platform.""" +from typing import Any + import pytest from homeassistant import setup -from homeassistant.components import lock +from homeassistant.components import lock, template from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ( ATTR_CODE, @@ -14,25 +16,38 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ConfigurationStyle from tests.common import assert_setup_component -OPTIMISTIC_LOCK_CONFIG = { - "platform": "template", +TEST_OBJECT_ID = "test_template_lock" +TEST_ENTITY_ID = f"lock.{TEST_OBJECT_ID}" +TEST_STATE_ENTITY_ID = "switch.test_state" + +LOCK_ACTION = { "lock": { "service": "test.automation", "data_template": { "action": "lock", "caller": "{{ this.entity_id }}", + "code": "{{ code if code is defined else None }}", }, }, +} +UNLOCK_ACTION = { "unlock": { "service": "test.automation", "data_template": { "action": "unlock", "caller": "{{ this.entity_id }}", + "code": "{{ code if code is defined else None }}", }, }, +} +OPEN_ACTION = { "open": { "service": "test.automation", "data_template": { @@ -42,424 +57,565 @@ OPTIMISTIC_LOCK_CONFIG = { }, } -OPTIMISTIC_CODED_LOCK_CONFIG = { - "platform": "template", - "lock": { - "service": "test.automation", - "data_template": { - "action": "lock", - "caller": "{{ this.entity_id }}", - "code": "{{ code }}", - }, - }, - "unlock": { - "service": "test.automation", - "data_template": { - "action": "unlock", - "caller": "{{ this.entity_id }}", - "code": "{{ code }}", - }, - }, + +OPTIMISTIC_LOCK = { + **LOCK_ACTION, + **UNLOCK_ACTION, } -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +OPTIMISTIC_LOCK_CONFIG = { + "platform": "template", + **LOCK_ACTION, + **UNLOCK_ACTION, + **OPEN_ACTION, +} + +OPTIMISTIC_CODED_LOCK_CONFIG = { + "platform": "template", + **LOCK_ACTION, + **UNLOCK_ACTION, +} + + +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, lock_config: dict[str, Any] +) -> None: + """Do setup of lock integration via legacy format.""" + config = {"lock": {"platform": "template", "name": TEST_OBJECT_ID, **lock_config}} + + with assert_setup_component(count, lock.DOMAIN): + assert await async_setup_component( + hass, + lock.DOMAIN, + config, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_modern_format( + hass: HomeAssistant, count: int, lock_config: dict[str, Any] +) -> None: + """Do setup of lock integration via modern format.""" + config = {"template": {"lock": {"name": TEST_OBJECT_ID, **lock_config}}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_lock( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + lock_config: dict[str, Any], +) -> None: + """Do setup of lock integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, lock_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, lock_config) + + +@pytest.fixture +async def setup_base_lock( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + extra_config: dict, +): + """Do setup of cover integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + {"value_template": state_template, **extra_config}, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {"state": state_template, **extra_config}, + ) + + +@pytest.fixture +async def setup_state_lock( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of cover integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + **OPTIMISTIC_LOCK, + "value_template": state_template, + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + **OPTIMISTIC_LOCK, + "state": state_template, + }, + ) + + +@pytest.fixture +async def setup_state_lock_with_extra_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + extra_config: dict, +): + """Do setup of cover integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + {**OPTIMISTIC_LOCK, "value_template": state_template, **extra_config}, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {**OPTIMISTIC_LOCK, "state": state_template, **extra_config}, + ) + + +@pytest.fixture +async def setup_state_lock_with_attribute( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + attribute: str, + attribute_template: str, +): + """Do setup of cover integration using a state template.""" + extra = {attribute: attribute_template} if attribute else {} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + **OPTIMISTIC_LOCK, + "value_template": state_template, + **extra, + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {**OPTIMISTIC_LOCK, "state": state_template, **extra}, + ) + + @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "name": "Test template lock", - "value_template": "{{ states.switch.test_state.state }}", - } - }, - ], + ("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock") async def test_template_state(hass: HomeAssistant) -> None: """Test template.""" - hass.states.async_set("switch.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() state = hass.states.get("lock.test_template_lock") assert state.state == LockState.LOCKED - hass.states.async_set("switch.test_state", STATE_OFF) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() state = hass.states.get("lock.test_template_lock") assert state.state == LockState.UNLOCKED - hass.states.async_set("switch.test_state", STATE_OPEN) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OPEN) await hass.async_block_till_done() state = hass.states.get("lock.test_template_lock") assert state.state == LockState.OPEN -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "name": "Test lock", - "optimistic": True, - "value_template": "{{ states.switch.test_state.state }}", - } - }, - ], + ("count", "state_template", "extra_config"), + [(1, "{{ states.switch.test_state.state }}", {"optimistic": True, **OPEN_ACTION})], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock_with_extra_config") async def test_open_lock_optimistic( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test optimistic open.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("lock.test_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_OPEN, - {ATTR_ENTITY_ID: "lock.test_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "open" - assert calls[0].data["caller"] == "lock.test_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID - state = hass.states.get("lock.test_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.OPEN -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 1 }}", - } - }, - ], + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_lock") async def test_template_state_boolean_on(hass: HomeAssistant) -> None: """Test the setting of the state with boolean on.""" - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 2 }}")]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 2 }}", - } - }, - ], + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_lock") async def test_template_state_boolean_off(hass: HomeAssistant) -> None: """Test the setting of the state with off.""" - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED -@pytest.mark.parametrize(("count", "domain"), [(0, lock.DOMAIN)]) +@pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( - "config", + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("state_template", "extra_config"), [ - { - lock.DOMAIN: { - "platform": "template", - "value_template": "{% if rubbish %}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, - } - }, - { - "switch": { - "platform": "lock", - "name": "{{%}", - "value_template": "{{ rubbish }", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, - }, - }, - {lock.DOMAIN: {"platform": "template", "value_template": "Invalid"}}, - { - lock.DOMAIN: { - "platform": "template", + ("{% if rubbish %}", OPTIMISTIC_LOCK), + ("{{ rubbish }", OPTIMISTIC_LOCK), + ("Invalid", {}), + ( + "{{ 1==1 }}", + { "not_value_template": "{{ states.switch.test_state.state }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, - } - }, - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 1 }}", - "code_format_template": "{{ rubbish }", - } - }, - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 1 }}", - "code_format_template": "{% if rubbish %}", - } - }, + **OPTIMISTIC_LOCK, + }, + ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_base_lock") async def test_template_syntax_error(hass: HomeAssistant) -> None: """Test templating syntax errors don't create entities.""" assert hass.states.async_all("lock") == [] -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize(("count", "state_template"), [(0, "{{ 1==1 }}")]) +@pytest.mark.parametrize("attribute_template", ["{{ rubbish }", "{% if rubbish %}"]) @pytest.mark.parametrize( - "config", + ("style", "attribute"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 + 1 }}", - } - }, + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_lock_with_attribute") +async def test_template_code_template_syntax_error(hass: HomeAssistant) -> None: + """Test templating code_format syntax errors don't create entities.""" + assert hass.states.async_all("lock") == [] + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 + 1 }}")]) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock") async def test_template_static(hass: HomeAssistant) -> None: """Test that we allow static templates.""" - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED - hass.states.async_set("lock.template_lock", LockState.LOCKED) + hass.states.async_set(TEST_ENTITY_ID, LockState.LOCKED) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("state_template", "expected"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - } - }, + ("{{ True }}", LockState.LOCKED), + ("{{ False }}", LockState.UNLOCKED), + ("{{ x - 12 }}", STATE_UNAVAILABLE), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_lock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test lock action.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_OFF) +@pytest.mark.usefixtures("setup_state_lock") +async def test_state_template(hass: HomeAssistant, expected: str) -> None: + """Test state and value_template template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1==1 }}")]) +@pytest.mark.parametrize( + "attribute_template", + ["{% if states.switch.test_state.state %}/local/switch.png{% endif %}"], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "picture"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") +async def test_picture_template(hass: HomeAssistant) -> None: + """Test entity_picture template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("entity_picture") in ("", None) + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["entity_picture"] == "/local/switch.png" + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1==1 }}")]) +@pytest.mark.parametrize( + "attribute_template", + ["{% if states.switch.test_state.state %}mdi:eye{% endif %}"], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "icon"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") +async def test_icon_template(hass: HomeAssistant) -> None: + """Test entity_picture template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") in ("", None) + + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["icon"] == "mdi:eye" + + +@pytest.mark.parametrize( + ("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock") +async def test_lock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: + """Test lock action.""" + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "lock" - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - } - }, - ], + ("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock") async def test_unlock_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test unlock action.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "unlock" - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - } - }, - ], + ("count", "state_template", "extra_config"), + [(1, "{{ states.switch.test_state.state }}", OPEN_ACTION)], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_lock_with_extra_config") async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test open action.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_OPEN, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "open" - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_CODED_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - "code_format_template": "{{ '.+' }}", - } - }, + ( + 1, + "{{ states.switch.test_state.state }}", + "{{ '.+' }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_action_with_code( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test lock action with defined code format and supplied lock code.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_OFF) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "LOCK_CODE"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "LOCK_CODE"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "lock" - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID assert calls[0].data["code"] == "LOCK_CODE" -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_CODED_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - "code_format_template": "{{ '.+' }}", - } - }, + ( + 1, + "{{ states.switch.test_state.state }}", + "{{ '.+' }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_unlock_action_with_code( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test unlock action with code format and supplied unlock code.""" await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_ON) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.LOCKED await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "UNLOCK_CODE"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "UNLOCK_CODE"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == "unlock" - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID assert calls[0].data["code"] == "UNLOCK_CODE" -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 1 }}", - "code_format_template": "{{ '\\\\d+' }}", - } - }, + ( + 1, + "{{ 1 == 1 }}", + "{{ '\\\\d+' }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), ], ) @pytest.mark.parametrize( @@ -469,7 +625,7 @@ async def test_unlock_action_with_code( lock.SERVICE_UNLOCK, ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_actions_fail_with_invalid_code( hass: HomeAssistant, calls: list[ServiceCall], test_action ) -> None: @@ -477,32 +633,36 @@ async def test_lock_actions_fail_with_invalid_code( await hass.services.async_call( lock.DOMAIN, test_action, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "non-number-value"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "non-number-value"}, ) await hass.services.async_call( lock.DOMAIN, test_action, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 0 -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 == 1 }}", - "code_format_template": "{{ 1/0 }}", - } - }, + ( + 1, + "{{ 1 == 1 }}", + "{{ 1/0 }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_lock_actions_dont_execute_with_code_template_rendering_error( hass: HomeAssistant, calls: list[ServiceCall] ) -> None: @@ -510,142 +670,146 @@ async def test_lock_actions_dont_execute_with_code_template_rendering_error( await hass.services.async_call( lock.DOMAIN, lock.SERVICE_LOCK, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.services.async_call( lock.DOMAIN, lock.SERVICE_UNLOCK, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "any-value"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "any-value"}, ) await hass.async_block_till_done() assert len(calls) == 0 -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) -@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_CODED_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - "code_format_template": "{{ None }}", - } - }, + ( + 1, + "{{ states.switch.test_state.state }}", + "{{ None }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), + ], +) +@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_actions_with_none_as_codeformat_ignores_code( hass: HomeAssistant, action, calls: list[ServiceCall] ) -> None: """Test lock actions with supplied lock code.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_OFF) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, action, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "any code"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "any code"}, ) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["action"] == action - assert calls[0].data["caller"] == "lock.template_lock" + assert calls[0].data["caller"] == TEST_ENTITY_ID assert calls[0].data["code"] == "any code" -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) -@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states.switch.test_state.state }}", - "code_format_template": "[12]{1", - } - }, + ( + 1, + "{{ states.switch.test_state.state }}", + "[12]{1", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "code_format_template"), + (ConfigurationStyle.MODERN, "code_format"), + ], +) +@pytest.mark.parametrize("action", [lock.SERVICE_LOCK, lock.SERVICE_UNLOCK]) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_actions_with_invalid_regexp_as_codeformat_never_execute( hass: HomeAssistant, action, calls: list[ServiceCall] ) -> None: """Test lock actions don't execute with invalid regexp.""" - await setup.async_setup_component(hass, "switch", {}) - hass.states.async_set("switch.test_state", STATE_OFF) + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == LockState.UNLOCKED await hass.services.async_call( lock.DOMAIN, action, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "1"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "1"}, ) await hass.services.async_call( lock.DOMAIN, action, - {ATTR_ENTITY_ID: "lock.template_lock", ATTR_CODE: "x"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_CODE: "x"}, ) await hass.services.async_call( lock.DOMAIN, action, - {ATTR_ENTITY_ID: "lock.template_lock"}, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, ) await hass.async_block_till_done() assert len(calls) == 0 -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", - [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states.input_select.test_state.state }}", - } - }, - ], + ("count", "state_template"), [(1, "{{ states.input_select.test_state.state }}")] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) @pytest.mark.parametrize( "test_state", [LockState.UNLOCKING, LockState.LOCKING, LockState.JAMMED] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_lock") async def test_lock_state(hass: HomeAssistant, test_state) -> None: """Test value template.""" hass.states.async_set("input_select.test_state", test_state) await hass.async_block_till_done() - state = hass.states.get("lock.template_lock") + state = hass.states.get(TEST_ENTITY_ID) assert state.state == test_state -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ states('switch.test_state') }}", - "availability_template": "{{ is_state('availability_state.state', 'on') }}", - } - }, + ( + 1, + "{{ states('switch.test_state') }}", + "{{ is_state('availability_state.state', 'on') }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_available_template_with_entities(hass: HomeAssistant) -> None: """Test availability templates with values from other entities.""" # When template returns true.. @@ -653,35 +817,39 @@ async def test_available_template_with_entities(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Device State should not be unavailable - assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE # When Availability template returns false hass.states.async_set("availability_state.state", STATE_OFF) await hass.async_block_till_done() # device state should be unavailable - assert hass.states.get("lock.template_lock").state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE -@pytest.mark.parametrize(("count", "domain"), [(1, lock.DOMAIN)]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute_template"), [ - { - lock.DOMAIN: { - **OPTIMISTIC_LOCK_CONFIG, - "value_template": "{{ 1 + 1 }}", - "availability_template": "{{ x - 12 }}", - } - }, + ( + 1, + "{{ 1 + 1 }}", + "{{ x - 12 }}", + ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + ], +) +@pytest.mark.usefixtures("setup_state_lock_with_attribute") async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" - assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE assert ("UndefinedError: 'x' is undefined") in caplog_setup_text @@ -700,7 +868,7 @@ async def test_invalid_availability_template_keeps_component_available( ], ) @pytest.mark.usefixtures("start_ha") -async def test_unique_id(hass: HomeAssistant) -> None: +async def test_legacy_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one lock per id.""" await setup.async_setup_component( hass, @@ -722,6 +890,85 @@ async def test_unique_id(hass: HomeAssistant) -> None: assert len(hass.states.async_all("lock")) == 1 +async def test_modern_unique_id(hass: HomeAssistant) -> None: + """Test unique_id option only creates one cover per id.""" + config = { + "template": { + "lock": [ + { + "name": "test_template_lock_01", + "unique_id": "not-so-unique-anymore", + "state": "{{ false }}", + **OPTIMISTIC_LOCK, + }, + { + "name": "test_template_lock_02", + "unique_id": "not-so-unique-anymore", + "state": "{{ false }}", + **OPTIMISTIC_LOCK, + }, + ] + } + } + + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a template unique_id propagates to lock unique_ids.""" + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "lock": [ + { + **OPTIMISTIC_LOCK, + "name": "test_a", + "unique_id": "a", + "state": "{{ true }}", + }, + { + **OPTIMISTIC_LOCK, + "name": "test_b", + "unique_id": "b", + "state": "{{ true }}", + }, + ], + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("lock")) == 2 + + entry = entity_registry.async_get("lock.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("lock.test_b") + assert entry + assert entry.unique_id == "x-b" + + async def test_emtpy_action_config(hass: HomeAssistant) -> None: """Test configuration with empty script.""" with assert_setup_component(1, lock.DOMAIN): From fa3edb5c017aee1ce7c8a0b45b996462de0c7161 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 15 May 2025 10:56:54 +0200 Subject: [PATCH 0486/1175] Fix Netgear handeling of missing MAC in device registry (#144722) --- homeassistant/components/netgear/router.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index d81f556193b..23ee47e7a2d 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -150,7 +150,11 @@ class NetgearRouter: if device_entry.via_device_id is None: continue # do not add the router itself - device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] + device_mac = dict(device_entry.connections).get( + dr.CONNECTION_NETWORK_MAC + ) + if device_mac is None: + continue self.devices[device_mac] = { "mac": device_mac, "name": device_entry.name, From 66ecc4d69d8fe458a9b1bcaf14cccbbabf110fe7 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 15 May 2025 05:46:57 -0400 Subject: [PATCH 0487/1175] Add modern configuration for template alarm control panel (#144834) * Add modern configuration for template alarm control panel * address comments and add tests for coverage --------- Co-authored-by: Erik Montnemery --- .../template/alarm_control_panel.py | 166 +++-- homeassistant/components/template/config.py | 10 +- .../template/test_alarm_control_panel.py | 607 ++++++++++++------ 3 files changed, 557 insertions(+), 226 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 208077a4153..d035edd26ac 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum import logging -from typing import Any +from typing import TYPE_CHECKING import voluptuous as vol @@ -21,6 +21,7 @@ from homeassistant.const import ( ATTR_CODE, CONF_DEVICE_ID, CONF_NAME, + CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_UNAVAILABLE, @@ -28,7 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers import config_validation as cv, selector, template from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import ( @@ -37,10 +38,15 @@ from homeassistant.helpers.entity_platform import ( ) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify -from .const import DOMAIN -from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf +from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .template_entity import ( + LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, + TEMPLATE_ENTITY_ICON_SCHEMA, + TemplateEntity, + rewrite_common_legacy_to_modern_conf, +) _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ @@ -51,21 +57,22 @@ _VALID_STATES = [ AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMING, AlarmControlPanelState.DISARMED, + AlarmControlPanelState.DISARMING, AlarmControlPanelState.PENDING, AlarmControlPanelState.TRIGGERED, STATE_UNAVAILABLE, ] +CONF_ALARM_CONTROL_PANELS = "panels" CONF_ARM_AWAY_ACTION = "arm_away" CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" CONF_ARM_VACATION_ACTION = "arm_vacation" -CONF_DISARM_ACTION = "disarm" -CONF_TRIGGER_ACTION = "trigger" -CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_FORMAT = "code_format" +CONF_DISARM_ACTION = "disarm" +CONF_TRIGGER_ACTION = "trigger" class TemplateCodeFormat(Enum): @@ -76,73 +83,140 @@ class TemplateCodeFormat(Enum): text = CodeFormat.TEXT -ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( +LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | { + CONF_VALUE_TEMPLATE: CONF_STATE, +} + +DEFAULT_NAME = "Template Alarm Control Panel" + +ALARM_CONTROL_PANEL_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, + vol.Optional( + CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name + ): cv.enum(TemplateCodeFormat), + vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_NAME): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema), +) + + +LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( { - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat ), + vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( { vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( - ALARM_CONTROL_PANEL_SCHEMA + LEGACY_ALARM_CONTROL_PANEL_SCHEMA ), } ) ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME): cv.template, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat ), vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), + vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_NAME): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, } ) -async def _async_create_entities( - hass: HomeAssistant, config: dict[str, Any] -) -> list[AlarmControlPanelTemplate]: - """Create Template Alarm Control Panels.""" +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, config: dict[str, dict] +) -> list[dict]: + """Rewrite legacy alarm control panel configuration definitions to modern ones.""" alarm_control_panels = [] - for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) - unique_id = entity_config.get(CONF_UNIQUE_ID) + for object_id, entity_conf in config.items(): + entity_conf = {**entity_conf, CONF_OBJECT_ID: object_id} + + entity_conf = rewrite_common_legacy_to_modern_conf( + hass, entity_conf, LEGACY_FIELDS + ) + + if CONF_NAME not in entity_conf: + entity_conf[CONF_NAME] = template.Template(object_id, hass) + + alarm_control_panels.append(entity_conf) + + return alarm_control_panels + + +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the template alarm control panels.""" + alarm_control_panels = [] + + for entity_conf in definitions: + unique_id = entity_conf.get(CONF_UNIQUE_ID) + + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" alarm_control_panels.append( AlarmControlPanelTemplate( hass, - object_id, - entity_config, + entity_conf, unique_id, ) ) - return alarm_control_panels + async_add_entities(alarm_control_panels) + + +def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]: + """Rewrite option configuration to modern configuration.""" + option_config = {**option_config} + + if CONF_VALUE_TEMPLATE in option_config: + option_config[CONF_STATE] = option_config.pop(CONF_VALUE_TEMPLATE) + + return option_config async def async_setup_entry( @@ -153,12 +227,12 @@ async def async_setup_entry( """Initialize config entry.""" _options = dict(config_entry.options) _options.pop("template_type") + _options = rewrite_options_to_modern_conf(_options) validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options) async_add_entities( [ AlarmControlPanelTemplate( hass, - slugify(_options[CONF_NAME]), validated_config, config_entry.entry_id, ) @@ -172,8 +246,22 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Template Alarm Control Panels.""" - async_add_entities(await _async_create_entities(hass, config)) + """Set up the Template cover.""" + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(hass, config[CONF_ALARM_CONTROL_PANELS]), + None, + ) + return + + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], + ) class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, RestoreEntity): @@ -184,20 +272,20 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore def __init__( self, hass: HomeAssistant, - object_id: str, config: dict, unique_id: str | None, ) -> None: """Initialize the panel.""" - super().__init__( - hass, config=config, fallback_name=object_id, unique_id=unique_id - ) - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) + super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) name = self._attr_name - assert name is not None - self._template = config.get(CONF_VALUE_TEMPLATE) + if TYPE_CHECKING: + assert name is not None + self._template = config.get(CONF_STATE) + self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED] self._attr_code_format = config[CONF_CODE_FORMAT].value diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 1dc20d07c0e..9e684e89f62 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -7,6 +7,9 @@ from typing import Any import voluptuous as vol +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.blueprint import ( is_blueprint_instance_config, @@ -45,6 +48,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_notify_setup_error from . import ( + alarm_control_panel as alarm_control_panel_platform, binary_sensor as binary_sensor_platform, button as button_platform, cover as cover_platform, @@ -114,6 +118,10 @@ CONFIG_SECTION_SCHEMA = vol.All( vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys( binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA ), + vol.Optional(ALARM_CONTROL_PANEL_DOMAIN): vol.All( + cv.ensure_list, + [alarm_control_panel_platform.ALARM_CONTROL_PANEL_SCHEMA], + ), vol.Optional(SELECT_DOMAIN): vol.All( cv.ensure_list, [select_platform.SELECT_SCHEMA] ), @@ -144,7 +152,7 @@ CONFIG_SECTION_SCHEMA = vol.All( }, ), ensure_domains_do_not_have_trigger_or_action( - BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN + ALARM_CONTROL_PANEL_DOMAIN, BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN ), ) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 2a99e00a9ce..f9820243600 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -1,5 +1,7 @@ """The tests for the Template alarm control panel platform.""" +from typing import Any + import pytest from syrupy.assertion import SnapshotAssertion @@ -13,6 +15,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -20,10 +23,13 @@ from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component +from .conftest import ConfigurationStyle + from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache -TEMPLATE_NAME = "alarm_control_panel.test_template_panel" -PANEL_NAME = "alarm_control_panel.test" +TEST_OBJECT_ID = "test_template_panel" +TEST_ENTITY_ID = f"alarm_control_panel.{TEST_OBJECT_ID}" +TEST_STATE_ENTITY_ID = "alarm_control_panel.test" @pytest.fixture @@ -93,50 +99,295 @@ EMPTY_ACTIONS = { } +UNIQUE_ID_CONFIG = { + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "unique_id": "not-so-unique-anymore", +} + + TEMPLATE_ALARM_CONFIG = { "value_template": "{{ states('alarm_control_panel.test') }}", **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, } -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) +async def async_setup_legacy_format( + hass: HomeAssistant, count: int, panel_config: dict[str, Any] +) -> None: + """Do setup of alarm control panel integration via legacy format.""" + config = {"alarm_control_panel": {"platform": "template", "panels": panel_config}} + + with assert_setup_component(count, ALARM_DOMAIN): + assert await async_setup_component( + hass, + ALARM_DOMAIN, + config, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +async def async_setup_modern_format( + hass: HomeAssistant, count: int, panel_config: dict[str, Any] +) -> None: + """Do setup of alarm control panel integration via modern format.""" + config = {"template": {"alarm_control_panel": panel_config}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_panel( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + panel_config: dict[str, Any], +) -> None: + """Do setup of alarm control panel integration.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format(hass, count, panel_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, panel_config) + + +async def async_setup_state_panel( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of alarm control panel integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + "value_template": state_template, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + "state": state_template, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + }, + ) + + +@pytest.fixture +async def setup_state_panel( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of alarm control panel integration using a state template.""" + await async_setup_state_panel(hass, count, style, state_template) + + +@pytest.fixture +async def setup_base_panel( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str | None, + panel_config: str, +): + """Do setup of alarm control panel integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + extra = {"value_template": state_template} if state_template else {} + await async_setup_legacy_format( + hass, + count, + {TEST_OBJECT_ID: {**extra, **panel_config}}, + ) + elif style == ConfigurationStyle.MODERN: + extra = {"state": state_template} if state_template else {} + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + **extra, + **panel_config, + }, + ) + + +@pytest.fixture +async def setup_single_attribute_state_panel( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, + attribute: str, + attribute_template: str, +) -> None: + """Do setup of alarm control panel integration testing a single attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "value_template": state_template, + **extra, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "state": state_template, + **extra, + }, + ) + + @pytest.mark.parametrize( - "config", - [ - { - "alarm_control_panel": { - "platform": "template", - "panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG}, - } - }, - ], + ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_panel") async def test_template_state_text(hass: HomeAssistant) -> None: """Test the state text of a template.""" for set_state in ( - AlarmControlPanelState.ARMED_HOME, AlarmControlPanelState.ARMED_AWAY, + AlarmControlPanelState.ARMED_CUSTOM_BYPASS, + AlarmControlPanelState.ARMED_HOME, AlarmControlPanelState.ARMED_NIGHT, AlarmControlPanelState.ARMED_VACATION, - AlarmControlPanelState.ARMED_CUSTOM_BYPASS, AlarmControlPanelState.ARMING, AlarmControlPanelState.DISARMED, + AlarmControlPanelState.DISARMING, AlarmControlPanelState.PENDING, AlarmControlPanelState.TRIGGERED, ): - hass.states.async_set(PANEL_NAME, set_state) + hass.states.async_set(TEST_STATE_ENTITY_ID, set_state) await hass.async_block_till_done() - state = hass.states.get(TEMPLATE_NAME) + state = hass.states.get(TEST_ENTITY_ID) assert state.state == set_state - hass.states.async_set(PANEL_NAME, "invalid_state") + hass.states.async_set(TEST_STATE_ENTITY_ID, "invalid_state") await hass.async_block_till_done() - state = hass.states.get(TEMPLATE_NAME) + state = hass.states.get(TEST_ENTITY_ID) assert state.state == "unknown" +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("state_template", "expected"), + [ + ("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED), + ("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME), + ("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY), + ("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT), + ("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION), + ("{{ 'armed_custom_bypass' }}", AlarmControlPanelState.ARMED_CUSTOM_BYPASS), + ("{{ 'pending' }}", AlarmControlPanelState.PENDING), + ("{{ 'arming' }}", AlarmControlPanelState.ARMING), + ("{{ 'disarming' }}", AlarmControlPanelState.DISARMING), + ("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED), + ("{{ x - 1 }}", STATE_UNKNOWN), + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_state_panel") +async def test_state_template_states(hass: HomeAssistant, expected: str) -> None: + """Test the state template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + "{{ 'disarmed' }}", + "{% if states.switch.test_state.state %}mdi:check{% endif %}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "icon"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_panel") +async def test_icon_template( + hass: HomeAssistant, +) -> None: + """Test icon template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") in ("", None) + + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["icon"] == "mdi:check" + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + "{{ 'disarmed' }}", + "{% if states.switch.test_state.state %}local/panel.png{% endif %}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "picture"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_panel") +async def test_picture_template( + hass: HomeAssistant, +) -> None: + """Test icon template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("entity_picture") in ("", None) + + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["entity_picture"] == "local/panel.png" + + async def test_setup_config_entry( hass: HomeAssistant, snapshot: SnapshotAssertion ) -> None: @@ -172,29 +423,18 @@ async def test_setup_config_entry( assert state.state == AlarmControlPanelState.DISARMED -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) +@pytest.mark.parametrize(("count", "state_template"), [(1, None)]) @pytest.mark.parametrize( - "config", - [ - { - "alarm_control_panel": { - "platform": "template", - "panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, - } - }, - { - "alarm_control_panel": { - "platform": "template", - "panels": {"test_template_panel": EMPTY_ACTIONS}, - } - }, - ], + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + "panel_config", [OPTIMISTIC_TEMPLATE_ALARM_CONFIG, EMPTY_ACTIONS] +) +@pytest.mark.usefixtures("setup_base_panel") async def test_optimistic_states(hass: HomeAssistant) -> None: """Test the optimistic state.""" - state = hass.states.get(TEMPLATE_NAME) + state = hass.states.get(TEST_ENTITY_ID) await hass.async_block_till_done() assert state.state == "unknown" @@ -210,31 +450,45 @@ async def test_optimistic_states(hass: HomeAssistant) -> None: await hass.services.async_call( ALARM_DOMAIN, service, - {"entity_id": TEMPLATE_NAME, "code": "1234"}, + {"entity_id": TEST_ENTITY_ID, "code": "1234"}, blocking=True, ) await hass.async_block_till_done() - assert hass.states.get(TEMPLATE_NAME).state == set_state + assert hass.states.get(TEST_ENTITY_ID).state == set_state + + +@pytest.mark.parametrize("count", [0]) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("panel_config", "state_template", "msg"), + [ + ( + OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "{% if blah %}", + "invalid template", + ), + ( + {"code_format": "bad_format", **OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, + "disarmed", + "value must be one of ['no_code', 'number', 'text']", + ), + ], +) +@pytest.mark.usefixtures("setup_base_panel") +async def test_template_syntax_error( + hass: HomeAssistant, msg, caplog_setup_text +) -> None: + """Test templating syntax error.""" + assert len(hass.states.async_all("alarm_control_panel")) == 0 + assert (msg) in caplog_setup_text @pytest.mark.parametrize(("count", "domain"), [(0, "alarm_control_panel")]) @pytest.mark.parametrize( ("config", "msg"), [ - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{% if blah %}", - **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, - } - }, - } - }, - "invalid template", - ), ( { "alarm_control_panel": { @@ -264,25 +518,10 @@ async def test_optimistic_states(hass: HomeAssistant) -> None: }, "required key 'panels' not provided", ), - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "disarmed", - **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, - "code_format": "bad_format", - } - }, - } - }, - "value must be one of ['no_code', 'number', 'text']", - ), ], ) @pytest.mark.usefixtures("start_ha") -async def test_template_syntax_error( +async def test_legacy_template_syntax_error( hass: HomeAssistant, msg, caplog_setup_text ) -> None: """Test templating syntax error.""" @@ -290,43 +529,30 @@ async def test_template_syntax_error( assert (msg) in caplog_setup_text -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize( - "config", + ("count", "state_template", "attribute", "attribute_template"), + [(1, "disarmed", "name", '{{ "Template Alarm Panel" }}')], +) +@pytest.mark.parametrize( + ("style", "test_entity_id"), [ - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "name": '{{ "Template Alarm Panel" }}', - "value_template": "disarmed", - **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, - } - }, - } - }, + (ConfigurationStyle.LEGACY, TEST_ENTITY_ID), + (ConfigurationStyle.MODERN, "alarm_control_panel.template_alarm_panel"), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_name(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_panel") +async def test_name(hass: HomeAssistant, test_entity_id: str) -> None: """Test the accessibility of the name attribute.""" - state = hass.states.get(TEMPLATE_NAME) + state = hass.states.get(test_entity_id) assert state is not None assert state.attributes.get("friendly_name") == "Template Alarm Panel" -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize( - "config", - [ - { - "alarm_control_panel": { - "platform": "template", - "panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG}, - } - }, - ], + ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) @pytest.mark.parametrize( "service", @@ -340,7 +566,7 @@ async def test_name(hass: HomeAssistant) -> None: "alarm_trigger", ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_state_panel") async def test_actions( hass: HomeAssistant, service, call_service_events: list[Event] ) -> None: @@ -348,128 +574,147 @@ async def test_actions( await hass.services.async_call( ALARM_DOMAIN, service, - {"entity_id": TEMPLATE_NAME, "code": "1234"}, + {"entity_id": TEST_ENTITY_ID, "code": "1234"}, blocking=True, ) await hass.async_block_till_done() assert len(call_service_events) == 1 assert call_service_events[0].data["service"] == service - assert call_service_events[0].data["service_data"]["code"] == TEMPLATE_NAME + assert call_service_events[0].data["service_data"]["code"] == TEST_ENTITY_ID -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + ("panel_config", "style"), [ - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_alarm_control_panel_01": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ true }}", - }, - "test_template_alarm_control_panel_02": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ false }}", - }, + ( + { + "test_template_alarm_control_panel_01": { + "value_template": "{{ true }}", + **UNIQUE_ID_CONFIG, + }, + "test_template_alarm_control_panel_02": { + "value_template": "{{ false }}", + **UNIQUE_ID_CONFIG, }, }, - }, + ConfigurationStyle.LEGACY, + ), + ( + [ + { + "name": "test_template_alarm_control_panel_01", + "state": "{{ true }}", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_alarm_control_panel_02", + "state": "{{ false }}", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_panel") async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one alarm control panel per id.""" assert len(hass.states.async_all()) == 1 -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a template unique_id propagates to alarm_control_panel unique_ids.""" + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "alarm_control_panel": [ + { + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "name": "test_a", + "unique_id": "a", + "state": "{{ true }}", + }, + { + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, + "name": "test_b", + "unique_id": "b", + "state": "{{ true }}", + }, + ], + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("alarm_control_panel")) == 2 + + entry = entity_registry.async_get("alarm_control_panel.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("alarm_control_panel.test_b") + assert entry + assert entry.unique_id == "x-b" + + +@pytest.mark.parametrize(("count", "state_template"), [(1, "disarmed")]) @pytest.mark.parametrize( - ("config", "code_format", "code_arm_required"), + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("panel_config", "code_format", "code_arm_required"), [ ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "disarmed", - } - }, - } - }, + {}, "number", True, ), ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "disarmed", - "code_format": "text", - } - }, - } - }, + {"code_format": "text"}, "text", True, ), ( { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "disarmed", - "code_format": "no_code", - "code_arm_required": False, - } - }, - } + "code_format": "no_code", + "code_arm_required": False, }, None, False, ), ( { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "disarmed", - "code_format": "text", - "code_arm_required": False, - } - }, - } + "code_format": "text", + "code_arm_required": False, }, "text", False, ), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_base_panel") async def test_code_config(hass: HomeAssistant, code_format, code_arm_required) -> None: """Test configuration options related to alarm code.""" - state = hass.states.get(TEMPLATE_NAME) + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("code_format") == code_format assert state.attributes.get("code_arm_required") == code_arm_required -@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize( - "config", - [ - { - "alarm_control_panel": { - "platform": "template", - "panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG}, - } - }, - ], + ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] ) @pytest.mark.parametrize( ("restored_state", "initial_state"), @@ -508,11 +753,11 @@ async def test_code_config(hass: HomeAssistant, code_format, code_arm_required) ) async def test_restore_state( hass: HomeAssistant, - count, - domain, - config, - restored_state, - initial_state, + count: int, + state_template: str, + style: ConfigurationStyle, + restored_state: str, + initial_state: str, ) -> None: """Test restoring template alarm control panel.""" @@ -522,17 +767,7 @@ async def test_restore_state( {}, ) mock_restore_cache(hass, (fake_state,)) - with assert_setup_component(count, domain): - assert await async_setup_component( - hass, - domain, - config, - ) - - await hass.async_block_till_done() - - await hass.async_start() - await hass.async_block_till_done() + await async_setup_state_panel(hass, count, style, state_template) state = hass.states.get("alarm_control_panel.test_template_panel") assert state.state == initial_state From 1d47dc41c9f3f20e1889ad506c146f2ec4084df4 Mon Sep 17 00:00:00 2001 From: alorente Date: Thu, 15 May 2025 13:05:46 +0200 Subject: [PATCH 0488/1175] Add reactive energy device class and units (#143941) --- homeassistant/components/number/const.py | 10 +++++++++ homeassistant/components/number/icons.json | 3 +++ homeassistant/components/number/strings.json | 3 +++ homeassistant/components/random/strings.json | 1 + .../components/recorder/statistics.py | 2 ++ homeassistant/components/sensor/const.py | 14 ++++++++++++ .../components/sensor/device_condition.py | 3 +++ .../components/sensor/device_trigger.py | 3 +++ homeassistant/components/sensor/icons.json | 3 +++ homeassistant/components/sensor/strings.json | 5 +++++ homeassistant/components/sql/strings.json | 1 + .../components/template/strings.json | 1 + homeassistant/const.py | 8 +++++++ homeassistant/util/unit_conversion.py | 12 ++++++++++ tests/components/sensor/common.py | 2 ++ .../sensor/test_device_condition.py | 2 +- .../components/sensor/test_device_trigger.py | 2 +- tests/components/sensor/test_init.py | 1 + tests/util/test_unit_conversion.py | 22 +++++++++++++++++++ 19 files changed, 96 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 58fa8ed1012..6a5809610ee 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -33,6 +33,7 @@ from homeassistant.const import ( UnitOfPower, UnitOfPrecipitationDepth, UnitOfPressure, + UnitOfReactiveEnergy, UnitOfReactivePower, UnitOfSoundPressure, UnitOfSpeed, @@ -44,6 +45,7 @@ from homeassistant.const import ( ) from homeassistant.util.unit_conversion import ( BaseUnitConverter, + ReactiveEnergyConverter, TemperatureConverter, VolumeFlowRateConverter, ) @@ -320,6 +322,12 @@ class NumberDeviceClass(StrEnum): - `psi` """ + REACTIVE_ENERGY = "reactive_energy" + """Reactive energy. + + Unit of measurement: `varh`, `kvarh` + """ + REACTIVE_POWER = "reactive_power" """Reactive power. @@ -498,6 +506,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), NumberDeviceClass.PRESSURE: set(UnitOfPressure), + NumberDeviceClass.REACTIVE_ENERGY: set(UnitOfReactiveEnergy), NumberDeviceClass.REACTIVE_POWER: set(UnitOfReactivePower), NumberDeviceClass.SIGNAL_STRENGTH: { SIGNAL_STRENGTH_DECIBELS, @@ -531,6 +540,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { } UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = { + NumberDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter, NumberDeviceClass.TEMPERATURE: TemperatureConverter, NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, } diff --git a/homeassistant/components/number/icons.json b/homeassistant/components/number/icons.json index 49103f5cd41..dcce09984bd 100644 --- a/homeassistant/components/number/icons.json +++ b/homeassistant/components/number/icons.json @@ -111,6 +111,9 @@ "pressure": { "default": "mdi:gauge" }, + "reactive_energy": { + "default": "mdi:lightning-bolt" + }, "reactive_power": { "default": "mdi:flash" }, diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index 993120ef3ad..998b9ffba38 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -130,6 +130,9 @@ "pressure": { "name": "[%key:component::sensor::entity_component::pressure::name%]" }, + "reactive_energy": { + "name": "[%key:component::sensor::entity_component::reactive_energy::name%]" + }, "reactive_power": { "name": "[%key:component::sensor::entity_component::reactive_power::name%]" }, diff --git a/homeassistant/components/random/strings.json b/homeassistant/components/random/strings.json index af0efb823b9..d57f2dc8eec 100644 --- a/homeassistant/components/random/strings.json +++ b/homeassistant/components/random/strings.json @@ -120,6 +120,7 @@ "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_energy": "[%key:component::sensor::entity_component::reactive_energy::name%]", "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 80c0028ef7a..bdb5062e88e 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -57,6 +57,7 @@ from homeassistant.util.unit_conversion import ( MassConverter, PowerConverter, PressureConverter, + ReactiveEnergyConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -208,6 +209,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **dict.fromkeys(MassConverter.VALID_UNITS, MassConverter), **dict.fromkeys(PowerConverter.VALID_UNITS, PowerConverter), **dict.fromkeys(PressureConverter.VALID_UNITS, PressureConverter), + **dict.fromkeys(ReactiveEnergyConverter.VALID_UNITS, ReactiveEnergyConverter), **dict.fromkeys(SpeedConverter.VALID_UNITS, SpeedConverter), **dict.fromkeys(TemperatureConverter.VALID_UNITS, TemperatureConverter), **dict.fromkeys(UnitlessRatioConverter.VALID_UNITS, UnitlessRatioConverter), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 2a8ac8099ab..31b33303dd4 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -33,6 +33,7 @@ from homeassistant.const import ( UnitOfPower, UnitOfPrecipitationDepth, UnitOfPressure, + UnitOfReactiveEnergy, UnitOfReactivePower, UnitOfSoundPressure, UnitOfSpeed, @@ -58,6 +59,7 @@ from homeassistant.util.unit_conversion import ( MassConverter, PowerConverter, PressureConverter, + ReactiveEnergyConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -349,6 +351,12 @@ class SensorDeviceClass(StrEnum): - `psi` """ + REACTIVE_ENERGY = "reactive_energy" + """Reactive energy. + + Unit of measurement: `varh`, `kvarh` + """ + REACTIVE_POWER = "reactive_power" """Reactive power. @@ -529,6 +537,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.PRECIPITATION: DistanceConverter, SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter, SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: UnitlessRatioConverter, @@ -597,6 +606,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), SensorDeviceClass.PRESSURE: set(UnitOfPressure), + SensorDeviceClass.REACTIVE_ENERGY: set(UnitOfReactiveEnergy), SensorDeviceClass.REACTIVE_POWER: set(UnitOfReactivePower), SensorDeviceClass.SIGNAL_STRENGTH: { SIGNAL_STRENGTH_DECIBELS, @@ -672,6 +682,10 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.PRECIPITATION: set(SensorStateClass), SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.REACTIVE_ENERGY: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.SIGNAL_STRENGTH: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.SOUND_PRESSURE: {SensorStateClass.MEASUREMENT}, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index f52393f28ff..2b1eb350c3e 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -70,6 +70,7 @@ CONF_IS_PRECIPITATION = "is_precipitation" CONF_IS_PRECIPITATION_INTENSITY = "is_precipitation_intensity" CONF_IS_PRESSURE = "is_pressure" CONF_IS_SPEED = "is_speed" +CONF_IS_REACTIVE_ENERGY = "is_reactive_energy" CONF_IS_REACTIVE_POWER = "is_reactive_power" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_SOUND_PRESSURE = "is_sound_pressure" @@ -128,6 +129,7 @@ ENTITY_CONDITIONS = { {CONF_TYPE: CONF_IS_PRECIPITATION_INTENSITY} ], SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + SensorDeviceClass.REACTIVE_ENERGY: [{CONF_TYPE: CONF_IS_REACTIVE_ENERGY}], SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_IS_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], SensorDeviceClass.SOUND_PRESSURE: [{CONF_TYPE: CONF_IS_SOUND_PRESSURE}], @@ -193,6 +195,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_PRECIPITATION, CONF_IS_PRECIPITATION_INTENSITY, CONF_IS_PRESSURE, + CONF_IS_REACTIVE_ENERGY, CONF_IS_REACTIVE_POWER, CONF_IS_SIGNAL_STRENGTH, CONF_IS_SOUND_PRESSURE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index dee48434294..d44611a49db 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -68,6 +68,7 @@ CONF_POWER_FACTOR = "power_factor" CONF_PRECIPITATION = "precipitation" CONF_PRECIPITATION_INTENSITY = "precipitation_intensity" CONF_PRESSURE = "pressure" +CONF_REACTIVE_ENERGY = "reactive_energy" CONF_REACTIVE_POWER = "reactive_power" CONF_SIGNAL_STRENGTH = "signal_strength" CONF_SOUND_PRESSURE = "sound_pressure" @@ -127,6 +128,7 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_PRECIPITATION_INTENSITY} ], SensorDeviceClass.PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + SensorDeviceClass.REACTIVE_ENERGY: [{CONF_TYPE: CONF_REACTIVE_ENERGY}], SensorDeviceClass.REACTIVE_POWER: [{CONF_TYPE: CONF_REACTIVE_POWER}], SensorDeviceClass.SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], SensorDeviceClass.SOUND_PRESSURE: [{CONF_TYPE: CONF_SOUND_PRESSURE}], @@ -193,6 +195,7 @@ TRIGGER_SCHEMA = vol.All( CONF_PRECIPITATION, CONF_PRECIPITATION_INTENSITY, CONF_PRESSURE, + CONF_REACTIVE_ENERGY, CONF_REACTIVE_POWER, CONF_SIGNAL_STRENGTH, CONF_SOUND_PRESSURE, diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index 497c1544b3b..cc64290d241 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -114,6 +114,9 @@ "pressure": { "default": "mdi:gauge" }, + "reactive_energy": { + "default": "mdi:lightning-bolt" + }, "reactive_power": { "default": "mdi:flash" }, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 123c30da72e..4ad6597692c 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -38,6 +38,7 @@ "is_precipitation": "Current {entity_name} precipitation", "is_precipitation_intensity": "Current {entity_name} precipitation intensity", "is_pressure": "Current {entity_name} pressure", + "is_reactive_energy": "Current {entity_name} reactive energy", "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", "is_sound_pressure": "Current {entity_name} sound pressure", @@ -92,6 +93,7 @@ "precipitation": "{entity_name} precipitation changes", "precipitation_intensity": "{entity_name} precipitation intensity changes", "pressure": "{entity_name} pressure changes", + "reactive_energy": "{entity_name} reactive energy changes", "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", "sound_pressure": "{entity_name} sound pressure changes", @@ -256,6 +258,9 @@ "pressure": { "name": "Pressure" }, + "reactive_energy": { + "name": "Reactive energy" + }, "reactive_power": { "name": "Reactive power" }, diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index ac861e72b72..486fb5946b4 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -106,6 +106,7 @@ "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_energy": "[%key:component::sensor::entity_component::reactive_energy::name%]", "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 0b431d661cd..729f76a84ec 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -326,6 +326,7 @@ "precipitation": "[%key:component::sensor::entity_component::precipitation::name%]", "precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]", "pressure": "[%key:component::sensor::entity_component::pressure::name%]", + "reactive_energy": "[%key:component::sensor::entity_component::reactive_energy::name%]", "reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]", "signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]", "sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]", diff --git a/homeassistant/const.py b/homeassistant/const.py index f0615e7415b..a3674d6e5d6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -634,6 +634,14 @@ class UnitOfEnergy(StrEnum): GIGA_CALORIE = "Gcal" +# Reactive energy units +class UnitOfReactiveEnergy(StrEnum): + """Reactive energy units.""" + + VOLT_AMPERE_REACTIVE_HOUR = "varh" + KILO_VOLT_AMPERE_REACTIVE_HOUR = "kvarh" + + # Energy Distance units class UnitOfEnergyDistance(StrEnum): """Energy Distance units.""" diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index e4312a7865f..05c6d2f381d 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -24,6 +24,7 @@ from homeassistant.const import ( UnitOfMass, UnitOfPower, UnitOfPressure, + UnitOfReactiveEnergy, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -429,6 +430,17 @@ class PressureConverter(BaseUnitConverter): } +class ReactiveEnergyConverter(BaseUnitConverter): + """Utility to convert reactive energy values.""" + + UNIT_CLASS = "energy" + _UNIT_CONVERSION: dict[str | None, float] = { + UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR: 1, + UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR: 1 / 1e3, + } + VALID_UNITS = set(UnitOfReactiveEnergy) + + class SpeedConverter(BaseUnitConverter): """Utility to convert speed values.""" diff --git a/tests/components/sensor/common.py b/tests/components/sensor/common.py index 458009b2690..4fb9a1e4f7f 100644 --- a/tests/components/sensor/common.py +++ b/tests/components/sensor/common.py @@ -14,6 +14,7 @@ from homeassistant.const import ( UnitOfApparentPower, UnitOfFrequency, UnitOfPressure, + UnitOfReactiveEnergy, UnitOfReactivePower, UnitOfVolume, ) @@ -44,6 +45,7 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.ENERGY: "kWh", # energy (Wh/kWh/MWh) SensorDeviceClass.FREQUENCY: UnitOfFrequency.GIGAHERTZ, # energy (Hz/kHz/MHz/GHz) SensorDeviceClass.POWER_FACTOR: PERCENTAGE, # power factor (no unit, min: -1.0, max: 1.0) + SensorDeviceClass.REACTIVE_ENERGY: UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # reactive energy (varh) SensorDeviceClass.REACTIVE_POWER: UnitOfReactivePower.VOLT_AMPERE_REACTIVE, # reactive power (var) SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # µg/m³ of vocs SensorDeviceClass.VOLTAGE: "V", # voltage (V) diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index a9781e0b800..68488d29c67 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -119,7 +119,7 @@ async def test_get_conditions( conditions = await async_get_device_automations( hass, DeviceAutomationType.CONDITION, device_entry.id ) - assert len(conditions) == 27 + assert len(conditions) == 28 assert conditions == unordered(expected_conditions) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index f35c9520f71..bf7147e30e1 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -121,7 +121,7 @@ async def test_get_triggers( triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_entry.id ) - assert len(triggers) == 27 + assert len(triggers) == 28 assert triggers == unordered(expected_triggers) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 9666e29579b..e8daff09b7c 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1994,6 +1994,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement( SensorDeviceClass.PRECIPITATION_INTENSITY, SensorDeviceClass.PRECIPITATION, SensorDeviceClass.PRESSURE, + SensorDeviceClass.REACTIVE_ENERGY, SensorDeviceClass.REACTIVE_POWER, SensorDeviceClass.SIGNAL_STRENGTH, SensorDeviceClass.SOUND_PRESSURE, diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 883b17c733c..885757b7eb4 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -24,6 +24,7 @@ from homeassistant.const import ( UnitOfMass, UnitOfPower, UnitOfPressure, + UnitOfReactiveEnergy, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -49,6 +50,7 @@ from homeassistant.util.unit_conversion import ( MassConverter, PowerConverter, PressureConverter, + ReactiveEnergyConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -78,6 +80,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { MassConverter, PowerConverter, PressureConverter, + ReactiveEnergyConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -127,6 +130,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473), PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), PressureConverter: (UnitOfPressure.HPA, UnitOfPressure.INHG, 33.86389), + ReactiveEnergyConverter: ( + UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, + UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, + 1000, + ), SpeedConverter: ( UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR, @@ -622,6 +630,20 @@ _CONVERTED_VALUE: dict[ (30, UnitOfPressure.MMHG, 1.181102, UnitOfPressure.INHG), (5, UnitOfPressure.BAR, 72.51887, UnitOfPressure.PSI), ], + ReactiveEnergyConverter: [ + ( + 5, + UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, + 5000, + UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, + ), + ( + 5, + UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, + 0.005, + UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, + ), + ], SpeedConverter: [ # 5 km/h / 1.609 km/mi = 3.10686 mi/h (5, UnitOfSpeed.KILOMETERS_PER_HOUR, 3.106856, UnitOfSpeed.MILES_PER_HOUR), From 334f9deaec3f953106ba1ff629d2da350a500551 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 15 May 2025 13:46:15 +0200 Subject: [PATCH 0489/1175] Bump deebot-client to 13.2.0 (#144957) --- 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 e670a36cf72..b1674e123fa 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42c54364162..d8b1ac109b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -759,7 +759,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.1.0 +deebot-client==13.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aba6bd90c02..cefc6b5819a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -650,7 +650,7 @@ dbus-fast==2.43.0 debugpy==1.8.14 # homeassistant.components.ecovacs -deebot-client==13.1.0 +deebot-client==13.2.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From e8281bb009f9257cf5fe5ca636dbe448464a1eb7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 14:43:35 +0200 Subject: [PATCH 0490/1175] Use runtime_data in iotawatt (#144977) --- homeassistant/components/iotawatt/__init__.py | 14 +++++--------- homeassistant/components/iotawatt/coordinator.py | 6 ++++-- homeassistant/components/iotawatt/sensor.py | 9 ++++----- tests/components/iotawatt/conftest.py | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 8f35d4e0796..1dc38ba01c6 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -1,26 +1,22 @@ """The iotawatt integration.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import IotawattUpdater +from .coordinator import IotawattConfigEntry, IotawattUpdater PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IotawattConfigEntry) -> bool: """Set up iotawatt from a config entry.""" coordinator = IotawattUpdater(hass, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IotawattConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/iotawatt/coordinator.py b/homeassistant/components/iotawatt/coordinator.py index 13802ebdd76..48d55dad818 100644 --- a/homeassistant/components/iotawatt/coordinator.py +++ b/homeassistant/components/iotawatt/coordinator.py @@ -21,14 +21,16 @@ _LOGGER = logging.getLogger(__name__) # Matches iotwatt data log interval REQUEST_REFRESH_DEFAULT_COOLDOWN = 5 +type IotawattConfigEntry = ConfigEntry[IotawattUpdater] + class IotawattUpdater(DataUpdateCoordinator): """Class to manage fetching update data from the IoTaWatt Energy Device.""" api: Iotawatt | None = None - config_entry: ConfigEntry + config_entry: IotawattConfigEntry - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: IotawattConfigEntry) -> None: """Initialize IotaWattUpdater object.""" super().__init__( hass=hass, diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index f5210f7fbba..591397ad6e7 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, UnitOfApparentPower, @@ -31,8 +30,8 @@ from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from .const import DOMAIN, VOLT_AMPERE_REACTIVE, VOLT_AMPERE_REACTIVE_HOURS -from .coordinator import IotawattUpdater +from .const import VOLT_AMPERE_REACTIVE, VOLT_AMPERE_REACTIVE_HOURS +from .coordinator import IotawattConfigEntry, IotawattUpdater _LOGGER = logging.getLogger(__name__) @@ -113,11 +112,11 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IotawattConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add sensors for passed config_entry in HA.""" - coordinator: IotawattUpdater = hass.data[DOMAIN][config_entry.entry_id] + coordinator = config_entry.runtime_data created = set() @callback diff --git a/tests/components/iotawatt/conftest.py b/tests/components/iotawatt/conftest.py index 9380154b53e..3b30783494e 100644 --- a/tests/components/iotawatt/conftest.py +++ b/tests/components/iotawatt/conftest.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant.components.iotawatt import DOMAIN +from homeassistant.components.iotawatt.const import DOMAIN from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry From 28990e1db55edc0d268d8f73a9f34e47710d6e59 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 14:43:58 +0200 Subject: [PATCH 0491/1175] Use runtime_data in ipma (#144972) * Use runtime_data in ipma * Cleanup const --- homeassistant/components/ipma/__init__.py | 28 +++++++++++--------- homeassistant/components/ipma/const.py | 3 --- homeassistant/components/ipma/diagnostics.py | 9 +++---- homeassistant/components/ipma/sensor.py | 10 +++---- homeassistant/components/ipma/weather.py | 19 +++++-------- tests/components/ipma/conftest.py | 2 +- tests/components/ipma/test_init.py | 2 +- 7 files changed, 32 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 68289d13289..6c48ae4c925 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,6 +1,7 @@ """Component for the Portuguese weather service - IPMA.""" import asyncio +from dataclasses import dataclass import logging from pyipma import IPMAException @@ -14,7 +15,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .config_flow import IpmaFlowHandler # noqa: F401 -from .const import DATA_API, DATA_LOCATION, DOMAIN DEFAULT_NAME = "ipma" @@ -22,8 +22,18 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] _LOGGER = logging.getLogger(__name__) +type IpmaConfigEntry = ConfigEntry[IpmaRuntimeData] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + +@dataclass +class IpmaRuntimeData: + """IPMA runtime data.""" + + api: IPMA_API + location: Location + + +async def async_setup_entry(hass: HomeAssistant, config_entry: IpmaConfigEntry) -> bool: """Set up IPMA station as config entry.""" latitude = config_entry.data[CONF_LATITUDE] @@ -48,20 +58,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b location.global_id_local, ) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location} + config_entry.runtime_data = IpmaRuntimeData(api=api, location=location) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IpmaConfigEntry) -> bool: """Unload a config entry.""" - - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index dd6f1fba64a..1cb1af17d95 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -27,9 +27,6 @@ DOMAIN = "ipma" HOME_LOCATION_NAME = "Home" -DATA_API = "api" -DATA_LOCATION = "location" - ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) diff --git a/homeassistant/components/ipma/diagnostics.py b/homeassistant/components/ipma/diagnostics.py index 948b69ee3e5..bf868324593 100644 --- a/homeassistant/components/ipma/diagnostics.py +++ b/homeassistant/components/ipma/diagnostics.py @@ -4,20 +4,19 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from .const import DATA_API, DATA_LOCATION, DOMAIN +from . import IpmaConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: IpmaConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - location = hass.data[DOMAIN][entry.entry_id][DATA_LOCATION] - api = hass.data[DOMAIN][entry.entry_id][DATA_API] + location = entry.runtime_data.location + api = entry.runtime_data.api return { "location_information": { diff --git a/homeassistant/components/ipma/sensor.py b/homeassistant/components/ipma/sensor.py index 78fd018cf9a..7e71457513b 100644 --- a/homeassistant/components/ipma/sensor.py +++ b/homeassistant/components/ipma/sensor.py @@ -14,12 +14,12 @@ from pyipma.rcm import RCM from pyipma.uv import UV from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import Throttle -from .const import DATA_API, DATA_LOCATION, DOMAIN, MIN_TIME_BETWEEN_UPDATES +from . import IpmaConfigEntry +from .const import MIN_TIME_BETWEEN_UPDATES from .entity import IPMADevice _LOGGER = logging.getLogger(__name__) @@ -87,12 +87,12 @@ SENSOR_TYPES: tuple[IPMASensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IpmaConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the IPMA sensor platform.""" - api = hass.data[DOMAIN][entry.entry_id][DATA_API] - location = hass.data[DOMAIN][entry.entry_id][DATA_LOCATION] + location = entry.runtime_data.location + api = entry.runtime_data.api entities = [IPMASensor(api, location, description) for description in SENSOR_TYPES] diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index d285f9e1ad3..74344da8aff 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -23,7 +23,6 @@ from homeassistant.components.weather import ( WeatherEntity, WeatherEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_MODE, UnitOfPressure, @@ -35,14 +34,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import Throttle -from .const import ( - ATTRIBUTION, - CONDITION_MAP, - DATA_API, - DATA_LOCATION, - DOMAIN, - MIN_TIME_BETWEEN_UPDATES, -) +from . import IpmaConfigEntry +from .const import ATTRIBUTION, CONDITION_MAP, MIN_TIME_BETWEEN_UPDATES from .entity import IPMADevice _LOGGER = logging.getLogger(__name__) @@ -50,12 +43,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IpmaConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - api = hass.data[DOMAIN][config_entry.entry_id][DATA_API] - location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION] + location = config_entry.runtime_data.location + api = config_entry.runtime_data.api async_add_entities([IPMAWeather(api, location, config_entry)], True) @@ -72,7 +65,7 @@ class IPMAWeather(WeatherEntity, IPMADevice): ) def __init__( - self, api: IPMA_API, location: Location, config_entry: ConfigEntry + self, api: IPMA_API, location: Location, config_entry: IpmaConfigEntry ) -> None: """Initialise the platform with a data instance and station name.""" IPMADevice.__init__(self, api, location) diff --git a/tests/components/ipma/conftest.py b/tests/components/ipma/conftest.py index 8f2a017dcb8..caf49f594fb 100644 --- a/tests/components/ipma/conftest.py +++ b/tests/components/ipma/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.ipma import DOMAIN +from homeassistant.components.ipma.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant diff --git a/tests/components/ipma/test_init.py b/tests/components/ipma/test_init.py index 7967b97dd23..4a0314a0d9a 100644 --- a/tests/components/ipma/test_init.py +++ b/tests/components/ipma/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyipma import IPMAException -from homeassistant.components.ipma import DOMAIN +from homeassistant.components.ipma.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.core import HomeAssistant From 912798ee34490b183c2fdbbba200cd636da16faf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 14:57:26 +0200 Subject: [PATCH 0492/1175] Use runtime_data in intellifire (#144979) --- .../components/intellifire/__init__.py | 25 ++++++++++--------- .../components/intellifire/binary_sensor.py | 8 +++--- .../components/intellifire/climate.py | 9 +++---- .../components/intellifire/coordinator.py | 6 +++-- homeassistant/components/intellifire/fan.py | 9 +++---- homeassistant/components/intellifire/light.py | 9 +++---- .../components/intellifire/number.py | 9 +++---- .../components/intellifire/sensor.py | 8 +++--- .../components/intellifire/switch.py | 8 +++--- 9 files changed, 42 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index cda30820a2f..cc5da82ab92 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -8,7 +8,6 @@ from intellifire4py import UnifiedFireplace from intellifire4py.cloud_interface import IntelliFireCloudInterface from intellifire4py.model import IntelliFireCommonFireplaceData -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -27,12 +26,11 @@ from .const import ( CONF_SERIAL, CONF_USER_ID, CONF_WEB_CLIENT_ID, - DOMAIN, INIT_WAIT_TIME_SECONDS, LOGGER, STARTUP_TIMEOUT, ) -from .coordinator import IntellifireDataUpdateCoordinator +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, @@ -45,7 +43,9 @@ PLATFORMS = [ ] -def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData: +def _construct_common_data( + entry: IntellifireConfigEntry, +) -> IntelliFireCommonFireplaceData: """Convert config entry data into IntelliFireCommonFireplaceData.""" return IntelliFireCommonFireplaceData( @@ -60,7 +60,9 @@ def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData ) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry( + hass: HomeAssistant, config_entry: IntellifireConfigEntry +) -> bool: """Migrate entries.""" LOGGER.debug( "Migrating configuration from version %s.%s", @@ -105,7 +107,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry) -> bool: """Set up IntelliFire from a config entry.""" if CONF_USERNAME not in entry.data: @@ -133,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.debug("Fireplace to Initialized - Awaiting first refresh") await data_update_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_update_coordinator + entry.runtime_data = data_update_coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -151,9 +153,8 @@ async def _async_wait_for_initialization( await asyncio.sleep(INIT_WAIT_TIME_SECONDS) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: IntellifireConfigEntry +) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/intellifire/binary_sensor.py b/homeassistant/components/intellifire/binary_sensor.py index 3da1d2e3dc0..7cc22290e3c 100644 --- a/homeassistant/components/intellifire/binary_sensor.py +++ b/homeassistant/components/intellifire/binary_sensor.py @@ -10,13 +10,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import IntellifireDataUpdateCoordinator -from .const import DOMAIN +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator from .entity import IntellifireEntity @@ -151,11 +149,11 @@ INTELLIFIRE_BINARY_SENSORS: tuple[IntellifireBinarySensorEntityDescription, ...] async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up a IntelliFire On/Off Sensor.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( IntellifireBinarySensor(coordinator=coordinator, description=description) diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index f067f2a849d..0af438a7374 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -10,13 +10,12 @@ from homeassistant.components.climate import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import IntellifireDataUpdateCoordinator -from .const import DEFAULT_THERMOSTAT_TEMP, DOMAIN, LOGGER +from .const import DEFAULT_THERMOSTAT_TEMP, LOGGER +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator from .entity import IntellifireEntity INTELLIFIRE_CLIMATES: tuple[ClimateEntityDescription, ...] = ( @@ -26,11 +25,11 @@ INTELLIFIRE_CLIMATES: tuple[ClimateEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Configure the fan entry..""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if coordinator.data.has_thermostat: async_add_entities( diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 6a23e7438db..dc9aa45d58b 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -16,16 +16,18 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER +type IntellifireConfigEntry = ConfigEntry[IntellifireDataUpdateCoordinator] + class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData]): """Class to manage the polling of the fireplace API.""" - config_entry: ConfigEntry + config_entry: IntellifireConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IntellifireConfigEntry, fireplace: UnifiedFireplace, ) -> None: """Initialize the Coordinator.""" diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py index 174d964d357..3075a5fb2a8 100644 --- a/homeassistant/components/intellifire/fan.py +++ b/homeassistant/components/intellifire/fan.py @@ -15,7 +15,6 @@ from homeassistant.components.fan import ( FanEntityDescription, FanEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.percentage import ( @@ -23,8 +22,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import DOMAIN, LOGGER -from .coordinator import IntellifireDataUpdateCoordinator +from .const import LOGGER +from .coordinator import IntellifireConfigEntry from .entity import IntellifireEntity @@ -57,11 +56,11 @@ INTELLIFIRE_FANS: tuple[IntellifireFanEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the fans.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if coordinator.data.has_fan: async_add_entities( diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py index 0cf5c7774ed..c73614bfade 100644 --- a/homeassistant/components/intellifire/light.py +++ b/homeassistant/components/intellifire/light.py @@ -15,12 +15,11 @@ from homeassistant.components.light import ( LightEntity, LightEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, LOGGER -from .coordinator import IntellifireDataUpdateCoordinator +from .const import LOGGER +from .coordinator import IntellifireConfigEntry from .entity import IntellifireEntity @@ -84,11 +83,11 @@ class IntellifireLight(IntellifireEntity, LightEntity): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the fans.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data if coordinator.data.has_light: async_add_entities( diff --git a/homeassistant/components/intellifire/number.py b/homeassistant/components/intellifire/number.py index 0776835833e..68097d30b44 100644 --- a/homeassistant/components/intellifire/number.py +++ b/homeassistant/components/intellifire/number.py @@ -9,22 +9,21 @@ from homeassistant.components.number import ( NumberEntityDescription, NumberMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, LOGGER -from .coordinator import IntellifireDataUpdateCoordinator +from .const import LOGGER +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator from .entity import IntellifireEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the fans.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data description = NumberEntityDescription( key="flame_control", diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index 7763fb1b9b2..287f9a60ca0 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -12,14 +12,12 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import utcnow -from .const import DOMAIN -from .coordinator import IntellifireDataUpdateCoordinator +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator from .entity import IntellifireEntity @@ -142,12 +140,12 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Define setup entry call.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( IntelliFireSensor(coordinator=coordinator, description=description) for description in INTELLIFIRE_SENSORS diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py index 2185ad47cae..a6ab89d6bd7 100644 --- a/homeassistant/components/intellifire/switch.py +++ b/homeassistant/components/intellifire/switch.py @@ -7,12 +7,10 @@ from dataclasses import dataclass from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import IntellifireDataUpdateCoordinator -from .const import DOMAIN +from .coordinator import IntellifireConfigEntry, IntellifireDataUpdateCoordinator from .entity import IntellifireEntity @@ -52,11 +50,11 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IntellifireConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Configure switch entities.""" - coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( IntellifireSwitch(coordinator=coordinator, description=description) From 3bf99087899748426619783221b8b8112e5e1266 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 15 May 2025 09:46:00 -0400 Subject: [PATCH 0493/1175] Add template vacuum modern style (#144843) * Add template vacuum modern style * address comments and add tests for coverage * address comments * update vacuum and sort domains --- homeassistant/components/template/config.py | 116 +- homeassistant/components/template/vacuum.py | 143 +- tests/components/template/test_vacuum.py | 1314 +++++++++++-------- 3 files changed, 908 insertions(+), 665 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 9e684e89f62..f1b58ebffa0 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -8,24 +8,25 @@ from typing import Any import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, + DOMAIN as DOMAIN_ALARM_CONTROL_PANEL, ) -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR from homeassistant.components.blueprint import ( is_blueprint_instance_config, schemas as blueprint_schemas, ) -from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.components.button import DOMAIN as DOMAIN_BUTTON +from homeassistant.components.cover import DOMAIN as DOMAIN_COVER +from homeassistant.components.fan import DOMAIN as DOMAIN_FAN +from homeassistant.components.image import DOMAIN as DOMAIN_IMAGE +from homeassistant.components.light import DOMAIN as DOMAIN_LIGHT +from homeassistant.components.lock import DOMAIN as DOMAIN_LOCK +from homeassistant.components.number import DOMAIN as DOMAIN_NUMBER +from homeassistant.components.select import DOMAIN as DOMAIN_SELECT +from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR +from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH +from homeassistant.components.vacuum import DOMAIN as DOMAIN_VACUUM +from homeassistant.components.weather import DOMAIN as DOMAIN_WEATHER from homeassistant.config import async_log_schema_error, config_without_domain from homeassistant.const import ( CONF_ACTION, @@ -60,6 +61,7 @@ from . import ( select as select_platform, sensor as sensor_platform, switch as switch_platform, + vacuum as vacuum_platform, weather as weather_platform, ) from .const import DOMAIN, PLATFORMS, TemplateConfig @@ -98,61 +100,69 @@ CONFIG_SECTION_SCHEMA = vol.All( _backward_compat_schema, vol.Schema( { - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA, vol.Optional(CONF_ACTIONS): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, - vol.Optional(NUMBER_DOMAIN): vol.All( - cv.ensure_list, [number_platform.NUMBER_SCHEMA] - ), - vol.Optional(SENSOR_DOMAIN): vol.All( - cv.ensure_list, [sensor_platform.SENSOR_SCHEMA] - ), - vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys( - sensor_platform.LEGACY_SENSOR_SCHEMA - ), - vol.Optional(BINARY_SENSOR_DOMAIN): vol.All( - cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA] - ), vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys( binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA ), - vol.Optional(ALARM_CONTROL_PANEL_DOMAIN): vol.All( + vol.Optional(CONF_CONDITIONS): cv.CONDITIONS_SCHEMA, + vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys( + sensor_platform.LEGACY_SENSOR_SCHEMA + ), + vol.Optional(CONF_TRIGGERS): cv.TRIGGER_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, + vol.Optional(DOMAIN_ALARM_CONTROL_PANEL): vol.All( cv.ensure_list, [alarm_control_panel_platform.ALARM_CONTROL_PANEL_SCHEMA], ), - vol.Optional(SELECT_DOMAIN): vol.All( - cv.ensure_list, [select_platform.SELECT_SCHEMA] + vol.Optional(DOMAIN_BINARY_SENSOR): vol.All( + cv.ensure_list, [binary_sensor_platform.BINARY_SENSOR_SCHEMA] ), - vol.Optional(BUTTON_DOMAIN): vol.All( + vol.Optional(DOMAIN_BUTTON): vol.All( cv.ensure_list, [button_platform.BUTTON_SCHEMA] ), - vol.Optional(IMAGE_DOMAIN): vol.All( - cv.ensure_list, [image_platform.IMAGE_SCHEMA] - ), - vol.Optional(LIGHT_DOMAIN): vol.All( - cv.ensure_list, [light_platform.LIGHT_SCHEMA] - ), - vol.Optional(LOCK_DOMAIN): vol.All( - cv.ensure_list, [lock_platform.LOCK_SCHEMA] - ), - vol.Optional(WEATHER_DOMAIN): vol.All( - cv.ensure_list, [weather_platform.WEATHER_SCHEMA] - ), - vol.Optional(SWITCH_DOMAIN): vol.All( - cv.ensure_list, [switch_platform.SWITCH_SCHEMA] - ), - vol.Optional(COVER_DOMAIN): vol.All( + vol.Optional(DOMAIN_COVER): vol.All( cv.ensure_list, [cover_platform.COVER_SCHEMA] ), - vol.Optional(FAN_DOMAIN): vol.All( + vol.Optional(DOMAIN_FAN): vol.All( cv.ensure_list, [fan_platform.FAN_SCHEMA] ), + vol.Optional(DOMAIN_IMAGE): vol.All( + cv.ensure_list, [image_platform.IMAGE_SCHEMA] + ), + vol.Optional(DOMAIN_LIGHT): vol.All( + cv.ensure_list, [light_platform.LIGHT_SCHEMA] + ), + vol.Optional(DOMAIN_LOCK): vol.All( + cv.ensure_list, [lock_platform.LOCK_SCHEMA] + ), + vol.Optional(DOMAIN_NUMBER): vol.All( + cv.ensure_list, [number_platform.NUMBER_SCHEMA] + ), + vol.Optional(DOMAIN_SELECT): vol.All( + cv.ensure_list, [select_platform.SELECT_SCHEMA] + ), + vol.Optional(DOMAIN_SENSOR): vol.All( + cv.ensure_list, [sensor_platform.SENSOR_SCHEMA] + ), + vol.Optional(DOMAIN_SWITCH): vol.All( + cv.ensure_list, [switch_platform.SWITCH_SCHEMA] + ), + vol.Optional(DOMAIN_VACUUM): vol.All( + cv.ensure_list, [vacuum_platform.VACUUM_SCHEMA] + ), + vol.Optional(DOMAIN_WEATHER): vol.All( + cv.ensure_list, [weather_platform.WEATHER_SCHEMA] + ), }, ), ensure_domains_do_not_have_trigger_or_action( - ALARM_CONTROL_PANEL_DOMAIN, BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN + DOMAIN_ALARM_CONTROL_PANEL, + DOMAIN_BUTTON, + DOMAIN_COVER, + DOMAIN_FAN, + DOMAIN_LOCK, + DOMAIN_VACUUM, ), ) @@ -247,12 +257,12 @@ async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> Conf for old_key, new_key, transform in ( ( CONF_SENSORS, - SENSOR_DOMAIN, + DOMAIN_SENSOR, sensor_platform.rewrite_legacy_to_modern_conf, ), ( CONF_BINARY_SENSORS, - BINARY_SENSOR_DOMAIN, + DOMAIN_BINARY_SENSOR, binary_sensor_platform.rewrite_legacy_to_modern_conf, ), ): diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 1e18b06436a..462f7d672ff 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -24,21 +24,27 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ( CONF_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_NAME, + CONF_STATE, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import DOMAIN +from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN from .template_entity import ( + LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, + TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA, TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY, + TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, + TEMPLATE_ENTITY_ICON_SCHEMA, TemplateEntity, rewrite_common_legacy_to_modern_conf, ) @@ -46,8 +52,10 @@ from .template_entity import ( _LOGGER = logging.getLogger(__name__) CONF_VACUUMS = "vacuums" +CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_LEVEL_TEMPLATE = "battery_level_template" CONF_FAN_SPEED_LIST = "fan_speeds" +CONF_FAN_SPEED = "fan_speed" CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" @@ -60,24 +68,55 @@ _VALID_STATES = [ VacuumActivity.ERROR, ] +LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | { + CONF_BATTERY_LEVEL_TEMPLATE: CONF_BATTERY_LEVEL, + CONF_FAN_SPEED_TEMPLATE: CONF_FAN_SPEED, + CONF_VALUE_TEMPLATE: CONF_STATE, +} + VACUUM_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_BATTERY_LEVEL): cv.template, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list, + vol.Optional(CONF_FAN_SPEED): cv.template, + vol.Optional(CONF_NAME): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_STATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA, + vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, + } + ) + .extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema), +) + +LEGACY_VACUUM_SCHEMA = vol.All( cv.deprecated(CONF_ENTITY_ID), vol.Schema( { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, - vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list, - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, } ) .extend(TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY.schema) @@ -85,28 +124,56 @@ VACUUM_SCHEMA = vol.All( ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - {vol.Required(CONF_VACUUMS): vol.Schema({cv.slug: VACUUM_SCHEMA})} + {vol.Required(CONF_VACUUMS): cv.schema_with_slug_keys(LEGACY_VACUUM_SCHEMA)} ) -async def _async_create_entities(hass: HomeAssistant, config: ConfigType): - """Create the Template Vacuums.""" +def rewrite_legacy_to_modern_conf( + hass: HomeAssistant, config: dict[str, dict] +) -> list[dict]: + """Rewrite legacy switch configuration definitions to modern ones.""" vacuums = [] - for object_id, entity_config in config[CONF_VACUUMS].items(): - entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) - unique_id = entity_config.get(CONF_UNIQUE_ID) + for object_id, entity_conf in config.items(): + entity_conf = {**entity_conf, CONF_OBJECT_ID: object_id} + + entity_conf = rewrite_common_legacy_to_modern_conf( + hass, entity_conf, LEGACY_FIELDS + ) + + if CONF_NAME not in entity_conf: + entity_conf[CONF_NAME] = template.Template(object_id, hass) + + vacuums.append(entity_conf) + + return vacuums + + +@callback +def _async_create_template_tracking_entities( + async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + definitions: list[dict], + unique_id_prefix: str | None, +) -> None: + """Create the template switches.""" + vacuums = [] + + for entity_conf in definitions: + unique_id = entity_conf.get(CONF_UNIQUE_ID) + + if unique_id and unique_id_prefix: + unique_id = f"{unique_id_prefix}-{unique_id}" vacuums.append( TemplateVacuum( hass, - object_id, - entity_config, + entity_conf, unique_id, ) ) - return vacuums + async_add_entities(vacuums) async def async_setup_platform( @@ -115,8 +182,22 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the template vacuums.""" - async_add_entities(await _async_create_entities(hass, config)) + """Set up the Template cover.""" + if discovery_info is None: + _async_create_template_tracking_entities( + async_add_entities, + hass, + rewrite_legacy_to_modern_conf(hass, config[CONF_VACUUMS]), + None, + ) + return + + _async_create_template_tracking_entities( + async_add_entities, + hass, + discovery_info["entities"], + discovery_info["unique_id"], + ) class TemplateVacuum(TemplateEntity, StateVacuumEntity): @@ -127,24 +208,22 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): def __init__( self, hass: HomeAssistant, - object_id, config: ConfigType, unique_id, ) -> None: """Initialize the vacuum.""" - super().__init__( - hass, config=config, fallback_name=object_id, unique_id=unique_id - ) - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) + super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) name = self._attr_name if TYPE_CHECKING: assert name is not None - self._template = config.get(CONF_VALUE_TEMPLATE) - self._battery_level_template = config.get(CONF_BATTERY_LEVEL_TEMPLATE) - self._fan_speed_template = config.get(CONF_FAN_SPEED_TEMPLATE) + self._template = config.get(CONF_STATE) + self._battery_level_template = config.get(CONF_BATTERY_LEVEL) + self._fan_speed_template = config.get(CONF_FAN_SPEED) self._attr_supported_features = ( VacuumEntityFeature.START | VacuumEntityFeature.STATE ) diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index cc5bc9b39e3..90ca0b56afb 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -4,16 +4,17 @@ from typing import Any import pytest -from homeassistant import setup -from homeassistant.components import vacuum +from homeassistant.components import template, vacuum from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, + ATTR_FAN_SPEED, VacuumActivity, VacuumEntityFeature, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component @@ -22,19 +23,91 @@ from .conftest import ConfigurationStyle from tests.common import assert_setup_component from tests.components.vacuum import common -_TEST_OBJECT_ID = "test_vacuum" -_TEST_VACUUM = f"vacuum.{_TEST_OBJECT_ID}" -_STATE_INPUT_SELECT = "input_select.state" -_SPOT_CLEANING_INPUT_BOOLEAN = "input_boolean.spot_cleaning" -_LOCATING_INPUT_BOOLEAN = "input_boolean.locating" -_FAN_SPEED_INPUT_SELECT = "input_select.fan_speed" -_BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" +TEST_OBJECT_ID = "test_vacuum" +TEST_ENTITY_ID = f"vacuum.{TEST_OBJECT_ID}" + +STATE_INPUT_SELECT = "input_select.state" +BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" + +START_ACTION = { + "start": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "start", + }, + }, +} + + +TEMPLATE_VACUUM_ACTIONS = { + **START_ACTION, + "pause": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "pause", + }, + }, + "stop": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "stop", + }, + }, + "return_to_base": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "return_to_base", + }, + }, + "clean_spot": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "clean_spot", + }, + }, + "locate": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "locate", + }, + }, + "set_fan_speed": { + "service": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "set_fan_speed", + "fan_speed": "{{ fan_speed }}", + }, + }, +} + +UNIQUE_ID_CONFIG = {"unique_id": "not-so-unique-anymore", **TEMPLATE_VACUUM_ACTIONS} + + +def _verify( + hass: HomeAssistant, + expected_state: str, + expected_battery_level: int | None = None, + expected_fan_speed: int | None = None, +) -> None: + """Verify vacuum's state and speed.""" + state = hass.states.get(TEST_ENTITY_ID) + attributes = state.attributes + assert state.state == expected_state + assert attributes.get(ATTR_BATTERY_LEVEL) == expected_battery_level + assert attributes.get(ATTR_FAN_SPEED) == expected_fan_speed async def async_setup_legacy_format( hass: HomeAssistant, count: int, vacuum_config: dict[str, Any] ) -> None: - """Do setup of number integration via new format.""" + """Do setup of vacuum integration via new format.""" config = {"vacuum": {"platform": "template", "vacuums": vacuum_config}} with assert_setup_component(count, vacuum.DOMAIN): @@ -49,6 +122,24 @@ async def async_setup_legacy_format( await hass.async_block_till_done() +async def async_setup_modern_format( + hass: HomeAssistant, count: int, vacuum_config: dict[str, Any] +) -> None: + """Do setup of vacuum integration via modern format.""" + config = {"template": {"vacuum": vacuum_config}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + @pytest.fixture async def setup_vacuum( hass: HomeAssistant, @@ -59,6 +150,8 @@ async def setup_vacuum( """Do setup of number integration.""" if style == ConfigurationStyle.LEGACY: await async_setup_legacy_format(hass, count, vacuum_config) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format(hass, count, vacuum_config) @pytest.fixture @@ -70,160 +163,406 @@ async def setup_test_vacuum_with_extra_config( extra_config: dict[str, Any], ) -> None: """Do setup of number integration.""" - config = {_TEST_OBJECT_ID: {**vacuum_config, **extra_config}} if style == ConfigurationStyle.LEGACY: - await async_setup_legacy_format(hass, count, config) + await async_setup_legacy_format( + hass, count, {TEST_OBJECT_ID: {**vacuum_config, **extra_config}} + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, count, {"name": TEST_OBJECT_ID, **vacuum_config, **extra_config} + ) -@pytest.mark.parametrize(("count", "domain"), [(1, "vacuum")]) +@pytest.fixture +async def setup_state_vacuum( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str, +): + """Do setup of vacuum integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + "value_template": state_template, + **TEMPLATE_VACUUM_ACTIONS, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + "state": state_template, + **TEMPLATE_VACUUM_ACTIONS, + }, + ) + + +@pytest.fixture +async def setup_base_vacuum( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str | None, + extra_config: dict, +): + """Do setup of vacuum integration using a state template.""" + if style == ConfigurationStyle.LEGACY: + state_config = {"value_template": state_template} if state_template else {} + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **state_config, + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + state_config = {"state": state_template} if state_template else {} + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + **state_config, + **extra_config, + }, + ) + + +@pytest.fixture +async def setup_single_attribute_state_vacuum( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str | None, + attribute: str, + attribute_template: str, + extra_config: dict, +) -> None: + """Do setup of vacuum integration testing a single attribute.""" + extra = {attribute: attribute_template} if attribute and attribute_template else {} + if style == ConfigurationStyle.LEGACY: + state_config = {"value_template": state_template} if state_template else {} + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + **state_config, + **TEMPLATE_VACUUM_ACTIONS, + **extra, + **extra_config, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + state_config = {"state": state_template} if state_template else {} + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + **state_config, + **TEMPLATE_VACUUM_ACTIONS, + **extra, + **extra_config, + }, + ) + + +@pytest.fixture +async def setup_attributes_state_vacuum( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + state_template: str | None, + attributes: dict, +) -> None: + """Do setup of vacuum integration testing a single attribute.""" + if style == ConfigurationStyle.LEGACY: + state_config = {"value_template": state_template} if state_template else {} + await async_setup_legacy_format( + hass, + count, + { + TEST_OBJECT_ID: { + "attribute_templates": attributes, + **state_config, + **TEMPLATE_VACUUM_ACTIONS, + } + }, + ) + elif style == ConfigurationStyle.MODERN: + state_config = {"state": state_template} if state_template else {} + await async_setup_modern_format( + hass, + count, + { + "name": TEST_OBJECT_ID, + "attributes": attributes, + **state_config, + **TEMPLATE_VACUUM_ACTIONS, + }, + ) + + +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - ("parm1", "parm2", "config"), + ("style", "state_template", "extra_config", "parm1", "parm2"), [ ( + ConfigurationStyle.LEGACY, + None, + {"start": {"service": "script.vacuum_start"}}, STATE_UNKNOWN, None, - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": {"start": {"service": "script.vacuum_start"}} - }, - } - }, ), ( + ConfigurationStyle.MODERN, + None, + {"start": {"service": "script.vacuum_start"}}, + STATE_UNKNOWN, + None, + ), + ( + ConfigurationStyle.LEGACY, + "{{ 'cleaning' }}", + { + "battery_level_template": "{{ 100 }}", + "start": {"service": "script.vacuum_start"}, + }, VacuumActivity.CLEANING, 100, - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": { - "value_template": "{{ 'cleaning' }}", - "battery_level_template": "{{ 100 }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } - }, ), ( - STATE_UNKNOWN, - None, + ConfigurationStyle.MODERN, + "{{ 'cleaning' }}", { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": { - "value_template": "{{ 'abc' }}", - "battery_level_template": "{{ 101 }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } + "battery_level": "{{ 100 }}", + "start": {"service": "script.vacuum_start"}, }, + VacuumActivity.CLEANING, + 100, ), ( + ConfigurationStyle.LEGACY, + "{{ 'abc' }}", + { + "battery_level_template": "{{ 101 }}", + "start": {"service": "script.vacuum_start"}, + }, STATE_UNKNOWN, None, + ), + ( + ConfigurationStyle.MODERN, + "{{ 'abc' }}", { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": { - "value_template": "{{ this_function_does_not_exist() }}", - "battery_level_template": "{{ this_function_does_not_exist() }}", - "fan_speed_template": "{{ this_function_does_not_exist() }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } + "battery_level": "{{ 101 }}", + "start": {"service": "script.vacuum_start"}, }, + STATE_UNKNOWN, + None, + ), + ( + ConfigurationStyle.LEGACY, + "{{ this_function_does_not_exist() }}", + { + "battery_level_template": "{{ this_function_does_not_exist() }}", + "fan_speed_template": "{{ this_function_does_not_exist() }}", + "start": {"service": "script.vacuum_start"}, + }, + STATE_UNKNOWN, + None, + ), + ( + ConfigurationStyle.MODERN, + "{{ this_function_does_not_exist() }}", + { + "battery_level": "{{ this_function_does_not_exist() }}", + "fan_speed": "{{ this_function_does_not_exist() }}", + "start": {"service": "script.vacuum_start"}, + }, + STATE_UNKNOWN, + None, ), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_valid_configs(hass: HomeAssistant, count, parm1, parm2) -> None: +@pytest.mark.usefixtures("setup_base_vacuum") +async def test_valid_legacy_configs(hass: HomeAssistant, count, parm1, parm2) -> None: """Test: configs.""" assert len(hass.states.async_all("vacuum")) == count _verify(hass, parm1, parm2) -@pytest.mark.parametrize(("count", "domain"), [(0, "vacuum")]) +@pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( - "config", + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("state_template", "extra_config"), [ - { - "vacuum": { - "platform": "template", - "vacuums": {"test_vacuum": {"value_template": "{{ 'on' }}"}}, - } - }, - { - "platform": "template", - "vacuums": {"test_vacuum": {"start": {"service": "script.vacuum_start"}}}, - }, + ("{{ 'on' }}", {}), + (None, {"nothingburger": {"service": "script.vacuum_start"}}), ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_base_vacuum") async def test_invalid_configs(hass: HomeAssistant, count) -> None: """Test: configs.""" assert len(hass.states.async_all("vacuum")) == count @pytest.mark.parametrize( - ("count", "domain", "config"), + ("count", "state_template", "extra_config"), + [(1, "{{ states('input_select.state') }}", {})], +) +@pytest.mark.parametrize( + ("style", "attribute"), [ - ( - 1, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": { - "value_template": "{{ states('input_select.state') }}", - "battery_level_template": "{{ states('input_number.battery_level') }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } - }, - ) + (ConfigurationStyle.LEGACY, "battery_level_template"), + (ConfigurationStyle.MODERN, "battery_level"), ], ) -@pytest.mark.usefixtures("start_ha") -async def test_templates_with_entities(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("attribute_template", "expected"), + [ + ("{{ '0' }}", 0), + ("{{ 100 }}", 100), + ("{{ 101 }}", None), + ("{{ -1 }}", None), + ("{{ 'foo' }}", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_battery_level_template( + hass: HomeAssistant, expected: int | None +) -> None: """Test templates with values from other entities.""" - _verify(hass, STATE_UNKNOWN, None) - - hass.states.async_set(_STATE_INPUT_SELECT, VacuumActivity.CLEANING) - hass.states.async_set(_BATTERY_LEVEL_INPUT_NUMBER, 100) - await hass.async_block_till_done() - _verify(hass, VacuumActivity.CLEANING, 100) + _verify(hass, STATE_UNKNOWN, expected) @pytest.mark.parametrize( - ("count", "domain", "config"), + ("count", "state_template", "extra_config"), [ ( 1, - "vacuum", + "{{ states('input_select.state') }}", { - "vacuum": { - "platform": "template", - "vacuums": { - "test_template_vacuum": { - "availability_template": "{{ is_state('availability_state.state', 'on') }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } + "fan_speeds": ["low", "medium", "high"], }, ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "fan_speed_template"), + (ConfigurationStyle.MODERN, "fan_speed"), + ], +) +@pytest.mark.parametrize( + ("attribute_template", "expected"), + [ + ("{{ 'low' }}", "low"), + ("{{ 'medium' }}", "medium"), + ("{{ 'high' }}", "high"), + ("{{ 'invalid' }}", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_fan_speed_template(hass: HomeAssistant, expected: str | None) -> None: + """Test templates with values from other entities.""" + _verify(hass, STATE_UNKNOWN, None, expected) + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 'on' }}", + "{% if states.switch.test_state.state %}mdi:check{% endif %}", + {}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "icon"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_icon_template(hass: HomeAssistant) -> None: + """Test icon template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("icon") in ("", None) + + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["icon"] == "mdi:check" + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ 'on' }}", + "{% if states.switch.test_state.state %}local/vacuum.png{% endif %}", + {}, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.MODERN, "picture"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_picture_template(hass: HomeAssistant) -> None: + """Test picture template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get("entity_picture") in ("", None) + + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["entity_picture"] == "local/vacuum.png" + + +@pytest.mark.parametrize("extra_config", [{}]) +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + None, + "{{ is_state('availability_state.state', 'on') }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") async def test_available_template_with_entities(hass: HomeAssistant) -> None: """Test availability templates with values from other entities.""" @@ -232,105 +571,83 @@ async def test_available_template_with_entities(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Device State should not be unavailable - assert hass.states.get("vacuum.test_template_vacuum").state != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE # When Availability template returns false hass.states.async_set("availability_state.state", STATE_OFF) await hass.async_block_till_done() # device state should be unavailable - assert hass.states.get("vacuum.test_template_vacuum").state == STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE +@pytest.mark.parametrize("extra_config", [{}]) @pytest.mark.parametrize( - ("count", "domain", "config"), + ("count", "state_template", "attribute_template"), [ ( 1, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_template_vacuum": { - "availability_template": "{{ x - 12 }}", - "start": {"service": "script.vacuum_start"}, - } - }, - } - }, + None, + "{{ x - 12 }}", ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "availability_template"), + (ConfigurationStyle.MODERN, "availability"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") async def test_invalid_availability_template_keeps_component_available( hass: HomeAssistant, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" - assert hass.states.get("vacuum.test_template_vacuum") != STATE_UNAVAILABLE + assert hass.states.get(TEST_ENTITY_ID) != STATE_UNAVAILABLE assert "UndefinedError: 'x' is undefined" in caplog_setup_text @pytest.mark.parametrize( - ("count", "domain", "config"), + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("count", "state_template", "attributes"), [ ( 1, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_template_vacuum": { - "value_template": "{{ 'cleaning' }}", - "start": {"service": "script.vacuum_start"}, - "attribute_templates": { - "test_attribute": "It {{ states.sensor.test_state.state }}." - }, - } - }, - } - }, + "{{ 'cleaning' }}", + {"test_attribute": "It {{ states.sensor.test_state.state }}."}, ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_attributes_state_vacuum") async def test_attribute_templates(hass: HomeAssistant) -> None: """Test attribute_templates template.""" - state = hass.states.get("vacuum.test_template_vacuum") + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["test_attribute"] == "It ." hass.states.async_set("sensor.test_state", "Works") await hass.async_block_till_done() - await async_update_entity(hass, "vacuum.test_template_vacuum") - state = hass.states.get("vacuum.test_template_vacuum") + await async_update_entity(hass, TEST_ENTITY_ID) + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["test_attribute"] == "It Works." @pytest.mark.parametrize( - ("count", "domain", "config"), + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + ("count", "state_template", "attributes"), [ ( 1, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "invalid_template": { - "value_template": "{{ states('input_select.state') }}", - "start": {"service": "script.vacuum_start"}, - "attribute_templates": { - "test_attribute": "{{ this_function_does_not_exist() }}" - }, - } - }, - } - }, + "{{ states('input_select.state') }}", + {"test_attribute": "{{ this_function_does_not_exist() }}"}, ) ], ) -@pytest.mark.usefixtures("start_ha") +@pytest.mark.usefixtures("setup_attributes_state_vacuum") async def test_invalid_attribute_template( hass: HomeAssistant, caplog_setup_text ) -> None: @@ -340,420 +657,6 @@ async def test_invalid_attribute_template( assert "TemplateError" in caplog_setup_text -@pytest.mark.parametrize( - ("count", "domain", "config"), - [ - ( - 1, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_template_vacuum_01": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ true }}", - "start": {"service": "script.vacuum_start"}, - }, - "test_template_vacuum_02": { - "unique_id": "not-so-unique-anymore", - "value_template": "{{ false }}", - "start": {"service": "script.vacuum_start"}, - }, - }, - } - }, - ), - ], -) -@pytest.mark.usefixtures("start_ha") -async def test_unique_id(hass: HomeAssistant) -> None: - """Test unique_id option only creates one vacuum per id.""" - assert len(hass.states.async_all("vacuum")) == 1 - - -async def test_unused_services(hass: HomeAssistant) -> None: - """Test calling unused services raises.""" - await _register_basic_vacuum(hass) - - # Pause vacuum - with pytest.raises(HomeAssistantError): - await common.async_pause(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # Stop vacuum - with pytest.raises(HomeAssistantError): - await common.async_stop(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # Return vacuum to base - with pytest.raises(HomeAssistantError): - await common.async_return_to_base(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # Spot cleaning - with pytest.raises(HomeAssistantError): - await common.async_clean_spot(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # Locate vacuum - with pytest.raises(HomeAssistantError): - await common.async_locate(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # Set fan's speed - with pytest.raises(HomeAssistantError): - await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM) - await hass.async_block_till_done() - - _verify(hass, STATE_UNKNOWN, None) - - -async def test_state_services(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test state services.""" - await _register_components(hass) - - # Start vacuum - await common.async_start(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.CLEANING - _verify(hass, VacuumActivity.CLEANING, None) - assert len(calls) == 1 - assert calls[-1].data["action"] == "start" - assert calls[-1].data["caller"] == _TEST_VACUUM - - # Pause vacuum - await common.async_pause(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.PAUSED - _verify(hass, VacuumActivity.PAUSED, None) - assert len(calls) == 2 - assert calls[-1].data["action"] == "pause" - assert calls[-1].data["caller"] == _TEST_VACUUM - - # Stop vacuum - await common.async_stop(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.IDLE - _verify(hass, VacuumActivity.IDLE, None) - assert len(calls) == 3 - assert calls[-1].data["action"] == "stop" - assert calls[-1].data["caller"] == _TEST_VACUUM - - # Return vacuum to base - await common.async_return_to_base(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_STATE_INPUT_SELECT).state == VacuumActivity.RETURNING - _verify(hass, VacuumActivity.RETURNING, None) - assert len(calls) == 4 - assert calls[-1].data["action"] == "return_to_base" - assert calls[-1].data["caller"] == _TEST_VACUUM - - -async def test_clean_spot_service( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: - """Test clean spot service.""" - await _register_components(hass) - - # Clean spot - await common.async_clean_spot(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_SPOT_CLEANING_INPUT_BOOLEAN).state == STATE_ON - assert len(calls) == 1 - assert calls[-1].data["action"] == "clean_spot" - assert calls[-1].data["caller"] == _TEST_VACUUM - - -async def test_locate_service(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test locate service.""" - await _register_components(hass) - - # Locate vacuum - await common.async_locate(hass, _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_LOCATING_INPUT_BOOLEAN).state == STATE_ON - assert len(calls) == 1 - assert calls[-1].data["action"] == "locate" - assert calls[-1].data["caller"] == _TEST_VACUUM - - -async def test_set_fan_speed(hass: HomeAssistant, calls: list[ServiceCall]) -> None: - """Test set valid fan speed.""" - await _register_components(hass) - - # Set vacuum's fan speed to high - await common.async_set_fan_speed(hass, "high", _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "high" - assert len(calls) == 1 - assert calls[-1].data["action"] == "set_fan_speed" - assert calls[-1].data["caller"] == _TEST_VACUUM - assert calls[-1].data["option"] == "high" - - # Set fan's speed to medium - await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "medium" - assert len(calls) == 2 - assert calls[-1].data["action"] == "set_fan_speed" - assert calls[-1].data["caller"] == _TEST_VACUUM - assert calls[-1].data["option"] == "medium" - - -async def test_set_invalid_fan_speed( - hass: HomeAssistant, calls: list[ServiceCall] -) -> None: - """Test set invalid fan speed when fan has valid speed.""" - await _register_components(hass) - - # Set vacuum's fan speed to high - await common.async_set_fan_speed(hass, "high", _TEST_VACUUM) - await hass.async_block_till_done() - - # verify - assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "high" - - # Set vacuum's fan speed to 'invalid' - await common.async_set_fan_speed(hass, "invalid", _TEST_VACUUM) - await hass.async_block_till_done() - - # verify fan speed is unchanged - assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "high" - - -def _verify( - hass: HomeAssistant, expected_state: str, expected_battery_level: int -) -> None: - """Verify vacuum's state and speed.""" - state = hass.states.get(_TEST_VACUUM) - attributes = state.attributes - assert state.state == expected_state - assert attributes.get(ATTR_BATTERY_LEVEL) == expected_battery_level - - -async def _register_basic_vacuum(hass: HomeAssistant) -> None: - """Register basic vacuum with only required options for testing.""" - with assert_setup_component(1, "input_select"): - assert await setup.async_setup_component( - hass, - "input_select", - { - "input_select": { - "state": {"name": "State", "options": [VacuumActivity.CLEANING]} - } - }, - ) - - with assert_setup_component(1, "vacuum"): - assert await setup.async_setup_component( - hass, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": { - "test_vacuum": { - "start": { - "service": "input_select.select_option", - "data": { - "entity_id": _STATE_INPUT_SELECT, - "option": VacuumActivity.CLEANING, - }, - } - } - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - -async def _register_components(hass: HomeAssistant) -> None: - """Register basic components for testing.""" - with assert_setup_component(2, "input_boolean"): - assert await setup.async_setup_component( - hass, - "input_boolean", - {"input_boolean": {"spot_cleaning": None, "locating": None}}, - ) - - with assert_setup_component(2, "input_select"): - assert await setup.async_setup_component( - hass, - "input_select", - { - "input_select": { - "state": { - "name": "State", - "options": [ - VacuumActivity.CLEANING, - VacuumActivity.DOCKED, - VacuumActivity.IDLE, - VacuumActivity.PAUSED, - VacuumActivity.RETURNING, - ], - }, - "fan_speed": { - "name": "Fan speed", - "options": ["", "low", "medium", "high"], - }, - } - }, - ) - - with assert_setup_component(1, "vacuum"): - test_vacuum_config = { - "value_template": "{{ states('input_select.state') }}", - "fan_speed_template": "{{ states('input_select.fan_speed') }}", - "start": [ - { - "service": "input_select.select_option", - "data": { - "entity_id": _STATE_INPUT_SELECT, - "option": VacuumActivity.CLEANING, - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "start", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "pause": [ - { - "service": "input_select.select_option", - "data": { - "entity_id": _STATE_INPUT_SELECT, - "option": VacuumActivity.PAUSED, - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "pause", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "stop": [ - { - "service": "input_select.select_option", - "data": { - "entity_id": _STATE_INPUT_SELECT, - "option": VacuumActivity.IDLE, - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "stop", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "return_to_base": [ - { - "service": "input_select.select_option", - "data": { - "entity_id": _STATE_INPUT_SELECT, - "option": VacuumActivity.RETURNING, - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "return_to_base", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "clean_spot": [ - { - "service": "input_boolean.turn_on", - "entity_id": _SPOT_CLEANING_INPUT_BOOLEAN, - }, - { - "service": "test.automation", - "data_template": { - "action": "clean_spot", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "locate": [ - { - "service": "input_boolean.turn_on", - "entity_id": _LOCATING_INPUT_BOOLEAN, - }, - { - "service": "test.automation", - "data_template": { - "action": "locate", - "caller": "{{ this.entity_id }}", - }, - }, - ], - "set_fan_speed": [ - { - "service": "input_select.select_option", - "data_template": { - "entity_id": _FAN_SPEED_INPUT_SELECT, - "option": "{{ fan_speed }}", - }, - }, - { - "service": "test.automation", - "data_template": { - "action": "set_fan_speed", - "caller": "{{ this.entity_id }}", - "option": "{{ fan_speed }}", - }, - }, - ], - "fan_speeds": ["low", "medium", "high"], - "attribute_templates": { - "test_attribute": "It {{ states.sensor.test_state.state }}." - }, - } - - assert await setup.async_setup_component( - hass, - "vacuum", - { - "vacuum": { - "platform": "template", - "vacuums": {"test_vacuum": test_vacuum_config}, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( ("style", "vacuum_config"), @@ -761,11 +664,262 @@ async def _register_components(hass: HomeAssistant) -> None: ( ConfigurationStyle.LEGACY, { - "start": [], + "test_template_vacuum_01": { + "value_template": "{{ true }}", + **UNIQUE_ID_CONFIG, + }, + "test_template_vacuum_02": { + "value_template": "{{ false }}", + **UNIQUE_ID_CONFIG, + }, }, ), + ( + ConfigurationStyle.MODERN, + [ + { + "name": "test_template_vacuum_01", + "state": "{{ true }}", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_vacuum_02", + "state": "{{ false }}", + **UNIQUE_ID_CONFIG, + }, + ], + ), ], ) +@pytest.mark.usefixtures("setup_vacuum") +async def test_unique_id(hass: HomeAssistant) -> None: + """Test unique_id option only creates one vacuum per id.""" + assert len(hass.states.async_all("vacuum")) == 1 + + +@pytest.mark.parametrize( + ("count", "state_template", "extra_config"), [(1, None, START_ACTION)] +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.usefixtures("setup_base_vacuum") +async def test_unused_services(hass: HomeAssistant) -> None: + """Test calling unused services raises.""" + # Pause vacuum + with pytest.raises(HomeAssistantError): + await common.async_pause(hass, TEST_ENTITY_ID) + await hass.async_block_till_done() + + # Stop vacuum + with pytest.raises(HomeAssistantError): + await common.async_stop(hass, TEST_ENTITY_ID) + await hass.async_block_till_done() + + # Return vacuum to base + with pytest.raises(HomeAssistantError): + await common.async_return_to_base(hass, TEST_ENTITY_ID) + await hass.async_block_till_done() + + # Spot cleaning + with pytest.raises(HomeAssistantError): + await common.async_clean_spot(hass, TEST_ENTITY_ID) + await hass.async_block_till_done() + + # Locate vacuum + with pytest.raises(HomeAssistantError): + await common.async_locate(hass, TEST_ENTITY_ID) + await hass.async_block_till_done() + + # Set fan's speed + with pytest.raises(HomeAssistantError): + await common.async_set_fan_speed(hass, "medium", TEST_ENTITY_ID) + await hass.async_block_till_done() + + _verify(hass, STATE_UNKNOWN, None) + + +@pytest.mark.parametrize( + ("count", "state_template"), + [(1, "{{ states('input_select.state') }}")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) +@pytest.mark.parametrize( + "action", + [ + "start", + "pause", + "stop", + "clean_spot", + "return_to_base", + "locate", + ], +) +@pytest.mark.usefixtures("setup_state_vacuum") +async def test_state_services( + hass: HomeAssistant, action: str, calls: list[ServiceCall] +) -> None: + """Test locate service.""" + + await hass.services.async_call( + "vacuum", + action, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == action + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template", "extra_config"), + [ + ( + 1, + "{{ states('input_select.state') }}", + "{{ states('input_select.fan_speed') }}", + { + "fan_speeds": ["low", "medium", "high"], + }, + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "fan_speed_template"), + (ConfigurationStyle.MODERN, "fan_speed"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_set_fan_speed(hass: HomeAssistant, calls: list[ServiceCall]) -> None: + """Test set valid fan speed.""" + + # Set vacuum's fan speed to high + await common.async_set_fan_speed(hass, "high", TEST_ENTITY_ID) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["fan_speed"] == "high" + + # Set fan's speed to medium + await common.async_set_fan_speed(hass, "medium", TEST_ENTITY_ID) + await hass.async_block_till_done() + + # verify + assert len(calls) == 2 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["fan_speed"] == "medium" + + +@pytest.mark.parametrize( + "extra_config", + [ + { + "fan_speeds": ["low", "medium", "high"], + } + ], +) +@pytest.mark.parametrize( + ("count", "state_template", "attribute_template"), + [ + ( + 1, + "{{ states('input_select.state') }}", + "{{ states('input_select.fan_speed') }}", + ) + ], +) +@pytest.mark.parametrize( + ("style", "attribute"), + [ + (ConfigurationStyle.LEGACY, "fan_speed_template"), + (ConfigurationStyle.MODERN, "fan_speed"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_state_vacuum") +async def test_set_invalid_fan_speed( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test set invalid fan speed when fan has valid speed.""" + + # Set vacuum's fan speed to high + await common.async_set_fan_speed(hass, "high", TEST_ENTITY_ID) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["fan_speed"] == "high" + + # Set vacuum's fan speed to 'invalid' + await common.async_set_fan_speed(hass, "invalid", TEST_ENTITY_ID) + await hass.async_block_till_done() + + # verify fan speed is unchanged + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + assert calls[-1].data["fan_speed"] == "high" + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test a template unique_id propagates to switch unique_ids.""" + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "vacuum": [ + { + **TEMPLATE_VACUUM_ACTIONS, + "name": "test_a", + "unique_id": "a", + }, + { + **TEMPLATE_VACUUM_ACTIONS, + "name": "test_b", + "unique_id": "b", + }, + ], + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("vacuum")) == 2 + + entry = entity_registry.async_get("vacuum.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("vacuum.test_b") + assert entry + assert entry.unique_id == "x-b" + + +@pytest.mark.parametrize(("count", "vacuum_config"), [(1, {"start": []})]) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] +) @pytest.mark.parametrize( ("extra_config", "supported_features"), [ @@ -813,10 +967,10 @@ async def test_empty_action_config( setup_test_vacuum_with_extra_config, ) -> None: """Test configuration with empty script.""" - await common.async_start(hass, _TEST_VACUUM) + await common.async_start(hass, TEST_ENTITY_ID) await hass.async_block_till_done() - state = hass.states.get(_TEST_VACUUM) + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes["supported_features"] == ( VacuumEntityFeature.STATE | VacuumEntityFeature.START | supported_features ) From f2a3a5cbbd8e08dc8372d2fa94689eab3494a205 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 15:50:46 +0200 Subject: [PATCH 0494/1175] Move iqvia coordinator to separate module (#144969) * Move iqvia coordinator to separate module * Adjust --- homeassistant/components/iqvia/__init__.py | 24 ++-------- homeassistant/components/iqvia/coordinator.py | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/iqvia/coordinator.py diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 3fabb88b041..049c23965b1 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -3,25 +3,18 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine -from datetime import timedelta -from functools import partial -from typing import Any from pyiqvia import Client -from pyiqvia.errors import IQVIAError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( CONF_ZIP_CODE, DOMAIN, - LOGGER, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, @@ -30,9 +23,9 @@ from .const import ( TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, ) +from .coordinator import IqviaUpdateCoordinator DEFAULT_ATTRIBUTION = "Data provided by IQVIA™" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) PLATFORMS = [Platform.SENSOR] @@ -52,15 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # blocking) startup: client.disable_request_retries() - async def async_get_data_from_api( - api_coro: Callable[[], Coroutine[Any, Any, dict[str, Any]]], - ) -> dict[str, Any]: - """Get data from a particular API coroutine.""" - try: - return await api_coro() - except IQVIAError as err: - raise UpdateFailed from err - coordinators = {} init_data_update_tasks = [] @@ -73,13 +57,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (TYPE_DISEASE_FORECAST, client.disease.extended), (TYPE_DISEASE_INDEX, client.disease.current), ): - coordinator = coordinators[sensor_type] = DataUpdateCoordinator( + coordinator = coordinators[sensor_type] = IqviaUpdateCoordinator( hass, - LOGGER, config_entry=entry, name=f"{entry.data[CONF_ZIP_CODE]} {sensor_type}", - update_interval=DEFAULT_SCAN_INTERVAL, - update_method=partial(async_get_data_from_api, api_coro), + update_method=api_coro, ) init_data_update_tasks.append(coordinator.async_refresh()) diff --git a/homeassistant/components/iqvia/coordinator.py b/homeassistant/components/iqvia/coordinator.py new file mode 100644 index 00000000000..420cbadbefa --- /dev/null +++ b/homeassistant/components/iqvia/coordinator.py @@ -0,0 +1,47 @@ +"""Support for IQVIA.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from datetime import timedelta +from typing import Any + +from pyiqvia.errors import IQVIAError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + + +class IqviaUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Custom DataUpdateCoordinator for IQVIA.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + name: str, + update_method: Callable[[], Coroutine[Any, Any, dict[str, Any]]], + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + LOGGER, + name=name, + config_entry=config_entry, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + self._update_method = update_method + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch data from the API.""" + try: + return await self._update_method() + except IQVIAError as err: + raise UpdateFailed from err From d24a60777b55ce047d55ccc3805cd589d675542b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 15 May 2025 16:07:53 +0200 Subject: [PATCH 0495/1175] Fix Home Assistant Yellow config entry data (#144948) --- .../homeassistant_yellow/__init__.py | 9 +-- .../homeassistant_yellow/config_flow.py | 7 +- .../homeassistant_yellow/test_config_flow.py | 4 +- .../homeassistant_yellow/test_init.py | 71 +++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 71aa8ef99b7..27c40e35946 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -90,16 +90,17 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> minor_version=2, ) - if config_entry.minor_version == 2: - # Add a `firmware_version` key + if config_entry.minor_version <= 3: + # Add a `firmware_version` key if it doesn't exist to handle entries created + # with minor version 1.3 where the firmware version was not set. hass.config_entries.async_update_entry( config_entry, data={ **config_entry.data, - FIRMWARE_VERSION: None, + FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION), }, version=1, - minor_version=3, + minor_version=4, ) _LOGGER.debug( diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py index 5472c346e94..1fac6bcac96 100644 --- a/homeassistant/components/homeassistant_yellow/config_flow.py +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): """Handle a config flow for Home Assistant Yellow.""" VERSION = 1 - MINOR_VERSION = 3 + MINOR_VERSION = 4 def __init__(self, *args: Any, **kwargs: Any) -> None: """Instantiate config flow.""" @@ -116,6 +116,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN): if self._probed_firmware_info is not None else ApplicationType.EZSP ).value, + FIRMWARE_VERSION: ( + self._probed_firmware_info.firmware_version + if self._probed_firmware_info is not None + else None + ), }, ) diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 46fec0a1f30..1d5a64eafb9 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -101,12 +101,12 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" - assert result["data"] == {"firmware": "ezsp"} + assert result["data"] == {"firmware": "ezsp", "firmware_version": None} assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == {"firmware": "ezsp"} + assert config_entry.data == {"firmware": "ezsp", "firmware_version": None} assert config_entry.options == {} assert config_entry.title == "Home Assistant Yellow" diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 57d63c7441e..00e3383cf77 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -10,6 +10,9 @@ from homeassistant.components.homeassistant_hardware.util import ( ApplicationType, FirmwareInfo, ) +from homeassistant.components.homeassistant_yellow.config_flow import ( + HomeAssistantYellowConfigFlow, +) from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -248,3 +251,71 @@ async def test_setup_entry_addon_info_fails( await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("start_version", "data", "migrated_data"), + [ + (1, {}, {"firmware": "ezsp", "firmware_version": None}), + (2, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}), + ( + 2, + {"firmware": "ezsp", "firmware_version": "123"}, + {"firmware": "ezsp", "firmware_version": "123"}, + ), + (3, {"firmware": "ezsp"}, {"firmware": "ezsp", "firmware_version": None}), + ( + 3, + {"firmware": "ezsp", "firmware_version": "123"}, + {"firmware": "ezsp", "firmware_version": "123"}, + ), + ], +) +async def test_migrate_entry( + hass: HomeAssistant, + start_version: int, + data: dict, + migrated_data: dict, +) -> None: + """Test migration of a config entry.""" + mock_integration(hass, MockModule("hassio")) + await async_setup_component(hass, HASSIO_DOMAIN, {}) + + # Setup the config entry + config_entry = MockConfigEntry( + data=data, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + version=1, + minor_version=start_version, + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ), + patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant_yellow.guess_firmware_info", + return_value=FirmwareInfo( # Nothing is setup + device="/dev/ttyAMA1", + firmware_version="1234", + firmware_type=ApplicationType.EZSP, + source="unknown", + owners=[], + ), + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.data == migrated_data + assert config_entry.options == {} + assert config_entry.minor_version == HomeAssistantYellowConfigFlow.MINOR_VERSION + assert config_entry.version == HomeAssistantYellowConfigFlow.VERSION From d33a0f75fdb48c7f3d1496739463da6534c94c8c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 15 May 2025 17:42:38 +0200 Subject: [PATCH 0496/1175] Add water heater support to SmartThings (#144927) * Add another EHS SmartThings fixture * Add another EHS * Add water heater support to SmartThings * Add water heater support to SmartThings * Add water heater support to SmartThings * Add water heater support to SmartThings * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Add more tests * Make target temp setting conditional * Make target temp setting conditional * Finish tests * Fix --- .../components/smartthings/__init__.py | 1 + .../components/smartthings/climate.py | 3 +- homeassistant/components/smartthings/const.py | 4 + .../components/smartthings/strings.json | 9 + .../components/smartthings/water_heater.py | 226 ++++++++ .../snapshots/test_water_heater.ambr | 218 +++++++ .../smartthings/test_water_heater.py | 542 ++++++++++++++++++ 7 files changed, 1001 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/smartthings/water_heater.py create mode 100644 tests/components/smartthings/snapshots/test_water_heater.ambr create mode 100644 tests/components/smartthings/test_water_heater.py diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index fe1b965db30..b78d2695370 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -102,6 +102,7 @@ PLATFORMS = [ Platform.SWITCH, Platform.UPDATE, Platform.VALVE, + Platform.WATER_HEATER, ] diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index c594ca237a4..2859500b5f6 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry -from .const import MAIN +from .const import MAIN, UNIT_MAP from .entity import SmartThingsEntity ATTR_OPERATION_STATE = "operation_state" @@ -90,7 +90,6 @@ FAN_OSCILLATION_TO_SWING = { WIND = "wind" WINDFREE = "windFree" -UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 8f27b785688..1925d973ef4 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -2,6 +2,8 @@ from pysmartthings import Attribute, Capability, Category +from homeassistant.const import UnitOfTemperature + DOMAIN = "smartthings" SCOPES = [ @@ -118,3 +120,5 @@ INVALID_SWITCH_CATEGORIES = { Category.MICROWAVE, Category.DISHWASHER, } + +UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT} diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 9cec158b5a9..50cb864e7d7 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -530,6 +530,15 @@ "sabbath_mode": { "name": "Sabbath mode" } + }, + "water_heater": { + "water_heater": { + "state": { + "standard": "Standard", + "force": "Forced", + "power": "Power" + } + } } }, "issues": { diff --git a/homeassistant/components/smartthings/water_heater.py b/homeassistant/components/smartthings/water_heater.py new file mode 100644 index 00000000000..fe09531931b --- /dev/null +++ b/homeassistant/components/smartthings/water_heater.py @@ -0,0 +1,226 @@ +"""Support for water heaters through the SmartThings cloud API.""" + +from __future__ import annotations + +from typing import Any + +from pysmartthings import Attribute, Capability, Command, SmartThings + +from homeassistant.components.water_heater import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + STATE_ECO, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util.unit_conversion import TemperatureConverter + +from . import FullDevice, SmartThingsConfigEntry +from .const import MAIN, UNIT_MAP +from .entity import SmartThingsEntity + +OPERATION_MAP_TO_HA: dict[str, str] = { + "eco": STATE_ECO, + "std": "standard", + "force": "force", + "power": "power", +} + +HA_TO_OPERATION_MAP = {v: k for k, v in OPERATION_MAP_TO_HA.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SmartThingsConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Add water heaters for a config entry.""" + entry_data = entry.runtime_data + async_add_entities( + SmartThingsWaterHeater(entry_data.client, device) + for device in entry_data.devices.values() + if all( + capability in device.status[MAIN] + for capability in ( + Capability.SWITCH, + Capability.AIR_CONDITIONER_MODE, + Capability.TEMPERATURE_MEASUREMENT, + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Capability.THERMOSTAT_COOLING_SETPOINT, + Capability.SAMSUNG_CE_EHS_THERMOSTAT, + Capability.CUSTOM_OUTING_MODE, + ) + ) + ) + + +class SmartThingsWaterHeater(SmartThingsEntity, WaterHeaterEntity): + """Define a SmartThings Water Heater.""" + + _attr_name = None + _attr_translation_key = "water_heater" + + def __init__(self, client: SmartThings, device: FullDevice) -> None: + """Init the class.""" + super().__init__( + client, + device, + { + Capability.SWITCH, + Capability.AIR_CONDITIONER_MODE, + Capability.TEMPERATURE_MEASUREMENT, + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Capability.THERMOSTAT_COOLING_SETPOINT, + Capability.CUSTOM_OUTING_MODE, + }, + ) + unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][ + Attribute.TEMPERATURE + ].unit + assert unit is not None + self._attr_temperature_unit = UNIT_MAP[unit] + + @property + def supported_features(self) -> WaterHeaterEntityFeature: + """Return the supported features.""" + features = ( + WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + | WaterHeaterEntityFeature.ON_OFF + ) + if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on": + features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE + return features + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_temperature = TemperatureConverter.convert( + DEFAULT_MIN_TEMP, UnitOfTemperature.FAHRENHEIT, self._attr_temperature_unit + ) + return min(min_temperature, self.target_temperature_low) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_temperature = TemperatureConverter.convert( + DEFAULT_MAX_TEMP, UnitOfTemperature.FAHRENHEIT, self._attr_temperature_unit + ) + return max(max_temperature, self.target_temperature_high) + + @property + def operation_list(self) -> list[str]: + """Return the list of available operation modes.""" + return [ + STATE_OFF, + *( + OPERATION_MAP_TO_HA[mode] + for mode in self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES + ) + if mode in OPERATION_MAP_TO_HA + ), + ] + + @property + def current_operation(self) -> str | None: + """Return the current operation mode.""" + if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "off": + return STATE_OFF + return OPERATION_MAP_TO_HA.get( + self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.AIR_CONDITIONER_MODE + ) + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.get_attribute_value( + Capability.TEMPERATURE_MEASUREMENT, Attribute.TEMPERATURE + ) + + @property + def target_temperature(self) -> float | None: + """Return the target temperature.""" + return self.get_attribute_value( + Capability.THERMOSTAT_COOLING_SETPOINT, Attribute.COOLING_SETPOINT + ) + + @property + def target_temperature_low(self) -> float: + """Return the minimum temperature.""" + return self.get_attribute_value( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, Attribute.MINIMUM_SETPOINT + ) + + @property + def target_temperature_high(self) -> float: + """Return the maximum temperature.""" + return self.get_attribute_value( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, Attribute.MAXIMUM_SETPOINT + ) + + @property + def is_away_mode_on(self) -> bool: + """Return if away mode is on.""" + return ( + self.get_attribute_value( + Capability.CUSTOM_OUTING_MODE, Attribute.OUTING_MODE + ) + == "on" + ) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + if operation_mode == STATE_OFF: + await self.async_turn_off() + return + if self.current_operation == STATE_OFF: + await self.async_turn_on() + await self.execute_device_command( + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + argument=HA_TO_OPERATION_MAP[operation_mode], + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self.execute_device_command( + Capability.THERMOSTAT_COOLING_SETPOINT, + Command.SET_COOLING_SETPOINT, + argument=kwargs[ATTR_TEMPERATURE], + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater on.""" + await self.execute_device_command( + Capability.SWITCH, + Command.ON, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + await self.execute_device_command( + Capability.SWITCH, + Command.OFF, + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.execute_device_command( + Capability.CUSTOM_OUTING_MODE, + Command.SET_OUTING_MODE, + argument="on", + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.execute_device_command( + Capability.CUSTOM_OUTING_MODE, + Command.SET_OUTING_MODE, + argument="off", + ) diff --git a/tests/components/smartthings/snapshots/test_water_heater.ambr b/tests/components/smartthings/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..88f8bf8f6a7 --- /dev/null +++ b/tests/components/smartthings/snapshots/test_water_heater.ambr @@ -0,0 +1,218 @@ +# serializer version: 1 +# name: test_all_entities[da_ac_ehs_01001][water_heater.heat_pump-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 69, + 'min_temp': 38, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'power', + 'force', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.heat_pump', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][water_heater.heat_pump-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'away_mode': 'off', + 'current_temperature': 57, + 'friendly_name': 'Heat pump', + 'max_temp': 69, + 'min_temp': 38, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'power', + 'force', + ]), + 'operation_mode': 'off', + 'supported_features': , + 'target_temp_high': 69, + 'target_temp_low': 38, + 'temperature': 56, + }), + 'context': , + 'entity_id': 'water_heater.heat_pump', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub][water_heater.eco_heating_system-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 60.0, + 'min_temp': 40, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'force', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.eco_heating_system', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub][water_heater.eco_heating_system-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'away_mode': 'off', + 'current_temperature': 54.3, + 'friendly_name': 'Eco Heating System', + 'max_temp': 60.0, + 'min_temp': 40, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'force', + ]), + 'operation_mode': 'off', + 'supported_features': , + 'target_temp_high': 55, + 'target_temp_low': 40, + 'temperature': 48, + }), + 'context': , + 'entity_id': 'water_heater.eco_heating_system', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][water_heater.warmepumpe-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 60.0, + 'min_temp': 40, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'power', + 'force', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.warmepumpe', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'water_heater', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][water_heater.warmepumpe-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'away_mode': 'off', + 'current_temperature': 49.6, + 'friendly_name': 'Wärmepumpe', + 'max_temp': 60.0, + 'min_temp': 40, + 'operation_list': list([ + 'off', + 'eco', + 'standard', + 'power', + 'force', + ]), + 'operation_mode': 'standard', + 'supported_features': , + 'target_temp_high': 57, + 'target_temp_low': 40, + 'temperature': 52, + }), + 'context': , + 'entity_id': 'water_heater.warmepumpe', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'standard', + }) +# --- diff --git a/tests/components/smartthings/test_water_heater.py b/tests/components/smartthings/test_water_heater.py new file mode 100644 index 00000000000..54df6aa12e6 --- /dev/null +++ b/tests/components/smartthings/test_water_heater.py @@ -0,0 +1,542 @@ +"""Test for the SmartThings water heater platform.""" + +from unittest.mock import AsyncMock, call + +from pysmartthings import Attribute, Capability, Command +from pysmartthings.models import HealthStatus +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.smartthings import MAIN +from homeassistant.components.water_heater import ( + ATTR_AWAY_MODE, + ATTR_CURRENT_TEMPERATURE, + ATTR_OPERATION_LIST, + ATTR_OPERATION_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + WaterHeaterEntityFeature, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + setup_integration, + snapshot_smartthings_entities, + trigger_health_update, + trigger_update, +) + +from tests.common import MockConfigEntry + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + snapshot_smartthings_entities( + hass, entity_registry, snapshot, Platform.WATER_HEATER + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +@pytest.mark.parametrize( + ("operation_mode", "argument"), + [ + (STATE_ECO, "eco"), + ("standard", "std"), + ("force", "force"), + ("power", "power"), + ], +) +async def test_set_operation_mode( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + operation_mode: str, + argument: str, +) -> None: + """Test set operation mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + ATTR_OPERATION_MODE: operation_mode, + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + MAIN, + argument=argument, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_set_operation_mode_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test set operation mode to off.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + ATTR_OPERATION_MODE: STATE_OFF, + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + Command.OFF, + MAIN, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000001_sub"]) +async def test_set_operation_mode_from_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test set operation mode.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("water_heater.eco_heating_system").state == STATE_OFF + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.eco_heating_system", + ATTR_OPERATION_MODE: STATE_ECO, + }, + blocking=True, + ) + assert devices.execute_device_command.mock_calls == [ + call( + "1f98ebd0-ac48-d802-7f62-000001200100", + Capability.SWITCH, + Command.ON, + MAIN, + ), + call( + "1f98ebd0-ac48-d802-7f62-000001200100", + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + MAIN, + argument="eco", + ), + ] + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_set_operation_to_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test set operation mode to off.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + ATTR_OPERATION_MODE: STATE_OFF, + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + Command.OFF, + MAIN, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +@pytest.mark.parametrize( + ("service", "command"), + [ + (SERVICE_TURN_ON, Command.ON), + (SERVICE_TURN_OFF, Command.OFF), + ], +) +async def test_turn_on_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + command: Command, +) -> None: + """Test turn on and off.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + service, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + command, + MAIN, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_set_temperature( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test set operation mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + ATTR_TEMPERATURE: 56, + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.THERMOSTAT_COOLING_SETPOINT, + Command.SET_COOLING_SETPOINT, + MAIN, + argument=56, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +@pytest.mark.parametrize( + ("on", "argument"), + [ + (True, "on"), + (False, "off"), + ], +) +async def test_away_mode( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + on: bool, + argument: str, +) -> None: + """Test set away mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_AWAY_MODE, + { + ATTR_ENTITY_ID: "water_heater.warmepumpe", + ATTR_AWAY_MODE: on, + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.CUSTOM_OUTING_MODE, + Command.SET_OUTING_MODE, + MAIN, + argument=argument, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_operation_list_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("water_heater.warmepumpe").attributes[ + ATTR_OPERATION_LIST + ] == [ + STATE_OFF, + STATE_ECO, + "standard", + "power", + "force", + ] + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Attribute.SUPPORTED_AC_MODES, + ["eco", "force", "power"], + ) + + assert hass.states.get("water_heater.warmepumpe").attributes[ + ATTR_OPERATION_LIST + ] == [ + STATE_OFF, + STATE_ECO, + "force", + "power", + ] + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_current_operation_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("water_heater.warmepumpe").state == "standard" + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Attribute.AIR_CONDITIONER_MODE, + "eco", + ) + + assert hass.states.get("water_heater.warmepumpe").state == STATE_ECO + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_switch_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("water_heater.warmepumpe") + assert state.state == "standard" + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + ) + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + Attribute.SWITCH, + "off", + ) + + state = hass.states.get("water_heater.warmepumpe") + assert state.state == STATE_OFF + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_current_temperature_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_CURRENT_TEMPERATURE] + == 49.6 + ) + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.TEMPERATURE_MEASUREMENT, + Attribute.TEMPERATURE, + 50.0, + ) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_CURRENT_TEMPERATURE] + == 50.0 + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_target_temperature_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_TEMPERATURE] == 52.0 + ) + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.THERMOSTAT_COOLING_SETPOINT, + Attribute.COOLING_SETPOINT, + 50.0, + ) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_TEMPERATURE] == 50.0 + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +@pytest.mark.parametrize( + ("attribute", "old_value", "state_attribute"), + [ + (Attribute.MINIMUM_SETPOINT, 40, ATTR_TARGET_TEMP_LOW), + (Attribute.MAXIMUM_SETPOINT, 57, ATTR_TARGET_TEMP_HIGH), + ], +) +async def test_target_temperature_bound_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + attribute: Attribute, + old_value: float, + state_attribute: str, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[state_attribute] + == old_value + ) + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + attribute, + 50.0, + ) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[state_attribute] == 50.0 + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_away_mode_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_AWAY_MODE] + == STATE_OFF + ) + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.CUSTOM_OUTING_MODE, + Attribute.OUTING_MODE, + "on", + ) + + assert ( + hass.states.get("water_heater.warmepumpe").attributes[ATTR_AWAY_MODE] + == STATE_ON + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_availability( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test availability.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("water_heater.warmepumpe").state == "standard" + + await trigger_health_update( + hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.OFFLINE + ) + + assert hass.states.get("water_heater.warmepumpe").state == STATE_UNAVAILABLE + + await trigger_health_update( + hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.ONLINE + ) + + assert hass.states.get("water_heater.warmepumpe").state == "standard" + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_availability_at_start( + hass: HomeAssistant, + unavailable_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unavailable at boot.""" + await setup_integration(hass, mock_config_entry) + assert hass.states.get("water_heater.warmepumpe").state == STATE_UNAVAILABLE From ace12958d1d6b95dbdae5776a964c4e8d34038ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 17:48:02 +0200 Subject: [PATCH 0497/1175] Use runtime_data in iqvia (#144984) --- homeassistant/components/iqvia/__init__.py | 17 +++++------------ homeassistant/components/iqvia/coordinator.py | 6 ++++-- homeassistant/components/iqvia/diagnostics.py | 13 ++++--------- homeassistant/components/iqvia/entity.py | 15 +++++---------- homeassistant/components/iqvia/sensor.py | 6 +++--- tests/components/iqvia/test_config_flow.py | 2 +- 6 files changed, 22 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 049c23965b1..ad8b78bf9e3 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -6,7 +6,6 @@ import asyncio from pyiqvia import Client -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -14,7 +13,6 @@ from homeassistant.helpers import aiohttp_client from .const import ( CONF_ZIP_CODE, - DOMAIN, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, @@ -23,14 +21,14 @@ from .const import ( TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, ) -from .coordinator import IqviaUpdateCoordinator +from .coordinator import IqviaConfigEntry, IqviaUpdateCoordinator DEFAULT_ATTRIBUTION = "Data provided by IQVIA™" PLATFORMS = [Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IqviaConfigEntry) -> bool: """Set up IQVIA as config entry.""" if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -75,18 +73,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Once we've successfully authenticated, we re-enable client request retries: client.enable_request_retries() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinators + entry.runtime_data = coordinators await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IqviaConfigEntry) -> bool: """Unload an OpenUV config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/iqvia/coordinator.py b/homeassistant/components/iqvia/coordinator.py index 420cbadbefa..ef926d1112d 100644 --- a/homeassistant/components/iqvia/coordinator.py +++ b/homeassistant/components/iqvia/coordinator.py @@ -16,16 +16,18 @@ from .const import LOGGER DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) +type IqviaConfigEntry = ConfigEntry[dict[str, IqviaUpdateCoordinator]] + class IqviaUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Custom DataUpdateCoordinator for IQVIA.""" - config_entry: ConfigEntry + config_entry: IqviaConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IqviaConfigEntry, name: str, update_method: Callable[[], Coroutine[Any, Any, dict[str, Any]]], ) -> None: diff --git a/homeassistant/components/iqvia/diagnostics.py b/homeassistant/components/iqvia/diagnostics.py index 64827f183ff..953d42eafc2 100644 --- a/homeassistant/components/iqvia/diagnostics.py +++ b/homeassistant/components/iqvia/diagnostics.py @@ -5,12 +5,11 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_ZIP_CODE, DOMAIN +from .const import CONF_ZIP_CODE +from .coordinator import IqviaConfigEntry CONF_CITY = "City" CONF_DISPLAY_LOCATION = "DisplayLocation" @@ -33,19 +32,15 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: IqviaConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: dict[str, DataUpdateCoordinator[dict[str, Any]]] = hass.data[DOMAIN][ - entry.entry_id - ] - return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "data": async_redact_data( { data_type: coordinator.data - for data_type, coordinator in coordinators.items() + for data_type, coordinator in entry.runtime_data.items() }, TO_REDACT, ), diff --git a/homeassistant/components/iqvia/entity.py b/homeassistant/components/iqvia/entity.py index e77c0f7e32a..1964a7cb039 100644 --- a/homeassistant/components/iqvia/entity.py +++ b/homeassistant/components/iqvia/entity.py @@ -2,28 +2,23 @@ from __future__ import annotations -from typing import Any - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.entity import EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_ZIP_CODE, DOMAIN, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK +from .coordinator import IqviaConfigEntry, IqviaUpdateCoordinator -class IQVIAEntity(CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]]): +class IQVIAEntity(CoordinatorEntity[IqviaUpdateCoordinator]): """Define a base IQVIA entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, Any]], - entry: ConfigEntry, + coordinator: IqviaUpdateCoordinator, + entry: IqviaConfigEntry, description: EntityDescription, ) -> None: """Initialize.""" diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 64492c634e9..c0401b27368 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_STATE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -32,6 +31,7 @@ from .const import ( TYPE_DISEASE_INDEX, TYPE_DISEASE_TODAY, ) +from .coordinator import IqviaConfigEntry from .entity import IQVIAEntity ATTR_ALLERGEN_AMOUNT = "allergen_amount" @@ -128,13 +128,13 @@ INDEX_SENSOR_DESCRIPTIONS = ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IqviaConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up IQVIA sensors based on a config entry.""" sensors: list[ForecastSensor | IndexSensor] = [ ForecastSensor( - hass.data[DOMAIN][entry.entry_id][ + entry.runtime_data[ API_CATEGORY_MAPPING.get(description.key, description.key) ], entry, diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 22f473a3fb5..9a973ebe49c 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -4,7 +4,7 @@ from typing import Any import pytest -from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN +from homeassistant.components.iqvia.const import CONF_ZIP_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType From 3a58d974964edca92d9732deee7ca31f05b7ba13 Mon Sep 17 00:00:00 2001 From: alorente Date: Thu, 15 May 2025 19:27:16 +0200 Subject: [PATCH 0498/1175] Fix wrong UNIT_CLASS for reactive energy converter (#144982) --- homeassistant/components/recorder/websocket_api.py | 2 ++ homeassistant/util/unit_conversion.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index f4058943971..76a75a5849e 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -30,6 +30,7 @@ from homeassistant.util.unit_conversion import ( MassConverter, PowerConverter, PressureConverter, + ReactiveEnergyConverter, SpeedConverter, TemperatureConverter, UnitlessRatioConverter, @@ -73,6 +74,7 @@ UNIT_SCHEMA = vol.Schema( vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS), vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 05c6d2f381d..0355aa96aca 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -433,7 +433,7 @@ class PressureConverter(BaseUnitConverter): class ReactiveEnergyConverter(BaseUnitConverter): """Utility to convert reactive energy values.""" - UNIT_CLASS = "energy" + UNIT_CLASS = "reactive_energy" _UNIT_CONVERSION: dict[str | None, float] = { UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR: 1, UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR: 1 / 1e3, From 50e6c83dd87f5897203389f9579d2a8804c0ffb5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 19:53:12 +0200 Subject: [PATCH 0499/1175] Fix missing mock in hue v2 bridge tests (#144947) --- tests/components/hue/conftest.py | 2 ++ tests/components/hue/test_diagnostics.py | 7 ++++++- tests/components/hue/test_services.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 7fc6c5ae33f..e6ade431ee6 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -254,6 +254,8 @@ async def setup_bridge( with patch("homeassistant.components.hue.HueBridge", return_value=mock_bridge): await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ConfigEntryState.LOADED + async def setup_platform( hass: HomeAssistant, diff --git a/tests/components/hue/test_diagnostics.py b/tests/components/hue/test_diagnostics.py index 49681601ebf..a9171d2a12a 100644 --- a/tests/components/hue/test_diagnostics.py +++ b/tests/components/hue/test_diagnostics.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonArrayType from .conftest import setup_platform @@ -21,9 +22,13 @@ async def test_diagnostics_v1( async def test_diagnostics_v2( - hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_bridge_v2: Mock + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_bridge_v2: Mock, + v2_resources_test_data: JsonArrayType, ) -> None: """Test diagnostics v2.""" + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) mock_bridge_v2.api.get_diagnostics.return_value = {"hello": "world"} await setup_platform(hass, mock_bridge_v2, []) config_entry = hass.config_entries.async_entries("hue")[0] diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py index 26a4cab8261..2fd8379a73a 100644 --- a/tests/components/hue/test_services.py +++ b/tests/components/hue/test_services.py @@ -9,6 +9,7 @@ from homeassistant.components.hue.const import ( CONF_ALLOW_UNREACHABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonArrayType from .conftest import setup_bridge, setup_component @@ -190,6 +191,7 @@ async def test_hue_multi_bridge_activate_scene_all_respond( mock_bridge_v2: Mock, mock_config_entry_v1: MockConfigEntry, mock_config_entry_v2: MockConfigEntry, + v2_resources_test_data: JsonArrayType, ) -> None: """Test that makes multiple bridges successfully activate a scene.""" await setup_component(hass) @@ -198,6 +200,8 @@ async def test_hue_multi_bridge_activate_scene_all_respond( mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) @@ -224,6 +228,7 @@ async def test_hue_multi_bridge_activate_scene_one_responds( mock_bridge_v2: Mock, mock_config_entry_v1: MockConfigEntry, mock_config_entry_v2: MockConfigEntry, + v2_resources_test_data: JsonArrayType, ) -> None: """Test that makes only one bridge successfully activate a scene.""" await setup_component(hass) @@ -232,6 +237,8 @@ async def test_hue_multi_bridge_activate_scene_one_responds( mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) @@ -257,6 +264,7 @@ async def test_hue_multi_bridge_activate_scene_zero_responds( mock_bridge_v2: Mock, mock_config_entry_v1: MockConfigEntry, mock_config_entry_v2: MockConfigEntry, + v2_resources_test_data: JsonArrayType, ) -> None: """Test that makes no bridge successfully activate a scene.""" await setup_component(hass) @@ -264,6 +272,8 @@ async def test_hue_multi_bridge_activate_scene_zero_responds( mock_api_v1.mock_group_responses.append(GROUP_RESPONSE) mock_api_v1.mock_scene_responses.append(SCENE_RESPONSE) + await mock_bridge_v2.api.load_test_data(v2_resources_test_data) + await setup_bridge(hass, mock_bridge_v1, mock_config_entry_v1) await setup_bridge(hass, mock_bridge_v2, mock_config_entry_v2) From d195726ed2dbdc1c5aaef6fa3adcb17aa683c6ac Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 15 May 2025 20:50:48 +0200 Subject: [PATCH 0500/1175] Use runtime_data in isy994 (#144961) --- homeassistant/components/isy994/__init__.py | 40 ++++++------------- .../components/isy994/binary_sensor.py | 10 ++--- homeassistant/components/isy994/button.py | 9 ++--- homeassistant/components/isy994/climate.py | 10 ++--- .../components/isy994/config_flow.py | 6 +-- homeassistant/components/isy994/cover.py | 12 +++--- homeassistant/components/isy994/fan.py | 12 +++--- homeassistant/components/isy994/light.py | 11 +++-- homeassistant/components/isy994/lock.py | 11 ++--- homeassistant/components/isy994/models.py | 3 ++ homeassistant/components/isy994/number.py | 14 ++----- homeassistant/components/isy994/select.py | 9 ++--- homeassistant/components/isy994/sensor.py | 10 ++--- homeassistant/components/isy994/services.py | 12 ++---- homeassistant/components/isy994/switch.py | 8 ++-- .../components/isy994/system_health.py | 14 ++----- homeassistant/components/isy994/util.py | 13 +++--- tests/components/isy994/test_system_health.py | 17 +++++--- 18 files changed, 90 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 1e227b08206..bed86b2d0fe 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -10,7 +10,6 @@ from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParse from pyisy.constants import CONFIG_NETWORKING, CONFIG_PORTAL import voluptuous as vol -from homeassistant import config_entries from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -46,7 +45,7 @@ from .const import ( SCHEME_HTTPS, ) from .helpers import _categorize_nodes, _categorize_programs -from .models import IsyData +from .models import IsyConfigEntry, IsyData from .services import async_setup_services, async_unload_services from .util import _async_cleanup_registry_entries @@ -56,13 +55,8 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool: """Set up the ISY 994 integration.""" - hass.data.setdefault(DOMAIN, {}) - isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData() - isy_config = entry.data isy_options = entry.options @@ -127,6 +121,7 @@ async def async_setup_entry( f"Invalid response ISY, device is likely still starting: {err}" ) from err + isy_data = entry.runtime_data = IsyData() _categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(isy_data, isy.programs) # Gather ISY Variables to be added. @@ -156,7 +151,7 @@ async def async_setup_entry( await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Clean-up any old entities that we no longer provide. - _async_cleanup_registry_entries(hass, entry.entry_id) + _async_cleanup_registry_entries(hass, entry) @callback def _async_stop_auto_update(event: Event) -> None: @@ -178,16 +173,14 @@ async def async_setup_entry( return True -async def _async_update_listener( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: +async def _async_update_listener(hass: HomeAssistant, entry: IsyConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) @callback def _async_get_or_create_isy_device_in_registry( - hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY + hass: HomeAssistant, entry: IsyConfigEntry, isy: ISY ) -> None: device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -221,34 +214,25 @@ def _create_service_device_info(isy: ISY, name: str, unique_id: str) -> DeviceIn ) -async def async_unload_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - - isy = isy_data.root - _LOGGER.debug("ISY Stopping Event Stream and automatic updates") - isy.websocket.stop() + entry.runtime_data.root.websocket.stop() - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - async_unload_services(hass) + if not hass.config_entries.async_loaded_entries(DOMAIN): + async_unload_services(hass) return unload_ok async def async_remove_config_entry_device( hass: HomeAssistant, - config_entry: config_entries.ConfigEntry, + config_entry: IsyConfigEntry, device_entry: dr.DeviceEntry, ) -> bool: """Remove ISY config entry from a device.""" - isy_data = hass.data[DOMAIN][config_entry.entry_id] return not device_entry.identifiers.intersection( - (DOMAIN, unique_id) for unique_id in isy_data.devices + (DOMAIN, unique_id) for unique_id in config_entry.runtime_data.devices ) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 8c9ce7dcc12..d452b5bacef 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -19,7 +19,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -31,7 +30,6 @@ from .const import ( _LOGGER, BINARY_SENSOR_DEVICE_TYPES_ISY, BINARY_SENSOR_DEVICE_TYPES_ZWAVE, - DOMAIN, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_DUSK_DAWN, @@ -44,7 +42,7 @@ from .const import ( TYPE_INSTEON_MOTION, ) from .entity import ISYNodeEntity, ISYProgramEntity -from .models import IsyData +from .models import IsyConfigEntry DEVICE_PARENT_REQUIRED = [ BinarySensorDeviceClass.OPENING, @@ -55,7 +53,7 @@ DEVICE_PARENT_REQUIRED = [ async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY binary sensor platform.""" @@ -82,8 +80,8 @@ async def async_setup_entry( | ISYBinarySensorProgramEntity ) - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices for node in isy_data.nodes[Platform.BINARY_SENSOR]: assert isinstance(node, Node) device_info = devices.get(node.primary_node) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index a895312c45a..cfb077c7dc0 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -15,24 +15,23 @@ from pyisy.networking import NetworkCommand from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import CONF_NETWORK, DOMAIN -from .models import IsyData +from .models import IsyConfigEntry async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up ISY/IoX button from config entry.""" - isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] - isy: ISY = isy_data.root + isy_data = config_entry.runtime_data + isy = isy_data.root device_info = isy_data.devices entities: list[ ISYNodeQueryButtonEntity diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 57c1b6aa79d..ce39cae5428 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_TENTHS, @@ -42,7 +41,6 @@ from homeassistant.util.enum import try_parse_enum from .const import ( _LOGGER, - DOMAIN, HA_FAN_TO_ISY, HA_HVAC_TO_ISY, ISY_HVAC_MODES, @@ -57,18 +55,18 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass -from .models import IsyData +from .models import IsyConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY thermostat platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices async_add_entities( ISYThermostatEntity(node, devices.get(node.primary_node)) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index b44096e2ccd..2acebee8599 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -16,7 +16,6 @@ import voluptuous as vol from homeassistant.config_entries import ( SOURCE_IGNORE, - ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -54,6 +53,7 @@ from .const import ( SCHEME_HTTPS, UDN_UUID_PREFIX, ) +from .models import IsyConfigEntry _LOGGER = logging.getLogger(__name__) @@ -137,12 +137,12 @@ class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the ISY/IoX config flow.""" self.discovered_conf: dict[str, str] = {} - self._existing_entry: ConfigEntry | None = None + self._existing_entry: IsyConfigEntry | None = None @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: IsyConfigEntry, ) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler() diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 6a660aaaf6f..f940fe55332 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -11,25 +11,23 @@ from homeassistant.components.cover import ( CoverEntity, CoverEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE +from .const import _LOGGER, UOM_8_BIT_RANGE from .entity import ISYNodeEntity, ISYProgramEntity -from .models import IsyData +from .models import IsyConfigEntry async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY cover platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [ ISYCoverEntity(node, devices.get(node.primary_node)) for node in isy_data.nodes[Platform.COVER] diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index aa6059abf49..02542462788 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -8,10 +8,8 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.percentage import ( percentage_to_ranged_value, @@ -19,21 +17,21 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range -from .const import _LOGGER, DOMAIN +from .const import _LOGGER from .entity import ISYNodeEntity, ISYProgramEntity -from .models import IsyData +from .models import IsyConfigEntry SPEED_RANGE = (1, 255) # off is not included async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY fan platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices entities: list[ISYFanEntity | ISYFanProgramEntity] = [ ISYFanEntity(node, devices.get(node.primary_node)) for node in isy_data.nodes[Platform.FAN] diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 29df8398f97..d3edc25c3e2 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -9,28 +9,27 @@ from pyisy.helpers import NodeProperty from pyisy.nodes import Node from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE +from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, UOM_PERCENTAGE from .entity import ISYNodeEntity -from .models import IsyData +from .models import IsyConfigEntry ATTR_LAST_BRIGHTNESS = "last_brightness" async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY light platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index d6866a8e00c..056d1d0d492 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -7,19 +7,16 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, async_get_current_platform, ) -from .const import DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity -from .models import IsyData +from .models import IsyConfigEntry from .services import ( SERVICE_DELETE_USER_CODE_SCHEMA, SERVICE_DELETE_ZWAVE_LOCK_USER_CODE, @@ -49,12 +46,12 @@ def async_setup_lock_services(hass: HomeAssistant) -> None: async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY lock platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] - devices: dict[str, DeviceInfo] = isy_data.devices + isy_data = entry.runtime_data + devices = isy_data.devices entities: list[ISYLockEntity | ISYLockProgramEntity] = [ ISYLockEntity(node, devices.get(node.primary_node)) for node in isy_data.nodes[Platform.LOCK] diff --git a/homeassistant/components/isy994/models.py b/homeassistant/components/isy994/models.py index 5b599df9458..4fc7b96fcd5 100644 --- a/homeassistant/components/isy994/models.py +++ b/homeassistant/components/isy994/models.py @@ -12,6 +12,7 @@ from pyisy.nodes import Group, Node from pyisy.programs import Program from pyisy.variables import Variable +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.helpers.device_registry import DeviceInfo @@ -24,6 +25,8 @@ from .const import ( VARIABLE_PLATFORMS, ) +type IsyConfigEntry = ConfigEntry[IsyData] + @dataclass class IsyData: diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index fc30e6296d4..c5797491e31 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -26,7 +26,6 @@ from homeassistant.components.number import ( NumberMode, RestoreNumber, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_VARIABLES, PERCENTAGE, @@ -44,15 +43,10 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import ( - CONF_VAR_SENSOR_STRING, - DEFAULT_VAR_SENSOR_STRING, - DOMAIN, - UOM_8_BIT_RANGE, -) +from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, UOM_8_BIT_RANGE from .entity import ISYAuxControlEntity from .helpers import convert_isy_value_to_hass -from .models import IsyData +from .models import IsyConfigEntry ISY_MAX_SIZE = (2**32) / 2 ON_RANGE = (1, 255) # Off is not included @@ -79,11 +73,11 @@ BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up ISY/IoX number entities from config entry.""" - isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] + isy_data = config_entry.runtime_data device_info = isy_data.devices entities: list[ ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index 868c96375bb..ce5e224bc88 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -23,7 +23,6 @@ from pyisy.helpers import EventListener, NodeProperty from pyisy.nodes import Node, NodeChangedEvent from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -37,9 +36,9 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import _LOGGER, DOMAIN, UOM_INDEX +from .const import _LOGGER, UOM_INDEX from .entity import ISYAuxControlEntity -from .models import IsyData +from .models import IsyConfigEntry def time_string(i: int) -> str: @@ -55,11 +54,11 @@ BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up ISY/IoX select entities from config entry.""" - isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] + isy_data = config_entry.runtime_data device_info = isy_data.devices entities: list[ ISYAuxControlIndexSelectEntity diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 2d27f4602c6..6e0b5a89637 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -29,7 +29,6 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -37,7 +36,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( _LOGGER, - DOMAIN, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, UOM_INDEX, @@ -46,7 +44,7 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass -from .models import IsyData +from .models import IsyConfigEntry # Disable general purpose and redundant sensors by default AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] @@ -109,13 +107,13 @@ ISY_CONTROL_TO_ENTITY_CATEGORY = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY sensor platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] + isy_data = entry.runtime_data entities: list[ISYSensorEntity] = [] - devices: dict[str, DeviceInfo] = isy_data.devices + devices = isy_data.devices for node in isy_data.nodes[Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 24cfa9aefb1..39f72a5cc2c 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -21,7 +21,7 @@ from homeassistant.helpers.service import entity_service_call from homeassistant.helpers.typing import VolDictType from .const import _LOGGER, DOMAIN -from .models import IsyData +from .models import IsyConfigEntry # Common Services for All Platforms: SERVICE_SEND_PROGRAM_COMMAND = "send_program_command" @@ -149,9 +149,9 @@ def async_setup_services(hass: HomeAssistant) -> None: command = service.data[CONF_COMMAND] isy_name = service.data.get(CONF_ISY) - for config_entry_id in hass.data[DOMAIN]: - isy_data: IsyData = hass.data[DOMAIN][config_entry_id] - isy = isy_data.root + config_entry: IsyConfigEntry + for config_entry in hass.config_entries.async_loaded_entries(DOMAIN): + isy = config_entry.runtime_data.root if isy_name and isy_name != isy.conf["name"]: continue program = None @@ -235,10 +235,6 @@ def async_setup_services(hass: HomeAssistant) -> None: @callback def async_unload_services(hass: HomeAssistant) -> None: """Unload services for the ISY integration.""" - if hass.data[DOMAIN]: - # There is still another config entry for this domain, don't remove services. - return - existing_services = hass.services.async_services_for_domain(DOMAIN) if not existing_services or SERVICE_SEND_PROGRAM_COMMAND not in existing_services: return diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index d5c8a23cbea..f44613317c5 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -20,16 +20,14 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN from .entity import ISYAuxControlEntity, ISYNodeEntity, ISYProgramEntity -from .models import IsyData +from .models import IsyConfigEntry @dataclass(frozen=True) @@ -43,11 +41,11 @@ class ISYSwitchEntityDescription(SwitchEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IsyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the ISY switch platform.""" - isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] + isy_data = entry.runtime_data entities: list[ ISYSwitchProgramEntity | ISYSwitchEntity | ISYEnableSwitchEntity ] = [] diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index dfc45c267dd..9c5a04ba34a 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -4,15 +4,12 @@ from __future__ import annotations from typing import Any -from pyisy import ISY - from homeassistant.components import system_health -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback from .const import DOMAIN, ISY_URL_POSTFIX -from .models import IsyData +from .models import IsyConfigEntry @callback @@ -27,14 +24,9 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" health_info = {} - config_entry_id = next( - iter(hass.data[DOMAIN]) - ) # Only first ISY is supported for now - isy_data: IsyData = hass.data[DOMAIN][config_entry_id] - isy: ISY = isy_data.root + entry: IsyConfigEntry = hass.config_entries.async_loaded_entries(DOMAIN)[0] + isy = entry.runtime_data.root - entry = hass.config_entries.async_get_entry(config_entry_id) - assert isinstance(entry, ConfigEntry) health_info["host_reachable"] = await system_health.async_check_can_reach_url( hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}" ) diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index ca5c5ea46a9..87cb450d08b 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -5,16 +5,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from .const import _LOGGER, DOMAIN +from .const import _LOGGER +from .models import IsyConfigEntry @callback -def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None: +def _async_cleanup_registry_entries(hass: HomeAssistant, entry: IsyConfigEntry) -> None: """Remove extra entities that are no longer part of the integration.""" entity_registry = er.async_get(hass) - isy_data = hass.data[DOMAIN][entry_id] + isy_data = entry.runtime_data - existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id) + existing_entries = er.async_entries_for_config_entry( + entity_registry, entry.entry_id + ) entities = { (entity.domain, entity.unique_id): entity.entity_id for entity in existing_entries @@ -31,5 +34,5 @@ def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None: _LOGGER.debug( ("Cleaning up ISY entities: removed %s extra entities for config entry %s"), len(extra_entities), - entry_id, + entry.entry_id, ) diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index 5f472189513..0a6e4b403b8 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -6,6 +6,7 @@ from unittest.mock import Mock from aiohttp import ClientError from homeassistant.components.isy994.const import DOMAIN, ISY_URL_POSTFIX +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -30,12 +31,14 @@ async def test_system_health( assert await async_setup_component(hass, "system_health", {}) await hass.async_block_till_done() - MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, entry_id=MOCK_ENTRY_ID, data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, unique_id=MOCK_UUID, - ).add_to_hass(hass) + state=ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) isy_data = Mock( root=Mock( @@ -46,7 +49,7 @@ async def test_system_health( ), ) ) - hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} + entry.runtime_data = isy_data info = await get_system_health_info(hass, DOMAIN) @@ -70,12 +73,14 @@ async def test_system_health_failed_connect( assert await async_setup_component(hass, "system_health", {}) await hass.async_block_till_done() - MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, entry_id=MOCK_ENTRY_ID, data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, unique_id=MOCK_UUID, - ).add_to_hass(hass) + state=ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) isy_data = Mock( root=Mock( @@ -86,7 +91,7 @@ async def test_system_health_failed_connect( ), ) ) - hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} + entry.runtime_data = isy_data info = await get_system_health_info(hass, DOMAIN) From cc62943835d2638e461ca5dc4f31089aa87d4126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Odd=20Str=C3=A5b=C3=B8?= Date: Fri, 16 May 2025 01:57:16 +0200 Subject: [PATCH 0501/1175] Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970) --- homeassistant/components/esphome/entity.py | 6 ++- tests/components/esphome/test_entity.py | 62 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 8eded610194..15ea54422d4 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -239,7 +239,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): self._states = cast(dict[int, _StateT], entry_data.state[state_type]) assert entry_data.device_info is not None device_info = entry_data.device_info - self._device_info = device_info self._on_entry_data_changed() self._key = entity_info.key self._state_type = state_type @@ -327,6 +326,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): @callback def _on_entry_data_changed(self) -> None: entry_data = self._entry_data + # Update the device info since it can change + # when the device is reconnected + if TYPE_CHECKING: + assert entry_data.device_info is not None + self._device_info = entry_data.device_info self._api_version = entry_data.api_version self._client = entry_data.client if self._device_info.has_deep_sleep: diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index ee6e6b6785f..36185efeb72 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -1,6 +1,7 @@ """Test ESPHome binary sensors.""" import asyncio +from dataclasses import asdict from typing import Any from unittest.mock import AsyncMock @@ -8,6 +9,7 @@ from aioesphomeapi import ( APIClient, BinarySensorInfo, BinarySensorState, + DeviceInfo, SensorInfo, SensorState, build_unique_id, @@ -665,3 +667,63 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage( ) state = hass.states.get("binary_sensor.user_named") assert state is not None + + +async def test_deep_sleep_added_after_setup( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test deep sleep added after setup.""" + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[ + BinarySensorInfo( + object_id="test", + key=1, + name="test", + unique_id="test", + ), + ], + user_service=[], + states=[ + BinarySensorState(key=1, state=True, missing_state=False), + ], + device_info={"has_deep_sleep": False}, + ) + + entity_id = "binary_sensor.test_test" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + + # No deep sleep, should be unavailable + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await mock_device.mock_connect() + + # reconnect, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + await mock_device.mock_disconnect(expected_disconnect=True) + new_device_info = DeviceInfo( + **{**asdict(mock_device.device_info), "has_deep_sleep": True} + ) + mock_device.client.device_info = AsyncMock(return_value=new_device_info) + mock_device.device_info = new_device_info + + await mock_device.mock_connect() + + # Now disconnect that deep sleep is set in device info + await mock_device.mock_disconnect(expected_disconnect=True) + + # Deep sleep, should be available + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON From 52e8196d0a49bd3d3fb7e52d006315b452e3aba2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 16 May 2025 02:34:55 +0200 Subject: [PATCH 0502/1175] Mark Reolink doorbell visitor sensor as always available (#145002) Mark doorbell visitor sensor as always available --- homeassistant/components/reolink/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 95c5f1982c3..2d08e42a6c8 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -115,6 +115,7 @@ BINARY_PUSH_SENSORS = ( translation_key="visitor", value=lambda api, ch: api.visitor_detected(ch), supported=lambda api, ch: api.is_doorbell(ch), + always_available=True, ), ReolinkBinarySensorEntityDescription( key="cry", From e15963b422581167a6e0db787bb87a7cfe15bd8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 08:54:13 +0200 Subject: [PATCH 0503/1175] Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#145023) 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 e0070874882..53de33b99e4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1320,7 +1320,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: fail_ci_if_error: true flags: full-suite @@ -1463,7 +1463,7 @@ jobs: pattern: coverage-* - name: Upload coverage to Codecov if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v5.4.2 + uses: codecov/codecov-action@v5.4.3 with: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} From 270780ef5f0924f856c4979347bda34315949d6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 09:42:24 +0200 Subject: [PATCH 0504/1175] Bump docker/build-push-action from 6.16.0 to 6.17.0 (#145022) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index bd45753d010..9b76f3550fd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -509,7 +509,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build Docker image - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: context: . # So action will not pull the repository again file: ./script/hassfest/docker/Dockerfile @@ -522,7 +522,7 @@ jobs: - name: Push Docker image if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' id: push - uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: context: . # So action will not pull the repository again file: ./script/hassfest/docker/Dockerfile From 9bbc49e8427d83205e418d80cffd36d204a098f4 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Fri, 16 May 2025 20:21:41 +1200 Subject: [PATCH 0505/1175] Add DHCP discovery flow to bosch_alarm (#142250) * Add dhcp discovery * Update homeassistant/components/bosch_alarm/config_flow.py Co-authored-by: Joost Lekkerkerker * put mac address in entry instead of unique id * Update host and mac via dhcp discovery * add mac to connections * Abort dhcp flow if there is already an ongoing flow * apply changes from review * apply change from review * remove outdated test * fix snapshots * apply change from review * clean definition for connections * update quality scale --------- Co-authored-by: Joost Lekkerkerker --- .../components/bosch_alarm/__init__.py | 6 +- .../components/bosch_alarm/config_flow.py | 69 +- .../components/bosch_alarm/manifest.json | 5 + .../components/bosch_alarm/quality_scale.yaml | 4 +- .../components/bosch_alarm/strings.json | 2 + homeassistant/generated/dhcp.py | 4 + tests/components/bosch_alarm/conftest.py | 22 +- .../snapshots/test_alarm_control_panel.ambr | 110 +- .../snapshots/test_binary_sensor.ambr | 2236 ++++++++--------- .../snapshots/test_diagnostics.ambr | 197 +- .../bosch_alarm/snapshots/test_sensor.ambr | 410 +-- .../bosch_alarm/snapshots/test_switch.ambr | 408 +-- .../bosch_alarm/test_config_flow.py | 156 +- 13 files changed, 1926 insertions(+), 1703 deletions(-) diff --git a/homeassistant/components/bosch_alarm/__init__.py b/homeassistant/components/bosch_alarm/__init__.py index 06ec98e91ba..410adbd8d51 100644 --- a/homeassistant/components/bosch_alarm/__init__.py +++ b/homeassistant/components/bosch_alarm/__init__.py @@ -7,10 +7,11 @@ from ssl import SSLError from bosch_alarm_mode2 import Panel from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN @@ -53,8 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) - device_registry = dr.async_get(hass) + mac = entry.data.get(CONF_MAC) + device_registry.async_get_or_create( config_entry_id=entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(), identifiers={(DOMAIN, entry.unique_id or entry.entry_id)}, name=f"Bosch {panel.model}", manufacturer="Bosch Security Systems", diff --git a/homeassistant/components/bosch_alarm/config_flow.py b/homeassistant/components/bosch_alarm/config_flow.py index 9e664e49ca9..71e15f5959a 100644 --- a/homeassistant/components/bosch_alarm/config_flow.py +++ b/homeassistant/components/bosch_alarm/config_flow.py @@ -6,12 +6,13 @@ import asyncio from collections.abc import Mapping import logging import ssl -from typing import Any +from typing import Any, Self from bosch_alarm_mode2 import Panel import voluptuous as vol from homeassistant.config_entries import ( + SOURCE_DHCP, SOURCE_RECONFIGURE, SOURCE_USER, ConfigFlow, @@ -20,11 +21,14 @@ from homeassistant.config_entries import ( from homeassistant.const import ( CONF_CODE, CONF_HOST, + CONF_MAC, CONF_MODEL, CONF_PASSWORD, CONF_PORT, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN @@ -88,6 +92,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): """Init config flow.""" self._data: dict[str, Any] = {} + self.mac: str | None = None + self.host: str | None = None + + def is_matching(self, other_flow: Self) -> bool: + """Return True if other_flow is matching this flow.""" + return self.mac == other_flow.mac or self.host == other_flow.host async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -96,9 +106,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: + self.host = user_input[CONF_HOST] + if self.source == SOURCE_USER: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: # Use load_selector = 0 to fetch the panel model without authentication. - (model, serial) = await try_connect(user_input, 0) + (model, _) = await try_connect(user_input, 0) except ( OSError, ConnectionRefusedError, @@ -129,6 +142,55 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle DHCP discovery.""" + self.mac = format_mac(discovery_info.macaddress) + self.host = discovery_info.ip + if self.hass.config_entries.flow.async_has_matching_flow(self): + return self.async_abort(reason="already_in_progress") + + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_MAC] == self.mac: + result = self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_HOST: discovery_info.ip, + }, + ) + if result: + self.hass.config_entries.async_schedule_reload(entry.entry_id) + return self.async_abort(reason="already_configured") + try: + # Use load_selector = 0 to fetch the panel model without authentication. + (model, _) = await try_connect( + {CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0 + ) + except ( + OSError, + ConnectionRefusedError, + ssl.SSLError, + asyncio.exceptions.TimeoutError, + ): + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + self.context["title_placeholders"] = { + "model": model, + "host": discovery_info.ip, + } + self._data = { + CONF_HOST: discovery_info.ip, + CONF_MAC: self.mac, + CONF_MODEL: model, + CONF_PORT: 7700, + } + + return await self.async_step_auth() + async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -172,7 +234,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): else: if serial_number: await self.async_set_unique_id(str(serial_number)) - if self.source == SOURCE_USER: + if self.source in (SOURCE_USER, SOURCE_DHCP): if serial_number: self._abort_if_unique_id_configured() else: @@ -184,6 +246,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): ) if serial_number: self._abort_if_unique_id_mismatch(reason="device_mismatch") + return self.async_update_reload_and_abort( self._get_reconfigure_entry(), data=self._data, diff --git a/homeassistant/components/bosch_alarm/manifest.json b/homeassistant/components/bosch_alarm/manifest.json index eefcc400ee7..160d6141959 100644 --- a/homeassistant/components/bosch_alarm/manifest.json +++ b/homeassistant/components/bosch_alarm/manifest.json @@ -3,6 +3,11 @@ "name": "Bosch Alarm", "codeowners": ["@mag1024", "@sanjay900"], "config_flow": true, + "dhcp": [ + { + "macaddress": "000463*" + } + ], "documentation": "https://www.home-assistant.io/integrations/bosch_alarm", "integration_type": "device", "iot_class": "local_push", diff --git a/homeassistant/components/bosch_alarm/quality_scale.yaml b/homeassistant/components/bosch_alarm/quality_scale.yaml index 3a64667a407..0ea2b147c4a 100644 --- a/homeassistant/components/bosch_alarm/quality_scale.yaml +++ b/homeassistant/components/bosch_alarm/quality_scale.yaml @@ -46,8 +46,8 @@ rules: # Gold devices: done diagnostics: todo - discovery-update-info: todo - discovery: todo + discovery-update-info: done + discovery: done docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index b9176c41a08..8edc4ba60b8 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{model} ({host})", "step": { "user": { "data": { @@ -42,6 +43,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 94f5e06bf54..20b49919ace 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -108,6 +108,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "bond-*", "macaddress": "F44E38*", }, + { + "domain": "bosch_alarm", + "macaddress": "000463*", + }, { "domain": "broadlink", "registered_devices": True, diff --git a/tests/components/bosch_alarm/conftest.py b/tests/components/bosch_alarm/conftest.py index 3be4ba2c816..283eb158d5c 100644 --- a/tests/components/bosch_alarm/conftest.py +++ b/tests/components/bosch_alarm/conftest.py @@ -13,7 +13,14 @@ from homeassistant.components.bosch_alarm.const import ( CONF_USER_CODE, DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_PASSWORD, + CONF_PORT, +) +from homeassistant.helpers.device_registry import format_mac from tests.common import MockConfigEntry @@ -38,6 +45,12 @@ def extra_config_entry_data( return {CONF_MODEL: model_name} | config_flow_data +@pytest.fixture(params=[None]) +def mac_address(request: pytest.FixtureRequest) -> str | None: + """Return entity mac address.""" + return request.param + + @pytest.fixture def config_flow_data(model: str) -> dict[str, Any]: """Return extra config entry data.""" @@ -63,7 +76,7 @@ def model_name(model: str) -> str | None: @pytest.fixture def serial_number(model: str) -> str | None: """Return extra config entry data.""" - if model == "solution_3000": + if model == "b5512": return "1234567890" return None @@ -183,7 +196,9 @@ def mock_panel( @pytest.fixture def mock_config_entry( - extra_config_entry_data: dict[str, Any], serial_number: str | None + extra_config_entry_data: dict[str, Any], + serial_number: str | None, + mac_address: str | None, ) -> MockConfigEntry: """Mock config entry for bosch alarm.""" return MockConfigEntry( @@ -194,6 +209,7 @@ def mock_config_entry( CONF_HOST: "0.0.0.0", CONF_PORT: 7700, CONF_MODEL: "bosch_alarm_test_data.model", + CONF_MAC: mac_address and format_mac(mac_address), } | extra_config_entry_data, ) diff --git a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr index 76568cef56c..26c67879f7c 100644 --- a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_alarm_control_panel[amax_3000][alarm_control_panel.area1-entry] +# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -33,7 +33,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_alarm_control_panel[amax_3000][alarm_control_panel.area1-state] +# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, @@ -50,58 +50,7 @@ 'state': 'disarmed', }) # --- -# name: test_alarm_control_panel[b5512][alarm_control_panel.area1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'alarm_control_panel', - 'entity_category': None, - 'entity_id': 'alarm_control_panel.area1', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_alarm_control_panel[b5512][alarm_control_panel.area1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'changed_by': None, - 'code_arm_required': False, - 'code_format': None, - 'friendly_name': 'Area1', - 'supported_features': , - }), - 'context': , - 'entity_id': 'alarm_control_panel.area1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'disarmed', - }) -# --- -# name: test_alarm_control_panel[solution_3000][alarm_control_panel.area1-entry] +# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -135,7 +84,58 @@ 'unit_of_measurement': None, }) # --- -# name: test_alarm_control_panel[solution_3000][alarm_control_panel.area1-state] +# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'changed_by': None, + 'code_arm_required': False, + 'code_format': None, + 'friendly_name': 'Area1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'alarm_control_panel.area1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'disarmed', + }) +# --- +# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.area1', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': None, diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr index e5396b662f3..377a9e23426 100644 --- a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_away-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_away-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -33,7 +33,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_away-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_away-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm away', @@ -46,7 +46,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_home-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -80,7 +80,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.area1_area_ready_to_arm_home-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.area1_area_ready_to_arm_home-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm home', @@ -93,7 +93,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bedroom-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bedroom-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -127,7 +127,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bedroom-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bedroom-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bedroom', @@ -140,7 +140,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_ac_failure-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_ac_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -174,7 +174,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_ac_failure-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_ac_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -188,7 +188,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -222,7 +222,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', @@ -236,7 +236,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery_missing-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery_missing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -270,7 +270,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_battery_missing-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_battery_missing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -284,7 +284,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -318,7 +318,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_crc_failure_in_panel_configuration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -332,7 +332,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -366,7 +366,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bosch AMAX 3000 Failure to call RPS since RPS hang up', @@ -379,7 +379,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_overflow-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_overflow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -413,7 +413,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_overflow-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_overflow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -427,7 +427,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -461,7 +461,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_log_threshold_reached-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -475,7 +475,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -509,7 +509,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_phone_line_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', @@ -523,7 +523,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -557,7 +557,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -571,7 +571,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_problem-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -605,7 +605,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_problem-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -619,7 +619,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -653,7 +653,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -667,7 +667,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -701,7 +701,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', @@ -715,7 +715,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.co_detector-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.co_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -749,7 +749,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.co_detector-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.co_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'CO Detector', @@ -762,7 +762,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.door-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -796,7 +796,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.door-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Door', @@ -809,7 +809,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.glassbreak_sensor-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.glassbreak_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -843,7 +843,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.glassbreak_sensor-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.glassbreak_sensor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Glassbreak Sensor', @@ -856,7 +856,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.motion_detector-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.motion_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -890,7 +890,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.motion_detector-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.motion_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Motion Detector', @@ -903,7 +903,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.smoke_detector-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.smoke_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -937,7 +937,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.smoke_detector-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.smoke_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smoke Detector', @@ -950,7 +950,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.window-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.window-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -984,7 +984,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[amax_3000][binary_sensor.window-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.window-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Window', @@ -997,1005 +997,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_away-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', - 'has_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 ready to arm away', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'area_ready_to_arm_away', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_away-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Area ready to arm away', - }), - 'context': , - 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_home-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.area1_area_ready_to_arm_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': 'Area ready to arm home', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'area_ready_to_arm_home', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.area1_area_ready_to_arm_home-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Area ready to arm home', - }), - 'context': , - 'entity_id': 'binary_sensor.area1_area_ready_to_arm_home', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bedroom-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.bedroom', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bedroom-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bedroom', - }), - 'context': , - 'entity_id': 'binary_sensor.bedroom', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_ac_failure-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'AC Failure', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_ac_fail', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_ac_failure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) AC Failure', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'battery', - 'friendly_name': 'Bosch B5512 (US1B) Battery', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery_missing-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', - '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 missing', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_battery_mising', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_battery_missing-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Battery missing', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'CRC failure in panel configuration', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) CRC failure in panel configuration', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Failure to call RPS since RPS hang up', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since RPS hang up', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_overflow-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Log overflow', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_log_overflow', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_overflow-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Log overflow', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Log threshold reached', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_log_threshold', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Log threshold reached', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Phone line failure', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_phone_line_failure', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', - 'friendly_name': 'Bosch B5512 (US1B) Phone line failure', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Point bus failure since RPS hang up', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Point bus failure since RPS hang up', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_problem-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Problem', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_problem-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Problem', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'SDI failure since RPS hang up', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) SDI failure since RPS hang up', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'User code tamper since RPS hang up', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) User code tamper since RPS hang up', - }), - 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.co_detector-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.co_detector', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.co_detector-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'CO Detector', - }), - 'context': , - 'entity_id': 'binary_sensor.co_detector', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.door', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Door', - }), - 'context': , - 'entity_id': 'binary_sensor.door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.glassbreak_sensor-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.glassbreak_sensor', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.glassbreak_sensor-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Glassbreak Sensor', - }), - 'context': , - 'entity_id': 'binary_sensor.glassbreak_sensor', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.motion_detector-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.motion_detector', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.motion_detector-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Motion Detector', - }), - 'context': , - 'entity_id': 'binary_sensor.motion_detector', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.smoke_detector-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.smoke_detector', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.smoke_detector-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Smoke Detector', - }), - 'context': , - 'entity_id': 'binary_sensor.smoke_detector', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.window-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.window', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensor[b5512][binary_sensor.window-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Window', - }), - 'context': , - 'entity_id': 'binary_sensor.window', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_away-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_away-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2029,7 +1031,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_away-state] +# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_away-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm away', @@ -2042,7 +1044,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_home-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2076,7 +1078,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.area1_area_ready_to_arm_home-state] +# name: test_binary_sensor[None-b5512][binary_sensor.area1_area_ready_to_arm_home-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Area ready to arm home', @@ -2089,7 +1091,7 @@ 'state': 'on', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bedroom-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bedroom-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2123,7 +1125,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bedroom-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bedroom-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Bedroom', @@ -2136,7 +1138,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_ac_failure-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_ac_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2149,7 +1151,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2170,21 +1172,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_ac_failure-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_ac_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 AC Failure', + 'friendly_name': 'Bosch B5512 (US1B) AC Failure', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_ac_failure', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2197,7 +1199,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_battery', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2218,21 +1220,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Bosch Solution 3000 Battery', + 'friendly_name': 'Bosch B5512 (US1B) Battery', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_battery', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery_missing-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery_missing-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2245,7 +1247,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2266,21 +1268,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_battery_missing-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_battery_missing-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Battery missing', + 'friendly_name': 'Bosch B5512 (US1B) Battery missing', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_battery_missing', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2293,7 +1295,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2314,21 +1316,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 CRC failure in panel configuration', + 'friendly_name': 'Bosch B5512 (US1B) CRC failure in panel configuration', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_crc_failure_in_panel_configuration', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2341,7 +1343,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2362,20 +1364,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since RPS hang up', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_overflow-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_overflow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2388,7 +1390,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2409,21 +1411,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_overflow-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_overflow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Log overflow', + 'friendly_name': 'Bosch B5512 (US1B) Log overflow', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_overflow', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2436,7 +1438,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2457,21 +1459,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_log_threshold_reached-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Log threshold reached', + 'friendly_name': 'Bosch B5512 (US1B) Log threshold reached', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_log_threshold_reached', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2484,7 +1486,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2505,21 +1507,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_phone_line_failure-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'connectivity', - 'friendly_name': 'Bosch Solution 3000 Phone line failure', + 'friendly_name': 'Bosch B5512 (US1B) Phone line failure', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_phone_line_failure', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2532,7 +1534,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2553,21 +1555,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Point bus failure since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) Point bus failure since RPS hang up', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_problem-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_problem-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2580,7 +1582,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2601,21 +1603,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_problem-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_problem-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Problem', + 'friendly_name': 'Bosch B5512 (US1B) Problem', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_problem', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2628,7 +1630,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2649,21 +1651,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 SDI failure since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) SDI failure since RPS hang up', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2676,7 +1678,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2697,21 +1699,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 User code tamper since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) User code tamper since RPS hang up', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.co_detector-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.co_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2745,7 +1747,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.co_detector-state] +# name: test_binary_sensor[None-b5512][binary_sensor.co_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'CO Detector', @@ -2758,7 +1760,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.door-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2792,7 +1794,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.door-state] +# name: test_binary_sensor[None-b5512][binary_sensor.door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Door', @@ -2805,7 +1807,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.glassbreak_sensor-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.glassbreak_sensor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2839,7 +1841,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.glassbreak_sensor-state] +# name: test_binary_sensor[None-b5512][binary_sensor.glassbreak_sensor-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Glassbreak Sensor', @@ -2852,7 +1854,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.motion_detector-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.motion_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2886,7 +1888,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.motion_detector-state] +# name: test_binary_sensor[None-b5512][binary_sensor.motion_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Motion Detector', @@ -2899,7 +1901,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.smoke_detector-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.smoke_detector-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2933,7 +1935,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.smoke_detector-state] +# name: test_binary_sensor[None-b5512][binary_sensor.smoke_detector-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Smoke Detector', @@ -2946,7 +1948,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.window-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.window-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2980,7 +1982,1005 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[solution_3000][binary_sensor.window-state] +# name: test_binary_sensor[None-b5512][binary_sensor.window-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Window', + }), + 'context': , + 'entity_id': 'binary_sensor.window', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_away-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'has_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 ready to arm away', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_away', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_away-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm away', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_away', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_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': 'Area ready to arm home', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'area_ready_to_arm_home', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.area1_area_ready_to_arm_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Area ready to arm home', + }), + 'context': , + 'entity_id': 'binary_sensor.area1_area_ready_to_arm_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bedroom-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.bedroom', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bedroom-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom', + }), + 'context': , + 'entity_id': 'binary_sensor.bedroom', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_ac_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_ac_fail', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_ac_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 AC Failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_ac_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Bosch Solution 3000 Battery', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery_missing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + '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 missing', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_battery_mising', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_battery_missing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Battery missing', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_battery_missing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'CRC failure in panel configuration', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 CRC failure in panel configuration', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_crc_failure_in_panel_configuration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Failure to call RPS since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_overflow-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log overflow', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_overflow', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_overflow-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Log overflow', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_overflow', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Log threshold reached', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_log_threshold', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_log_threshold_reached-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Log threshold reached', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_log_threshold_reached', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Phone line failure', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_phone_line_failure', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_phone_line_failure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Bosch Solution 3000 Phone line failure', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_phone_line_failure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Point bus failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Point bus failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'SDI failure since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 SDI failure since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'User code tamper since RPS hang up', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Bosch Solution 3000 User code tamper since RPS hang up', + }), + 'context': , + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.co_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.co_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.co_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'CO Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.co_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.door', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Door', + }), + 'context': , + 'entity_id': 'binary_sensor.door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.glassbreak_sensor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.glassbreak_sensor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Glassbreak Sensor', + }), + 'context': , + 'entity_id': 'binary_sensor.glassbreak_sensor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.motion_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.motion_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.motion_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Motion Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.motion_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.smoke_detector-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.smoke_detector', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.smoke_detector-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smoke Detector', + }), + 'context': , + 'entity_id': 'binary_sensor.smoke_detector', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.window-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.window', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[None-solution_3000][binary_sensor.window-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Window', diff --git a/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr b/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr index 459ddf7a213..670db709a1a 100644 --- a/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr +++ b/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_diagnostics[amax_3000] +# name: test_diagnostics[amax_3000-None] dict({ 'data': dict({ 'areas': list([ @@ -89,13 +89,14 @@ 'entry_data': dict({ 'host': '0.0.0.0', 'installer_code': '**REDACTED**', + 'mac': None, 'model': 'AMAX 3000', 'password': '**REDACTED**', 'port': 7700, }), }) # --- -# name: test_diagnostics[b5512] +# name: test_diagnostics[b5512-None] dict({ 'data': dict({ 'areas': list([ @@ -180,105 +181,107 @@ }), ]), 'protocol_version': '1.0.0', - 'serial_number': None, - }), - 'entry_data': dict({ - 'host': '0.0.0.0', - 'model': 'B5512 (US1B)', - 'password': '**REDACTED**', - 'port': 7700, - }), - }) -# --- -# name: test_diagnostics[solution_3000] - dict({ - 'data': dict({ - 'areas': list([ - dict({ - 'alarms': list([ - ]), - 'all_armed': False, - 'all_ready': True, - 'armed': False, - 'arming': False, - 'disarmed': True, - 'faults': 0, - 'id': 1, - 'name': 'Area1', - 'part_armed': False, - 'part_ready': True, - 'pending': False, - 'triggered': False, - }), - ]), - 'doors': list([ - dict({ - 'id': 1, - 'locked': True, - 'name': 'Main Door', - 'open': False, - }), - ]), - 'firmware_version': '1.0.0', - 'history_events': list([ - ]), - 'model': 'Solution 3000', - 'outputs': list([ - dict({ - 'active': False, - 'id': 1, - 'name': 'Output A', - }), - ]), - 'points': list([ - dict({ - 'id': 0, - 'name': 'Window', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 1, - 'name': 'Door', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 2, - 'name': 'Motion Detector', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 3, - 'name': 'CO Detector', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 4, - 'name': 'Smoke Detector', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 5, - 'name': 'Glassbreak Sensor', - 'normal': True, - 'open': False, - }), - dict({ - 'id': 6, - 'name': 'Bedroom', - 'normal': True, - 'open': False, - }), - ]), - 'protocol_version': '1.0.0', 'serial_number': '1234567890', }), 'entry_data': dict({ 'host': '0.0.0.0', + 'mac': None, + 'model': 'B5512 (US1B)', + 'password': '**REDACTED**', + 'port': 7700, + }), + }) +# --- +# name: test_diagnostics[solution_3000-None] + dict({ + 'data': dict({ + 'areas': list([ + dict({ + 'alarms': list([ + ]), + 'all_armed': False, + 'all_ready': True, + 'armed': False, + 'arming': False, + 'disarmed': True, + 'faults': 0, + 'id': 1, + 'name': 'Area1', + 'part_armed': False, + 'part_ready': True, + 'pending': False, + 'triggered': False, + }), + ]), + 'doors': list([ + dict({ + 'id': 1, + 'locked': True, + 'name': 'Main Door', + 'open': False, + }), + ]), + 'firmware_version': '1.0.0', + 'history_events': list([ + ]), + 'model': 'Solution 3000', + 'outputs': list([ + dict({ + 'active': False, + 'id': 1, + 'name': 'Output A', + }), + ]), + 'points': list([ + dict({ + 'id': 0, + 'name': 'Window', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 1, + 'name': 'Door', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 2, + 'name': 'Motion Detector', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 3, + 'name': 'CO Detector', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 4, + 'name': 'Smoke Detector', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 5, + 'name': 'Glassbreak Sensor', + 'normal': True, + 'open': False, + }), + dict({ + 'id': 6, + 'name': 'Bedroom', + 'normal': True, + 'open': False, + }), + ]), + 'protocol_version': '1.0.0', + 'serial_number': None, + }), + 'entry_data': dict({ + 'host': '0.0.0.0', + 'mac': None, 'model': 'Solution 3000', 'port': 7700, 'user_code': '**REDACTED**', diff --git a/tests/components/bosch_alarm/snapshots/test_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_sensor.ambr index 64a02e730f6..4f4c55dd845 100644 --- a/tests/components/bosch_alarm/snapshots/test_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensor[amax_3000][sensor.area1_burglary_alarm_issues-entry] +# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -33,7 +33,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[amax_3000][sensor.area1_burglary_alarm_issues-state] +# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Burglary alarm issues', @@ -46,7 +46,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[amax_3000][sensor.area1_faulting_points-entry] +# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -80,7 +80,7 @@ 'unit_of_measurement': 'points', }) # --- -# name: test_sensor[amax_3000][sensor.area1_faulting_points-state] +# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Faulting points', @@ -94,7 +94,7 @@ 'state': '0', }) # --- -# name: test_sensor[amax_3000][sensor.area1_fire_alarm_issues-entry] +# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -128,7 +128,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[amax_3000][sensor.area1_fire_alarm_issues-state] +# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Fire alarm issues', @@ -141,7 +141,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[amax_3000][sensor.area1_gas_alarm_issues-entry] +# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -175,7 +175,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[amax_3000][sensor.area1_gas_alarm_issues-state] +# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Gas alarm issues', @@ -188,196 +188,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[b5512][sensor.area1_burglary_alarm_issues-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.area1_burglary_alarm_issues', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Burglary alarm issues', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'alarms_burglary', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[b5512][sensor.area1_burglary_alarm_issues-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Burglary alarm issues', - }), - 'context': , - 'entity_id': 'sensor.area1_burglary_alarm_issues', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'no_issues', - }) -# --- -# name: test_sensor[b5512][sensor.area1_faulting_points-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.area1_faulting_points', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Faulting points', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'faulting_points', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_faulting_points', - 'unit_of_measurement': 'points', - }) -# --- -# name: test_sensor[b5512][sensor.area1_faulting_points-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Faulting points', - 'unit_of_measurement': 'points', - }), - 'context': , - 'entity_id': 'sensor.area1_faulting_points', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0', - }) -# --- -# name: test_sensor[b5512][sensor.area1_fire_alarm_issues-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.area1_fire_alarm_issues', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Fire alarm issues', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'alarms_fire', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[b5512][sensor.area1_fire_alarm_issues-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Fire alarm issues', - }), - 'context': , - 'entity_id': 'sensor.area1_fire_alarm_issues', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'no_issues', - }) -# --- -# name: test_sensor[b5512][sensor.area1_gas_alarm_issues-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.area1_gas_alarm_issues', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Gas alarm issues', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'alarms_gas', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensor[b5512][sensor.area1_gas_alarm_issues-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Area1 Gas alarm issues', - }), - 'context': , - 'entity_id': 'sensor.area1_gas_alarm_issues', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'no_issues', - }) -# --- -# name: test_sensor[solution_3000][sensor.area1_burglary_alarm_issues-entry] +# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -411,7 +222,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[solution_3000][sensor.area1_burglary_alarm_issues-state] +# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Burglary alarm issues', @@ -424,7 +235,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[solution_3000][sensor.area1_faulting_points-entry] +# name: test_sensor[None-b5512][sensor.area1_faulting_points-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -458,7 +269,7 @@ 'unit_of_measurement': 'points', }) # --- -# name: test_sensor[solution_3000][sensor.area1_faulting_points-state] +# name: test_sensor[None-b5512][sensor.area1_faulting_points-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Faulting points', @@ -472,7 +283,7 @@ 'state': '0', }) # --- -# name: test_sensor[solution_3000][sensor.area1_fire_alarm_issues-entry] +# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -506,7 +317,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[solution_3000][sensor.area1_fire_alarm_issues-state] +# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Fire alarm issues', @@ -519,7 +330,7 @@ 'state': 'no_issues', }) # --- -# name: test_sensor[solution_3000][sensor.area1_gas_alarm_issues-entry] +# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -553,7 +364,196 @@ 'unit_of_measurement': None, }) # --- -# name: test_sensor[solution_3000][sensor.area1_gas_alarm_issues-state] +# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Gas alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burglary alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_burglary', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Burglary alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_burglary_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_faulting_points', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Faulting points', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'faulting_points', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_faulting_points', + 'unit_of_measurement': 'points', + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Faulting points', + 'unit_of_measurement': 'points', + }), + 'context': , + 'entity_id': 'sensor.area1_faulting_points', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fire alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_fire', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Area1 Fire alarm issues', + }), + 'context': , + 'entity_id': 'sensor.area1_fire_alarm_issues', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_issues', + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.area1_gas_alarm_issues', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gas alarm issues', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_gas', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Area1 Gas alarm issues', diff --git a/tests/components/bosch_alarm/snapshots/test_switch.ambr b/tests/components/bosch_alarm/snapshots/test_switch.ambr index 079e765c35c..ad508f257ba 100644 --- a/tests/components/bosch_alarm/snapshots/test_switch.ambr +++ b/tests/components/bosch_alarm/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_switch[amax_3000][switch.main_door_cycling-entry] +# name: test_switch[None-amax_3000][switch.main_door_cycling-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -33,7 +33,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[amax_3000][switch.main_door_cycling-state] +# name: test_switch[None-amax_3000][switch.main_door_cycling-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Cycling', @@ -46,7 +46,7 @@ 'state': 'off', }) # --- -# name: test_switch[amax_3000][switch.main_door_locked-entry] +# name: test_switch[None-amax_3000][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -80,7 +80,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[amax_3000][switch.main_door_locked-state] +# name: test_switch[None-amax_3000][switch.main_door_locked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Locked', @@ -93,7 +93,7 @@ 'state': 'on', }) # --- -# name: test_switch[amax_3000][switch.main_door_secured-entry] +# name: test_switch[None-amax_3000][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -127,7 +127,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[amax_3000][switch.main_door_secured-state] +# name: test_switch[None-amax_3000][switch.main_door_secured-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Secured', @@ -140,7 +140,7 @@ 'state': 'off', }) # --- -# name: test_switch[amax_3000][switch.output_a-entry] +# name: test_switch[None-amax_3000][switch.output_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -174,7 +174,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[amax_3000][switch.output_a-state] +# name: test_switch[None-amax_3000][switch.output_a-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Output A', @@ -187,195 +187,7 @@ 'state': 'off', }) # --- -# name: test_switch[b5512][switch.main_door_cycling-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_cycling', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cycling', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cycling', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[b5512][switch.main_door_cycling-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Cycling', - }), - 'context': , - 'entity_id': 'switch.main_door_cycling', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switch[b5512][switch.main_door_locked-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_locked', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Locked', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'locked', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[b5512][switch.main_door_locked-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Locked', - }), - 'context': , - 'entity_id': 'switch.main_door_locked', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- -# name: test_switch[b5512][switch.main_door_secured-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_secured', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Secured', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'secured', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[b5512][switch.main_door_secured-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Secured', - }), - 'context': , - 'entity_id': 'switch.main_door_secured', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switch[b5512][switch.output_a-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.output_a', - 'has_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': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[b5512][switch.output_a-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Output A', - }), - 'context': , - 'entity_id': 'switch.output_a', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_switch[solution_3000][switch.main_door_cycling-entry] +# name: test_switch[None-b5512][switch.main_door_cycling-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -409,7 +221,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[solution_3000][switch.main_door_cycling-state] +# name: test_switch[None-b5512][switch.main_door_cycling-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Cycling', @@ -422,7 +234,7 @@ 'state': 'off', }) # --- -# name: test_switch[solution_3000][switch.main_door_locked-entry] +# name: test_switch[None-b5512][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -456,7 +268,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[solution_3000][switch.main_door_locked-state] +# name: test_switch[None-b5512][switch.main_door_locked-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Locked', @@ -469,7 +281,7 @@ 'state': 'on', }) # --- -# name: test_switch[solution_3000][switch.main_door_secured-entry] +# name: test_switch[None-b5512][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -503,7 +315,7 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[solution_3000][switch.main_door_secured-state] +# name: test_switch[None-b5512][switch.main_door_secured-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Main Door Secured', @@ -516,7 +328,7 @@ 'state': 'off', }) # --- -# name: test_switch[solution_3000][switch.output_a-entry] +# name: test_switch[None-b5512][switch.output_a-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -550,7 +362,195 @@ 'unit_of_measurement': None, }) # --- -# name: test_switch[solution_3000][switch.output_a-state] +# name: test_switch[None-b5512][switch.output_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Output A', + }), + 'context': , + 'entity_id': 'switch.output_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_cycling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_cycling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cycling', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_cycling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Cycling', + }), + 'context': , + 'entity_id': 'switch.main_door_cycling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_locked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_locked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Locked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'locked', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_locked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Locked', + }), + 'context': , + 'entity_id': 'switch.main_door_locked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_secured-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_secured', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Secured', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'secured', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_secured-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Secured', + }), + 'context': , + 'entity_id': 'switch.main_door_secured', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch[None-solution_3000][switch.output_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.output_a', + 'has_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': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-solution_3000][switch.output_a-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Output A', diff --git a/tests/components/bosch_alarm/test_config_flow.py b/tests/components/bosch_alarm/test_config_flow.py index 9e79d1c1f5f..afdd98bb1c0 100644 --- a/tests/components/bosch_alarm/test_config_flow.py +++ b/tests/components/bosch_alarm/test_config_flow.py @@ -6,12 +6,12 @@ from unittest.mock import AsyncMock import pytest -from homeassistant import config_entries from homeassistant.components.bosch_alarm.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_PORT +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_RECONFIGURE, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from . import setup_integration @@ -77,7 +77,7 @@ async def test_form_exceptions( """Test we handle exceptions correctly.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" @@ -174,13 +174,6 @@ async def test_entry_already_configured_host( result["flow_id"], {CONF_HOST: "0.0.0.0"} ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "auth" - assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], config_flow_data - ) - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -200,7 +193,7 @@ async def test_entry_already_configured_serial( ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "0.0.0.0"} + result["flow_id"], {CONF_HOST: "1.1.1.1"} ) assert result["type"] is FlowResultType.FORM @@ -214,6 +207,140 @@ async def test_entry_already_configured_serial( assert result["reason"] == "already_configured" +async def test_dhcp_can_finish( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_panel: AsyncMock, + model_name: str, + serial_number: str, + config_flow_data: dict[str, Any], +) -> None: + """Test DHCP discovery flow can finish right away.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="1.1.1.1", + macaddress="34ea34b43b5a", + ), + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + config_flow_data, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"Bosch {model_name}" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_MAC: "34:ea:34:b4:3b:5a", + CONF_PORT: 7700, + CONF_MODEL: model_name, + **config_flow_data, + } + + +@pytest.mark.parametrize( + ("exception", "message"), + [ + (asyncio.exceptions.TimeoutError(), "cannot_connect"), + (Exception(), "unknown"), + ], +) +async def test_dhcp_exceptions( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_panel: AsyncMock, + model_name: str, + serial_number: str, + config_flow_data: dict[str, Any], + exception: Exception, + message: str, +) -> None: + """Test DHCP discovery flow that fails to connect.""" + mock_panel.connect.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="1.1.1.1", + macaddress="34ea34b43b5a", + ), + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == message + + +@pytest.mark.parametrize("mac_address", ["34ea34b43b5a"]) +async def test_dhcp_updates_host( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_panel: AsyncMock, + mac_address: str | None, + serial_number: str, + config_flow_data: dict[str, Any], +) -> None: + """Test DHCP updates host.""" + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="4.5.6.7", + macaddress=mac_address, + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "4.5.6.7" + + +@pytest.mark.parametrize("model", ["solution_3000", "amax_3000"]) +async def test_dhcp_abort_ongoing_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_panel: AsyncMock, + config_flow_data: dict[str, Any], +) -> None: + """Test if a dhcp flow is aborted if there is already an ongoing flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "0.0.0.0"} + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="0.0.0.0", + macaddress="34ea34b43b5a", + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + async def test_reauth_flow_success( hass: HomeAssistant, mock_setup_entry: AsyncMock, @@ -274,7 +401,6 @@ async def test_reauth_flow_error( ) assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == message - mock_panel.connect.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -301,7 +427,7 @@ async def test_reconfig_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={ - "source": config_entries.SOURCE_RECONFIGURE, + "source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id, }, ) @@ -347,7 +473,7 @@ async def test_reconfig_flow_incorrect_model( result = await hass.config_entries.flow.async_init( DOMAIN, context={ - "source": config_entries.SOURCE_RECONFIGURE, + "source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id, }, ) From 053e5417a7899f1f2648316c6589feb766bfce3d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 16 May 2025 04:25:24 -0400 Subject: [PATCH 0506/1175] Strip `_CLIENT` suffix from ZHA event `unique_id` (#145006) --- homeassistant/components/zha/helpers.py | 15 ++++- tests/components/zha/test_device_action.py | 64 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py index c819f94ceba..084e1c882ac 100644 --- a/homeassistant/components/zha/helpers.py +++ b/homeassistant/components/zha/helpers.py @@ -419,13 +419,26 @@ class ZHADeviceProxy(EventBase): @callback def handle_zha_event(self, zha_event: ZHAEvent) -> None: """Handle a ZHA event.""" + if ATTR_UNIQUE_ID in zha_event.data: + unique_id = zha_event.data[ATTR_UNIQUE_ID] + + # Client cluster handler unique IDs in the ZHA lib were disambiguated by + # adding a suffix of `_CLIENT`. Unfortunately, this breaks existing + # automations that match the `unique_id` key. This can be removed in a + # future release with proper notice of a breaking change. + unique_id = unique_id.removesuffix("_CLIENT") + else: + unique_id = zha_event.unique_id + self.gateway_proxy.hass.bus.async_fire( ZHA_EVENT, { ATTR_DEVICE_IEEE: str(zha_event.device_ieee), - ATTR_UNIQUE_ID: zha_event.unique_id, ATTR_DEVICE_ID: self.device_id, **zha_event.data, + # The order of these keys is intentional, `zha_event.data` can contain + # a `unique_id` key, which we explicitly replace + ATTR_UNIQUE_ID: unique_id, }, ) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 6708250e448..becf9d81557 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -258,3 +258,67 @@ async def test_invalid_zha_event_type( # `zha_send_event` accepts only zigpy responses, lists, and dicts with pytest.raises(TypeError): cluster_handler.zha_send_event(COMMAND_SINGLE, 123) + + +async def test_client_unique_id_suffix_stripped( + hass: HomeAssistant, setup_zha, zigpy_device_mock +) -> None: + """Test that the `_CLIENT_` unique ID suffix is stripped.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "zha_event", + "event_data": { + "unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006", # no `_CLIENT` suffix + "endpoint_id": 1, + "cluster_id": 6, + "command": "on", + "args": [], + "params": {}, + }, + }, + "action": {"service": "zha.test"}, + } + }, + ) + + service_calls = async_mock_service(hass, DOMAIN, "test") + + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + + zha_device.emit_zha_event( + { + "unique_id": "38:5b:44:ff:fe:a7:cc:69:1:0x0006_CLIENT", + "endpoint_id": 1, + "cluster_id": 6, + "command": "on", + "args": [], + "params": {}, + } + ) + + await hass.async_block_till_done(wait_background_tasks=True) + assert len(service_calls) == 1 From 71108d9ca047627eb3bd99284ccd07a0a269485f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 16 May 2025 10:26:00 +0200 Subject: [PATCH 0507/1175] Do not show an empty component name on MQTT device subentries not as `None` if it is not set (#144792) --- homeassistant/components/mqtt/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b3c82dce65e..ca5c597dfaf 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -2168,7 +2168,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): entities = [ SelectOptionDict( value=key, - label=f"{device_name} {component_data.get(CONF_NAME, '-')}" + label=f"{device_name} {component_data.get(CONF_NAME, '-') or '-'}" f" ({component_data[CONF_PLATFORM]})", ) for key, component_data in self._subentry_data["components"].items() @@ -2400,7 +2400,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): self._component_id = None mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME] mqtt_items = ", ".join( - f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})" + f"{mqtt_device} {component_data.get(CONF_NAME, '-') or '-'} " + f"({component_data[CONF_PLATFORM]})" for component_data in self._subentry_data["components"].values() ) menu_options = [ From 6dff975711d3d0ef53ecba246c652532bffba1e7 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Fri, 16 May 2025 10:27:59 +0200 Subject: [PATCH 0508/1175] Initialize select _attr_current_option with None (#145026) --- homeassistant/components/select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/select/__init__.py b/homeassistant/components/select/__init__.py index 4196106edd2..18f520f9a23 100644 --- a/homeassistant/components/select/__init__.py +++ b/homeassistant/components/select/__init__.py @@ -127,7 +127,7 @@ class SelectEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _entity_component_unrecorded_attributes = frozenset({ATTR_OPTIONS}) entity_description: SelectEntityDescription - _attr_current_option: str | None + _attr_current_option: str | None = None _attr_options: list[str] _attr_state: None = None From bbe975baef2c16341d1cbd21fe0a6f11d5916d04 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 16 May 2025 10:28:57 +0200 Subject: [PATCH 0509/1175] Bump plugwise to v1.7.4 (#145021) --- 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 3f812c1a63b..264afd79ed2 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["plugwise"], "quality_scale": "platinum", - "requirements": ["plugwise==1.7.3"], + "requirements": ["plugwise==1.7.4"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index d8b1ac109b1..76bbbf610d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1679,7 +1679,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.7.3 +plugwise==1.7.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cefc6b5819a..0ec61188f77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1393,7 +1393,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.7.3 +plugwise==1.7.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 3de740ed1e3d5f655e7c5194670375dbf8a00bc8 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Fri, 16 May 2025 16:30:30 +0800 Subject: [PATCH 0510/1175] Bump PySwitchbot to 0.62.2 (#145018) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 8c3dcac8f65..064ebf5e2f4 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -40,5 +40,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.62.0"] + "requirements": ["PySwitchbot==0.62.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76bbbf610d4..b2b9e27350f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.62.0 +PySwitchbot==0.62.2 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ec61188f77..fed9d95a375 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.62.0 +PySwitchbot==0.62.2 # homeassistant.components.syncthru PySyncThru==0.8.0 From e76b483067a045c33df8135f8e42283e88feee85 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 10:36:58 +0200 Subject: [PATCH 0511/1175] Add lamp capability to SmartThings (#144918) --- .../components/smartthings/icons.json | 7 ++ .../components/smartthings/select.py | 36 +++++- .../components/smartthings/strings.json | 11 ++ .../device_status/da_ks_range_0101x.json | 4 +- .../smartthings/snapshots/test_select.ambr | 112 ++++++++++++++++++ tests/components/smartthings/test_select.py | 33 ++++++ 6 files changed, 199 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 51978590e2e..394035aafb6 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -41,6 +41,13 @@ "stop": "mdi:stop" } }, + "lamp": { + "default": "mdi:lightbulb", + "state": { + "on": "mdi:lightbulb-on", + "off": "mdi:lightbulb-off" + } + }, "detergent_amount": { "default": "mdi:car-coolant-level" }, diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 16051cb08f1..4fcd7fd080f 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -16,6 +16,10 @@ from . import FullDevice, SmartThingsConfigEntry from .const import MAIN from .entity import SmartThingsEntity +LAMP_TO_HA = { + "extraHigh": "extra_high", +} + @dataclass(frozen=True, kw_only=True) class SmartThingsSelectDescription(SelectEntityDescription): @@ -26,6 +30,7 @@ class SmartThingsSelectDescription(SelectEntityDescription): options_attribute: Attribute status_attribute: Attribute command: Command + options_map: dict[str, str] | None = None default_options: list[str] | None = None @@ -75,6 +80,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { command=Command.SET_AMOUNT, entity_category=EntityCategory.CONFIG, ), + Capability.SAMSUNG_CE_LAMP: SmartThingsSelectDescription( + key=Capability.SAMSUNG_CE_LAMP, + translation_key="lamp", + options_attribute=Attribute.SUPPORTED_BRIGHTNESS_LEVEL, + status_attribute=Attribute.BRIGHTNESS_LEVEL, + command=Command.SET_BRIGHTNESS_LEVEL, + options_map=LAMP_TO_HA, + entity_category=EntityCategory.CONFIG, + ), } @@ -117,20 +131,29 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity): @property def options(self) -> list[str]: """Return the list of options.""" - return ( + options: list[str] = ( self.get_attribute_value( self.entity_description.key, self.entity_description.options_attribute ) or self.entity_description.default_options or [] ) + if self.entity_description.options_map: + options = [ + self.entity_description.options_map.get(option, option) + for option in options + ] + return options @property def current_option(self) -> str | None: """Return the current option.""" - return self.get_attribute_value( + option = self.get_attribute_value( self.entity_description.key, self.entity_description.status_attribute ) + if self.entity_description.options_map: + option = self.entity_description.options_map.get(option) + return option async def async_select_option(self, option: str) -> None: """Select an option.""" @@ -144,6 +167,15 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity): raise ServiceValidationError( "Can only be updated when remote control is enabled" ) + if self.entity_description.options_map: + option = next( + ( + key + for key, value in self.entity_description.options_map.items() + if value == option + ), + option, + ) await self.execute_device_command( self.entity_description.key, self.entity_description.command, diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 50cb864e7d7..66bb97e4f40 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -115,6 +115,17 @@ "stop": "[%key:common::state::stopped%]" } }, + "lamp": { + "name": "Lamp", + "state": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", + "low": "Low", + "mid": "Mid", + "high": "High", + "extra_high": "Extra high" + } + }, "detergent_amount": { "name": "Detergent dispense amount", "state": { diff --git a/tests/components/smartthings/fixtures/device_status/da_ks_range_0101x.json b/tests/components/smartthings/fixtures/device_status/da_ks_range_0101x.json index 6d15aa4696d..09c5a13613a 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ks_range_0101x.json +++ b/tests/components/smartthings/fixtures/device_status/da_ks_range_0101x.json @@ -669,11 +669,11 @@ }, "samsungce.lamp": { "brightnessLevel": { - "value": "off", + "value": "extraHigh", "timestamp": "2025-03-13T21:23:27.659Z" }, "supportedBrightnessLevel": { - "value": ["off", "high"], + "value": ["off", "extraHigh"], "timestamp": "2025-03-13T21:23:27.659Z" } }, diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index 17d8e10d230..b2c3234847e 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -1,4 +1,116 @@ # serializer version: 1 +# name: test_all_entities[da_ks_oven_01061][select.oven_lamp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.oven_lamp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lamp', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lamp', + 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_samsungce.lamp_brightnessLevel_brightnessLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_oven_01061][select.oven_lamp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Oven Lamp', + 'options': list([ + 'off', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.oven_lamp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[da_ks_range_0101x][select.vulcan_lamp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'extra_high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.vulcan_lamp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lamp', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lamp', + 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_samsungce.lamp_brightnessLevel_brightnessLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_range_0101x][select.vulcan_lamp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vulcan Lamp', + 'options': list([ + 'off', + 'extra_high', + ]), + }), + 'context': , + 'entity_id': 'select.vulcan_lamp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'extra_high', + }) +# --- # name: test_all_entities[da_wm_dw_000001][select.dishwasher-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_select.py b/tests/components/smartthings/test_select.py index ce3bea08ca2..da27565ead5 100644 --- a/tests/components/smartthings/test_select.py +++ b/tests/components/smartthings/test_select.py @@ -9,6 +9,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, + ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) @@ -95,6 +96,38 @@ async def test_select_option( ) +@pytest.mark.parametrize("device_fixture", ["da_ks_range_0101x"]) +async def test_select_option_map( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("select.vulcan_lamp") + assert state + assert state.state == "extra_high" + assert state.attributes[ATTR_OPTIONS] == [ + "off", + "extra_high", + ] + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.vulcan_lamp", ATTR_OPTION: "extra_high"}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "2c3cbaa0-1899-5ddc-7b58-9d657bd48f18", + Capability.SAMSUNG_CE_LAMP, + Command.SET_BRIGHTNESS_LEVEL, + MAIN, + argument="extraHigh", + ) + + @pytest.mark.parametrize("device_fixture", ["da_wm_wd_000001"]) async def test_select_option_without_remote_control( hass: HomeAssistant, From 3942e6a84198a22a760dfdd8066c3be3c9b04d9d Mon Sep 17 00:00:00 2001 From: rjblake Date: Fri, 16 May 2025 10:37:11 +0200 Subject: [PATCH 0512/1175] Fix some Home Connect translation strings (#144905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update strings.json Corrected program names: changed "Pre_rinse" to "Pre-Rinse" changed "Kurz 60°C" to "Speed 60°C" Both match the Home Connect app; although the UK documentation refers to "Speed 60°C" as "Quick 60°C" * Adjust casing --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/home_connect/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 9c0da723b04..3fc509e79f3 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -234,7 +234,7 @@ "consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye", "consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye", "consumer_products_coffee_maker_program_beverage_hot_water": "Hot water", - "dishcare_dishwasher_program_pre_rinse": "Pre_rinse", + "dishcare_dishwasher_program_pre_rinse": "Pre-rinse", "dishcare_dishwasher_program_auto_1": "Auto 1", "dishcare_dishwasher_program_auto_2": "Auto 2", "dishcare_dishwasher_program_auto_3": "Auto 3", @@ -252,7 +252,7 @@ "dishcare_dishwasher_program_intensiv_power": "Intensive power", "dishcare_dishwasher_program_magic_daily": "Magic daily", "dishcare_dishwasher_program_super_60": "Super 60ºC", - "dishcare_dishwasher_program_kurz_60": "Kurz 60ºC", + "dishcare_dishwasher_program_kurz_60": "Speed 60ºC", "dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC", "dishcare_dishwasher_program_machine_care": "Machine care", "dishcare_dishwasher_program_steam_fresh": "Steam fresh", From 3e92f23680152aa2482517820fe4bb272ebfcff6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 10:38:17 +0200 Subject: [PATCH 0513/1175] Cleanup huisbaasje tests (#144954) --- tests/components/huisbaasje/test_init.py | 24 +++++----------------- tests/components/huisbaasje/test_sensor.py | 8 +++----- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 5f1bcb0094d..245cde5e9af 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -4,24 +4,16 @@ from unittest.mock import patch from energyflip import EnergyFlipException -from homeassistant.components import huisbaasje +from homeassistant.components.huisbaasje.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .test_data import MOCK_CURRENT_MEASUREMENTS from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistant) -> None: - """Test for successfully setting up the platform.""" - assert await async_setup_component(hass, huisbaasje.DOMAIN, {}) - await hass.async_block_till_done() - assert huisbaasje.DOMAIN in hass.config.components - - async def test_setup_entry(hass: HomeAssistant) -> None: """Test for successfully setting a config entry.""" with ( @@ -36,10 +28,9 @@ async def test_setup_entry(hass: HomeAssistant) -> None: return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements, ): - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, - domain=huisbaasje.DOMAIN, + domain=DOMAIN, title="userId", data={ CONF_ID: "userId", @@ -56,9 +47,6 @@ async def test_setup_entry(hass: HomeAssistant) -> None: # Assert integration is loaded assert config_entry.state is ConfigEntryState.LOADED - assert huisbaasje.DOMAIN in hass.config.components - assert huisbaasje.DOMAIN in hass.data - assert config_entry.entry_id in hass.data[huisbaasje.DOMAIN] # Assert entities are loaded entities = hass.states.async_entity_ids("sensor") @@ -75,10 +63,9 @@ async def test_setup_entry_error(hass: HomeAssistant) -> None: with patch( "energyflip.EnergyFlip.authenticate", side_effect=EnergyFlipException ) as mock_authenticate: - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, - domain=huisbaasje.DOMAIN, + domain=DOMAIN, title="userId", data={ CONF_ID: "userId", @@ -95,7 +82,7 @@ async def test_setup_entry_error(hass: HomeAssistant) -> None: # Assert integration is loaded with error assert config_entry.state is ConfigEntryState.SETUP_ERROR - assert huisbaasje.DOMAIN not in hass.data + assert DOMAIN not in hass.data # Assert entities are not loaded entities = hass.states.async_entity_ids("sensor") @@ -119,10 +106,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements, ): - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, - domain=huisbaasje.DOMAIN, + domain=DOMAIN, title="userId", data={ CONF_ID: "userId", diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 5f5707bdd5d..4302efa98c8 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant.components import huisbaasje +from homeassistant.components.huisbaasje.const import DOMAIN from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -40,10 +40,9 @@ async def test_setup_entry(hass: HomeAssistant) -> None: return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements, ): - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, - domain=huisbaasje.DOMAIN, + domain=DOMAIN, title="userId", data={ CONF_ID: "userId", @@ -331,10 +330,9 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant) -> None: return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, ) as mock_current_measurements, ): - hass.config.components.add(huisbaasje.DOMAIN) config_entry = MockConfigEntry( version=1, - domain=huisbaasje.DOMAIN, + domain=DOMAIN, title="userId", data={ CONF_ID: "userId", From 7410b8778a98d9d420edb7abed32476cf6ef8940 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 10:47:23 +0200 Subject: [PATCH 0514/1175] Deprecate DHW switch for SmartThings (#145011) --- .../components/smartthings/strings.json | 8 + .../components/smartthings/switch.py | 14 +- homeassistant/components/smartthings/util.py | 3 +- .../smartthings/snapshots/test_switch.ambr | 141 ------------------ tests/components/smartthings/test_switch.py | 20 ++- 5 files changed, 41 insertions(+), 145 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 66bb97e4f40..c2719c3e2f9 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -585,6 +585,14 @@ "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", "description": "The switch `{entity_id}` is deprecated and a media player entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new media player entity in the above automations or scripts and disable the entity to fix this issue." }, + "deprecated_switch_dhw": { + "title": "Heat pump switch deprecated", + "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nPlease use the new water heater entity in the above automations or scripts and disable the entity to fix this issue." + }, + "deprecated_switch_dhw_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_switch_dhw::title%]", + "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new water heater entity in the above automations or scripts and disable the entity to fix this issue." + }, "deprecated_media_player": { "title": "Media player sensors deprecated", "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nPlease update your dashboards, templates to use the new media player entity and disable the entity to fix this issue." diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 56e67ad2a13..f610a97f16e 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -152,14 +152,24 @@ async def async_setup_entry( device.device.components[MAIN].manufacturer_category in INVALID_SWITCH_CATEGORIES ) - if media_player or appliance: - issue = "media_player" if media_player else "appliance" + dhw = Capability.SAMSUNG_CE_EHS_FSV_SETTINGS in device.status[MAIN] + if media_player or appliance or dhw: + if appliance: + issue = "appliance" + version = "2025.10.0" + elif media_player: + issue = "media_player" + version = "2025.10.0" + else: + issue = "dhw" + version = "2025.12.0" if deprecate_entity( hass, entity_registry, SWITCH_DOMAIN, f"{device.device.device_id}_{MAIN}_{Capability.SWITCH}_{Attribute.SWITCH}_{Attribute.SWITCH}", f"deprecated_switch_{issue}", + version, ): entities.append( SmartThingsSwitch( diff --git a/homeassistant/components/smartthings/util.py b/homeassistant/components/smartthings/util.py index b21652ca629..7d74e22477f 100644 --- a/homeassistant/components/smartthings/util.py +++ b/homeassistant/components/smartthings/util.py @@ -19,6 +19,7 @@ def deprecate_entity( platform_domain: str, entity_unique_id: str, issue_string: str, + version: str = "2025.10.0", ) -> bool: """Create an issue for deprecated entities.""" if entity_id := entity_registry.async_get_entity_id( @@ -51,7 +52,7 @@ def deprecate_entity( hass, DOMAIN, f"{issue_string}_{entity_id}", - breaks_in_ha_version="2025.10.0", + breaks_in_ha_version=version, is_fixable=False, severity=IssueSeverity.WARNING, translation_key=translation_key, diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index d43fa207ddf..060f1d3a374 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -46,53 +46,6 @@ 'state': 'on', }) # --- -# name: test_all_entities[da_ac_ehs_01001][switch.heat_pump-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.heat_pump', - 'has_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ac_ehs_01001][switch.heat_pump-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Heat pump', - }), - 'context': , - 'entity_id': 'switch.heat_pump', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_ref_normal_000001][switch.refrigerator_ice_maker-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -281,100 +234,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_sac_ehs_000001_sub][switch.eco_heating_system-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.eco_heating_system', - 'has_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub][switch.eco_heating_system-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Eco Heating System', - }), - 'context': , - 'entity_id': 'switch.eco_heating_system', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- -# name: test_all_entities[da_sac_ehs_000002_sub][switch.warmepumpe-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.warmepumpe', - 'has_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_switch_switch_switch', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_sac_ehs_000002_sub][switch.warmepumpe-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Wärmepumpe', - }), - 'context': , - 'entity_id': 'switch.warmepumpe', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'on', - }) -# --- # name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 0f759d8e6b5..2be2c670faf 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -280,61 +280,77 @@ async def test_create_issue_with_items( @pytest.mark.parametrize( - ("device_fixture", "device_id", "suggested_object_id", "issue_string"), + ("device_fixture", "device_id", "suggested_object_id", "issue_string", "version"), [ ( "da_ks_cooktop_31001", "808dbd84-f357-47e2-a0cd-3b66fa22d584", "induction_hob", "appliance", + "2025.10.0", ), ( "da_ks_microwave_0101x", "2bad3237-4886-e699-1b90-4a51a3d55c8a", "microwave", "appliance", + "2025.10.0", ), ( "da_wm_dw_000001", "f36dc7ce-cac0-0667-dc14-a3704eb5e676", "dishwasher", "appliance", + "2025.10.0", ), ( "da_wm_sc_000001", "b93211bf-9d96-bd21-3b2f-964fcc87f5cc", "airdresser", "appliance", + "2025.10.0", ), ( "da_wm_wd_000001", "02f7256e-8353-5bdd-547f-bd5b1647e01b", "dryer", "appliance", + "2025.10.0", ), ( "da_wm_wm_000001", "f984b91d-f250-9d42-3436-33f09a422a47", "washer", "appliance", + "2025.10.0", ), ( "hw_q80r_soundbar", "afcf3b91-0000-1111-2222-ddff2a0a6577", "soundbar", "media_player", + "2025.10.0", ), ( "vd_network_audio_002s", "0d94e5db-8501-2355-eb4f-214163702cac", "soundbar_living", "media_player", + "2025.10.0", ), ( "vd_stv_2017_k", "4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1", "tv_samsung_8_series_49", "media_player", + "2025.10.0", + ), + ( + "da_sac_ehs_000002_sub", + "3810e5ad-5351-d9f9-12ff-000001200000", + "warmepumpe", + "dhw", + "2025.12.0", ), ], ) @@ -347,6 +363,7 @@ async def test_create_issue( device_id: str, suggested_object_id: str, issue_string: str, + version: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" entity_id = f"switch.{suggested_object_id}" @@ -372,6 +389,7 @@ async def test_create_issue( "entity_id": entity_id, "entity_name": suggested_object_id, } + assert issue.breaks_in_ha_version == version entity_registry.async_update_entity( entity_entry.entity_id, From 82a9e67b7e75b07bca59ce5d4f27494a473aecf2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 10:53:24 +0200 Subject: [PATCH 0515/1175] Use generic in iaqualink entity (#144989) --- homeassistant/components/iaqualink/binary_sensor.py | 4 +++- homeassistant/components/iaqualink/climate.py | 2 +- homeassistant/components/iaqualink/entity.py | 4 ++-- homeassistant/components/iaqualink/light.py | 2 +- homeassistant/components/iaqualink/sensor.py | 2 +- homeassistant/components/iaqualink/switch.py | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 5546e5e9006..94f2dc94f2c 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -34,7 +34,9 @@ async def async_setup_entry( ) -class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorEntity): +class HassAqualinkBinarySensor( + AqualinkEntity[AqualinkBinarySensor], BinarySensorEntity +): """Representation of a binary sensor.""" def __init__(self, dev: AqualinkBinarySensor) -> None: diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index fdd16205be4..e0d5a1d7cf4 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -42,7 +42,7 @@ async def async_setup_entry( ) -class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): +class HassAqualinkThermostat(AqualinkEntity[AqualinkThermostat], ClimateEntity): """Representation of a thermostat.""" _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] diff --git a/homeassistant/components/iaqualink/entity.py b/homeassistant/components/iaqualink/entity.py index d0176ed8bfe..0b3751e5fbc 100644 --- a/homeassistant/components/iaqualink/entity.py +++ b/homeassistant/components/iaqualink/entity.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN -class AqualinkEntity(Entity): +class AqualinkEntity[AqualinkDeviceT: AqualinkDevice](Entity): """Abstract class for all Aqualink platforms. Entity state is updated via the interval timer within the integration. @@ -23,7 +23,7 @@ class AqualinkEntity(Entity): _attr_should_poll = False - def __init__(self, dev: AqualinkDevice) -> None: + def __init__(self, dev: AqualinkDeviceT) -> None: """Initialize the entity.""" self.dev = dev self._attr_unique_id = f"{dev.system.serial}_{dev.name}" diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 868480b0913..946971223b2 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -38,7 +38,7 @@ async def async_setup_entry( ) -class HassAqualinkLight(AqualinkEntity, LightEntity): +class HassAqualinkLight(AqualinkEntity[AqualinkLight], LightEntity): """Representation of a light.""" def __init__(self, dev: AqualinkLight) -> None: diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index a28d527b239..ef614ff066e 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -32,7 +32,7 @@ async def async_setup_entry( ) -class HassAqualinkSensor(AqualinkEntity, SensorEntity): +class HassAqualinkSensor(AqualinkEntity[AqualinkSensor], SensorEntity): """Representation of a sensor.""" def __init__(self, dev: AqualinkSensor) -> None: diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index e01e294355f..959ff6e16c5 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -31,7 +31,7 @@ async def async_setup_entry( ) -class HassAqualinkSwitch(AqualinkEntity, SwitchEntity): +class HassAqualinkSwitch(AqualinkEntity[AqualinkSwitch], SwitchEntity): """Representation of a switch.""" def __init__(self, dev: AqualinkSwitch) -> None: From b8df9c7e97898a879794ac377f9403e8279c510e Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Fri, 16 May 2025 21:26:22 +1200 Subject: [PATCH 0516/1175] Set parallel_updates for bosch_alarm (#145028) --- homeassistant/components/bosch_alarm/alarm_control_panel.py | 3 +++ homeassistant/components/bosch_alarm/quality_scale.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_alarm/alarm_control_panel.py b/homeassistant/components/bosch_alarm/alarm_control_panel.py index 2854298f815..7115bae415a 100644 --- a/homeassistant/components/bosch_alarm/alarm_control_panel.py +++ b/homeassistant/components/bosch_alarm/alarm_control_panel.py @@ -34,6 +34,9 @@ async def async_setup_entry( ) +PARALLEL_UPDATES = 0 + + class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity): """An alarm control panel entity for a bosch alarm panel.""" diff --git a/homeassistant/components/bosch_alarm/quality_scale.yaml b/homeassistant/components/bosch_alarm/quality_scale.yaml index 0ea2b147c4a..5bbd1df0ebb 100644 --- a/homeassistant/components/bosch_alarm/quality_scale.yaml +++ b/homeassistant/components/bosch_alarm/quality_scale.yaml @@ -39,7 +39,7 @@ rules: entity-unavailable: todo integration-owner: done log-when-unavailable: todo - parallel-updates: todo + parallel-updates: done reauthentication-flow: done test-coverage: done From e74aeeab1ae4e4a6e58ff91018cadb19de6e085a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 11:41:16 +0200 Subject: [PATCH 0517/1175] Use runtime_data in iaqualink (#144988) --- .../components/iaqualink/__init__.py | 94 +++++++++---------- .../components/iaqualink/binary_sensor.py | 8 +- homeassistant/components/iaqualink/climate.py | 9 +- homeassistant/components/iaqualink/light.py | 9 +- homeassistant/components/iaqualink/sensor.py | 13 +-- homeassistant/components/iaqualink/switch.py | 10 +- 6 files changed, 62 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 26bffc4e982..68a8a093c09 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine +from dataclasses import dataclass from datetime import datetime from functools import wraps import logging @@ -19,11 +20,6 @@ from iaqualink.device import ( ) from iaqualink.exception import AqualinkServiceException -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant @@ -48,21 +44,27 @@ PLATFORMS = [ Platform.SWITCH, ] +type AqualinkConfigEntry = ConfigEntry[AqualinkRuntimeData] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + +@dataclass +class AqualinkRuntimeData: + """Runtime data for Aqualink.""" + + client: AqualinkClient + # These will contain the initialized devices + binary_sensors: list[AqualinkBinarySensor] + lights: list[AqualinkLight] + sensors: list[AqualinkSensor] + switches: list[AqualinkSwitch] + thermostats: list[AqualinkThermostat] + + +async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) -> bool: """Set up Aqualink from a config entry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - hass.data.setdefault(DOMAIN, {}) - - # These will contain the initialized devices - binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] - climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] - lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] - sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] - switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - aqualink = AqualinkClient(username, password, httpx_client=get_async_client(hass)) try: await aqualink.login() @@ -90,6 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await aqualink.close() return False + runtime_data = AqualinkRuntimeData( + aqualink, binary_sensors=[], lights=[], sensors=[], switches=[], thermostats=[] + ) for system in systems: try: devices = await system.get_devices() @@ -101,36 +106,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for dev in devices.values(): if isinstance(dev, AqualinkThermostat): - climates += [dev] + runtime_data.thermostats += [dev] elif isinstance(dev, AqualinkLight): - lights += [dev] + runtime_data.lights += [dev] elif isinstance(dev, AqualinkSwitch): - switches += [dev] + runtime_data.switches += [dev] elif isinstance(dev, AqualinkBinarySensor): - binary_sensors += [dev] + runtime_data.binary_sensors += [dev] elif isinstance(dev, AqualinkSensor): - sensors += [dev] + runtime_data.sensors += [dev] - platforms = [] - if binary_sensors: - _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) - platforms.append(Platform.BINARY_SENSOR) - if climates: - _LOGGER.debug("Got %s climates: %s", len(climates), climates) - platforms.append(Platform.CLIMATE) - if lights: - _LOGGER.debug("Got %s lights: %s", len(lights), lights) - platforms.append(Platform.LIGHT) - if sensors: - _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) - platforms.append(Platform.SENSOR) - if switches: - _LOGGER.debug("Got %s switches: %s", len(switches), switches) - platforms.append(Platform.SWITCH) + _LOGGER.debug( + "Got %s binary sensors: %s", + len(runtime_data.binary_sensors), + runtime_data.binary_sensors, + ) + _LOGGER.debug("Got %s lights: %s", len(runtime_data.lights), runtime_data.lights) + _LOGGER.debug("Got %s sensors: %s", len(runtime_data.sensors), runtime_data.sensors) + _LOGGER.debug( + "Got %s switches: %s", len(runtime_data.switches), runtime_data.switches + ) + _LOGGER.debug( + "Got %s thermostats: %s", + len(runtime_data.thermostats), + runtime_data.thermostats, + ) - hass.data[DOMAIN]["client"] = aqualink + entry.runtime_data = runtime_data - await hass.config_entries.async_forward_entry_setups(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_systems_update(_: datetime) -> None: """Refresh internal state for all systems.""" @@ -161,18 +165,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) -> bool: """Unload a config entry.""" - aqualink = hass.data[DOMAIN]["client"] - await aqualink.close() - - platforms_to_unload = [ - platform for platform in PLATFORMS if platform in hass.data[DOMAIN] - ] - - del hass.data[DOMAIN] - - return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload) + await entry.runtime_data.client.close() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) def refresh_system[_AqualinkEntityT: AqualinkEntity, **_P]( diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 94f2dc94f2c..3c260c7ef03 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -5,15 +5,13 @@ from __future__ import annotations from iaqualink.device import AqualinkBinarySensor from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from . import AqualinkConfigEntry from .entity import AqualinkEntity PARALLEL_UPDATES = 0 @@ -21,14 +19,14 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AqualinkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up discovered binary sensors.""" async_add_entities( ( HassAqualinkBinarySensor(dev) - for dev in hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] + for dev in config_entry.runtime_data.binary_sensors ), True, ) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index e0d5a1d7cf4..36aec12976a 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -9,19 +9,16 @@ 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 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system -from .const import DOMAIN +from . import AqualinkConfigEntry, refresh_system from .entity import AqualinkEntity from .utils import await_or_reraise @@ -32,12 +29,12 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AqualinkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up discovered switches.""" async_add_entities( - (HassAqualinkThermostat(dev) for dev in hass.data[DOMAIN][CLIMATE_DOMAIN]), + (HassAqualinkThermostat(dev) for dev in config_entry.runtime_data.thermostats), True, ) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 946971223b2..55b14065cef 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -9,17 +9,14 @@ from iaqualink.device import AqualinkLight from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, - DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system -from .const import DOMAIN +from . import AqualinkConfigEntry, refresh_system from .entity import AqualinkEntity from .utils import await_or_reraise @@ -28,12 +25,12 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AqualinkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up discovered lights.""" async_add_entities( - (HassAqualinkLight(dev) for dev in hass.data[DOMAIN][LIGHT_DOMAIN]), + (HassAqualinkLight(dev) for dev in config_entry.runtime_data.lights), True, ) diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index ef614ff066e..baeca799bc3 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -4,17 +4,12 @@ from __future__ import annotations from iaqualink.device import AqualinkSensor -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from . import AqualinkConfigEntry from .entity import AqualinkEntity PARALLEL_UPDATES = 0 @@ -22,12 +17,12 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AqualinkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up discovered sensors.""" async_add_entities( - (HassAqualinkSensor(dev) for dev in hass.data[DOMAIN][SENSOR_DOMAIN]), + (HassAqualinkSensor(dev) for dev in config_entry.runtime_data.sensors), True, ) diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index 959ff6e16c5..851554a1972 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -6,13 +6,11 @@ from typing import Any from iaqualink.device import AqualinkSwitch -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import refresh_system -from .const import DOMAIN +from . import AqualinkConfigEntry, refresh_system from .entity import AqualinkEntity from .utils import await_or_reraise @@ -21,12 +19,12 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: AqualinkConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up discovered switches.""" async_add_entities( - (HassAqualinkSwitch(dev) for dev in hass.data[DOMAIN][SWITCH_DOMAIN]), + (HassAqualinkSwitch(dev) for dev in config_entry.runtime_data.switches), True, ) From 0c5ee37721bd260299d78f94e29feecdff5f18ff Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Fri, 16 May 2025 21:43:31 +1200 Subject: [PATCH 0518/1175] Update bosch_alarm door switch strings so they are more user friendly (#144607) * Update door switch strings so they are more user friendly * Update door switch strings so they are more user friendly * Update door switch strings so they are more user friendly * update strings * update strings --- .../components/bosch_alarm/strings.json | 4 +- .../bosch_alarm/snapshots/test_switch.ambr | 282 +++++++++--------- tests/components/bosch_alarm/test_switch.py | 2 +- 3 files changed, 144 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 8edc4ba60b8..7a9d291a67f 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -58,7 +58,7 @@ "message": "Incorrect credentials for panel." }, "incorrect_door_state": { - "message": "Door cannot be manipulated while it is being cycled." + "message": "Door cannot be manipulated while it is momentarily unlocked." } }, "entity": { @@ -113,7 +113,7 @@ "name": "Secured" }, "cycling": { - "name": "Cycling" + "name": "Momentarily unlocked" }, "locked": { "name": "Locked" diff --git a/tests/components/bosch_alarm/snapshots/test_switch.ambr b/tests/components/bosch_alarm/snapshots/test_switch.ambr index ad508f257ba..0604787924f 100644 --- a/tests/components/bosch_alarm/snapshots/test_switch.ambr +++ b/tests/components/bosch_alarm/snapshots/test_switch.ambr @@ -1,51 +1,4 @@ # serializer version: 1 -# name: test_switch[None-amax_3000][switch.main_door_cycling-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_cycling', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cycling', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cycling', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[None-amax_3000][switch.main_door_cycling-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Cycling', - }), - 'context': , - 'entity_id': 'switch.main_door_cycling', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_switch[None-amax_3000][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -93,6 +46,53 @@ 'state': 'on', }) # --- +# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Momentarily unlocked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Momentarily unlocked', + }), + 'context': , + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switch[None-amax_3000][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -187,53 +187,6 @@ 'state': 'off', }) # --- -# name: test_switch[None-b5512][switch.main_door_cycling-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_cycling', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cycling', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cycling', - 'unique_id': '1234567890_door_1_cycling', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[None-b5512][switch.main_door_cycling-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Cycling', - }), - 'context': , - 'entity_id': 'switch.main_door_cycling', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_switch[None-b5512][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -281,6 +234,53 @@ 'state': 'on', }) # --- +# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Momentarily unlocked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '1234567890_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Momentarily unlocked', + }), + 'context': , + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switch[None-b5512][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -375,53 +375,6 @@ 'state': 'off', }) # --- -# name: test_switch[None-solution_3000][switch.main_door_cycling-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': None, - 'entity_id': 'switch.main_door_cycling', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Cycling', - 'platform': 'bosch_alarm', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cycling', - 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', - 'unit_of_measurement': None, - }) -# --- -# name: test_switch[None-solution_3000][switch.main_door_cycling-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Main Door Cycling', - }), - 'context': , - 'entity_id': 'switch.main_door_cycling', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_switch[None-solution_3000][switch.main_door_locked-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -469,6 +422,53 @@ 'state': 'on', }) # --- +# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Momentarily unlocked', + 'platform': 'bosch_alarm', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cycling', + 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Main Door Momentarily unlocked', + }), + 'context': , + 'entity_id': 'switch.main_door_momentarily_unlocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_switch[None-solution_3000][switch.main_door_secured-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/bosch_alarm/test_switch.py b/tests/components/bosch_alarm/test_switch.py index 6f25624dcbb..2c52c21099a 100644 --- a/tests/components/bosch_alarm/test_switch.py +++ b/tests/components/bosch_alarm/test_switch.py @@ -121,7 +121,7 @@ async def test_cycle_door( ) -> None: """Test that door state changes after unlocking the door.""" await setup_integration(hass, mock_config_entry) - entity_id = "switch.main_door_cycling" + entity_id = "switch.main_door_momentarily_unlocked" assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( SWITCH_DOMAIN, From cbb092f7926fd63379b43440c7e09862271fa0e6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 11:56:07 +0200 Subject: [PATCH 0519/1175] Move icloud services to separate module (#144980) --- homeassistant/components/icloud/__init__.py | 129 +----------------- homeassistant/components/icloud/account.py | 26 +--- homeassistant/components/icloud/const.py | 16 +++ homeassistant/components/icloud/services.py | 141 ++++++++++++++++++++ 4 files changed, 167 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/icloud/services.py diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 4ed66be6a4b..e3c50cded16 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -4,14 +4,10 @@ from __future__ import annotations from typing import Any -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import Store -from homeassistant.util import slugify from .account import IcloudAccount from .const import ( @@ -23,51 +19,7 @@ from .const import ( STORAGE_KEY, STORAGE_VERSION, ) - -ATTRIBUTION = "Data provided by Apple iCloud" - -# entity attributes -ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" -ATTR_BATTERY = "battery" -ATTR_BATTERY_STATUS = "battery_status" -ATTR_DEVICE_NAME = "device_name" -ATTR_DEVICE_STATUS = "device_status" -ATTR_LOW_POWER_MODE = "low_power_mode" -ATTR_OWNER_NAME = "owner_fullname" - -# services -SERVICE_ICLOUD_PLAY_SOUND = "play_sound" -SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" -SERVICE_ICLOUD_LOST_DEVICE = "lost_device" -SERVICE_ICLOUD_UPDATE = "update" -ATTR_ACCOUNT = "account" -ATTR_LOST_DEVICE_MESSAGE = "message" -ATTR_LOST_DEVICE_NUMBER = "number" -ATTR_LOST_DEVICE_SOUND = "sound" - -SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ACCOUNT): cv.string}) - -SERVICE_SCHEMA_PLAY_SOUND = vol.Schema( - {vol.Required(ATTR_ACCOUNT): cv.string, vol.Required(ATTR_DEVICE_NAME): cv.string} -) - -SERVICE_SCHEMA_DISPLAY_MESSAGE = vol.Schema( - { - vol.Required(ATTR_ACCOUNT): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, - vol.Optional(ATTR_LOST_DEVICE_SOUND): cv.boolean, - } -) - -SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( - { - vol.Required(ATTR_ACCOUNT): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_LOST_DEVICE_NUMBER): cv.string, - vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, - } -) +from .services import register_services async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -103,82 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - def play_sound(service: ServiceCall) -> None: - """Play sound on the device.""" - account = service.data[ATTR_ACCOUNT] - device_name: str = service.data[ATTR_DEVICE_NAME] - device_name = slugify(device_name.replace(" ", "", 99)) - - for device in _get_account(account).get_devices_with_name(device_name): - device.play_sound() - - def display_message(service: ServiceCall) -> None: - """Display a message on the device.""" - account = service.data[ATTR_ACCOUNT] - device_name: str = service.data[ATTR_DEVICE_NAME] - device_name = slugify(device_name.replace(" ", "", 99)) - message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) - sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) - - for device in _get_account(account).get_devices_with_name(device_name): - device.display_message(message, sound) - - def lost_device(service: ServiceCall) -> None: - """Make the device in lost state.""" - account = service.data[ATTR_ACCOUNT] - device_name: str = service.data[ATTR_DEVICE_NAME] - device_name = slugify(device_name.replace(" ", "", 99)) - number = service.data.get(ATTR_LOST_DEVICE_NUMBER) - message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) - - for device in _get_account(account).get_devices_with_name(device_name): - device.lost_device(number, message) - - def update_account(service: ServiceCall) -> None: - """Call the update function of an iCloud account.""" - if (account := service.data.get(ATTR_ACCOUNT)) is None: - for account in hass.data[DOMAIN].values(): - account.keep_alive() - else: - _get_account(account).keep_alive() - - def _get_account(account_identifier: str) -> IcloudAccount: - if account_identifier is None: - return None - - icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) - if icloud_account is None: - for account in hass.data[DOMAIN].values(): - if account.username == account_identifier: - icloud_account = account - - if icloud_account is None: - raise ValueError( - f"No iCloud account with username or name {account_identifier}" - ) - return icloud_account - - hass.services.async_register( - DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND - ) - - hass.services.async_register( - DOMAIN, - SERVICE_ICLOUD_DISPLAY_MESSAGE, - display_message, - schema=SERVICE_SCHEMA_DISPLAY_MESSAGE, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_ICLOUD_LOST_DEVICE, - lost_device, - schema=SERVICE_SCHEMA_LOST_DEVICE, - ) - - hass.services.async_register( - DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA - ) + register_services(hass) return True diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 9536cd9ee5c..e16d973277c 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -29,6 +29,13 @@ from homeassistant.util.dt import utcnow from homeassistant.util.location import distance from .const import ( + ATTR_ACCOUNT_FETCH_INTERVAL, + ATTR_BATTERY, + ATTR_BATTERY_STATUS, + ATTR_DEVICE_NAME, + ATTR_DEVICE_STATUS, + ATTR_LOW_POWER_MODE, + ATTR_OWNER_NAME, DEVICE_BATTERY_LEVEL, DEVICE_BATTERY_STATUS, DEVICE_CLASS, @@ -49,25 +56,6 @@ from .const import ( DOMAIN, ) -# entity attributes -ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" -ATTR_BATTERY = "battery" -ATTR_BATTERY_STATUS = "battery_status" -ATTR_DEVICE_NAME = "device_name" -ATTR_DEVICE_STATUS = "device_status" -ATTR_LOW_POWER_MODE = "low_power_mode" -ATTR_OWNER_NAME = "owner_fullname" - -# services -SERVICE_ICLOUD_PLAY_SOUND = "play_sound" -SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" -SERVICE_ICLOUD_LOST_DEVICE = "lost_device" -SERVICE_ICLOUD_UPDATE = "update" -ATTR_ACCOUNT = "account" -ATTR_LOST_DEVICE_MESSAGE = "message" -ATTR_LOST_DEVICE_NUMBER = "number" -ATTR_LOST_DEVICE_SOUND = "sound" - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index b7ea2691ca4..72b1d496121 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -4,6 +4,8 @@ from homeassistant.const import Platform DOMAIN = "icloud" +ATTRIBUTION = "Data provided by Apple iCloud" + CONF_WITH_FAMILY = "with_family" CONF_MAX_INTERVAL = "max_interval" CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" @@ -84,3 +86,17 @@ DEVICE_STATUS_CODES = { "203": "pending", "204": "unregistered", } + + +# entity / service attributes +ATTR_ACCOUNT = "account" +ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" +ATTR_BATTERY = "battery" +ATTR_BATTERY_STATUS = "battery_status" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_STATUS = "device_status" +ATTR_LOW_POWER_MODE = "low_power_mode" +ATTR_LOST_DEVICE_MESSAGE = "message" +ATTR_LOST_DEVICE_NUMBER = "number" +ATTR_LOST_DEVICE_SOUND = "sound" +ATTR_OWNER_NAME = "owner_fullname" diff --git a/homeassistant/components/icloud/services.py b/homeassistant/components/icloud/services.py new file mode 100644 index 00000000000..5897fcb06f7 --- /dev/null +++ b/homeassistant/components/icloud/services.py @@ -0,0 +1,141 @@ +"""The iCloud component.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.util import slugify + +from .account import IcloudAccount +from .const import ( + ATTR_ACCOUNT, + ATTR_DEVICE_NAME, + ATTR_LOST_DEVICE_MESSAGE, + ATTR_LOST_DEVICE_NUMBER, + ATTR_LOST_DEVICE_SOUND, + DOMAIN, +) + +# services +SERVICE_ICLOUD_PLAY_SOUND = "play_sound" +SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" +SERVICE_ICLOUD_LOST_DEVICE = "lost_device" +SERVICE_ICLOUD_UPDATE = "update" + +SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ACCOUNT): cv.string}) + +SERVICE_SCHEMA_PLAY_SOUND = vol.Schema( + {vol.Required(ATTR_ACCOUNT): cv.string, vol.Required(ATTR_DEVICE_NAME): cv.string} +) + +SERVICE_SCHEMA_DISPLAY_MESSAGE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + vol.Optional(ATTR_LOST_DEVICE_SOUND): cv.boolean, + } +) + +SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_NUMBER): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + } +) + + +def play_sound(service: ServiceCall) -> None: + """Play sound on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name: str = service.data[ATTR_DEVICE_NAME] + device_name = slugify(device_name.replace(" ", "", 99)) + + for device in _get_account(service.hass, account).get_devices_with_name( + device_name + ): + device.play_sound() + + +def display_message(service: ServiceCall) -> None: + """Display a message on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name: str = service.data[ATTR_DEVICE_NAME] + device_name = slugify(device_name.replace(" ", "", 99)) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) + + for device in _get_account(service.hass, account).get_devices_with_name( + device_name + ): + device.display_message(message, sound) + + +def lost_device(service: ServiceCall) -> None: + """Make the device in lost state.""" + account = service.data[ATTR_ACCOUNT] + device_name: str = service.data[ATTR_DEVICE_NAME] + device_name = slugify(device_name.replace(" ", "", 99)) + number = service.data.get(ATTR_LOST_DEVICE_NUMBER) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + + for device in _get_account(service.hass, account).get_devices_with_name( + device_name + ): + device.lost_device(number, message) + + +def update_account(service: ServiceCall) -> None: + """Call the update function of an iCloud account.""" + if (account := service.data.get(ATTR_ACCOUNT)) is None: + for account in service.hass.data[DOMAIN].values(): + account.keep_alive() + else: + _get_account(service.hass, account).keep_alive() + + +def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount: + if account_identifier is None: + return None + + icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) + if icloud_account is None: + for account in hass.data[DOMAIN].values(): + if account.username == account_identifier: + icloud_account = account + + if icloud_account is None: + raise ValueError( + f"No iCloud account with username or name {account_identifier}" + ) + return icloud_account + + +def register_services(hass: HomeAssistant) -> None: + """Set up an iCloud account from a config entry.""" + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_DISPLAY_MESSAGE, + display_message, + schema=SERVICE_SCHEMA_DISPLAY_MESSAGE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_LOST_DEVICE, + lost_device, + schema=SERVICE_SCHEMA_LOST_DEVICE, + ) + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA + ) From 97869636f8f472c6c8d6059145ccfc901af335d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 11:59:11 +0200 Subject: [PATCH 0520/1175] Use typed config entry in Habitica coordinator (#144956) --- homeassistant/components/habitica/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/habitica/coordinator.py b/homeassistant/components/habitica/coordinator.py index 3c3a16f591a..d0eb60312b4 100644 --- a/homeassistant/components/habitica/coordinator.py +++ b/homeassistant/components/habitica/coordinator.py @@ -52,10 +52,10 @@ type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator] class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]): """Habitica Data Update Coordinator.""" - config_entry: ConfigEntry + config_entry: HabiticaConfigEntry def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, habitica: Habitica + self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica ) -> None: """Initialize the Habitica data coordinator.""" super().__init__( From b4a1bdcb837a02facee1c241d9a24b41eb00bec5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 12:07:19 +0200 Subject: [PATCH 0521/1175] Move huisbaasje coordinator to separate module (#144955) --- .../components/huisbaasje/__init__.py | 104 +-------------- .../components/huisbaasje/coordinator.py | 126 ++++++++++++++++++ homeassistant/components/huisbaasje/sensor.py | 19 +-- 3 files changed, 136 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/huisbaasje/coordinator.py diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index f9703f67df5..e2414566fcb 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,29 +1,15 @@ """The EnergyFlip integration.""" -import asyncio -from datetime import timedelta import logging -from typing import Any from energyflip import EnergyFlip, EnergyFlipException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - DATA_COORDINATOR, - DOMAIN, - FETCH_TIMEOUT, - POLLING_INTERVAL, - SENSOR_TYPE_RATE, - SENSOR_TYPE_THIS_DAY, - SENSOR_TYPE_THIS_MONTH, - SENSOR_TYPE_THIS_WEEK, - SENSOR_TYPE_THIS_YEAR, - SOURCE_TYPES, -) +from .const import DATA_COORDINATOR, DOMAIN, FETCH_TIMEOUT, SOURCE_TYPES +from .coordinator import EnergyFlipUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -47,18 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Authentication failed: %s", str(exception)) return False - async def async_update_data() -> dict[str, dict[str, Any]]: - return await async_update_energyflip(energyflip) - # Create a coordinator for polling updates - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name="sensor", - update_method=async_update_data, - update_interval=timedelta(seconds=POLLING_INTERVAL), - ) + coordinator = EnergyFlipUpdateCoordinator(hass, entry, energyflip) await coordinator.async_config_entry_first_refresh() @@ -81,77 +57,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -async def async_update_energyflip(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: - """Update the data by performing a request to EnergyFlip.""" - try: - # Note: TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with asyncio.timeout(FETCH_TIMEOUT): - if not energyflip.is_authenticated(): - _LOGGER.warning("EnergyFlip is unauthenticated. Reauthenticating") - await energyflip.authenticate() - - current_measurements = await energyflip.current_measurements() - - return { - source_type: { - SENSOR_TYPE_RATE: _get_measurement_rate( - current_measurements, source_type - ), - SENSOR_TYPE_THIS_DAY: _get_cumulative_value( - current_measurements, source_type, SENSOR_TYPE_THIS_DAY - ), - SENSOR_TYPE_THIS_WEEK: _get_cumulative_value( - current_measurements, source_type, SENSOR_TYPE_THIS_WEEK - ), - SENSOR_TYPE_THIS_MONTH: _get_cumulative_value( - current_measurements, source_type, SENSOR_TYPE_THIS_MONTH - ), - SENSOR_TYPE_THIS_YEAR: _get_cumulative_value( - current_measurements, source_type, SENSOR_TYPE_THIS_YEAR - ), - } - for source_type in SOURCE_TYPES - } - except EnergyFlipException as exception: - raise UpdateFailed(f"Error communicating with API: {exception}") from exception - - -def _get_cumulative_value( - current_measurements: dict, - source_type: str, - period_type: str, -): - """Get the cumulative energy consumption for a certain period. - - :param current_measurements: The result from the EnergyFlip client - :param source_type: The source of energy (electricity or gas) - :param period_type: The period for which cumulative value should be given. - """ - if source_type in current_measurements: - if ( - period_type in current_measurements[source_type] - and current_measurements[source_type][period_type] is not None - ): - return current_measurements[source_type][period_type]["value"] - else: - _LOGGER.error( - "Source type %s not present in %s", source_type, current_measurements - ) - return None - - -def _get_measurement_rate(current_measurements: dict, source_type: str): - if source_type in current_measurements: - if ( - "measurement" in current_measurements[source_type] - and current_measurements[source_type]["measurement"] is not None - ): - return current_measurements[source_type]["measurement"]["rate"] - else: - _LOGGER.error( - "Source type %s not present in %s", source_type, current_measurements - ) - return None diff --git a/homeassistant/components/huisbaasje/coordinator.py b/homeassistant/components/huisbaasje/coordinator.py new file mode 100644 index 00000000000..9467e1232c2 --- /dev/null +++ b/homeassistant/components/huisbaasje/coordinator.py @@ -0,0 +1,126 @@ +"""The EnergyFlip integration.""" + +import asyncio +from datetime import timedelta +import logging +from typing import Any + +from energyflip import EnergyFlip, EnergyFlipException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + FETCH_TIMEOUT, + POLLING_INTERVAL, + SENSOR_TYPE_RATE, + SENSOR_TYPE_THIS_DAY, + SENSOR_TYPE_THIS_MONTH, + SENSOR_TYPE_THIS_WEEK, + SENSOR_TYPE_THIS_YEAR, + SOURCE_TYPES, +) + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +class EnergyFlipUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): + """EnergyFlip data update coordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + energyflip: EnergyFlip, + ) -> None: + """Initialize the Huisbaasje data coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=config_entry, + name="sensor", + update_interval=timedelta(seconds=POLLING_INTERVAL), + ) + + self._energyflip = energyflip + + async def _async_update_data(self) -> dict[str, dict[str, Any]]: + """Update the data by performing a request to EnergyFlip.""" + try: + # Note: TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with asyncio.timeout(FETCH_TIMEOUT): + if not self._energyflip.is_authenticated(): + _LOGGER.warning("EnergyFlip is unauthenticated. Reauthenticating") + await self._energyflip.authenticate() + + current_measurements = await self._energyflip.current_measurements() + + return { + source_type: { + SENSOR_TYPE_RATE: _get_measurement_rate( + current_measurements, source_type + ), + SENSOR_TYPE_THIS_DAY: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_DAY + ), + SENSOR_TYPE_THIS_WEEK: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_WEEK + ), + SENSOR_TYPE_THIS_MONTH: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_MONTH + ), + SENSOR_TYPE_THIS_YEAR: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_YEAR + ), + } + for source_type in SOURCE_TYPES + } + except EnergyFlipException as exception: + raise UpdateFailed( + f"Error communicating with API: {exception}" + ) from exception + + +def _get_cumulative_value( + current_measurements: dict, + source_type: str, + period_type: str, +): + """Get the cumulative energy consumption for a certain period. + + :param current_measurements: The result from the EnergyFlip client + :param source_type: The source of energy (electricity or gas) + :param period_type: The period for which cumulative value should be given. + """ + if source_type in current_measurements: + if ( + period_type in current_measurements[source_type] + and current_measurements[source_type][period_type] is not None + ): + return current_measurements[source_type][period_type]["value"] + else: + _LOGGER.error( + "Source type %s not present in %s", source_type, current_measurements + ) + return None + + +def _get_measurement_rate(current_measurements: dict, source_type: str): + if source_type in current_measurements: + if ( + "measurement" in current_measurements[source_type] + and current_measurements[source_type]["measurement"] is not None + ): + return current_measurements[source_type]["measurement"]["rate"] + else: + _LOGGER.error( + "Source type %s not present in %s", source_type, current_measurements + ) + return None diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 91c953b2182..9c471ff64ec 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass import logging -from typing import Any from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, @@ -31,10 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DATA_COORDINATOR, @@ -45,6 +41,7 @@ from .const import ( SENSOR_TYPE_THIS_WEEK, SENSOR_TYPE_THIS_YEAR, ) +from .coordinator import EnergyFlipUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -222,9 +219,9 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]] = hass.data[DOMAIN][ - config_entry.entry_id - ][DATA_COORDINATOR] + coordinator: EnergyFlipUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ + DATA_COORDINATOR + ] user_id = config_entry.data[CONF_ID] async_add_entities( @@ -233,9 +230,7 @@ async def async_setup_entry( ) -class EnergyFlipSensor( - CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]], SensorEntity -): +class EnergyFlipSensor(CoordinatorEntity[EnergyFlipUpdateCoordinator], SensorEntity): """Defines a EnergyFlip sensor.""" entity_description: EnergyFlipSensorEntityDescription @@ -243,7 +238,7 @@ class EnergyFlipSensor( def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], + coordinator: EnergyFlipUpdateCoordinator, user_id: str, description: EnergyFlipSensorEntityDescription, ) -> None: From 3208815e102d2a83861b01e94868b4b6de9d4aaf Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 12:08:32 +0200 Subject: [PATCH 0522/1175] Fix non-DHW heat pump in SmartThings (#145008) --- .../components/smartthings/water_heater.py | 4 + tests/components/smartthings/conftest.py | 1 + .../da_sac_ehs_000001_sub_1.json | 704 ++++++++++++++++++ .../devices/da_sac_ehs_000001_sub_1.json | 237 ++++++ .../smartthings/snapshots/test_init.ambr | 33 + .../smartthings/snapshots/test_sensor.ambr | 376 ++++++++++ 6 files changed, 1355 insertions(+) create mode 100644 tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub_1.json create mode 100644 tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub_1.json diff --git a/homeassistant/components/smartthings/water_heater.py b/homeassistant/components/smartthings/water_heater.py index fe09531931b..addbfed2ec4 100644 --- a/homeassistant/components/smartthings/water_heater.py +++ b/homeassistant/components/smartthings/water_heater.py @@ -54,6 +54,10 @@ async def async_setup_entry( Capability.CUSTOM_OUTING_MODE, ) ) + and device.status[MAIN][Capability.TEMPERATURE_MEASUREMENT][ + Attribute.TEMPERATURE + ].value + is not None ) diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index be744ef7c33..6cad487c0bb 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -118,6 +118,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "vd_sensor_light_2023", "iphone", "da_sac_ehs_000001_sub", + "da_sac_ehs_000001_sub_1", "da_sac_ehs_000002_sub", "da_ac_ehs_01001", "da_wm_dw_000001", diff --git a/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub_1.json b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub_1.json new file mode 100644 index 00000000000..a6ced0e16e5 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub_1.json @@ -0,0 +1,704 @@ +{ + "components": { + "main": { + "samsungce.ehsBoosterHeater": { + "status": { + "value": "off", + "timestamp": "2025-05-14T22:47:01.955Z" + } + }, + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": null + }, + "maximumSetpoint": { + "value": null + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": null + }, + "airConditionerMode": { + "value": null + } + }, + "samsungce.deviceIdentification": { + "micomAssayCode": { + "value": null + }, + "modelName": { + "value": null + }, + "serialNumber": { + "value": null + }, + "serialNumberExtra": { + "value": null + }, + "modelClassificationCode": { + "value": null + }, + "description": { + "value": null + }, + "releaseYear": { + "value": 23, + "timestamp": "2025-04-14T15:04:59.182Z" + }, + "binaryId": { + "value": "SAC_EHS_MONO", + "timestamp": "2025-05-15T18:27:08.954Z" + } + }, + "switch": { + "switch": { + "value": null + } + }, + "ocf": { + "st": { + "value": "2025-05-14T23:22:43Z", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mndt": { + "value": "", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnfv": { + "value": "20250317.1", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnhw": { + "value": "", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "di": { + "value": "6a7d5349-0a66-0277-058d-000001200101", + "timestamp": "2025-05-14T22:47:01.717Z" + }, + "mnsl": { + "value": "", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "dmv": { + "value": "res.1.1.0,sh.1.1.0", + "timestamp": "2025-05-14T22:47:01.717Z" + }, + "n": { + "value": "Heat Pump", + "timestamp": "2025-05-14T22:47:01.717Z" + }, + "mnmo": { + "value": "SAC_EHS_MONO|231215|61007400001700000400000000000000", + "timestamp": "2025-05-15T18:27:08.954Z" + }, + "vid": { + "value": "DA-SAC-EHS-000001-SUB", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnmn": { + "value": "Samsung Electronics", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnml": { + "value": "", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnpv": { + "value": "4.0", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "mnos": { + "value": "Tizen", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "pi": { + "value": "6a7d5349-0a66-0277-058d-000001200101", + "timestamp": "2025-05-14T22:47:01.715Z" + }, + "icv": { + "value": "core.1.1.0", + "timestamp": "2025-05-14T22:47:01.717Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": null + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": [ + "remoteControlStatus", + "samsungce.systemAirConditionerReservation", + "demandResponseLoadControl" + ], + "timestamp": "2025-05-12T23:01:07.651Z" + } + }, + "samsungce.sensingOnSuspendMode": { + "sensingOnSuspendMode": { + "value": "available", + "timestamp": "2025-04-14T15:04:59.182Z" + } + }, + "samsungce.driverVersion": { + "versionNumber": { + "value": 25010101, + "timestamp": "2025-04-14T15:04:59.182Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": null + } + }, + "samsungce.ehsDiverterValve": { + "position": { + "value": "room", + "timestamp": "2025-05-06T09:03:32.916Z" + } + }, + "custom.deviceReportStateConfiguration": { + "reportStateRealtimePeriod": { + "value": "enabled", + "timestamp": "2025-05-06T09:03:32.870Z" + }, + "reportStateRealtime": { + "value": { + "state": "disabled" + }, + "timestamp": "2025-05-13T20:54:48.806Z" + }, + "reportStatePeriod": { + "value": "enabled", + "timestamp": "2025-05-06T09:03:32.870Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": null + } + }, + "demandResponseLoadControl": { + "drlcStatus": { + "value": { + "drlcType": 1, + "drlcLevel": -1, + "start": "1970-01-01T00:00:00Z", + "duration": 0, + "override": false + }, + "timestamp": "2025-05-06T22:47:03.830Z" + } + }, + "powerConsumptionReport": { + "powerConsumption": { + "value": { + "energy": 297584.0, + "deltaEnergy": 0, + "power": 0.015, + "powerEnergy": 0.004501854166388512, + "persistedEnergy": 297584.0, + "energySaved": 0, + "start": "2025-05-15T20:52:02Z", + "end": "2025-05-15T21:10:02Z" + }, + "timestamp": "2025-05-15T21:10:02.449Z" + } + }, + "samsungce.ehsCycleData": { + "outdoor": { + "value": [ + { + "timestamp": "2025-05-15T21:48:32Z", + "data": "000000005B62414A410207D0000000000000" + }, + { + "timestamp": "2025-05-15T21:53:32Z", + "data": "000000005A61414A410207D0000000000000" + }, + { + "timestamp": "2025-05-15T21:58:32Z", + "data": "000000005960424A420207D0000000000000" + } + ], + "unit": "C", + "timestamp": "2025-05-15T21:02:33.268Z" + }, + "indoor": { + "value": [ + { + "timestamp": "2025-05-15T21:48:32Z", + "data": "48055A050505000000000000000000000000000000008E85" + }, + { + "timestamp": "2025-05-15T21:53:32Z", + "data": "470559050505000000000000000000000000000000008E8B" + }, + { + "timestamp": "2025-05-15T21:58:32Z", + "data": "470559050505000000000000000000000000000000008E90" + } + ], + "unit": "C", + "timestamp": "2025-05-15T21:02:33.268Z" + } + }, + "custom.outingMode": { + "outingMode": { + "value": "off", + "timestamp": "2025-05-06T09:03:32.781Z" + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": null + } + }, + "refresh": {}, + "samsungce.ehsFsvSettings": { + "fsvSettings": { + "value": [ + { + "id": "1031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 37, + "maxValue": 75, + "value": 75, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 15, + "maxValue": 37, + "value": 25, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1051", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 50, + "maxValue": 70, + "value": 60, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "1052", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 30, + "maxValue": 40, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2011", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -20, + "maxValue": 5, + "value": -2, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 10, + "maxValue": 20, + "value": 15, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2021", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 60, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2022", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2031", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 60, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2032", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 17, + "maxValue": 75, + "value": 40, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "2091", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 1, + "isValid": true + }, + { + "id": "2092", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 4, + "value": 1, + "isValid": true + }, + { + "id": "2093", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 1, + "maxValue": 4, + "value": 4, + "isValid": true + }, + { + "id": "3011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 0, + "isValid": true + }, + { + "id": "3071", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + }, + { + "id": "4011", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 1, + "isValid": true + }, + { + "id": "4012", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": -15, + "maxValue": 20, + "value": 0, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4021", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 2, + "value": 0, + "isValid": true + }, + { + "id": "4042", + "inUse": true, + "resolution": 1, + "type": "temperature", + "minValue": 5, + "maxValue": 15, + "value": 10, + "isValid": true, + "temperatureUnit": "C" + }, + { + "id": "4061", + "inUse": true, + "resolution": 1, + "type": "etc", + "minValue": 0, + "maxValue": 1, + "value": 0, + "isValid": true + } + ], + "timestamp": "2025-05-07T18:12:08.200Z" + } + }, + "execute": { + "data": { + "value": null + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": null + } + }, + "samsungce.softwareVersion": { + "versions": { + "value": [ + { + "id": "0", + "swType": "Software", + "versionNumber": "DB91-02102A 2025-03-17", + "description": "Version" + }, + { + "id": "1", + "swType": "Firmware", + "versionNumber": "DB91-02100A 2020-07-10", + "description": "Version" + }, + { + "id": "2", + "swType": "Firmware", + "versionNumber": "DB91-02501A 2023-12-15", + "description": "" + }, + { + "id": "3", + "swType": "Firmware", + "versionNumber": "DB91-02572A 2024-07-17", + "description": "EHS MONO LOWTEMP" + } + ], + "timestamp": "2025-05-13T06:57:54.491Z" + } + }, + "remoteControlStatus": { + "remoteControlEnabled": { + "value": "true", + "timestamp": "2025-05-06T09:03:32.949Z" + } + }, + "custom.energyType": { + "energyType": { + "value": "2.0", + "timestamp": "2025-04-14T15:04:59.439Z" + }, + "energySavingSupport": { + "value": false, + "timestamp": "2025-04-14T15:04:59.418Z" + }, + "drMaxDuration": { + "value": null + }, + "energySavingLevel": { + "value": null + }, + "energySavingInfo": { + "value": null + }, + "supportedEnergySavingLevels": { + "value": null + }, + "energySavingOperation": { + "value": null + }, + "notificationTemplateID": { + "value": null + }, + "energySavingOperationSupport": { + "value": null + } + }, + "samsungce.softwareUpdate": { + "targetModule": { + "value": null + }, + "otnDUID": { + "value": null + }, + "lastUpdatedDate": { + "value": null + }, + "availableModules": { + "value": [], + "timestamp": "2025-04-14T15:04:59.272Z" + }, + "newVersionAvailable": { + "value": false, + "timestamp": "2025-05-06T09:03:32.778Z" + }, + "operatingState": { + "value": null + }, + "progress": { + "value": null + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": null + } + } + }, + "INDOOR": { + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "samsungce.ehsThermostat": { + "connectionState": { + "value": "connected", + "timestamp": "2025-05-06T09:03:32.830Z" + } + }, + "samsungce.toggleSwitch": { + "switch": { + "value": "on", + "timestamp": "2025-05-06T09:03:32.776Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.systemAirConditionerReservation"], + "timestamp": "2025-04-14T15:04:59.182Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 31, + "unit": "C", + "timestamp": "2025-05-15T21:08:08.464Z" + } + }, + "custom.thermostatSetpointControl": { + "minimumSetpoint": { + "value": 25, + "unit": "C", + "timestamp": "2025-05-14T22:23:55.963Z" + }, + "maximumSetpoint": { + "value": 65, + "unit": "C", + "timestamp": "2025-05-06T09:03:32.729Z" + } + }, + "samsungce.ehsDefrostMode": { + "status": { + "value": "off", + "timestamp": "2025-05-06T09:03:32.830Z" + } + }, + "airConditionerMode": { + "availableAcModes": { + "value": null + }, + "supportedAcModes": { + "value": ["auto", "cool", "heat"], + "timestamp": "2025-05-06T09:03:32.830Z" + }, + "airConditionerMode": { + "value": "heat", + "timestamp": "2025-05-06T09:03:32.830Z" + } + }, + "samsungce.ehsTemperatureReference": { + "temperatureReference": { + "value": "water", + "timestamp": "2025-05-06T09:03:32.729Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 30, + "unit": "C", + "timestamp": "2025-05-14T22:23:55.326Z" + } + }, + "samsungce.sacDisplayCondition": { + "switch": { + "value": "enabled", + "timestamp": "2025-05-06T09:03:32.776Z" + } + }, + "switch": { + "switch": { + "value": "on", + "timestamp": "2025-05-15T18:27:08.950Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub_1.json b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub_1.json new file mode 100644 index 00000000000..fd1dd902b1e --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub_1.json @@ -0,0 +1,237 @@ +{ + "items": [ + { + "deviceId": "6a7d5349-0a66-0277-058d-000001200101", + "name": "Heat Pump", + "label": "Heat Pump Main", + "manufacturerName": "Samsung Electronics", + "presentationId": "DA-SAC-EHS-000001-SUB", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "c411c5a8-ace8-4fa8-bb60-91525ac83273", + "ownerId": "d1da8ead-6b9d-64a2-ca29-2a25e4c259ca", + "roomId": "e6fa0aa4-08e7-45f7-8ec7-35c9c60908f9", + "deviceTypeName": "Samsung OCF Air Conditioner", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "ocf", + "version": 1 + }, + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "powerConsumptionReport", + "version": 1 + }, + { + "id": "demandResponseLoadControl", + "version": 1 + }, + { + "id": "remoteControlStatus", + "version": 1 + }, + { + "id": "refresh", + "version": 1 + }, + { + "id": "execute", + "version": 1 + }, + { + "id": "custom.energyType", + "version": 1 + }, + { + "id": "custom.outingMode", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "custom.deviceReportStateConfiguration", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "samsungce.deviceIdentification", + "version": 1 + }, + { + "id": "samsungce.driverVersion", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.sensingOnSuspendMode", + "version": 1 + }, + { + "id": "samsungce.softwareUpdate", + "version": 1 + }, + { + "id": "samsungce.softwareVersion", + "version": 1 + }, + { + "id": "samsungce.ehsBoosterHeater", + "version": 1 + }, + { + "id": "samsungce.ehsDiverterValve", + "version": 1 + }, + { + "id": "samsungce.ehsFsvSettings", + "version": 1 + }, + { + "id": "samsungce.ehsCycleData", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 + } + ], + "categories": [ + { + "name": "AirConditioner", + "categoryType": "manufacturer" + } + ], + "optional": false + }, + { + "id": "INDOOR", + "label": "INDOOR", + "capabilities": [ + { + "id": "switch", + "version": 1 + }, + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "airConditionerMode", + "version": 1 + }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, + { + "id": "custom.thermostatSetpointControl", + "version": 1 + }, + { + "id": "samsungce.ehsDefrostMode", + "version": 1 + }, + { + "id": "samsungce.ehsTemperatureReference", + "version": 1 + }, + { + "id": "samsungce.sacDisplayCondition", + "version": 1 + }, + { + "id": "samsungce.ehsThermostat", + "version": 1 + }, + { + "id": "samsungce.toggleSwitch", + "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 + } + ], + "categories": [ + { + "name": "Other", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2025-04-14T15:04:59.106Z", + "parentDeviceId": "6a7d5349-0a66-0277-058d-7c8a76501360", + "profile": { + "id": "89782721-6841-3ef6-a699-28e069d28b8b" + }, + "ocf": { + "ocfDeviceType": "oic.d.airconditioner", + "name": "Heat Pump", + "specVersion": "core.1.1.0", + "verticalDomainSpecVersion": "1.2.1", + "manufacturerName": "Samsung Electronics", + "modelNumber": "SAC_EHS_MONO|231215|61007400001700000400000000000000", + "platformVersion": "4.0", + "platformOS": "Tizen", + "hwVersion": "", + "firmwareVersion": "20250317.1", + "vendorId": "DA-SAC-EHS-000001-SUB", + "vendorResourceClientServerVersion": "4.0.54", + "lastSignupTime": "2025-04-14T15:04:58.476041486Z", + "transferCandidate": true, + "additionalAuthCodeRequired": false, + "modelCode": "" + }, + "type": "OCF", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 46c92bd2388..ff54a75c3f2 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -794,6 +794,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[da_sac_ehs_000001_sub_1] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': '', + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '6a7d5349-0a66-0277-058d-000001200101', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Samsung Electronics', + 'model': 'SAC_EHS_MONO', + 'model_id': None, + 'name': 'Heat Pump Main', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '20250317.1', + 'via_device_id': None, + }) +# --- # name: test_devices[da_sac_ehs_000002_sub] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 6f31a875d5c..3732a338964 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -6147,6 +6147,382 @@ 'state': '54.3', }) # --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_cooling_set_point-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_cooling_set_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': 'Cooling set point', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'thermostat_cooling_setpoint', + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_cooling_set_point-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Heat Pump Main Cooling set point', + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_cooling_set_point', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_energy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat Pump Main Energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '297.584', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy_difference-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_energy_difference', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy difference', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_difference', + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy_difference-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat Pump Main Energy difference', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_energy_difference', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy_saved-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_energy_saved', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy saved', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_saved', + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_energySaved_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy_saved-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat Pump Main Energy saved', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_energy_saved', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_power_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Heat Pump Main Power', + 'power_consumption_end': '2025-05-15T21:10:02Z', + 'power_consumption_start': '2025-05-15T20:52:02Z', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.015', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_power_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_power_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power energy', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_energy', + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_power_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Heat Pump Main Power energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_power_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.50185416638851e-06', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Heat Pump Main Temperature', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_cooling_set_point-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ff4aed1f6eea231173e2bde68eba27758845a875 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 12:22:17 +0200 Subject: [PATCH 0523/1175] Fix errors in strings in SmartThings (#145030) --- homeassistant/components/smartthings/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c2719c3e2f9..1113083c00f 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -575,31 +575,31 @@ }, "deprecated_switch_appliance_scripts": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch `{entity_id}` is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new binary sensor in the above automations or scripts and disable the entity to fix this issue." + "description": "The switch `{entity_id}` is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new binary sensor in the above automations or scripts and disable the switch to fix this issue." }, "deprecated_switch_media_player": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch `{entity_id}` is deprecated and a media player entity has been added to replace it.\n\nPlease use the new media player entity in the above automations or scripts and disable the entity to fix this issue." + "description": "The switch `{entity_id}` is deprecated and a media player entity has been added to replace it.\n\nPlease update your dashboards and templates accordingly and disable the switch to fix this issue." }, "deprecated_switch_media_player_scripts": { "title": "[%key:component::smartthings::issues::deprecated_switch_appliance::title%]", - "description": "The switch `{entity_id}` is deprecated and a media player entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new media player entity in the above automations or scripts and disable the entity to fix this issue." + "description": "The switch `{entity_id}` is deprecated and a media player entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new media player entity in the above automations or scripts and disable the switch to fix this issue." }, "deprecated_switch_dhw": { "title": "Heat pump switch deprecated", - "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nPlease use the new water heater entity in the above automations or scripts and disable the entity to fix this issue." + "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nPlease update your dashboards and templates accordingly and disable the switch to fix this issue." }, "deprecated_switch_dhw_scripts": { "title": "[%key:component::smartthings::issues::deprecated_switch_dhw::title%]", - "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new water heater entity in the above automations or scripts and disable the entity to fix this issue." + "description": "The switch `{entity_id}` is deprecated and a water heater entity has been added to replace it.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new water heater entity in the above automations or scripts and disable the switch to fix this issue." }, "deprecated_media_player": { "title": "Media player sensors deprecated", - "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nPlease update your dashboards, templates to use the new media player entity and disable the entity to fix this issue." + "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nPlease update your dashboards and templates to use the new media player entity and disable the sensor to fix this issue." }, "deprecated_media_player_scripts": { "title": "Deprecated sensor detected in some automations or scripts", - "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the new media player entity and disable the entity to fix this issue." + "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the new media player entity and disable the sensor to fix this issue." } } } From 07db244f9193040fd3cf9bc477d74fbc36184c12 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 16 May 2025 12:58:28 +0200 Subject: [PATCH 0524/1175] Cleanup wrongly combined Reolink devices (#144771) --- homeassistant/components/reolink/__init__.py | 121 ++++++++++++------- homeassistant/components/reolink/util.py | 9 +- tests/components/reolink/test_init.py | 62 +++++++++- 3 files changed, 147 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 433af396d63..48b5dc1a3d6 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,53 +364,90 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) + for dev_id in device.identifiers: + (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) + if not device_uid: + continue - if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: - if ch is None: - new_device_id = f"{host.unique_id}" - else: - new_device_id = f"{host.unique_id}_{device_uid[1]}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", device_uid, new_device_id - ) - new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device(device.id, new_identifiers=new_identifiers) - - if ch is None or is_chime: - continue # Do not consider the NVR itself or chimes - - # Check for wrongfully added MAC of the NVR/Hub to the camera - # Can be removed in HA 2025.12 - host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) - if host_connnection in device.connections: - new_connections = device.connections.copy() - new_connections.remove(host_connnection) - device_reg.async_update_device(device.id, new_connections=new_connections) - - ch_device_ids[device.id] = ch - if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): - if host.api.supported(None, "UID"): - new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" - else: - new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", device_uid, new_device_id - ) - new_identifiers = {(DOMAIN, new_device_id)} - existing_device = device_reg.async_get_device(identifiers=new_identifiers) - if existing_device is None: + if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: + if ch is None: + new_device_id = f"{host.unique_id}" + else: + new_device_id = f"{host.unique_id}_{device_uid[1]}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} device_reg.async_update_device( device.id, new_identifiers=new_identifiers ) - else: - _LOGGER.warning( - "Reolink device with uid %s already exists, " - "removing device with uid %s", - new_device_id, - device_uid, + + if ch is None or is_chime: + continue # Do not consider the NVR itself or chimes + + # Check for wrongfully combined host with NVR entities in one device + # Can be removed in HA 2025.12 + if (DOMAIN, host.unique_id) in device.identifiers: + new_identifiers = device.identifiers.copy() + for old_id in device.identifiers: + if old_id[0] == DOMAIN and old_id[1] != host.unique_id: + new_identifiers.remove(old_id) + _LOGGER.debug( + "Updating Reolink device identifiers from %s to %s", + device.identifiers, + new_identifiers, ) - device_reg.async_remove_device(device.id) + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + break + + # Check for wrongfully added MAC of the NVR/Hub to the camera + # Can be removed in HA 2025.12 + host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) + if host_connnection in device.connections: + new_connections = device.connections.copy() + new_connections.remove(host_connnection) + _LOGGER.debug( + "Updating Reolink device connections from %s to %s", + device.connections, + new_connections, + ) + device_reg.async_update_device( + device.id, new_connections=new_connections + ) + + ch_device_ids[device.id] = ch + if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid( + ch + ): + if host.api.supported(None, "UID"): + new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" + else: + new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + existing_device = device_reg.async_get_device( + identifiers=new_identifiers + ) + if existing_device is None: + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + else: + _LOGGER.warning( + "Reolink device with uid %s already exists, " + "removing device with uid %s", + new_device_id, + device_uid, + ) + device_reg.async_remove_device(device.id) entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) diff --git a/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index 17e666ac52c..a80e9f8962c 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -76,13 +76,18 @@ def get_store(hass: HomeAssistant, config_entry_id: str) -> Store[str]: def get_device_uid_and_ch( - device: dr.DeviceEntry, host: ReolinkHost + device: dr.DeviceEntry | tuple[str, str], host: ReolinkHost ) -> tuple[list[str], int | None, bool]: """Get the channel and the split device_uid from a reolink DeviceEntry.""" device_uid = [] is_chime = False - for dev_id in device.identifiers: + if isinstance(device, dr.DeviceEntry): + dev_ids = device.identifiers + else: + dev_ids = {device} + + for dev_id in dev_ids: if dev_id[0] == DOMAIN: device_uid = dev_id[1].split("_") if device_uid[0] == host.unique_id: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 6b57c1c253f..f2ae22913ad 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -630,7 +630,7 @@ async def test_cleanup_mac_connection( domain = Platform.SWITCH dev_entry = device_registry.async_get_or_create( - identifiers={(DOMAIN, dev_id)}, + identifiers={(DOMAIN, dev_id), ("OTHER_INTEGRATION", "SOME_ID")}, connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, config_entry_id=config_entry.entry_id, disabled_by=None, @@ -664,6 +664,66 @@ async def test_cleanup_mac_connection( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_combined_with_NVR( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the device registry if IPC camera device was combined with the NVR device.""" + reolink_connect.channels = [0] + reolink_connect.baichuan.mac_address.return_value = None + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + start_identifiers = { + (DOMAIN, dev_id), + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=entity_id, + config_entry=config_entry, + suggested_object_id=entity_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == {(DOMAIN, dev_id)} + host_device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_UID)}) + assert host_device + assert host_device.identifiers == { + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From 6475b1a44697f01d71578a904801018814770952 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 16 May 2025 12:58:59 +0200 Subject: [PATCH 0525/1175] Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue for new setups (#144940) --- homeassistant/components/fronius/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/config_flow.py b/homeassistant/components/fronius/config_flow.py index b8aa2da81c6..97e040abf98 100644 --- a/homeassistant/components/fronius/config_flow.py +++ b/homeassistant/components/fronius/config_flow.py @@ -35,7 +35,7 @@ async def validate_host( hass: HomeAssistant, host: str ) -> tuple[str, FroniusConfigEntryData]: """Validate the user input allows us to connect.""" - fronius = Fronius(async_get_clientsession(hass), host) + fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host) try: datalogger_info: dict[str, Any] From 8a32ffc7b9fcb358cb603a803f837275f4c31fde Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 13:10:58 +0200 Subject: [PATCH 0526/1175] Bump pySmartThings to 3.2.2 (#145033) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 043bdea71e2..f72405dae20 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.1"] + "requirements": ["pysmartthings==3.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index b2b9e27350f..5203e22f7b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2326,7 +2326,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.1 +pysmartthings==3.2.2 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fed9d95a375..6ba9ac72348 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1899,7 +1899,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.1 +pysmartthings==3.2.2 # homeassistant.components.smarty pysmarty2==0.10.2 From 2ca9d4689ea828a53340088d0f2e47cce00f812d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 13:17:56 +0200 Subject: [PATCH 0527/1175] Set SmartThings oven setpoint to unknown if its 1 Fahrenheit (#145038) --- homeassistant/components/smartthings/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index fac503399a9..2aa994ae32c 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -633,7 +633,7 @@ CAPABILITY_TO_SENSORS: dict[ device_class=SensorDeviceClass.TEMPERATURE, use_temperature_unit=True, # Set the value to None if it is 0 F (-17 C) - value_fn=lambda value: None if value in {0, -17} else value, + value_fn=lambda value: None if value in {-17, 0, 1} else value, ) ] }, From 38cee5399918f3da8356dffc4111552365a9ae4a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 16 May 2025 13:28:31 +0200 Subject: [PATCH 0528/1175] Small code optimization for Plugwise (#145037) --- homeassistant/components/plugwise/coordinator.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/plugwise/coordinator.py b/homeassistant/components/plugwise/coordinator.py index b346f26492c..4ed100b538d 100644 --- a/homeassistant/components/plugwise/coordinator.py +++ b/homeassistant/components/plugwise/coordinator.py @@ -99,12 +99,10 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData translation_key="unsupported_firmware", ) from err - self._async_add_remove_devices(data, self.config_entry) + self._async_add_remove_devices(data) return data - def _async_add_remove_devices( - self, data: dict[str, GwEntityData], entry: ConfigEntry - ) -> None: + def _async_add_remove_devices(self, data: dict[str, GwEntityData]) -> None: """Add new Plugwise devices, remove non-existing devices.""" # Check for new or removed devices self.new_devices = set(data) - self._current_devices @@ -112,11 +110,9 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData self._current_devices = set(data) if removed_devices: - self._async_remove_devices(data, entry) + self._async_remove_devices(data) - def _async_remove_devices( - self, data: dict[str, GwEntityData], entry: ConfigEntry - ) -> None: + def _async_remove_devices(self, data: dict[str, GwEntityData]) -> None: """Clean registries when removed devices found.""" device_reg = dr.async_get(self.hass) device_list = dr.async_entries_for_config_entry( @@ -136,7 +132,8 @@ class PlugwiseDataUpdateCoordinator(DataUpdateCoordinator[dict[str, GwEntityData and identifier[1] not in data ): device_reg.async_update_device( - device_entry.id, remove_config_entry_id=entry.entry_id + device_entry.id, + remove_config_entry_id=self.config_entry.entry_id, ) LOGGER.debug( "Removed %s device %s %s from device_registry", From 119d0c576a8bbfc63a5492b8a09f631c1ef1c8d8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 13:39:03 +0200 Subject: [PATCH 0529/1175] Add hood fan speed capability to SmartThings (#144919) --- .../components/smartthings/number.py | 68 ++++++++++++++++++- .../components/smartthings/strings.json | 3 + .../smartthings/snapshots/test_number.ambr | 56 +++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/number.py b/homeassistant/components/smartthings/number.py index 0a9b5dcb03f..1ad9486903a 100644 --- a/homeassistant/components/smartthings/number.py +++ b/homeassistant/components/smartthings/number.py @@ -21,11 +21,21 @@ async def async_setup_entry( ) -> None: """Add number entities for a config entry.""" entry_data = entry.runtime_data - async_add_entities( + entities: list[NumberEntity] = [ SmartThingsWasherRinseCyclesNumberEntity(entry_data.client, device) for device in entry_data.devices.values() if Capability.CUSTOM_WASHER_RINSE_CYCLES in device.status[MAIN] + ] + entities.extend( + SmartThingsHoodNumberEntity(entry_data.client, device) + for device in entry_data.devices.values() + if ( + (hood_component := device.status.get("hood")) is not None + and Capability.SAMSUNG_CE_HOOD_FAN_SPEED in hood_component + and Capability.SAMSUNG_CE_CONNECTION_STATE not in hood_component + ) ) + async_add_entities(entities) class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity): @@ -76,3 +86,59 @@ class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity): Command.SET_WASHER_RINSE_CYCLES, str(int(value)), ) + + +class SmartThingsHoodNumberEntity(SmartThingsEntity, NumberEntity): + """Define a SmartThings number.""" + + _attr_translation_key = "hood_fan_speed" + _attr_native_step = 1.0 + _attr_mode = NumberMode.SLIDER + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, client: SmartThings, device: FullDevice) -> None: + """Initialize the instance.""" + super().__init__( + client, device, {Capability.SAMSUNG_CE_HOOD_FAN_SPEED}, component="hood" + ) + self._attr_unique_id = f"{device.device.device_id}_hood_{Capability.SAMSUNG_CE_HOOD_FAN_SPEED}_{Attribute.HOOD_FAN_SPEED}_{Attribute.HOOD_FAN_SPEED}" + + @property + def options(self) -> list[int]: + """Return the list of options.""" + min_value = self.get_attribute_value( + Capability.SAMSUNG_CE_HOOD_FAN_SPEED, + Attribute.SETTABLE_MIN_FAN_SPEED, + ) + max_value = self.get_attribute_value( + Capability.SAMSUNG_CE_HOOD_FAN_SPEED, + Attribute.SETTABLE_MAX_FAN_SPEED, + ) + return list(range(min_value, max_value + 1)) + + @property + def native_value(self) -> int: + """Return the current value.""" + return int( + self.get_attribute_value( + Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.HOOD_FAN_SPEED + ) + ) + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + return min(self.options) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + return max(self.options) + + async def async_set_native_value(self, value: float) -> None: + """Set the value.""" + await self.execute_device_command( + Capability.SAMSUNG_CE_HOOD_FAN_SPEED, + Command.SET_HOOD_FAN_SPEED, + int(value), + ) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 1113083c00f..96fec1fb0e8 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -105,6 +105,9 @@ "washer_rinse_cycles": { "name": "Rinse cycles", "unit_of_measurement": "cycles" + }, + "hood_fan_speed": { + "name": "Fan speed" } }, "select": { diff --git a/tests/components/smartthings/snapshots/test_number.ambr b/tests/components/smartthings/snapshots/test_number.ambr index ee8dd42712a..8832336a1fa 100644 --- a/tests/components/smartthings/snapshots/test_number.ambr +++ b/tests/components/smartthings/snapshots/test_number.ambr @@ -1,4 +1,60 @@ # serializer version: 1 +# name: test_all_entities[da_ks_microwave_0101x][number.microwave_fan_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 3, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.microwave_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': 'Fan speed', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'hood_fan_speed', + 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_hood_samsungce.hoodFanSpeed_hoodFanSpeed_hoodFanSpeed', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_microwave_0101x][number.microwave_fan_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Microwave Fan speed', + 'max': 3, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'context': , + 'entity_id': 'number.microwave_fan_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[da_wm_wm_000001][number.washer_rinse_cycles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From a500eeb831073ccf2f2cbe56c6f1f36b39e722aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 14:35:46 +0200 Subject: [PATCH 0530/1175] Use runtime_data in hue (#144946) * Use runtime_data in hue * More * Tests --- homeassistant/components/hue/__init__.py | 13 +++++---- homeassistant/components/hue/binary_sensor.py | 8 +++--- homeassistant/components/hue/bridge.py | 10 ++++--- homeassistant/components/hue/config_flow.py | 10 +++---- .../components/hue/device_trigger.py | 27 ++++++++++--------- homeassistant/components/hue/diagnostics.py | 8 +++--- homeassistant/components/hue/event.py | 9 +++---- homeassistant/components/hue/light.py | 8 +++--- homeassistant/components/hue/migration.py | 6 ++--- homeassistant/components/hue/scene.py | 7 +++-- homeassistant/components/hue/sensor.py | 8 +++--- homeassistant/components/hue/services.py | 16 ++++++----- homeassistant/components/hue/switch.py | 8 +++--- .../components/hue/v1/binary_sensor.py | 12 ++++++--- .../components/hue/v1/device_trigger.py | 7 ++--- homeassistant/components/hue/v1/light.py | 15 +++++++---- homeassistant/components/hue/v1/sensor.py | 12 ++++++--- .../components/hue/v2/binary_sensor.py | 8 +++--- homeassistant/components/hue/v2/group.py | 7 +++-- homeassistant/components/hue/v2/light.py | 7 +++-- homeassistant/components/hue/v2/sensor.py | 8 +++--- tests/components/hue/conftest.py | 6 ++--- tests/components/hue/test_init.py | 8 +++--- tests/components/hue/test_light_v1.py | 2 +- 24 files changed, 116 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index d4c2959771b..991d7b51500 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -3,17 +3,17 @@ from aiohue.util import normalize_bridge_id from homeassistant.components import persistent_notification -from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry +from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .bridge import HueBridge +from .bridge import HueBridge, HueConfigEntry from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE from .migration import check_migration from .services import async_register_services -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool: """Set up a bridge from a config entry.""" # check (and run) migrations if needed await check_migration(hass, entry) @@ -104,10 +104,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool: """Unload a config entry.""" - unload_success = await hass.data[DOMAIN][entry.entry_id].async_reset() - if len(hass.data[DOMAIN]) == 0: - hass.data.pop(DOMAIN) + unload_success = await entry.runtime_data.async_reset() + if not hass.config_entries.async_loaded_entries(DOMAIN): hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE) return unload_success diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index ecaa6576775..1d5f10a8c91 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -2,23 +2,21 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueBridge -from .const import DOMAIN +from .bridge import HueConfigEntry from .v1.binary_sensor import async_setup_entry as setup_entry_v1 from .v2.binary_sensor import async_setup_entry as setup_entry_v2 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up binary sensor entities.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data if bridge.api_version == 1: await setup_entry_v1(hass, config_entry, async_add_entities) else: diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5397eeebd96..5dbb894c213 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -36,11 +36,13 @@ PLATFORMS_v2 = [ Platform.SWITCH, ] +type HueConfigEntry = ConfigEntry[HueBridge] + class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass: core.HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: core.HomeAssistant, config_entry: HueConfigEntry) -> None: """Initialize the system.""" self.config_entry = config_entry self.hass = hass @@ -58,7 +60,7 @@ class HueBridge: else: self.api = HueBridgeV2(self.host, app_key) # store (this) bridge object in hass data - hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self + self.config_entry.runtime_data = self @property def host(self) -> str: @@ -163,7 +165,7 @@ class HueBridge: ) if unload_success: - self.hass.data[DOMAIN].pop(self.config_entry.entry_id) + delattr(self.config_entry, "runtime_data") return unload_success @@ -179,7 +181,7 @@ class HueBridge: create_config_flow(self.hass, self.host) -async def _update_listener(hass: core.HomeAssistant, entry: ConfigEntry) -> None: +async def _update_listener(hass: core.HomeAssistant, entry: HueConfigEntry) -> None: """Handle ConfigEntry options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index db025922ef8..bec44352613 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -13,12 +13,7 @@ from aiohue.util import normalize_bridge_id import slugify as unicode_slug import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST from homeassistant.core import callback from homeassistant.helpers import ( @@ -28,6 +23,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo +from .bridge import HueConfigEntry from .const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, @@ -53,7 +49,7 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: HueConfigEntry, ) -> HueV1OptionsFlowHandler | HueV2OptionsFlowHandler: """Get the options flow for this handler.""" if config_entry.data.get(CONF_API_VERSION, 1) == 1: diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index dba5aba81da..9592be69e7e 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -26,14 +26,15 @@ if TYPE_CHECKING: from homeassistant.core import HomeAssistant from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo - from .bridge import HueBridge + from .bridge import HueConfigEntry async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - if DOMAIN not in hass.data: + entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN) + if not entries: # happens at startup return config device_id = config[CONF_DEVICE_ID] @@ -42,10 +43,10 @@ async def async_validate_trigger_config( if (device_entry := dev_reg.async_get(device_id)) is None: raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") - for conf_entry_id in device_entry.config_entries: - if conf_entry_id not in hass.data[DOMAIN]: + for entry in entries: + if entry.entry_id not in device_entry.config_entries: continue - bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] + bridge = entry.runtime_data if bridge.api_version == 1: return await async_validate_trigger_config_v1(bridge, device_entry, config) return await async_validate_trigger_config_v2(bridge, device_entry, config) @@ -65,10 +66,11 @@ async def async_attach_trigger( if (device_entry := dev_reg.async_get(device_id)) is None: raise InvalidDeviceAutomationConfig(f"Device ID {device_id} is not valid") - for conf_entry_id in device_entry.config_entries: - if conf_entry_id not in hass.data[DOMAIN]: + entry: HueConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.entry_id not in device_entry.config_entries: continue - bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] + bridge = entry.runtime_data if bridge.api_version == 1: return await async_attach_trigger_v1( bridge, device_entry, config, action, trigger_info @@ -85,7 +87,8 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """Get device triggers for given (hass) device id.""" - if DOMAIN not in hass.data: + entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN) + if not entries: return [] # lookup device in HASS DeviceRegistry dev_reg: dr.DeviceRegistry = dr.async_get(hass) @@ -94,10 +97,10 @@ async def async_get_triggers( # Iterate all config entries for this device # and work out the bridge version - for conf_entry_id in device_entry.config_entries: - if conf_entry_id not in hass.data[DOMAIN]: + for entry in entries: + if entry.entry_id not in device_entry.config_entries: continue - bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] + bridge = entry.runtime_data if bridge.api_version == 1: return async_get_triggers_v1(bridge, device_entry) diff --git a/homeassistant/components/hue/diagnostics.py b/homeassistant/components/hue/diagnostics.py index 6bb23d832cd..a45813151e4 100644 --- a/homeassistant/components/hue/diagnostics.py +++ b/homeassistant/components/hue/diagnostics.py @@ -4,18 +4,16 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .bridge import HueBridge -from .const import DOMAIN +from .bridge import HueConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: HueConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - bridge: HueBridge = hass.data[DOMAIN][entry.entry_id] + bridge = entry.runtime_data if bridge.api_version == 1: # diagnostics is only implemented for V2 bridges. return {} diff --git a/homeassistant/components/hue/event.py b/homeassistant/components/hue/event.py index 249f81687c0..4cffbb73a38 100644 --- a/homeassistant/components/hue/event.py +++ b/homeassistant/components/hue/event.py @@ -14,22 +14,21 @@ from homeassistant.components.event import ( EventEntity, EventEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueBridge -from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES, DOMAIN +from .bridge import HueConfigEntry +from .const import DEFAULT_BUTTON_EVENT_TYPES, DEVICE_SPECIFIC_EVENT_TYPES from .v2.entity import HueBaseEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up event platform from Hue button resources.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api if bridge.api_version == 1: diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 9906c9bffa4..332dc6978ad 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -2,12 +2,10 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueBridge -from .const import DOMAIN +from .bridge import HueConfigEntry from .v1.light import async_setup_entry as setup_entry_v1 from .v2.group import async_setup_entry as setup_groups_entry_v2 from .v2.light import async_setup_entry as setup_entry_v2 @@ -15,11 +13,11 @@ from .v2.light import async_setup_entry as setup_entry_v2 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up light entities.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data if bridge.api_version == 1: await setup_entry_v1(hass, config_entry, async_add_entities) diff --git a/homeassistant/components/hue/migration.py b/homeassistant/components/hue/migration.py index 1214f39d146..55edf7d5565 100644 --- a/homeassistant/components/hue/migration.py +++ b/homeassistant/components/hue/migration.py @@ -10,7 +10,6 @@ from aiohue.v2.models.resource import ResourceTypes from homeassistant import core from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST, CONF_USERNAME from homeassistant.helpers import ( aiohttp_client, @@ -18,12 +17,13 @@ from homeassistant.helpers import ( entity_registry as er, ) +from .bridge import HueConfigEntry from .const import DOMAIN LOGGER = logging.getLogger(__name__) -async def check_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: +async def check_migration(hass: core.HomeAssistant, entry: HueConfigEntry) -> None: """Check if config entry needs any migration actions.""" host = entry.data[CONF_HOST] @@ -66,7 +66,7 @@ async def check_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: hass.config_entries.async_update_entry(entry, data=data) -async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None: +async def handle_v2_migration(hass: core.HomeAssistant, entry: HueConfigEntry) -> None: """Perform migration of devices and entities to V2 Id's.""" host = entry.data[CONF_HOST] api_key = entry.data[CONF_API_KEY] diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 0b9eb4efbd6..5327a54fcc8 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -12,7 +12,6 @@ from aiohue.v2.models.smart_scene import SmartScene as HueSmartScene, SmartScene import voluptuous as vol from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import ( @@ -20,7 +19,7 @@ from homeassistant.helpers.entity_platform import ( async_get_current_platform, ) -from .bridge import HueBridge +from .bridge import HueBridge, HueConfigEntry from .const import DOMAIN from .v2.entity import HueBaseEntity from .v2.helpers import normalize_hue_brightness, normalize_hue_transition @@ -33,11 +32,11 @@ ATTR_BRIGHTNESS = "brightness" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up scene platform from Hue group scenes.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api if bridge.api_version == 1: diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 227742fdbab..60845c0be7a 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -2,23 +2,21 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueBridge -from .const import DOMAIN +from .bridge import HueConfigEntry from .v1.sensor import async_setup_entry as setup_entry_v1 from .v2.sensor import async_setup_entry as setup_entry_v2 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensor entities.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data if bridge.api_version == 1: await setup_entry_v1(hass, config_entry, async_add_entities) return diff --git a/homeassistant/components/hue/services.py b/homeassistant/components/hue/services.py index de6da161fba..18dd19e3391 100644 --- a/homeassistant/components/hue/services.py +++ b/homeassistant/components/hue/services.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import verify_domain_control -from .bridge import HueBridge +from .bridge import HueBridge, HueConfigEntry from .const import ( ATTR_DYNAMIC, ATTR_GROUP_NAME, @@ -37,14 +37,16 @@ def async_register_services(hass: HomeAssistant) -> None: dynamic = call.data.get(ATTR_DYNAMIC, False) # Call the set scene function on each bridge + entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN) tasks = [ - hue_activate_scene_v1(bridge, group_name, scene_name, transition) - if bridge.api_version == 1 - else hue_activate_scene_v2( - bridge, group_name, scene_name, transition, dynamic + hue_activate_scene_v1( + entry.runtime_data, group_name, scene_name, transition ) - for bridge in hass.data[DOMAIN].values() - if isinstance(bridge, HueBridge) + if entry.runtime_data.api_version == 1 + else hue_activate_scene_v2( + entry.runtime_data, group_name, scene_name, transition, dynamic + ) + for entry in entries ] results = await asyncio.gather(*tasks) diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index b6b21686d25..33dfe02dd49 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -19,23 +19,21 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .bridge import HueBridge -from .const import DOMAIN +from .bridge import HueConfigEntry from .v2.entity import HueBaseEntity async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hue switch platform from Hue resources.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api if bridge.api_version == 1: diff --git a/homeassistant/components/hue/v1/binary_sensor.py b/homeassistant/components/hue/v1/binary_sensor.py index 325c4d022fa..e06d61210b8 100644 --- a/homeassistant/components/hue/v1/binary_sensor.py +++ b/homeassistant/components/hue/v1/binary_sensor.py @@ -6,16 +6,22 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from ..const import DOMAIN as HUE_DOMAIN +from ..bridge import HueConfigEntry from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor PRESENCE_NAME_FORMAT = "{} motion" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HueConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Defer binary sensor setup to the shared sensor module.""" - bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data if not bridge.sensor_manager: return diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index 493c668f549..c55573899d2 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -27,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN if TYPE_CHECKING: - from ..bridge import HueBridge + from ..bridge import HueBridge, HueConfigEntry TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} @@ -111,8 +111,9 @@ REMOTES: dict[str, dict[tuple[str, str], dict[str, int]]] = { def _get_hue_event_from_device_id(hass, device_id): """Resolve hue event from device id.""" - for bridge in hass.data.get(DOMAIN, {}).values(): - for hue_event in bridge.sensor_manager.current_events.values(): + entries: list[HueConfigEntry] = hass.config_entries.async_loaded_entries(DOMAIN) + for entry in entries: + for hue_event in entry.runtime_data.sensor_manager.current_events.values(): if device_id == hue_event.device_registry_id: return hue_event diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index a806572e0f1..b7251382296 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -28,10 +28,11 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,7 +40,7 @@ from homeassistant.helpers.update_coordinator import ( ) from homeassistant.util import color as color_util -from ..bridge import HueBridge +from ..bridge import HueConfigEntry from ..const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, @@ -139,11 +140,15 @@ def create_light(item_class, coordinator, bridge, is_group, rooms, api, item_id) ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HueConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Set up the Hue lights from a config entry.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) - rooms = {} + rooms: dict[str, str] = {} allow_groups = config_entry.options.get( CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS diff --git a/homeassistant/components/hue/v1/sensor.py b/homeassistant/components/hue/v1/sensor.py index 88d494ed44b..765808bdf18 100644 --- a/homeassistant/components/hue/v1/sensor.py +++ b/homeassistant/components/hue/v1/sensor.py @@ -13,8 +13,10 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import LIGHT_LUX, PERCENTAGE, EntityCategory, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from ..const import DOMAIN as HUE_DOMAIN +from ..bridge import HueConfigEntry from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor LIGHT_LEVEL_NAME_FORMAT = "{} light level" @@ -22,9 +24,13 @@ REMOTE_NAME_FORMAT = "{} battery level" TEMPERATURE_NAME_FORMAT = "{} temperature" -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HueConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Defer sensor setup to the shared sensor module.""" - bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data if not bridge.sensor_manager: return diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index 6e4c7f98973..17584a0f5cb 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -27,13 +27,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from ..bridge import HueBridge -from ..const import DOMAIN +from ..bridge import HueConfigEntry from .entity import HueBaseEntity type SensorType = CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper @@ -48,11 +46,11 @@ type ControllerType = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hue Sensors from Config Entry.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api @callback diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 2f9f195df97..4db9bc16ca8 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -22,14 +22,13 @@ from homeassistant.components.light import ( LightEntityDescription, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import color as color_util -from ..bridge import HueBridge +from ..bridge import HueBridge, HueConfigEntry from ..const import DOMAIN from .entity import HueBaseEntity from .helpers import ( @@ -41,11 +40,11 @@ from .helpers import ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hue groups on light platform.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api async def async_add_light(event_type: EventType, resource: GroupedLight) -> None: diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 8eb7ec8936e..d83cdaa8009 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -26,13 +26,12 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.util import color as color_util -from ..bridge import HueBridge +from ..bridge import HueBridge, HueConfigEntry from ..const import DOMAIN from .entity import HueBaseEntity from .helpers import ( @@ -51,11 +50,11 @@ DEPRECATED_EFFECT_NONE = "None" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hue Light from Config Entry.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api controller: LightsController = api.lights make_light_entity = partial(HueLight, bridge, controller) diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index ae6e456a8b4..1eec4eaa6b9 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -25,13 +25,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from ..bridge import HueBridge -from ..const import DOMAIN +from ..bridge import HueBridge, HueConfigEntry from .entity import HueBaseEntity type SensorType = DevicePower | LightLevel | Temperature | ZigbeeConnectivity @@ -45,11 +43,11 @@ type ControllerType = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HueConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Hue Sensors from Config Entry.""" - bridge: HueBridge = hass.data[DOMAIN][config_entry.entry_id] + bridge = config_entry.runtime_data api: HueBridgeV2 = bridge.api ctrl_base: SensorsController = api.sensors diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index e6ade431ee6..9fb291c57b4 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -59,7 +59,7 @@ def create_mock_bridge(hass: HomeAssistant, api_version: int = 1) -> Mock: async def async_initialize_bridge(): if bridge.config_entry: - hass.data.setdefault(hue.DOMAIN, {})[bridge.config_entry.entry_id] = bridge + bridge.config_entry.runtime_data = bridge if bridge.api_version == 2: await async_setup_devices(bridge) return True @@ -73,7 +73,7 @@ def create_mock_bridge(hass: HomeAssistant, api_version: int = 1) -> Mock: async def async_reset(): if bridge.config_entry: - hass.data[hue.DOMAIN].pop(bridge.config_entry.entry_id) + delattr(bridge.config_entry, "runtime_data") return True bridge.async_reset = async_reset @@ -273,7 +273,7 @@ async def setup_platform( api_version=mock_bridge.api_version, host=hostname ) mock_bridge.config_entry = config_entry - hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} + config_entry.runtime_data = {config_entry.entry_id: mock_bridge} # simulate a full setup by manually adding the bridge config entry await setup_bridge(hass, mock_bridge, config_entry) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 5ce0d78ead9..6b162a22165 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -42,7 +42,7 @@ async def test_setup_with_no_config(hass: HomeAssistant) -> None: assert len(hass.config_entries.flow.async_progress()) == 0 # No configs stored - assert hue.DOMAIN not in hass.data + assert not hass.config_entries.async_entries(hue.DOMAIN) async def test_unload_entry(hass: HomeAssistant, mock_bridge_setup) -> None: @@ -55,15 +55,15 @@ async def test_unload_entry(hass: HomeAssistant, mock_bridge_setup) -> None: assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge_setup.mock_calls) == 1 - hass.data[hue.DOMAIN] = {entry.entry_id: mock_bridge_setup} + entry.runtime_data = mock_bridge_setup async def mock_reset(): - hass.data[hue.DOMAIN].pop(entry.entry_id) + delattr(entry, "runtime_data") return True mock_bridge_setup.async_reset = mock_reset assert await hue.async_unload_entry(hass, entry) - assert hue.DOMAIN not in hass.data + assert not hasattr(entry, "runtime_data") async def test_setting_unique_id(hass: HomeAssistant, mock_bridge_setup) -> None: diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index a9fc1e5c70b..2a366f96e53 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -185,7 +185,7 @@ async def setup_bridge(hass: HomeAssistant, mock_bridge_v1: Mock) -> None: ) config_entry.mock_state(hass, ConfigEntryState.LOADED) mock_bridge_v1.config_entry = config_entry - hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} + config_entry.runtime_data = mock_bridge_v1 await hass.config_entries.async_forward_entry_setups(config_entry, ["light"]) # To flush out the service call to update the group await hass.async_block_till_done() From bdc21da0762c4dea8e01897be2193fc4a5a82498 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 15:08:24 +0200 Subject: [PATCH 0531/1175] Sync SmartThings EHS fixture (#145042) --- .../device_status/da_sac_ehs_000001_sub.json | 237 ++++++++++++------ .../devices/da_sac_ehs_000001_sub.json | 49 +++- .../smartthings/snapshots/test_init.ambr | 2 +- .../smartthings/snapshots/test_sensor.ambr | 12 +- .../snapshots/test_water_heater.ambr | 2 +- 5 files changed, 205 insertions(+), 97 deletions(-) diff --git a/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub.json b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub.json index e27c6c3de21..a9a991f488c 100644 --- a/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub.json +++ b/tests/components/smartthings/fixtures/device_status/da_sac_ehs_000001_sub.json @@ -10,72 +10,64 @@ "duration": 0, "override": false }, - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" } }, "powerConsumptionReport": { "powerConsumption": { "value": { - "energy": 8193810.0, + "energy": 8901522.0, "deltaEnergy": 0, - "power": 2.539, - "powerEnergy": 0.009404173966911105, - "persistedEnergy": 8193810.0, + "power": 0.015, + "powerEnergy": 0.01082494583328565, + "persistedEnergy": 8901522.0, "energySaved": 0, - "start": "2025-03-09T11:14:44Z", - "end": "2025-03-09T11:14:57Z" + "start": "2025-05-16T11:18:12Z", + "end": "2025-05-16T12:01:29Z" }, - "timestamp": "2025-03-09T11:14:57.338Z" + "timestamp": "2025-05-16T12:01:29.990Z" } }, "samsungce.ehsCycleData": { "outdoor": { "value": [ { - "timestamp": "2025-03-09T02:00:29Z", - "data": "0038003870FF3C3B46020218019A00050000" + "timestamp": "2025-05-15T22:50:49Z", + "data": "0000000051FF4348450207D0000000000000" }, { - "timestamp": "2025-03-09T02:05:29Z", - "data": "0034003471FF3C3C46020218019A00050000" - }, - { - "timestamp": "2025-03-09T02:10:29Z", - "data": "002D002D71FF3D3D460201C9019A00050000" + "timestamp": "2025-05-15T22:55:49Z", + "data": "0000000051FF4448450207D0000000000000" } ], "unit": "C", - "timestamp": "2025-03-09T11:11:30.786Z" + "timestamp": "2025-05-16T07:00:51.349Z" }, "indoor": { "value": [ { - "timestamp": "2025-03-09T02:00:29Z", - "data": "5F055C050505002564000000000000000001FFFF00079440" + "timestamp": "2025-05-15T22:50:49Z", + "data": "47054C0505050000000000000000000000000000000832EB" }, { - "timestamp": "2025-03-09T02:05:29Z", - "data": "60055E050505002563000000000000000001FFFF00079445" - }, - { - "timestamp": "2025-03-09T02:10:29Z", - "data": "61055F050505002560000000000000000001FFFF0007944B" + "timestamp": "2025-05-15T22:55:49Z", + "data": "47054C0505050000000000000000000000000000000832ED" } ], "unit": "C", - "timestamp": "2025-03-09T11:11:30.786Z" + "timestamp": "2025-05-16T07:00:51.349Z" } }, "custom.outingMode": { "outingMode": { "value": "off", - "timestamp": "2025-03-09T08:00:05.571Z" + "timestamp": "2025-05-14T20:05:40.503Z" } }, "samsungce.ehsThermostat": { "connectionState": { "value": "disconnected", - "timestamp": "2025-03-09T08:00:05.562Z" + "timestamp": "2025-05-06T10:47:04.400Z" } }, "refresh": {}, @@ -83,12 +75,12 @@ "minimumSetpoint": { "value": 40, "unit": "C", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-15T02:34:53.575Z" }, "maximumSetpoint": { "value": 55, "unit": "C", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-15T02:34:53.575Z" } }, "airConditionerMode": { @@ -97,11 +89,11 @@ }, "supportedAcModes": { "value": ["eco", "std", "force"], - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" }, "airConditionerMode": { "value": "std", - "timestamp": "2025-03-09T08:00:05.562Z" + "timestamp": "2025-05-06T10:47:04.400Z" } }, "samsungce.ehsFsvSettings": { @@ -320,7 +312,7 @@ "isValid": true } ], - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-09T02:16:02.595Z" } }, "execute": { @@ -395,97 +387,97 @@ }, "binaryId": { "value": "SAC_EHS_MONO", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-16T08:18:08.723Z" } }, "samsungce.sacDisplayCondition": { "switch": { "value": "enabled", - "timestamp": "2025-03-09T08:00:05.514Z" + "timestamp": "2025-05-06T12:30:02.413Z" } }, "switch": { "switch": { "value": "off", - "timestamp": "2025-03-09T11:00:27.522Z" + "timestamp": "2025-05-16T12:01:29.844Z" } }, "ocf": { "st": { - "value": "2025-03-06T08:37:35Z", - "timestamp": "2025-03-09T08:18:05.953Z" + "value": "2025-05-14T18:33:05Z", + "timestamp": "2025-05-16T08:18:07.449Z" }, "mndt": { "value": "", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnfv": { - "value": "20240611.1", - "timestamp": "2025-03-09T08:18:05.953Z" + "value": "20250317.1", + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnhw": { "value": "", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "di": { "value": "1f98ebd0-ac48-d802-7f62-000001200100", - "timestamp": "2025-03-09T08:18:05.955Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnsl": { "value": "", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "dmv": { "value": "res.1.1.0,sh.1.1.0", - "timestamp": "2025-03-09T08:18:05.955Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "n": { "value": "Eco Heating System", - "timestamp": "2025-03-09T08:18:05.955Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnmo": { "value": "SAC_EHS_MONO|220614|61007400001600000400000000000000", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-16T08:18:08.723Z" }, "vid": { "value": "DA-SAC-EHS-000001-SUB", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnmn": { "value": "Samsung Electronics", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnml": { "value": "", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnpv": { "value": "4.0", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "mnos": { "value": "Tizen", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "pi": { "value": "1f98ebd0-ac48-d802-7f62-000001200100", - "timestamp": "2025-03-09T08:18:05.953Z" + "timestamp": "2025-05-16T08:18:07.449Z" }, "icv": { "value": "core.1.1.0", - "timestamp": "2025-03-09T08:18:05.955Z" + "timestamp": "2025-05-16T08:18:07.449Z" } }, "remoteControlStatus": { "remoteControlEnabled": { "value": "true", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" } }, "custom.energyType": { "energyType": { "value": "2.0", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-03-22T08:18:04.803Z" }, "energySavingSupport": { "value": false, @@ -516,19 +508,24 @@ "samsungce.toggleSwitch": { "switch": { "value": "off", - "timestamp": "2025-03-09T11:00:22.880Z" + "timestamp": "2025-05-16T07:00:23.689Z" } }, "custom.disabledCapabilities": { "disabledCapabilities": { - "value": ["remoteControlStatus", "demandResponseLoadControl"], - "timestamp": "2025-03-09T08:31:30.641Z" + "value": [ + "remoteControlStatus", + "samsungce.ehsCycleData", + "samsungce.systemAirConditionerReservation", + "demandResponseLoadControl" + ], + "timestamp": "2025-05-16T08:18:08.723Z" } }, "samsungce.driverVersion": { "versionNumber": { - "value": 23070101, - "timestamp": "2023-08-02T14:32:26.195Z" + "value": 25010101, + "timestamp": "2025-03-31T04:43:32.104Z" } }, "samsungce.softwareUpdate": { @@ -543,11 +540,11 @@ }, "availableModules": { "value": [], - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-03-22T07:41:31.476Z" }, "newVersionAvailable": { "value": false, - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" }, "operatingState": { "value": null @@ -561,31 +558,31 @@ "value": null }, "temperature": { - "value": 54.3, + "value": 40.8, "unit": "C", - "timestamp": "2025-03-09T10:43:24.134Z" + "timestamp": "2025-05-16T12:12:59.016Z" } }, "custom.deviceReportStateConfiguration": { "reportStateRealtimePeriod": { "value": "enabled", - "timestamp": "2024-11-08T01:41:37.280Z" + "timestamp": "2025-05-08T03:03:38.391Z" }, "reportStateRealtime": { "value": { "state": "disabled" }, - "timestamp": "2025-03-08T12:06:55.069Z" + "timestamp": "2025-05-14T20:25:52.192Z" }, "reportStatePeriod": { "value": "enabled", - "timestamp": "2024-11-08T01:41:37.280Z" + "timestamp": "2025-05-08T03:03:38.391Z" } }, "samsungce.ehsTemperatureReference": { "temperatureReference": { "value": "water", - "timestamp": "2025-03-09T07:15:48.438Z" + "timestamp": "2025-05-06T10:47:04.249Z" } }, "thermostatCoolingSetpoint": { @@ -595,21 +592,91 @@ "coolingSetpoint": { "value": 48, "unit": "C", - "timestamp": "2025-03-09T10:58:50.857Z" + "timestamp": "2025-05-15T02:34:53.575Z" + } + }, + "samsungce.ehsBoosterHeater": { + "status": { + "value": "off", + "timestamp": "2025-05-15T02:34:53.185Z" + } + }, + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, + "samsungce.sensingOnSuspendMode": { + "sensingOnSuspendMode": { + "value": null + } + }, + "samsungce.ehsDiverterValve": { + "position": { + "value": "room", + "timestamp": "2025-05-16T02:17:59.268Z" + } + }, + "samsungce.softwareVersion": { + "versions": { + "value": [ + { + "id": "0", + "swType": "Software", + "versionNumber": "DB91-02102A 2025-03-17", + "description": "Version" + }, + { + "id": "1", + "swType": "Firmware", + "versionNumber": "DB91-02100A 2020-07-10", + "description": "Version" + }, + { + "id": "2", + "swType": "Firmware", + "versionNumber": "DB91-02103B 2022-06-14", + "description": "" + }, + { + "id": "3", + "swType": "Firmware", + "versionNumber": "DB91-02450A 2022-07-06", + "description": "EHS MONO LOWTEMP" + } + ], + "timestamp": "2025-05-07T08:18:06.705Z" } } }, "INDOOR": { + "samsungce.systemAirConditionerReservation": { + "reservations": { + "value": null + }, + "maxNumberOfReservations": { + "value": null + } + }, "samsungce.ehsThermostat": { "connectionState": { "value": "disconnected", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" } }, "samsungce.toggleSwitch": { "switch": { "value": "off", - "timestamp": "2025-03-09T11:14:44.775Z" + "timestamp": "2025-05-14T20:05:45.533Z" + } + }, + "custom.disabledCapabilities": { + "disabledCapabilities": { + "value": ["samsungce.systemAirConditionerReservation"], + "timestamp": "2025-03-31T04:03:40.028Z" } }, "temperatureMeasurement": { @@ -617,21 +684,27 @@ "value": null }, "temperature": { - "value": 39.2, + "value": 23.1, "unit": "C", - "timestamp": "2025-03-09T11:15:49.852Z" + "timestamp": "2025-05-16T12:29:12.736Z" } }, "custom.thermostatSetpointControl": { "minimumSetpoint": { "value": 25, "unit": "C", - "timestamp": "2025-03-09T07:06:20.699Z" + "timestamp": "2025-05-15T02:34:53.531Z" }, "maximumSetpoint": { "value": 65, "unit": "C", - "timestamp": "2025-03-09T07:06:20.699Z" + "timestamp": "2025-05-06T10:23:24.471Z" + } + }, + "samsungce.ehsDefrostMode": { + "status": { + "value": "off", + "timestamp": "2025-05-07T08:18:06.705Z" } }, "airConditionerMode": { @@ -640,17 +713,17 @@ }, "supportedAcModes": { "value": ["auto", "cool", "heat"], - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" }, "airConditionerMode": { "value": "heat", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" } }, "samsungce.ehsTemperatureReference": { "temperatureReference": { "value": "water", - "timestamp": "2025-03-09T07:06:20.699Z" + "timestamp": "2025-05-06T10:23:24.471Z" } }, "thermostatCoolingSetpoint": { @@ -660,19 +733,19 @@ "coolingSetpoint": { "value": 25, "unit": "C", - "timestamp": "2025-03-09T11:14:44.734Z" + "timestamp": "2025-05-14T20:05:40.638Z" } }, "samsungce.sacDisplayCondition": { "switch": { "value": "enabled", - "timestamp": "2025-03-09T08:18:06.394Z" + "timestamp": "2025-05-07T08:18:06.705Z" } }, "switch": { "switch": { "value": "off", - "timestamp": "2025-03-09T11:14:57.238Z" + "timestamp": "2025-05-16T08:18:08.723Z" } } } diff --git a/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub.json b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub.json index dffe57b3280..25dff2ab2ac 100644 --- a/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub.json +++ b/tests/components/smartthings/fixtures/devices/da_sac_ehs_000001_sub.json @@ -88,10 +88,26 @@ "id": "samsungce.sacDisplayCondition", "version": 1 }, + { + "id": "samsungce.sensingOnSuspendMode", + "version": 1 + }, { "id": "samsungce.softwareUpdate", "version": 1 }, + { + "id": "samsungce.softwareVersion", + "version": 1 + }, + { + "id": "samsungce.ehsBoosterHeater", + "version": 1 + }, + { + "id": "samsungce.ehsDiverterValve", + "version": 1 + }, { "id": "samsungce.ehsFsvSettings", "version": 1 @@ -111,6 +127,10 @@ { "id": "samsungce.toggleSwitch", "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 } ], "categories": [ @@ -118,7 +138,8 @@ "name": "AirConditioner", "categoryType": "manufacturer" } - ] + ], + "optional": false }, { "id": "INDOOR", @@ -140,10 +161,18 @@ "id": "airConditionerMode", "version": 1 }, + { + "id": "custom.disabledCapabilities", + "version": 1 + }, { "id": "custom.thermostatSetpointControl", "version": 1 }, + { + "id": "samsungce.ehsDefrostMode", + "version": 1 + }, { "id": "samsungce.ehsTemperatureReference", "version": 1 @@ -159,6 +188,10 @@ { "id": "samsungce.toggleSwitch", "version": 1 + }, + { + "id": "samsungce.systemAirConditionerReservation", + "version": 1 } ], "categories": [ @@ -166,13 +199,14 @@ "name": "Other", "categoryType": "manufacturer" } - ] + ], + "optional": false } ], "createTime": "2023-08-02T14:32:26.006Z", "parentDeviceId": "1f98ebd0-ac48-d802-7f62-12592d8286b7", "profile": { - "id": "54b9789f-2c8c-310d-9e14-9a84903c792b" + "id": "89782721-6841-3ef6-a699-28e069d28b8b" }, "ocf": { "ocfDeviceType": "oic.d.airconditioner", @@ -184,12 +218,13 @@ "platformVersion": "4.0", "platformOS": "Tizen", "hwVersion": "", - "firmwareVersion": "20240611.1", + "firmwareVersion": "20250317.1", "vendorId": "DA-SAC-EHS-000001-SUB", - "vendorResourceClientServerVersion": "3.2.20", + "vendorResourceClientServerVersion": "4.0.54", "lastSignupTime": "2023-08-02T14:32:25.282882Z", - "transferCandidate": false, - "additionalAuthCodeRequired": false + "transferCandidate": true, + "additionalAuthCodeRequired": false, + "modelCode": "" }, "type": "OCF", "restrictionTier": 0, diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index ff54a75c3f2..dc5d7e6aeeb 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -790,7 +790,7 @@ 'primary_config_entry': , 'serial_number': None, 'suggested_area': None, - 'sw_version': '20240611.1', + 'sw_version': '20250317.1', 'via_device_id': None, }) # --- diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 3732a338964..850ee196ed9 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -5870,7 +5870,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '8193.81', + 'state': '8901.522', }) # --- # name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_energy_difference-entry] @@ -6027,8 +6027,8 @@ 'attributes': ReadOnlyDict({ 'device_class': 'power', 'friendly_name': 'Eco Heating System Power', - 'power_consumption_end': '2025-03-09T11:14:57Z', - 'power_consumption_start': '2025-03-09T11:14:44Z', + 'power_consumption_end': '2025-05-16T12:01:29Z', + 'power_consumption_start': '2025-05-16T11:18:12Z', 'state_class': , 'unit_of_measurement': , }), @@ -6037,7 +6037,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.539', + 'state': '0.015', }) # --- # name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_power_energy-entry] @@ -6092,7 +6092,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '9.4041739669111e-06', + 'state': '1.08249458332857e-05', }) # --- # name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_temperature-entry] @@ -6144,7 +6144,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '54.3', + 'state': '40.8', }) # --- # name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_cooling_set_point-entry] diff --git a/tests/components/smartthings/snapshots/test_water_heater.ambr b/tests/components/smartthings/snapshots/test_water_heater.ambr index 88f8bf8f6a7..759a95220de 100644 --- a/tests/components/smartthings/snapshots/test_water_heater.ambr +++ b/tests/components/smartthings/snapshots/test_water_heater.ambr @@ -119,7 +119,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'away_mode': 'off', - 'current_temperature': 54.3, + 'current_temperature': 40.8, 'friendly_name': 'Eco Heating System', 'max_temp': 60.0, 'min_temp': 40, From db3e596e48313ff19472f45c1f8c8342fa1de30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 16 May 2025 18:19:36 +0200 Subject: [PATCH 0532/1175] Update Matter MicrowaveOven fixture (#145057) Update microwave_oven.json PowerInWatts feature --- tests/components/matter/fixtures/nodes/microwave_oven.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/matter/fixtures/nodes/microwave_oven.json b/tests/components/matter/fixtures/nodes/microwave_oven.json index ed0a4accd6a..bbba8b12e25 100644 --- a/tests/components/matter/fixtures/nodes/microwave_oven.json +++ b/tests/components/matter/fixtures/nodes/microwave_oven.json @@ -368,6 +368,8 @@ "1/95/3": 20, "1/95/4": 90, "1/95/5": 10, + "1/95/6": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], + "1/95/7": 9, "1/95/8": 1000, "1/95/65532": 5, "1/95/65533": 1, @@ -395,7 +397,7 @@ "1/96/5": { "0": 0 }, - "1/96/65532": 0, + "1/96/65532": 2, "1/96/65533": 2, "1/96/65528": [4], "1/96/65529": [0, 1, 2, 3], From 911481638432bb3d7b0b6c6bdcf4be0c7b077d70 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 16 May 2025 19:51:30 +0300 Subject: [PATCH 0533/1175] Fix climate idle state for Comelit (#145059) --- homeassistant/components/comelit/climate.py | 4 +--- tests/components/comelit/snapshots/test_climate.ambr | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index be5b892e53c..e7890cddff8 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): self._attr_current_temperature = values[0] / 10 self._attr_hvac_action = None - if _mode == ClimaComelitMode.OFF: - self._attr_hvac_action = HVACAction.OFF if not _active: self._attr_hvac_action = HVACAction.IDLE - if _mode in API_STATUS: + elif _mode in API_STATUS: self._attr_hvac_action = API_STATUS[_mode]["hvac_action"] self._attr_hvac_mode = None diff --git a/tests/components/comelit/snapshots/test_climate.ambr b/tests/components/comelit/snapshots/test_climate.ambr index e5201067ee1..0233359bc45 100644 --- a/tests/components/comelit/snapshots/test_climate.ambr +++ b/tests/components/comelit/snapshots/test_climate.ambr @@ -48,7 +48,7 @@ 'attributes': ReadOnlyDict({ 'current_temperature': 22.1, 'friendly_name': 'Climate0', - 'hvac_action': , + 'hvac_action': , 'hvac_modes': list([ , , From 6b769ac2635d4c9fcf27bcf9c2a384378f70177c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 16 May 2025 19:37:22 +0200 Subject: [PATCH 0534/1175] Update frontend to 20250516.0 (#145062) --- 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 9471f863a72..5c5feca98b7 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==20250509.0"] + "requirements": ["home-assistant-frontend==20250516.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 59437b4c2ae..908655ce443 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.100.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5203e22f7b4..515be945a63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1158,7 +1158,7 @@ hole==0.8.0 holidays==0.72 # homeassistant.components.frontend -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ba9ac72348..c9f397dd91d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 holidays==0.72 # homeassistant.components.frontend -home-assistant-frontend==20250509.0 +home-assistant-frontend==20250516.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From be5685695e7afae998180de68f72da34ac079a04 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 19:38:18 +0200 Subject: [PATCH 0535/1175] Fix fan AC mode in SmartThings AC (#145064) --- homeassistant/components/smartthings/climate.py | 17 ++++++++++------- .../fixtures/device_status/da_ac_rac_01001.json | 2 +- tests/components/smartthings/test_climate.py | 8 +++++--- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 2859500b5f6..779909e3d84 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -66,6 +66,7 @@ AC_MODE_TO_STATE = { "heat": HVACMode.HEAT, "heatClean": HVACMode.HEAT, "fanOnly": HVACMode.FAN_ONLY, + "fan": HVACMode.FAN_ONLY, "wind": HVACMode.FAN_ONLY, } STATE_TO_AC_MODE = { @@ -88,6 +89,7 @@ FAN_OSCILLATION_TO_SWING = { } WIND = "wind" +FAN = "fan" WINDFREE = "windFree" @@ -387,14 +389,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): tasks.append(self.async_turn_on()) mode = STATE_TO_AC_MODE[hvac_mode] - # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" mode the AirConditioner new mode has to be "wind" - # The conversion make the mode change working - # The conversion is made only for device that wrongly has capability "wind" instead "fan_only" + # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" or "fan" mode the AirConditioner + # new mode has to be "wind" or "fan" if hvac_mode == HVACMode.FAN_ONLY: - if WIND in self.get_attribute_value( - Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES - ): - mode = WIND + for fan_mode in (WIND, FAN): + if fan_mode in self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES + ): + mode = fan_mode + break tasks.append( self.execute_device_command( diff --git a/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json b/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json index e8e71c53ace..3982e1174f4 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ac_rac_01001.json @@ -32,7 +32,7 @@ "timestamp": "2025-02-09T14:35:56.800Z" }, "supportedAcModes": { - "value": ["auto", "cool", "dry", "wind", "heat", "dryClean"], + "value": ["auto", "cool", "dry", "fan", "heat", "dryClean"], "timestamp": "2025-02-09T15:42:13.444Z" }, "airConditionerMode": { diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 138601ec08b..7864063235b 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -196,17 +196,19 @@ async def test_ac_set_hvac_mode_turns_on( @pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) -async def test_ac_set_hvac_mode_wind( +@pytest.mark.parametrize("mode", ["fan", "wind"]) +async def test_ac_set_hvac_mode_fan( hass: HomeAssistant, devices: AsyncMock, mock_config_entry: MockConfigEntry, + mode: str, ) -> None: """Test setting AC HVAC mode to wind if the device supports it.""" set_attribute_value( devices, Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES, - ["auto", "cool", "dry", "heat", "wind"], + ["auto", "cool", "dry", "heat", mode], ) set_attribute_value(devices, Capability.SWITCH, Attribute.SWITCH, "on") @@ -223,7 +225,7 @@ async def test_ac_set_hvac_mode_wind( Capability.AIR_CONDITIONER_MODE, Command.SET_AIR_CONDITIONER_MODE, MAIN, - argument="wind", + argument=mode, ) From e80069545f646d6eae6111c3942a44dc08e4b3ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 19:53:46 +0200 Subject: [PATCH 0536/1175] Only set suggested area for new SmartThings devices (#145063) --- .../components/smartthings/__init__.py | 17 ++++++++-- tests/components/smartthings/test_init.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index b78d2695370..0cb64fe4a33 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ATTR_HW_VERSION, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_SUGGESTED_AREA, ATTR_SW_VERSION, ATTR_VIA_DEVICE, CONF_ACCESS_TOKEN, @@ -454,14 +455,24 @@ def create_devices( ATTR_SW_VERSION: viper.software_version, } ) + if ( + device_registry.async_get_device({(DOMAIN, device.device.device_id)}) + is None + ): + kwargs.update( + { + ATTR_SUGGESTED_AREA: ( + rooms.get(device.device.room_id) + if device.device.room_id + else None + ) + } + ) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, device.device.device_id)}, configuration_url="https://account.smartthings.com", name=device.device.label, - suggested_area=( - rooms.get(device.device.room_id) if device.device.room_id else None - ), **kwargs, ) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 1d4b124c60d..fcb962449bf 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -59,6 +59,37 @@ async def test_devices( assert device == snapshot +@pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) +async def test_device_not_resetting_area( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device not resetting area.""" + await setup_integration(hass, mock_config_entry) + + device_id = devices.get_devices.return_value[0].device_id + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + + assert device.area_id == "theater" + + device_registry.async_update_device(device_id=device.id, area_id=None) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + + assert device.area_id is None + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + device = device_registry.async_get_device({(DOMAIN, device_id)}) + assert device.area_id is None + + @pytest.mark.parametrize("device_fixture", ["button"]) async def test_button_event( hass: HomeAssistant, From 87b60967a62e2734ca4d700dd6afd126a02ae2bd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 20:14:41 +0200 Subject: [PATCH 0537/1175] Map SmartThings auto mode correctly (#145061) --- .../components/smartthings/climate.py | 8 ++++---- .../smartthings/snapshots/test_climate.ambr | 20 +++++++++---------- tests/components/smartthings/test_climate.py | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 779909e3d84..2c826697edd 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -31,7 +31,7 @@ from .entity import SmartThingsEntity ATTR_OPERATION_STATE = "operation_state" MODE_TO_STATE = { - "auto": HVACMode.HEAT_COOL, + "auto": HVACMode.AUTO, "cool": HVACMode.COOL, "eco": HVACMode.AUTO, "rush hour": HVACMode.AUTO, @@ -40,7 +40,7 @@ MODE_TO_STATE = { "off": HVACMode.OFF, } STATE_TO_MODE = { - HVACMode.HEAT_COOL: "auto", + HVACMode.AUTO: "auto", HVACMode.COOL: "cool", HVACMode.HEAT: "heat", HVACMode.OFF: "off", @@ -58,7 +58,7 @@ OPERATING_STATE_TO_ACTION = { } AC_MODE_TO_STATE = { - "auto": HVACMode.HEAT_COOL, + "auto": HVACMode.AUTO, "cool": HVACMode.COOL, "dry": HVACMode.DRY, "coolClean": HVACMode.COOL, @@ -70,7 +70,7 @@ AC_MODE_TO_STATE = { "wind": HVACMode.FAN_ONLY, } STATE_TO_AC_MODE = { - HVACMode.HEAT_COOL: "auto", + HVACMode.AUTO: "auto", HVACMode.COOL: "cool", HVACMode.DRY: "dry", HVACMode.HEAT: "heat", diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index 633b02568fc..b23e7024e05 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -146,7 +146,7 @@ , , , - , + , , ]), 'max_temp': 35, @@ -206,7 +206,7 @@ , , , - , + , , ]), 'max_temp': 35, @@ -246,7 +246,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -308,7 +308,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -349,7 +349,7 @@ ]), 'hvac_modes': list([ , - , + , , , , @@ -414,7 +414,7 @@ 'friendly_name': 'Aire Dormitorio Principal', 'hvac_modes': list([ , - , + , , , , @@ -462,7 +462,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -513,7 +513,7 @@ , , , - , + , ]), 'max_temp': 35, 'min_temp': 7, @@ -541,7 +541,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, @@ -589,7 +589,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 7864063235b..8241e6de3b3 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -119,7 +119,7 @@ async def test_ac_set_hvac_mode_off( @pytest.mark.parametrize( ("hvac_mode", "argument"), [ - (HVACMode.HEAT_COOL, "auto"), + (HVACMode.AUTO, "auto"), (HVACMode.COOL, "cool"), (HVACMode.DRY, "dry"), (HVACMode.HEAT, "heat"), @@ -174,7 +174,7 @@ async def test_ac_set_hvac_mode_turns_on( SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: "climate.ac_office_granit", - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -268,7 +268,7 @@ async def test_ac_set_temperature_and_hvac_mode_while_off( { ATTR_ENTITY_ID: "climate.ac_office_granit", ATTR_TEMPERATURE: 23, - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -318,7 +318,7 @@ async def test_ac_set_temperature_and_hvac_mode( { ATTR_ENTITY_ID: "climate.ac_office_granit", ATTR_TEMPERATURE: 23, - ATTR_HVAC_MODE: HVACMode.HEAT_COOL, + ATTR_HVAC_MODE: HVACMode.AUTO, }, blocking=True, ) @@ -625,7 +625,7 @@ async def test_thermostat_set_hvac_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.AUTO}, blocking=True, ) devices.execute_device_command.assert_called_once_with( From 7fefd58b84c411436773cee2c27fb2c8dc14003d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 21:17:07 +0200 Subject: [PATCH 0538/1175] Don't create entities for Smartthings smarttags (#145066) --- .../components/smartthings/__init__.py | 11 ++ tests/components/smartthings/conftest.py | 1 + .../device_status/im_smarttag2_ble_uwb.json | 129 ++++++++++++ .../devices/im_smarttag2_ble_uwb.json | 184 ++++++++++++++++++ .../smartthings/snapshots/test_init.ambr | 33 ++++ 5 files changed, 358 insertions(+) create mode 100644 tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json create mode 100644 tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 0cb64fe4a33..52ce07e06e2 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -13,6 +13,7 @@ from aiohttp import ClientResponseError from pysmartthings import ( Attribute, Capability, + Category, ComponentStatus, Device, DeviceEvent, @@ -195,6 +196,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry) } devices = await client.get_devices() for device in devices: + if ( + (main_component := device.components.get(MAIN)) is not None + and main_component.manufacturer_category is Category.BLUETOOTH_TRACKER + ): + device_status[device.device_id] = FullDevice( + device=device, + status={}, + online=True, + ) + continue status = process_status(await client.get_device_status(device.device_id)) online = await client.get_device_health(device.device_id) device_status[device.device_id] = FullDevice( diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 6cad487c0bb..7a2945d4c02 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -153,6 +153,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "generic_ef00_v1", "bosch_radiator_thermostat_ii", "im_speaker_ai_0001", + "im_smarttag2_ble_uwb", "abl_light_b_001", "tplink_p110", "ikea_kadrilj", diff --git a/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json b/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json new file mode 100644 index 00000000000..e59db7476de --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/im_smarttag2_ble_uwb.json @@ -0,0 +1,129 @@ +{ + "components": { + "main": { + "tag.e2eEncryption": { + "encryption": { + "value": null + } + }, + "audioVolume": { + "volume": { + "value": null + } + }, + "geofence": { + "enableState": { + "value": null + }, + "geofence": { + "value": null + }, + "name": { + "value": null + } + }, + "tag.updatedInfo": { + "connection": { + "value": "connected", + "timestamp": "2024-02-27T17:44:57.638Z" + } + }, + "tag.factoryReset": {}, + "battery": { + "quantity": { + "value": null + }, + "battery": { + "value": null + }, + "type": { + "value": null + } + }, + "firmwareUpdate": { + "lastUpdateStatusReason": { + "value": null + }, + "availableVersion": { + "value": null + }, + "lastUpdateStatus": { + "value": null + }, + "supportedCommands": { + "value": null + }, + "state": { + "value": null + }, + "updateAvailable": { + "value": false, + "timestamp": "2024-06-25T05:56:22.227Z" + }, + "currentVersion": { + "value": null + }, + "lastUpdateTime": { + "value": null + } + }, + "tag.searchingStatus": { + "searchingStatus": { + "value": null + } + }, + "tag.tagStatus": { + "connectedUserId": { + "value": null + }, + "tagStatus": { + "value": null + }, + "connectedDeviceId": { + "value": null + } + }, + "alarm": { + "alarm": { + "value": null + } + }, + "tag.tagButton": { + "tagButton": { + "value": null + } + }, + "tag.uwbActivation": { + "uwbActivation": { + "value": null + } + }, + "geolocation": { + "method": { + "value": null + }, + "heading": { + "value": null + }, + "latitude": { + "value": null + }, + "accuracy": { + "value": null + }, + "altitudeAccuracy": { + "value": null + }, + "speed": { + "value": null + }, + "longitude": { + "value": null + }, + "lastUpdateTime": { + "value": null + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json b/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json new file mode 100644 index 00000000000..802b4da1514 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/im_smarttag2_ble_uwb.json @@ -0,0 +1,184 @@ +{ + "items": [ + { + "deviceId": "83d660e4-b0c8-4881-a674-d9f1730366c1", + "name": "Tag(UWB)", + "label": "SmartTag+ black", + "manufacturerName": "Samsung Electronics", + "presentationId": "IM-SmartTag-BLE-UWB", + "deviceManufacturerCode": "Samsung Electronics", + "locationId": "redacted_locid", + "ownerId": "redacted", + "roomId": "redacted_roomid", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "alarm", + "version": 1 + }, + { + "id": "tag.tagButton", + "version": 1 + }, + { + "id": "audioVolume", + "version": 1 + }, + { + "id": "battery", + "version": 1 + }, + { + "id": "tag.factoryReset", + "version": 1 + }, + { + "id": "tag.e2eEncryption", + "version": 1 + }, + { + "id": "tag.tagStatus", + "version": 1 + }, + { + "id": "geolocation", + "version": 1 + }, + { + "id": "geofence", + "version": 1 + }, + { + "id": "tag.uwbActivation", + "version": 1 + }, + { + "id": "tag.updatedInfo", + "version": 1 + }, + { + "id": "tag.searchingStatus", + "version": 1 + }, + { + "id": "firmwareUpdate", + "version": 1 + } + ], + "categories": [ + { + "name": "BluetoothTracker", + "categoryType": "manufacturer" + } + ] + } + ], + "createTime": "2023-05-25T09:42:59.720Z", + "profile": { + "id": "e443f3e8-a926-3deb-917c-e5c6de3af70f" + }, + "bleD2D": { + "encryptionKey": "ZTbd_04NISrhQODE7_i8JdcG2ZWwqmUfY60taptK7J0=", + "cipher": "AES_128-CBC-PKCS7Padding", + "identifier": "415D4Y16F97F", + "configurationVersion": "2.0", + "configurationUrl": "https://apis.samsungiotcloud.com/v1/miniature/profile/b8e65e7e-6152-4704-b9f5-f16352034237", + "bleDeviceType": "BLE", + "metadata": { + "regionCode": 11, + "privacyIdPoolSize": 2000, + "privacyIdSeed": "AAAAAAAX8IQ=", + "privacyIdInitialVector": "ZfqZKLRGSeCwgNhdqHFRpw==", + "numAllowableConnections": 2, + "firmware": { + "version": "1.03.07", + "specVersion": "0.5.6", + "updateTime": 1685007914000, + "latestFirmware": { + "id": 581, + "version": "1.03.07", + "data": { + "checksum": "50E7", + "size": "586004", + "supportedVersion": "0.5.6" + } + } + }, + "currentServerTime": 1739095473, + "searchingStatus": "stop", + "lastKnownConnection": { + "updated": 1713422813, + "connectedUser": { + "id": "sk3oyvsbkm", + "name": "" + }, + "connectedDevice": { + "id": "4f3faa4c-976c-3bd8-b209-607f3a5a9814", + "name": "" + }, + "d2dStatus": "bleScanned", + "nearby": true, + "onDemand": false + }, + "e2eEncryption": { + "enabled": false + }, + "timer": 1713422675, + "category": { + "id": 0 + }, + "remoteRing": { + "enabled": false + }, + "petWalking": { + "enabled": false + }, + "onboardedBy": { + "saGuid": "sk3oyvsbkm" + }, + "shareable": { + "enabled": false + }, + "agingCounter": { + "status": "VALID", + "updated": 1713422675 + }, + "vendor": { + "mnId": "0AFD", + "setupId": "432", + "modelName": "EI-T7300" + }, + "priorityConnection": { + "lba": false, + "cameraShutter": false + }, + "createTime": 1685007780, + "updateTime": 1713422675, + "fmmSearch": false, + "ooTime": { + "currentOoTime": 8, + "defaultOoTime": 8 + }, + "pidPoolSize": { + "desiredPidPoolSize": 2000, + "currentPidPoolSize": 2000 + }, + "activeMode": { + "mode": 0 + }, + "itemConfig": { + "searchingStatus": "stop" + } + } + }, + "type": "BLE_D2D", + "restrictionTier": 0, + "allowed": [], + "executionContext": "CLOUD" + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index dc5d7e6aeeb..e96615f3120 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1586,6 +1586,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[im_smarttag2_ble_uwb] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '83d660e4-b0c8-4881-a674-d9f1730366c1', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'SmartTag+ black', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[im_speaker_ai_0001] DeviceRegistryEntrySnapshot({ 'area_id': None, From dbc15a2ddac16b02bde793a4dbc4290af42d5659 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 16 May 2025 21:22:43 +0200 Subject: [PATCH 0539/1175] Fix Ecovacs mower area sensors (#145071) --- homeassistant/components/ecovacs/sensor.py | 39 +++++++++++++++++-- .../ecovacs/snapshots/test_sensor.ambr | 8 ++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 6c8ae080fc3..a8600d786a8 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -6,7 +6,8 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Generic -from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan +from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType +from deebot_client.device import Device from deebot_client.events import ( BatteryEvent, ErrorEvent, @@ -34,7 +35,7 @@ from homeassistant.const import ( UnitOfArea, UnitOfTime, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType @@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription( """Ecovacs sensor entity description.""" value_fn: Callable[[EventT], StateType] + native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None + + +@callback +def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None: + """Get the area native unit of measurement based on device type.""" + if device_type is DeviceType.MOWER: + return UnitOfArea.SQUARE_CENTIMETERS + return UnitOfArea.SQUARE_METERS ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( @@ -68,7 +78,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", - native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + native_unit_of_measurement_fn=get_area_native_unit_of_measurement, ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", @@ -85,7 +95,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( value_fn=lambda e: e.area, key="total_stats_area", translation_key="total_stats_area", - native_unit_of_measurement=UnitOfArea.SQUARE_METERS, + native_unit_of_measurement_fn=get_area_native_unit_of_measurement, state_class=SensorStateClass.TOTAL_INCREASING, ), EcovacsSensorEntityDescription[TotalStatsEvent]( @@ -249,6 +259,27 @@ class EcovacsSensor( entity_description: EcovacsSensorEntityDescription + def __init__( + self, + device: Device, + capability: CapabilityEvent, + entity_description: EcovacsSensorEntityDescription, + **kwargs: Any, + ) -> None: + """Initialize entity.""" + super().__init__(device, capability, entity_description, **kwargs) + if ( + entity_description.native_unit_of_measurement_fn + and ( + native_unit_of_measurement + := entity_description.native_unit_of_measurement_fn( + device.capabilities.device_type + ) + ) + is not None + ): + self._attr_native_unit_of_measurement = native_unit_of_measurement + async def async_added_to_hass(self) -> None: """Set up the event listeners now that hass is ready.""" await super().async_added_to_hass() diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index c4e5a5b1966..7fa7a41234d 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -181,14 +181,14 @@ 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Goat G1 Area cleaned', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_area_cleaned', @@ -523,7 +523,7 @@ 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state] @@ -531,7 +531,7 @@ 'attributes': ReadOnlyDict({ 'friendly_name': 'Goat G1 Total area cleaned', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_total_area_cleaned', From 0bbbd2cd544210b0aaae662f4b23ef053c54bf32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 21:45:11 +0200 Subject: [PATCH 0540/1175] Use runtime_data in hydrawise (#144950) --- .../components/hydrawise/__init__.py | 22 +++++++++---------- .../components/hydrawise/binary_sensor.py | 9 ++++---- .../components/hydrawise/coordinator.py | 11 +++++++--- homeassistant/components/hydrawise/sensor.py | 8 +++---- homeassistant/components/hydrawise/switch.py | 9 ++++---- homeassistant/components/hydrawise/valve.py | 8 +++---- 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index ce4d7a8f8c2..d15df52bb71 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -2,13 +2,13 @@ from pydrawise import auth, hybrid -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import APP_ID, DOMAIN +from .const import APP_ID from .coordinator import ( + HydrawiseConfigEntry, HydrawiseMainDataUpdateCoordinator, HydrawiseUpdateCoordinators, HydrawiseWaterUseDataUpdateCoordinator, @@ -24,7 +24,9 @@ PLATFORMS: list[Platform] = [ _REQUIRED_AUTH_KEYS = (CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry( + hass: HomeAssistant, config_entry: HydrawiseConfigEntry +) -> bool: """Set up Hydrawise from a config entry.""" if any(k not in config_entry.data for k in _REQUIRED_AUTH_KEYS): # If we are missing any required authentication keys, trigger a reauth flow. @@ -45,18 +47,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass, config_entry, hydrawise, main_coordinator ) await water_use_coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ( - HydrawiseUpdateCoordinators( - main=main_coordinator, - water_use=water_use_coordinator, - ) + config_entry.runtime_data = HydrawiseUpdateCoordinators( + main=main_coordinator, + water_use=water_use_coordinator, ) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HydrawiseConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index b2862930933..45537a2cc73 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -14,14 +14,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import VolDictType -from .const import DOMAIN, SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND -from .coordinator import HydrawiseUpdateCoordinators +from .const import SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND +from .coordinator import HydrawiseConfigEntry from .entity import HydrawiseEntity @@ -77,11 +76,11 @@ SCHEMA_SUSPEND: VolDictType = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HydrawiseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Hydrawise binary_sensor platform.""" - coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data entities: list[HydrawiseBinarySensor] = [] for controller in coordinators.main.data.controllers.values(): entities.extend( diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py index 35d816b341b..15d286801f9 100644 --- a/homeassistant/components/hydrawise/coordinator.py +++ b/homeassistant/components/hydrawise/coordinator.py @@ -14,6 +14,8 @@ from homeassistant.util.dt import now from .const import DOMAIN, LOGGER, MAIN_SCAN_INTERVAL, WATER_USE_SCAN_INTERVAL +type HydrawiseConfigEntry = ConfigEntry[HydrawiseUpdateCoordinators] + @dataclass class HydrawiseData: @@ -40,7 +42,7 @@ class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]): """Base class for Hydrawise Data Update Coordinators.""" api: HydrawiseBase - config_entry: ConfigEntry + config_entry: HydrawiseConfigEntry class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator): @@ -52,7 +54,10 @@ class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator): """ def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, api: HydrawiseBase + self, + hass: HomeAssistant, + config_entry: HydrawiseConfigEntry, + api: HydrawiseBase, ) -> None: """Initialize HydrawiseDataUpdateCoordinator.""" super().__init__( @@ -92,7 +97,7 @@ class HydrawiseWaterUseDataUpdateCoordinator(HydrawiseDataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HydrawiseConfigEntry, api: HydrawiseBase, main_coordinator: HydrawiseMainDataUpdateCoordinator, ) -> None: diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 60bc1d7dc63..ce0bc5a0997 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -14,14 +14,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTime, UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DOMAIN -from .coordinator import HydrawiseUpdateCoordinators +from .coordinator import HydrawiseConfigEntry from .entity import HydrawiseEntity @@ -130,11 +128,11 @@ FLOW_MEASUREMENT_KEYS = [x.key for x in FLOW_CONTROLLER_SENSORS] async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HydrawiseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Hydrawise sensor platform.""" - coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data entities: list[HydrawiseSensor] = [] for controller in coordinators.main.data.controllers.values(): entities.extend( diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index bc6b31e6d2e..7a77f27265b 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -14,13 +14,12 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from .const import DEFAULT_WATERING_TIME, DOMAIN -from .coordinator import HydrawiseUpdateCoordinators +from .const import DEFAULT_WATERING_TIME +from .coordinator import HydrawiseConfigEntry from .entity import HydrawiseEntity @@ -62,11 +61,11 @@ SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES] async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HydrawiseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Hydrawise switch platform.""" - coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data async_add_entities( HydrawiseSwitch(coordinators.main, description, controller, zone_id=zone.id) for controller in coordinators.main.data.controllers.values() diff --git a/homeassistant/components/hydrawise/valve.py b/homeassistant/components/hydrawise/valve.py index 13aff22ccbf..85a91c807b2 100644 --- a/homeassistant/components/hydrawise/valve.py +++ b/homeassistant/components/hydrawise/valve.py @@ -12,12 +12,10 @@ from homeassistant.components.valve import ( ValveEntityDescription, ValveEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import HydrawiseUpdateCoordinators +from .coordinator import HydrawiseConfigEntry from .entity import HydrawiseEntity VALVE_TYPES: tuple[ValveEntityDescription, ...] = ( @@ -30,11 +28,11 @@ VALVE_TYPES: tuple[ValveEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HydrawiseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Hydrawise valve platform.""" - coordinators: HydrawiseUpdateCoordinators = hass.data[DOMAIN][config_entry.entry_id] + coordinators = config_entry.runtime_data async_add_entities( HydrawiseValve(coordinators.main, description, controller, zone_id=zone.id) for controller in coordinators.main.data.controllers.values() From 9d2302f2f526471b625241a1ba85c212246915ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 16 May 2025 21:57:36 +0200 Subject: [PATCH 0541/1175] Use runtime_data in homeworks (#144944) --- .../components/homeworks/__init__.py | 48 +++++++++---------- .../components/homeworks/binary_sensor.py | 7 ++- homeassistant/components/homeworks/button.py | 8 ++-- homeassistant/components/homeworks/light.py | 8 ++-- 4 files changed, 31 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 75fdeb4f8cc..4beea27374a 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping from dataclasses import dataclass import logging from typing import Any @@ -58,6 +57,8 @@ SERVICE_SEND_COMMAND_SCHEMA = vol.Schema( } ) +type HomeworksConfigEntry = ConfigEntry[HomeworksData] + @dataclass class HomeworksData: @@ -72,45 +73,44 @@ class HomeworksData: 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, + async_send_command, schema=SERVICE_SEND_COMMAND_SCHEMA, ) -async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> None: +async def async_send_command(service_call: ServiceCall) -> 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()] + return [ + entry.runtime_data.controller_id + for entry in service_call.hass.config_entries.async_loaded_entries(DOMAIN) + ] 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 + entry: HomeworksConfigEntry + for entry in service_call.hass.config_entries.async_loaded_entries(DOMAIN): + if entry.runtime_data.controller_id == controller_id: + return entry.runtime_data return None - homeworks_data = get_homeworks_data(data[CONF_CONTROLLER_ID]) + homeworks_data = get_homeworks_data(service_call.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_id": service_call.data[CONF_CONTROLLER_ID], "controller_ids": ",".join(get_controller_ids()), }, ) - commands = data[CONF_COMMAND] + commands = service_call.data[CONF_COMMAND] _LOGGER.debug("Send commands: %s", commands) for command in commands: if command.lower().startswith("delay"): @@ -119,7 +119,7 @@ async def async_send_command(hass: HomeAssistant, data: Mapping[str, Any]) -> No await asyncio.sleep(delay / 1000) else: _LOGGER.debug("Sending command '%s'", command) - await hass.async_add_executor_job( + await service_call.hass.async_add_executor_job( homeworks_data.controller._send, # noqa: SLF001 command, ) @@ -132,10 +132,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) -> bool: """Set up Homeworks from a config entry.""" - hass.data.setdefault(DOMAIN, {}) controller_id = entry.options[CONF_CONTROLLER_ID] def hw_callback(msg_type: Any, values: Any) -> None: @@ -174,9 +173,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name = key_config[CONF_NAME] keypads[addr] = HomeworksKeypad(hass, controller, controller_id, addr, name) - hass.data[DOMAIN][entry.entry_id] = HomeworksData( - controller, controller_id, keypads - ) + entry.runtime_data = HomeworksData(controller, controller_id, keypads) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -184,19 +181,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HomeworksConfigEntry) -> bool: """Unload a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - data: HomeworksData = hass.data[DOMAIN].pop(entry.entry_id) - for keypad in data.keypads.values(): + for keypad in entry.runtime_data.keypads.values(): keypad.unsubscribe() - await hass.async_add_executor_job(data.controller.stop) + await hass.async_add_executor_job(entry.runtime_data.controller.stop) return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def update_listener(hass: HomeAssistant, entry: HomeworksConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/homeworks/binary_sensor.py b/homeassistant/components/homeworks/binary_sensor.py index 9bdea75479d..9c2b2e12bc2 100644 --- a/homeassistant/components/homeworks/binary_sensor.py +++ b/homeassistant/components/homeworks/binary_sensor.py @@ -8,14 +8,13 @@ from typing import Any from pyhomeworks.pyhomeworks import HW_KEYPAD_LED_CHANGED, Homeworks from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME 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 AddConfigEntryEntitiesCallback -from . import HomeworksData, HomeworksKeypad +from . import HomeworksConfigEntry, HomeworksKeypad from .const import ( CONF_ADDR, CONF_BUTTONS, @@ -32,11 +31,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HomeworksConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Homeworks binary sensors.""" - data: HomeworksData = hass.data[DOMAIN][entry.entry_id] + data = entry.runtime_data controller = data.controller controller_id = entry.options[CONF_CONTROLLER_ID] entities = [] diff --git a/homeassistant/components/homeworks/button.py b/homeassistant/components/homeworks/button.py index d76c18985e9..47c92a323ee 100644 --- a/homeassistant/components/homeworks/button.py +++ b/homeassistant/components/homeworks/button.py @@ -7,13 +7,12 @@ import asyncio from pyhomeworks.pyhomeworks import Homeworks from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import HomeworksData +from . import HomeworksConfigEntry from .const import ( CONF_ADDR, CONF_BUTTONS, @@ -28,12 +27,11 @@ from .entity import HomeworksEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HomeworksConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Homeworks buttons.""" - data: HomeworksData = hass.data[DOMAIN][entry.entry_id] - controller = data.controller + controller = entry.runtime_data.controller controller_id = entry.options[CONF_CONTROLLER_ID] entities = [] for keypad in entry.options.get(CONF_KEYPADS, []): diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index f07758bbace..a9ed35f859e 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -8,14 +8,13 @@ from typing import Any from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED, Homeworks from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME 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 AddConfigEntryEntitiesCallback -from . import HomeworksData +from . import HomeworksConfigEntry from .const import CONF_ADDR, CONF_CONTROLLER_ID, CONF_DIMMERS, CONF_RATE, DOMAIN from .entity import HomeworksEntity @@ -24,12 +23,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HomeworksConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Homeworks lights.""" - data: HomeworksData = hass.data[DOMAIN][entry.entry_id] - controller = data.controller + controller = entry.runtime_data.controller controller_id = entry.options[CONF_CONTROLLER_ID] entities = [] for dimmer in entry.options.get(CONF_DIMMERS, []): From 757c66613db5b3ba21e520719d93318f147441ff Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 21:59:12 +0200 Subject: [PATCH 0542/1175] Deprecate SmartThings water heater sensors (#145060) --- .../components/smartthings/sensor.py | 26 +- .../components/smartthings/strings.json | 8 + .../smartthings/snapshots/test_sensor.ambr | 402 ------------------ tests/components/smartthings/test_sensor.py | 52 +++ 4 files changed, 79 insertions(+), 409 deletions(-) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 2aa994ae32c..e5fe6ef1fd6 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -149,7 +149,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): component_fn: Callable[[str], bool] | None = None exists_fn: Callable[[Status], bool] | None = None use_temperature_unit: bool = False - deprecated: Callable[[ComponentStatus], str | None] | None = None + deprecated: Callable[[ComponentStatus], tuple[str, str] | None] | None = None CAPABILITY_TO_SENSORS: dict[ @@ -207,7 +207,7 @@ CAPABILITY_TO_SENSORS: dict[ translation_key="audio_volume", native_unit_of_measurement=PERCENTAGE, deprecated=( - lambda status: "media_player" + lambda status: ("2025.10.0", "media_player") if Capability.AUDIO_MUTE in status else None ), @@ -519,7 +519,7 @@ CAPABILITY_TO_SENSORS: dict[ device_class=SensorDeviceClass.ENUM, options_attribute=Attribute.SUPPORTED_INPUT_SOURCES, value_fn=lambda value: value.lower() if value else None, - deprecated=lambda _: "media_player", + deprecated=lambda _: ("2025.10.0", "media_player"), ) ] }, @@ -528,7 +528,7 @@ CAPABILITY_TO_SENSORS: dict[ SmartThingsSensorEntityDescription( key=Attribute.PLAYBACK_REPEAT_MODE, translation_key="media_playback_repeat", - deprecated=lambda _: "media_player", + deprecated=lambda _: ("2025.10.0", "media_player"), ) ] }, @@ -537,7 +537,7 @@ CAPABILITY_TO_SENSORS: dict[ SmartThingsSensorEntityDescription( key=Attribute.PLAYBACK_SHUFFLE, translation_key="media_playback_shuffle", - deprecated=lambda _: "media_player", + deprecated=lambda _: ("2025.10.0", "media_player"), ) ] }, @@ -556,7 +556,7 @@ CAPABILITY_TO_SENSORS: dict[ ], device_class=SensorDeviceClass.ENUM, value_fn=lambda value: MEDIA_PLAYBACK_STATE_MAP.get(value, value), - deprecated=lambda _: "media_player", + deprecated=lambda _: ("2025.10.0", "media_player"), ) ] }, @@ -837,6 +837,11 @@ CAPABILITY_TO_SENSORS: dict[ key=Attribute.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + deprecated=( + lambda status: ("2025.12.0", "dhw") + if Capability.CUSTOM_OUTING_MODE in status + else None + ), ) ] }, @@ -854,6 +859,11 @@ CAPABILITY_TO_SENSORS: dict[ }, THERMOSTAT_CAPABILITIES, ], + deprecated=( + lambda status: ("2025.12.0", "dhw") + if Capability.CUSTOM_OUTING_MODE in status + else None + ), ) ] }, @@ -1109,18 +1119,20 @@ async def async_setup_entry( if ( description.deprecated and ( - reason := description.deprecated( + deprecation_info := description.deprecated( device.status[MAIN] ) ) is not None ): + version, reason = deprecation_info if deprecate_entity( hass, entity_registry, SENSOR_DOMAIN, f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}", f"deprecated_{reason}", + version, ): entities.append( SmartThingsSensor( diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 96fec1fb0e8..4bcd7463b42 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -603,6 +603,14 @@ "deprecated_media_player_scripts": { "title": "Deprecated sensor detected in some automations or scripts", "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a media player entity.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the new media player entity and disable the sensor to fix this issue." + }, + "deprecated_dhw": { + "title": "Water heater sensors deprecated", + "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a water heater entity.\n\nPlease update your dashboards and templates to use the new water heater entity and disable the sensor to fix this issue." + }, + "deprecated_dhw_scripts": { + "title": "[%key:component::smartthings::issues::deprecated_dhw::title%]", + "description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with a water heater entity.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the new water heater entity and disable the sensor to fix this issue." } } } diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 850ee196ed9..26805a83799 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -1084,55 +1084,6 @@ 'state': '23.0', }) # --- -# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_cooling_set_point-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_pump_cooling_set_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': 'Cooling set point', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thermostat_cooling_setpoint', - 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_cooling_set_point-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Heat pump Cooling set point', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.heat_pump_cooling_set_point', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '56', - }) -# --- # name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1410,58 +1361,6 @@ 'state': '0.0', }) # --- -# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_pump_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ac_ehs_01001][sensor.heat_pump_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Heat pump Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.heat_pump_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '57', - }) -# --- # name: test_all_entities[da_ac_rac_000001][sensor.ac_office_granit_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5769,55 +5668,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_cooling_set_point-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.eco_heating_system_cooling_set_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': 'Cooling set point', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thermostat_cooling_setpoint', - 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_cooling_set_point-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Eco Heating System Cooling set point', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.eco_heating_system_cooling_set_point', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '48', - }) -# --- # name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6095,106 +5945,6 @@ 'state': '1.08249458332857e-05', }) # --- -# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.eco_heating_system_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Eco Heating System Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.eco_heating_system_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '40.8', - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_cooling_set_point-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_pump_main_cooling_set_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': 'Cooling set point', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thermostat_cooling_setpoint', - 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_cooling_set_point-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Heat Pump Main Cooling set point', - }), - 'context': , - 'entity_id': 'sensor.heat_pump_main_cooling_set_point', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6472,106 +6222,6 @@ 'state': '4.50185416638851e-06', }) # --- -# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.heat_pump_main_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Heat Pump Main Temperature', - 'state_class': , - }), - 'context': , - 'entity_id': 'sensor.heat_pump_main_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- -# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_cooling_set_point-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.warmepumpe_cooling_set_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': 'Cooling set point', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'thermostat_cooling_setpoint', - 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_cooling_set_point-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Wärmepumpe Cooling set point', - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.warmepumpe_cooling_set_point', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '52', - }) -# --- # name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6849,58 +6499,6 @@ 'state': '0.000222076093320449', }) # --- -# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.warmepumpe_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': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Wärmepumpe Temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.warmepumpe_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '49.6', - }) -# --- # name: test_all_entities[da_wm_dw_000001][sensor.dishwasher_completion_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index ecdcd700cab..04ad85ef02d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -71,6 +71,7 @@ async def test_state_update( "issue_string", "entity_id", "expected_state", + "version", ), [ ( @@ -80,6 +81,7 @@ async def test_state_update( "media_player", "sensor.tv_samsung_8_series_49_media_playback_status", STATE_UNKNOWN, + "2025.10.0", ), ( "vd_stv_2017_k", @@ -88,6 +90,7 @@ async def test_state_update( "media_player", "sensor.tv_samsung_8_series_49_volume", "13", + "2025.10.0", ), ( "vd_stv_2017_k", @@ -96,6 +99,7 @@ async def test_state_update( "media_player", "sensor.tv_samsung_8_series_49_media_input_source", "hdmi1", + "2025.10.0", ), ( "im_speaker_ai_0001", @@ -104,6 +108,7 @@ async def test_state_update( "media_player", "sensor.galaxy_home_mini_media_playback_repeat", "off", + "2025.10.0", ), ( "im_speaker_ai_0001", @@ -112,6 +117,25 @@ async def test_state_update( "media_player", "sensor.galaxy_home_mini_media_playback_shuffle", "disabled", + "2025.10.0", + ), + ( + "da_ac_ehs_01001", + f"4165c51e-bf6b-c5b6-fd53-127d6248754b_{MAIN}_{Capability.TEMPERATURE_MEASUREMENT}_{Attribute.TEMPERATURE}_{Attribute.TEMPERATURE}", + "temperature", + "dhw", + "sensor.temperature", + "57", + "2025.12.0", + ), + ( + "da_ac_ehs_01001", + f"4165c51e-bf6b-c5b6-fd53-127d6248754b_{MAIN}_{Capability.THERMOSTAT_COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}", + "cooling_setpoint", + "dhw", + "sensor.cooling_setpoint", + "56", + "2025.12.0", ), ], ) @@ -126,6 +150,7 @@ async def test_create_issue_with_items( issue_string: str, entity_id: str, expected_state: str, + version: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" issue_id = f"deprecated_{issue_string}_{entity_id}" @@ -189,6 +214,7 @@ async def test_create_issue_with_items( "entity_name": suggested_object_id, "items": "- [test](/config/automation/edit/test)\n- [test](/config/script/edit/test)", } + assert issue.breaks_in_ha_version == version entity_registry.async_update_entity( entity_entry.entity_id, @@ -211,6 +237,7 @@ async def test_create_issue_with_items( "issue_string", "entity_id", "expected_state", + "version", ), [ ( @@ -220,6 +247,7 @@ async def test_create_issue_with_items( "media_player", "sensor.tv_samsung_8_series_49_media_playback_status", STATE_UNKNOWN, + "2025.10.0", ), ( "vd_stv_2017_k", @@ -228,6 +256,7 @@ async def test_create_issue_with_items( "media_player", "sensor.tv_samsung_8_series_49_volume", "13", + "2025.10.0", ), ( "vd_stv_2017_k", @@ -236,6 +265,7 @@ async def test_create_issue_with_items( "media_player", "sensor.tv_samsung_8_series_49_media_input_source", "hdmi1", + "2025.10.0", ), ( "im_speaker_ai_0001", @@ -244,6 +274,7 @@ async def test_create_issue_with_items( "media_player", "sensor.galaxy_home_mini_media_playback_repeat", "off", + "2025.10.0", ), ( "im_speaker_ai_0001", @@ -252,6 +283,25 @@ async def test_create_issue_with_items( "media_player", "sensor.galaxy_home_mini_media_playback_shuffle", "disabled", + "2025.10.0", + ), + ( + "da_ac_ehs_01001", + f"4165c51e-bf6b-c5b6-fd53-127d6248754b_{MAIN}_{Capability.TEMPERATURE_MEASUREMENT}_{Attribute.TEMPERATURE}_{Attribute.TEMPERATURE}", + "temperature", + "dhw", + "sensor.temperature", + "57", + "2025.12.0", + ), + ( + "da_ac_ehs_01001", + f"4165c51e-bf6b-c5b6-fd53-127d6248754b_{MAIN}_{Capability.THERMOSTAT_COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}", + "cooling_setpoint", + "dhw", + "sensor.cooling_setpoint", + "56", + "2025.12.0", ), ], ) @@ -266,6 +316,7 @@ async def test_create_issue( issue_string: str, entity_id: str, expected_state: str, + version: str, ) -> None: """Test we create an issue when an automation or script is using a deprecated entity.""" issue_id = f"deprecated_{issue_string}_{entity_id}" @@ -290,6 +341,7 @@ async def test_create_issue( "entity_id": entity_id, "entity_name": suggested_object_id, } + assert issue.breaks_in_ha_version == version entity_registry.async_update_entity( entity_entry.entity_id, From f9231de82459b14a68c4c9d73b998e7f0e9aa6b6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 16 May 2025 22:12:59 +0200 Subject: [PATCH 0543/1175] Add additional explanation for Reolink password requirements (#145000) --- homeassistant/components/reolink/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 82941bd5af2..94d2ee3cf27 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -31,7 +31,7 @@ "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 authorization level \"{userlevel}\"", - "password_incompatible": "Password contains incompatible special character or is too long, maximum 31 characters and only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}", + "password_incompatible": "Password contains incompatible special character or is too long, maximum 31 characters and only these characters are allowed: a-z, A-Z, 0-9 or {special_chars}. The streaming protocols necessitate these additional password restrictions.", "unknown": "[%key:common::config_flow::error::unknown%]", "update_needed": "Failed to log in because of outdated firmware, please update the firmware to version {needed_firmware} using the Reolink Download Center: {download_center_url}, currently version {current_firmware} is installed", "webhook_exception": "Home Assistant URL is not available, go to Settings > System > Network > Home Assistant URL and correct the URLs, see {more_info}" From 0deed82bea6056a6efb345d5f30c4cb330c30552 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 May 2025 16:22:46 -0400 Subject: [PATCH 0544/1175] OpenAI prompt is optional (#145065) --- homeassistant/components/openai_conversation/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index fbe64492b3c..6d3f461981c 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -177,7 +177,9 @@ class OpenAIOptionsFlow(OptionsFlow): options = { CONF_RECOMMENDED: user_input[CONF_RECOMMENDED], - CONF_PROMPT: user_input[CONF_PROMPT], + CONF_PROMPT: user_input.get( + CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT + ), CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API), } From a501451038ac4b228d250a658b3fe2ba3ac3048a Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 16 May 2025 22:27:09 +0200 Subject: [PATCH 0545/1175] Remove address parameter from services.yaml (#145052) --- homeassistant/components/lcn/services.yaml | 105 +++------------------ 1 file changed, 15 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index f58e79b9f40..ad0e7dfec86 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -2,9 +2,10 @@ output_abs: fields: - device_id: + device_id: &device_id + required: true example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: &device_selector + selector: device: filter: - integration: lcn @@ -71,10 +72,6 @@ output_abs: model: LCN-UMF - integration: lcn model: LCN-WBH - address: - example: "myhome.s0.m7" - selector: - text: output: required: true selector: @@ -102,13 +99,7 @@ output_abs: output_rel: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id output: required: true selector: @@ -128,13 +119,7 @@ output_rel: output_toggle: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id output: required: true selector: @@ -155,13 +140,7 @@ output_toggle: relays: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id state: required: true example: "t---001-" @@ -170,13 +149,7 @@ relays: led: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id led: required: true selector: @@ -206,13 +179,7 @@ led: var_abs: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id variable: required: true default: native @@ -275,13 +242,7 @@ var_abs: var_reset: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id variable: required: true selector: @@ -310,13 +271,7 @@ var_reset: var_rel: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id variable: required: true selector: @@ -403,13 +358,7 @@ var_rel: lock_regulator: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id setpoint: required: true selector: @@ -439,13 +388,7 @@ lock_regulator: send_keys: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id keys: required: true example: "a1a5d8" @@ -488,13 +431,7 @@ send_keys: lock_keys: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id table: example: "a" default: a @@ -533,13 +470,7 @@ lock_keys: dyn_text: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id row: required: true selector: @@ -554,13 +485,7 @@ dyn_text: pck: fields: - device_id: - example: "91aa039a2fb6e0b9f9ec7eb219a6b7d2" - selector: *device_selector - address: - example: "myhome.s0.m7" - selector: - text: + device_id: *device_id pck: required: true example: "PIN4" From 5aff3499a0e3b3de6aa84c310b96807ba9ba11ab Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 16 May 2025 22:29:00 +0200 Subject: [PATCH 0546/1175] Add number entities for freezer setpoint in SmartThings (#145069) --- .../components/smartthings/icons.json | 3 + .../components/smartthings/number.py | 79 +++- .../components/smartthings/strings.json | 9 + .../smartthings/snapshots/test_number.ambr | 348 ++++++++++++++++++ 4 files changed, 437 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 394035aafb6..54dee9b29d2 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -31,6 +31,9 @@ "number": { "washer_rinse_cycles": { "default": "mdi:waves-arrow-up" + }, + "freezer_temperature": { + "default": "mdi:snowflake-thermometer" } }, "select": { diff --git a/homeassistant/components/smartthings/number.py b/homeassistant/components/smartthings/number.py index 1ad9486903a..6ac2f60d7a9 100644 --- a/homeassistant/components/smartthings/number.py +++ b/homeassistant/components/smartthings/number.py @@ -4,13 +4,13 @@ from __future__ import annotations from pysmartthings import Attribute, Capability, Command, SmartThings -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry -from .const import MAIN +from .const import MAIN, UNIT_MAP from .entity import SmartThingsEntity @@ -35,6 +35,15 @@ async def async_setup_entry( and Capability.SAMSUNG_CE_CONNECTION_STATE not in hood_component ) ) + entities.extend( + SmartThingsRefrigeratorTemperatureNumberEntity( + entry_data.client, device, component + ) + for device in entry_data.devices.values() + for component in device.status + if component in ("cooler", "freezer") + and Capability.THERMOSTAT_COOLING_SETPOINT in device.status[component] + ) async_add_entities(entities) @@ -142,3 +151,69 @@ class SmartThingsHoodNumberEntity(SmartThingsEntity, NumberEntity): Command.SET_HOOD_FAN_SPEED, int(value), ) + + +class SmartThingsRefrigeratorTemperatureNumberEntity(SmartThingsEntity, NumberEntity): + """Define a SmartThings number.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = NumberDeviceClass.TEMPERATURE + + def __init__(self, client: SmartThings, device: FullDevice, component: str) -> None: + """Initialize the instance.""" + super().__init__( + client, + device, + {Capability.THERMOSTAT_COOLING_SETPOINT}, + component=component, + ) + self._attr_unique_id = f"{device.device.device_id}_{component}_{Capability.THERMOSTAT_COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}_{Attribute.COOLING_SETPOINT}" + unit = self._internal_state[Capability.THERMOSTAT_COOLING_SETPOINT][ + Attribute.COOLING_SETPOINT + ].unit + assert unit is not None + self._attr_native_unit_of_measurement = UNIT_MAP[unit] + self._attr_translation_key = { + "cooler": "cooler_temperature", + "freezer": "freezer_temperature", + }[component] + + @property + def range(self) -> dict[str, int]: + """Return the list of options.""" + return self.get_attribute_value( + Capability.THERMOSTAT_COOLING_SETPOINT, + Attribute.COOLING_SETPOINT_RANGE, + ) + + @property + def native_value(self) -> int: + """Return the current value.""" + return int( + self.get_attribute_value( + Capability.THERMOSTAT_COOLING_SETPOINT, Attribute.COOLING_SETPOINT + ) + ) + + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + return self.range["minimum"] + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + return self.range["maximum"] + + @property + def native_step(self) -> float: + """Return the step value.""" + return self.range["step"] + + async def async_set_native_value(self, value: float) -> None: + """Set the value.""" + await self.execute_device_command( + Capability.THERMOSTAT_COOLING_SETPOINT, + Command.SET_COOLING_SETPOINT, + int(value), + ) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 4bcd7463b42..4005e769bc5 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -108,6 +108,15 @@ }, "hood_fan_speed": { "name": "Fan speed" + }, + "freezer_temperature": { + "name": "Freezer temperature" + }, + "cooler_temperature": { + "name": "Cooler temperature" + }, + "cool_select_plus_temperature": { + "name": "CoolSelect+ temperature" } }, "select": { diff --git a/tests/components/smartthings/snapshots/test_number.ambr b/tests/components/smartthings/snapshots/test_number.ambr index 8832336a1fa..34073173861 100644 --- a/tests/components/smartthings/snapshots/test_number.ambr +++ b/tests/components/smartthings/snapshots/test_number.ambr @@ -55,6 +55,354 @@ 'state': '0', }) # --- +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 7.0, + 'min': 1.0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.refrigerator_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Cooler temperature', + 'max': 7.0, + 'min': 1.0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.refrigerator_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': -15.0, + 'min': -23.0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.refrigerator_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Freezer temperature', + 'max': -15.0, + 'min': -23.0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.refrigerator_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-18.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 7.0, + 'min': 1.0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.refrigerator_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Cooler temperature', + 'max': 7.0, + 'min': 1.0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.refrigerator_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': -15.0, + 'min': -23.0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.refrigerator_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Freezer temperature', + 'max': -15.0, + 'min': -23.0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.refrigerator_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-18.0', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 7, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.frigo_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Cooler temperature', + 'max': 7, + 'min': 1, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.frigo_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': -15, + 'min': -23, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.frigo_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Freezer temperature', + 'max': -15, + 'min': -23, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.frigo_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17', + }) +# --- # name: test_all_entities[da_wm_wm_000001][number.washer_rinse_cycles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From c845f4e9b249aa8566d1cc7718eb20c6d10159f7 Mon Sep 17 00:00:00 2001 From: jb101010-2 <168106462+jb101010-2@users.noreply.github.com> Date: Fri, 16 May 2025 22:33:14 +0200 Subject: [PATCH 0547/1175] Bump pysuezV2 to 2.0.5 (#145047) --- homeassistant/components/suez_water/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index f09d2e22633..128f7aa4d8d 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["pysuez", "regex"], "quality_scale": "bronze", - "requirements": ["pysuezV2==2.0.4"] + "requirements": ["pysuezV2==2.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 515be945a63..9d6cbbac370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2362,7 +2362,7 @@ pysqueezebox==0.12.0 pystiebeleltron==0.1.0 # homeassistant.components.suez_water -pysuezV2==2.0.4 +pysuezV2==2.0.5 # homeassistant.components.switchbee pyswitchbee==1.8.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9f397dd91d..b36a255b210 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1935,7 +1935,7 @@ pysqueezebox==0.12.0 pystiebeleltron==0.1.0 # homeassistant.components.suez_water -pysuezV2==2.0.4 +pysuezV2==2.0.5 # homeassistant.components.switchbee pyswitchbee==1.8.3 From db5bcd9fc45fb21de02c7cdfc7068d258eb58008 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 16 May 2025 22:39:05 +0200 Subject: [PATCH 0548/1175] Pin rpds-py to 0.24.0 (#145074) --- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 908655ce443..ed9466073dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -217,3 +217,8 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 + +# rpds-py > 0.25.0 requires cargo 1.84.0 +# Stable Alpine current only ships cargo 1.83.0 +# No wheels upstream available for armhf & armv7 +rpds-py==0.24.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b4e18ea5962..307a9c42d53 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -246,6 +246,11 @@ aiofiles>=24.1.0 # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 multidict>=6.4.2 + +# rpds-py > 0.25.0 requires cargo 1.84.0 +# Stable Alpine current only ships cargo 1.83.0 +# No wheels upstream available for armhf & armv7 +rpds-py==0.24.0 """ GENERATED_MESSAGE = ( From 56b3dc02a77857be51ee00bfa0c311fc4e018147 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 17 May 2025 12:45:18 +0200 Subject: [PATCH 0549/1175] Bump motionblinds to 0.6.27 (#145094) --- homeassistant/components/motion_blinds/cover.py | 1 + homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index dbf43e3d30f..165c4c19675 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -51,6 +51,7 @@ POSITION_DEVICE_MAP = { BlindType.CurtainRight: CoverDeviceClass.CURTAIN, BlindType.SkylightBlind: CoverDeviceClass.SHADE, BlindType.InsectScreen: CoverDeviceClass.SHADE, + BlindType.RadioReceiver: CoverDeviceClass.SHADE, } TILT_DEVICE_MAP = { diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 1654d5b5937..1a6c9c5f82f 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -21,5 +21,5 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "iot_class": "local_push", "loggers": ["motionblinds"], - "requirements": ["motionblinds==0.6.26"] + "requirements": ["motionblinds==0.6.27"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9d6cbbac370..c510af76112 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.26 +motionblinds==0.6.27 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b36a255b210..7f6b4333ee2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ monzopy==1.4.2 mopeka-iot-ble==0.8.0 # homeassistant.components.motion_blinds -motionblinds==0.6.26 +motionblinds==0.6.27 # homeassistant.components.motionblinds_ble motionblindsble==0.1.3 From 4c40ec4948956244c7fe839a80f44e20707485ee Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sat, 17 May 2025 13:06:02 +0200 Subject: [PATCH 0550/1175] Bump aiontfy to 0.5.2 (#145044) --- homeassistant/components/ntfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ntfy/fixtures/account.json | 7 +++++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ntfy/manifest.json b/homeassistant/components/ntfy/manifest.json index 95204444fbb..fde1569d622 100644 --- a/homeassistant/components/ntfy/manifest.json +++ b/homeassistant/components/ntfy/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["aionfty"], "quality_scale": "bronze", - "requirements": ["aiontfy==0.5.1"] + "requirements": ["aiontfy==0.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index c510af76112..593ce825b20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -316,7 +316,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.1 +aiontfy==0.5.2 # homeassistant.components.nut aionut==4.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f6b4333ee2..3858e46d78e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,7 +298,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.1 +aiontfy==0.5.2 # homeassistant.components.nut aionut==4.3.4 diff --git a/tests/components/ntfy/fixtures/account.json b/tests/components/ntfy/fixtures/account.json index 8b4ee501a4d..29a96beb23b 100644 --- a/tests/components/ntfy/fixtures/account.json +++ b/tests/components/ntfy/fixtures/account.json @@ -55,5 +55,12 @@ "reservations_remaining": 2, "attachment_total_size": 0, "attachment_total_size_remaining": 104857600 + }, + "billing": { + "customer": true, + "subscription": true, + "status": "active", + "interval": "year", + "paid_until": 1754080667 } } From 2dc63eb8c5fba4bbbfd86bc5083d520e1ab18098 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sat, 17 May 2025 07:57:55 -0600 Subject: [PATCH 0551/1175] Refactor fan in vesync (#135744) * Refactor Fan * Add tower fan tests and mode * Schedule update after turn off * Adjust updates to refresh library * correct off command * Revert changes * Merge corrections * Remove unused code to increase test coverage * Ruff * Tests * Test for preset mode * Adjust to increase coverage * Test Corrections * tests to match other PR --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/vesync/common.py | 8 +- homeassistant/components/vesync/const.py | 33 +++- homeassistant/components/vesync/fan.py | 146 +++++++++--------- tests/components/vesync/common.py | 2 + tests/components/vesync/conftest.py | 20 +++ .../components/vesync/snapshots/test_fan.ambr | 6 +- tests/components/vesync/test_fan.py | 113 +++++++++++++- 7 files changed, 244 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index f817c1d0714..6dda6800c62 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -9,7 +9,7 @@ from pyvesync.vesyncswitch import VeSyncWallSwitch from homeassistant.core import HomeAssistant -from .const import VeSyncHumidifierDevice +from .const import VeSyncFanDevice, VeSyncHumidifierDevice _LOGGER = logging.getLogger(__name__) @@ -58,6 +58,12 @@ def is_humidifier(device: VeSyncBaseDevice) -> bool: return isinstance(device, VeSyncHumidifierDevice) +def is_fan(device: VeSyncBaseDevice) -> bool: + """Check if the device represents a fan.""" + + return isinstance(device, VeSyncFanDevice) + + def is_outlet(device: VeSyncBaseDevice) -> bool: """Check if the device represents an outlet.""" diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index ff55bcf2e37..08db4463e07 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -1,6 +1,12 @@ """Constants for VeSync Component.""" -from pyvesync.vesyncfan import VeSyncHumid200300S, VeSyncSuperior6000S +from pyvesync.vesyncfan import ( + VeSyncAir131, + VeSyncAirBaseV2, + VeSyncAirBypass, + VeSyncHumid200300S, + VeSyncSuperior6000S, +) DOMAIN = "vesync" VS_DISCOVERY = "vesync_discovery_{}" @@ -30,6 +36,27 @@ VS_HUMIDIFIER_MODE_HUMIDITY = "humidity" VS_HUMIDIFIER_MODE_MANUAL = "manual" VS_HUMIDIFIER_MODE_SLEEP = "sleep" +VS_FAN_MODE_AUTO = "auto" +VS_FAN_MODE_SLEEP = "sleep" +VS_FAN_MODE_ADVANCED_SLEEP = "advancedSleep" +VS_FAN_MODE_TURBO = "turbo" +VS_FAN_MODE_PET = "pet" +VS_FAN_MODE_MANUAL = "manual" +VS_FAN_MODE_NORMAL = "normal" + +# not a full list as manual is used as speed not present +VS_FAN_MODE_PRESET_LIST_HA = [ + VS_FAN_MODE_AUTO, + VS_FAN_MODE_SLEEP, + VS_FAN_MODE_ADVANCED_SLEEP, + VS_FAN_MODE_TURBO, + VS_FAN_MODE_PET, + VS_FAN_MODE_NORMAL, +] +NIGHT_LIGHT_LEVEL_BRIGHT = "bright" +NIGHT_LIGHT_LEVEL_DIM = "dim" +NIGHT_LIGHT_LEVEL_OFF = "off" + FAN_NIGHT_LIGHT_LEVEL_DIM = "dim" FAN_NIGHT_LIGHT_LEVEL_OFF = "off" FAN_NIGHT_LIGHT_LEVEL_ON = "on" @@ -41,6 +68,10 @@ HUMIDIFIER_NIGHT_LIGHT_LEVEL_OFF = "off" VeSyncHumidifierDevice = VeSyncHumid200300S | VeSyncSuperior6000S """Humidifier device types""" +VeSyncFanDevice = VeSyncAirBypass | VeSyncAirBypass | VeSyncAirBaseV2 | VeSyncAir131 +"""Fan device types""" + + DEV_TYPE_TO_HA = { "wifi-switch-1.3": "outlet", "ESW03-USA": "outlet", diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index daf734d50a8..d9336552744 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -11,6 +11,7 @@ from pyvesync.vesyncbasedevice import VeSyncBaseDevice from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.percentage import ( @@ -19,43 +20,27 @@ from homeassistant.util.percentage import ( ) from homeassistant.util.scaling import int_states_in_range +from .common import is_fan from .const import ( - DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_COORDINATOR, VS_DEVICES, VS_DISCOVERY, + VS_FAN_MODE_ADVANCED_SLEEP, + VS_FAN_MODE_AUTO, + VS_FAN_MODE_MANUAL, + VS_FAN_MODE_NORMAL, + VS_FAN_MODE_PET, + VS_FAN_MODE_PRESET_LIST_HA, + VS_FAN_MODE_SLEEP, + VS_FAN_MODE_TURBO, ) from .coordinator import VeSyncDataCoordinator from .entity import VeSyncBaseEntity _LOGGER = logging.getLogger(__name__) -FAN_MODE_AUTO = "auto" -FAN_MODE_SLEEP = "sleep" -FAN_MODE_PET = "pet" -FAN_MODE_TURBO = "turbo" -FAN_MODE_ADVANCED_SLEEP = "advancedSleep" -FAN_MODE_NORMAL = "normal" - - -PRESET_MODES = { - "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "Core200S": [FAN_MODE_SLEEP], - "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "EverestAir": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_TURBO], - "Vital200S": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_PET], - "Vital100S": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_PET], - "SmartTowerFan": [ - FAN_MODE_ADVANCED_SLEEP, - FAN_MODE_AUTO, - FAN_MODE_TURBO, - FAN_MODE_NORMAL, - ], -} SPEED_RANGE = { # off is not included "LV-PUR131S": (1, 3), "Core200S": (1, 3), @@ -97,13 +82,8 @@ def _setup_entities( coordinator: VeSyncDataCoordinator, ): """Check if device is fan and add entity.""" - entities = [ - VeSyncFanHA(dev, coordinator) - for dev in devices - if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type, "")) == "fan" - ] - async_add_entities(entities, update_before_add=True) + async_add_entities(VeSyncFanHA(dev, coordinator) for dev in devices if is_fan(dev)) class VeSyncFanHA(VeSyncBaseEntity, FanEntity): @@ -118,13 +98,6 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): _attr_name = None _attr_translation_key = "vesync" - def __init__( - self, fan: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator - ) -> None: - """Initialize the VeSync fan device.""" - super().__init__(fan, coordinator) - self.smartfan = fan - @property def is_on(self) -> bool: """Return True if device is on.""" @@ -134,8 +107,8 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): def percentage(self) -> int | None: """Return the current speed.""" if ( - self.smartfan.mode == "manual" - and (current_level := self.smartfan.fan_level) is not None + self.device.mode == VS_FAN_MODE_MANUAL + and (current_level := self.device.fan_level) is not None ): return ranged_value_to_percentage( SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], current_level @@ -152,13 +125,21 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): @property def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" - return PRESET_MODES[SKU_TO_BASE_DEVICE[self.device.device_type]] + if hasattr(self.device, "modes"): + return sorted( + [ + mode + for mode in self.device.modes + if mode in VS_FAN_MODE_PRESET_LIST_HA + ] + ) + return [] @property def preset_mode(self) -> str | None: """Get the current preset mode.""" - if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_TURBO): - return self.smartfan.mode + if self.device.mode in VS_FAN_MODE_PRESET_LIST_HA: + return self.device.mode return None @property @@ -166,65 +147,73 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): """Return the state attributes of the fan.""" attr = {} - if hasattr(self.smartfan, "active_time"): - attr["active_time"] = self.smartfan.active_time + if hasattr(self.device, "active_time"): + attr["active_time"] = self.device.active_time - if hasattr(self.smartfan, "screen_status"): - attr["screen_status"] = self.smartfan.screen_status + if hasattr(self.device, "screen_status"): + attr["screen_status"] = self.device.screen_status - if hasattr(self.smartfan, "child_lock"): - attr["child_lock"] = self.smartfan.child_lock + if hasattr(self.device, "child_lock"): + attr["child_lock"] = self.device.child_lock - if hasattr(self.smartfan, "night_light"): - attr["night_light"] = self.smartfan.night_light + if hasattr(self.device, "night_light"): + attr["night_light"] = self.device.night_light - if hasattr(self.smartfan, "mode"): - attr["mode"] = self.smartfan.mode + if hasattr(self.device, "mode"): + attr["mode"] = self.device.mode return attr def set_percentage(self, percentage: int) -> None: """Set the speed of the device.""" if percentage == 0: - self.smartfan.turn_off() - return + success = self.device.turn_off() + if not success: + raise HomeAssistantError("An error occurred while turning off.") + elif not self.device.is_on: + success = self.device.turn_on() + if not success: + raise HomeAssistantError("An error occurred while turning on.") - if not self.smartfan.is_on: - self.smartfan.turn_on() - - self.smartfan.manual_mode() - self.smartfan.change_fan_speed( + success = self.device.manual_mode() + if not success: + raise HomeAssistantError("An error occurred while manual mode.") + success = self.device.change_fan_speed( math.ceil( percentage_to_ranged_value( SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage ) ) ) + if not success: + raise HomeAssistantError("An error occurred while changing fan speed.") self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of device.""" - if preset_mode not in self.preset_modes: + if preset_mode not in VS_FAN_MODE_PRESET_LIST_HA: raise ValueError( f"{preset_mode} is not one of the valid preset modes: " - f"{self.preset_modes}" + f"{VS_FAN_MODE_PRESET_LIST_HA}" ) - if not self.smartfan.is_on: - self.smartfan.turn_on() + if not self.device.is_on: + self.device.turn_on() - if preset_mode == FAN_MODE_AUTO: - self.smartfan.auto_mode() - elif preset_mode == FAN_MODE_SLEEP: - self.smartfan.sleep_mode() - elif preset_mode == FAN_MODE_ADVANCED_SLEEP: - self.smartfan.advanced_sleep_mode() - elif preset_mode == FAN_MODE_PET: - self.smartfan.pet_mode() - elif preset_mode == FAN_MODE_TURBO: - self.smartfan.turbo_mode() - elif preset_mode == FAN_MODE_NORMAL: - self.smartfan.normal_mode() + if preset_mode == VS_FAN_MODE_AUTO: + success = self.device.auto_mode() + elif preset_mode == VS_FAN_MODE_SLEEP: + success = self.device.sleep_mode() + elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP: + success = self.device.advanced_sleep_mode() + elif preset_mode == VS_FAN_MODE_PET: + success = self.device.pet_mode() + elif preset_mode == VS_FAN_MODE_TURBO: + success = self.device.turbo_mode() + elif preset_mode == VS_FAN_MODE_NORMAL: + success = self.device.normal_mode() + if not success: + raise HomeAssistantError("An error occurred while setting preset mode.") self.schedule_update_ha_state() @@ -244,4 +233,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self.device.turn_off() + success = self.device.turn_off() + if not success: + raise HomeAssistantError("An error occurred while turning off.") + self.schedule_update_ha_state() diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index 5795c977120..cf2f49ff28f 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -15,6 +15,8 @@ ENTITY_HUMIDIFIER_MIST_LEVEL = "number.humidifier_200s_mist_level" ENTITY_HUMIDIFIER_HUMIDITY = "sensor.humidifier_200s_humidity" ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT = "select.humidifier_300s_night_light_level" +ENTITY_FAN = "fan.SmartTowerFan" + ENTITY_SWITCH_DISPLAY = "switch.humidifier_200s_display" ALL_DEVICES = load_json_object_fixture("vesync-devices.json", DOMAIN) diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index df6ebbdf6e7..32f23101755 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -198,6 +198,26 @@ async def install_humidifier_device( await hass.async_block_till_done() +@pytest.fixture(name="fan_config_entry") +async def fan_config_entry( + hass: HomeAssistant, requests_mock: requests_mock.Mocker, config +) -> MockConfigEntry: + """Create a mock VeSync config entry for `SmartTowerFan`.""" + entry = MockConfigEntry( + title="VeSync", + domain=DOMAIN, + data=config[DOMAIN], + ) + entry.add_to_hass(hass) + + device_name = "SmartTowerFan" + mock_multiple_device_responses(requests_mock, [device_name]) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + @pytest.fixture(name="switch_old_id_config_entry") async def switch_old_id_config_entry( hass: HomeAssistant, requests_mock: requests_mock.Mocker, config diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 92473647a39..412bd8a1b2e 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -640,8 +640,8 @@ 'preset_modes': list([ 'advancedSleep', 'auto', - 'turbo', 'normal', + 'turbo', ]), }), 'config_entry_id': , @@ -682,12 +682,12 @@ 'night_light': 'off', 'percentage': None, 'percentage_step': 7.6923076923076925, - 'preset_mode': None, + 'preset_mode': 'normal', 'preset_modes': list([ 'advancedSleep', 'auto', - 'turbo', 'normal', + 'turbo', ]), 'screen_status': False, 'supported_features': , diff --git a/tests/components/vesync/test_fan.py b/tests/components/vesync/test_fan.py index 4d444036a60..ccc8c5cd595 100644 --- a/tests/components/vesync/test_fan.py +++ b/tests/components/vesync/test_fan.py @@ -1,17 +1,24 @@ """Tests for the fan module.""" +from contextlib import nullcontext +from unittest.mock import patch + import pytest import requests_mock from syrupy import SnapshotAssertion -from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.fan import ATTR_PRESET_MODE, DOMAIN as FAN_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 homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import ALL_DEVICE_NAMES, mock_devices_response +from .common import ALL_DEVICE_NAMES, ENTITY_FAN, mock_devices_response from tests.common import MockConfigEntry +NoException = nullcontext() + @pytest.mark.parametrize("device_name", ALL_DEVICE_NAMES) async def test_fan_state( @@ -49,3 +56,105 @@ async def test_fan_state( # Check states for entity in entities: assert hass.states.get(entity.entity_id) == snapshot(name=entity.entity_id) + + +@pytest.mark.parametrize( + ("action", "command"), + [ + (SERVICE_TURN_ON, "pyvesync.vesyncfan.VeSyncTowerFan.turn_on"), + (SERVICE_TURN_OFF, "pyvesync.vesyncfan.VeSyncTowerFan.turn_off"), + ], +) +async def test_turn_on_off_success( + hass: HomeAssistant, + fan_config_entry: MockConfigEntry, + action: str, + command: str, +) -> None: + """Test turn_on and turn_off method.""" + + with ( + patch(command, return_value=True) as method_mock, + ): + with patch( + "homeassistant.components.vesync.fan.VeSyncFanHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + FAN_DOMAIN, + action, + {ATTR_ENTITY_ID: ENTITY_FAN}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() + + +@pytest.mark.parametrize( + ("action", "command"), + [ + (SERVICE_TURN_ON, "pyvesync.vesyncfan.VeSyncTowerFan.turn_on"), + (SERVICE_TURN_OFF, "pyvesync.vesyncfan.VeSyncTowerFan.turn_off"), + ], +) +async def test_turn_on_off_raises_error( + hass: HomeAssistant, + fan_config_entry: MockConfigEntry, + action: str, + command: str, +) -> None: + """Test turn_on and turn_off raises errors when fails.""" + + # returns False indicating failure in which case raises HomeAssistantError. + with ( + patch( + command, + return_value=False, + ) as method_mock, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + FAN_DOMAIN, + action, + {ATTR_ENTITY_ID: ENTITY_FAN}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(True, NoException), (False, pytest.raises(HomeAssistantError))], +) +async def test_set_preset_mode( + hass: HomeAssistant, + fan_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test handling of value in set_preset_mode method. Does this via turn on as it increases test coverage.""" + + # If VeSyncTowerFan.normal_mode fails (returns False), then HomeAssistantError is raised + with ( + expectation, + patch( + "pyvesync.vesyncfan.VeSyncTowerFan.normal_mode", + return_value=api_response, + ) as method_mock, + ): + with patch( + "homeassistant.components.vesync.fan.VeSyncFanHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PRESET_MODE: "normal"}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() From 180e1f462c34bf15af50a5f72d7ca2a933ad04d7 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 17 May 2025 16:44:53 +0200 Subject: [PATCH 0552/1175] Fix proberly Ecovacs mower area sensors (#145078) --- homeassistant/components/ecovacs/sensor.py | 4 ++ .../ecovacs/snapshots/test_sensor.ambr | 48 ++++++++++++++----- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index a8600d786a8..eab642119e4 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -78,7 +78,9 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( capability_fn=lambda caps: caps.stats.clean, value_fn=lambda e: e.area, translation_key="stats_area", + device_class=SensorDeviceClass.AREA, native_unit_of_measurement_fn=get_area_native_unit_of_measurement, + suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[StatsEvent]( key="stats_time", @@ -95,8 +97,10 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( value_fn=lambda e: e.area, key="total_stats_area", translation_key="total_stats_area", + device_class=SensorDeviceClass.AREA, native_unit_of_measurement_fn=get_area_native_unit_of_measurement, state_class=SensorStateClass.TOTAL_INCREASING, + suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[TotalStatsEvent]( capability_fn=lambda caps: caps.stats.total, diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 7fa7a41234d..c78df0e189a 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -172,8 +172,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -181,21 +184,22 @@ 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Goat G1 Area cleaned', - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_area_cleaned', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10', + 'state': '0.0010', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_battery:entity-registry] @@ -514,8 +518,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -523,22 +530,23 @@ 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Goat G1 Total area cleaned', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.goat_g1_total_area_cleaned', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '60', + 'state': '0.0060', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry] @@ -762,8 +770,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -777,6 +788,7 @@ # name: test_sensors[qhe2o2][sensor.dusty_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Dusty Area cleaned', 'unit_of_measurement': , }), @@ -1257,8 +1269,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -1272,6 +1287,7 @@ # name: test_sensors[qhe2o2][sensor.dusty_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Dusty Total area cleaned', 'state_class': , 'unit_of_measurement': , @@ -1553,8 +1569,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Area cleaned', 'platform': 'ecovacs', @@ -1568,6 +1587,7 @@ # name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Ozmo 950 Area cleaned', 'unit_of_measurement': , }), @@ -1943,8 +1963,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Total area cleaned', 'platform': 'ecovacs', @@ -1958,6 +1981,7 @@ # name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'area', 'friendly_name': 'Ozmo 950 Total area cleaned', 'state_class': , 'unit_of_measurement': , From 2956f4fea11a9d14daf5d9c6c69bc5b386586c10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 17 May 2025 12:36:14 -0400 Subject: [PATCH 0553/1175] Ensure that OpenAI tool call deltas have a role (#145085) --- .../openai_conversation/conversation.py | 5 ++ .../snapshots/test_conversation.ambr | 56 ++++++++++++++++--- .../openai_conversation/test_conversation.py | 42 ++++++++++++++ 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 67e79e270d7..126a4713fb5 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -141,6 +141,11 @@ async def _transform_stream( if isinstance(event.item, ResponseOutputMessage): yield {"role": event.item.role} elif isinstance(event.item, ResponseFunctionToolCall): + # OpenAI has tool calls as individual events + # while HA puts tool calls inside the assistant message. + # We turn them into individual assistant content for HA + # to ensure that tools are called as soon as possible. + yield {"role": "assistant"} current_tool_call = event.item elif isinstance(event, ResponseOutputItemDoneEvent): item = event.item.model_dump() diff --git a/tests/components/openai_conversation/snapshots/test_conversation.ambr b/tests/components/openai_conversation/snapshots/test_conversation.ambr index 77c28de2773..0f874969aff 100644 --- a/tests/components/openai_conversation/snapshots/test_conversation.ambr +++ b/tests/components/openai_conversation/snapshots/test_conversation.ambr @@ -17,13 +17,6 @@ }), 'tool_name': 'test_tool', }), - dict({ - 'id': 'call_call_2', - 'tool_args': dict({ - 'param1': 'call2', - }), - 'tool_name': 'test_tool', - }), ]), }), dict({ @@ -33,6 +26,20 @@ 'tool_name': 'test_tool', 'tool_result': 'value1', }), + dict({ + 'agent_id': 'conversation.openai', + 'content': None, + 'role': 'assistant', + 'tool_calls': list([ + dict({ + 'id': 'call_call_2', + 'tool_args': dict({ + 'param1': 'call2', + }), + 'tool_name': 'test_tool', + }), + ]), + }), dict({ 'agent_id': 'conversation.openai', 'role': 'tool_result', @@ -48,3 +55,38 @@ }), ]) # --- +# name: test_function_call_without_reasoning + list([ + dict({ + 'content': 'Please call the test function', + 'role': 'user', + }), + dict({ + 'agent_id': 'conversation.openai', + 'content': None, + 'role': 'assistant', + 'tool_calls': list([ + dict({ + 'id': 'call_call_1', + 'tool_args': dict({ + 'param1': 'call1', + }), + 'tool_name': 'test_tool', + }), + ]), + }), + dict({ + 'agent_id': 'conversation.openai', + 'role': 'tool_result', + 'tool_call_id': 'call_call_1', + 'tool_name': 'test_tool', + 'tool_result': 'value1', + }), + dict({ + 'agent_id': 'conversation.openai', + 'content': 'Cool', + 'role': 'assistant', + 'tool_calls': None, + }), + ]) +# --- diff --git a/tests/components/openai_conversation/test_conversation.py b/tests/components/openai_conversation/test_conversation.py index 269590b483a..99559cb3b61 100644 --- a/tests/components/openai_conversation/test_conversation.py +++ b/tests/components/openai_conversation/test_conversation.py @@ -596,6 +596,48 @@ async def test_function_call( assert mock_chat_log.content[1:] == snapshot +async def test_function_call_without_reasoning( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_init_component, + mock_create_stream: AsyncMock, + mock_chat_log: MockChatLog, # noqa: F811 + snapshot: SnapshotAssertion, +) -> None: + """Test function call from the assistant.""" + mock_create_stream.return_value = [ + # Initial conversation + ( + *create_function_tool_call_item( + id="fc_1", + arguments=['{"para', 'm1":"call1"}'], + call_id="call_call_1", + name="test_tool", + output_index=1, + ), + ), + # Response after tool responses + create_message_item(id="msg_A", text="Cool", output_index=0), + ] + mock_chat_log.mock_tool_results( + { + "call_call_1": "value1", + } + ) + + result = await conversation.async_converse( + hass, + "Please call the test function", + mock_chat_log.conversation_id, + Context(), + agent_id="conversation.openai", + ) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + # Don't test the prompt, as it's not deterministic + assert mock_chat_log.content[1:] == snapshot + + @pytest.mark.parametrize( ("description", "messages"), [ From a83eafd9493446defd0ea9955f7b018fc66f9187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Sat, 17 May 2025 20:17:15 +0200 Subject: [PATCH 0554/1175] Fix mapping from program_phase to vacuum_activity for Miele integration (#145115) --- homeassistant/components/miele/vacuum.py | 32 +++++++++---------- .../miele/fixtures/vacuum_device.json | 9 +++--- .../miele/snapshots/test_vacuum.ambr | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/miele/vacuum.py b/homeassistant/components/miele/vacuum.py index 66b3788fec5..29a89e39bdb 100644 --- a/homeassistant/components/miele/vacuum.py +++ b/homeassistant/components/miele/vacuum.py @@ -141,21 +141,21 @@ async def async_setup_entry( VACUUM_PHASE_TO_ACTIVITY = { - MieleVacuumStateCode.idle: VacuumActivity.IDLE, - MieleVacuumStateCode.docked: VacuumActivity.DOCKED, - MieleVacuumStateCode.cleaning: VacuumActivity.CLEANING, - MieleVacuumStateCode.going_to_target_area: VacuumActivity.CLEANING, - MieleVacuumStateCode.returning: VacuumActivity.RETURNING, - MieleVacuumStateCode.wheel_lifted: VacuumActivity.ERROR, - MieleVacuumStateCode.dirty_sensors: VacuumActivity.ERROR, - MieleVacuumStateCode.dust_box_missing: VacuumActivity.ERROR, - MieleVacuumStateCode.blocked_drive_wheels: VacuumActivity.ERROR, - MieleVacuumStateCode.blocked_brushes: VacuumActivity.ERROR, - MieleVacuumStateCode.check_dust_box_and_filter: VacuumActivity.ERROR, - MieleVacuumStateCode.internal_fault_reboot: VacuumActivity.ERROR, - MieleVacuumStateCode.blocked_front_wheel: VacuumActivity.ERROR, - MieleVacuumStateCode.paused: VacuumActivity.PAUSED, - MieleVacuumStateCode.remote_controlled: VacuumActivity.PAUSED, + MieleVacuumStateCode.idle.value: VacuumActivity.IDLE, + MieleVacuumStateCode.docked.value: VacuumActivity.DOCKED, + MieleVacuumStateCode.cleaning.value: VacuumActivity.CLEANING, + MieleVacuumStateCode.going_to_target_area.value: VacuumActivity.CLEANING, + MieleVacuumStateCode.returning.value: VacuumActivity.RETURNING, + MieleVacuumStateCode.wheel_lifted.value: VacuumActivity.ERROR, + MieleVacuumStateCode.dirty_sensors.value: VacuumActivity.ERROR, + MieleVacuumStateCode.dust_box_missing.value: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_drive_wheels.value: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_brushes.value: VacuumActivity.ERROR, + MieleVacuumStateCode.check_dust_box_and_filter.value: VacuumActivity.ERROR, + MieleVacuumStateCode.internal_fault_reboot.value: VacuumActivity.ERROR, + MieleVacuumStateCode.blocked_front_wheel.value: VacuumActivity.ERROR, + MieleVacuumStateCode.paused.value: VacuumActivity.PAUSED, + MieleVacuumStateCode.remote_controlled.value: VacuumActivity.PAUSED, } @@ -171,7 +171,7 @@ class MieleVacuum(MieleEntity, StateVacuumEntity): def activity(self) -> VacuumActivity | None: """Return activity.""" return VACUUM_PHASE_TO_ACTIVITY.get( - MieleVacuumStateCode(self.device.state_program_phase) + MieleVacuumStateCode(self.device.state_program_phase).value ) @property diff --git a/tests/components/miele/fixtures/vacuum_device.json b/tests/components/miele/fixtures/vacuum_device.json index 6f2d486a8bc..5aa402a3493 100644 --- a/tests/components/miele/fixtures/vacuum_device.json +++ b/tests/components/miele/fixtures/vacuum_device.json @@ -15,7 +15,10 @@ "matNumber": "11686510", "swids": ["", "", "", "<...>"] }, - "xkmIdentLabel": { "techType": "", "releaseVersion": "" } + "xkmIdentLabel": { + "techType": "", + "releaseVersion": "" + } }, "state": { "ProgramID": { @@ -34,9 +37,7 @@ "key_localized": "Program type" }, "programPhase": { - "xvalue_raw": 5889, - "zvalue_raw": 5904, - "value_raw": 5893, + "value_raw": 5889, "value_localized": "in the base station", "key_localized": "Program phase" }, diff --git a/tests/components/miele/snapshots/test_vacuum.ambr b/tests/components/miele/snapshots/test_vacuum.ambr index 71254f9c8b3..8147b56282d 100644 --- a/tests/components/miele/snapshots/test_vacuum.ambr +++ b/tests/components/miele/snapshots/test_vacuum.ambr @@ -58,6 +58,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'paused', + 'state': 'cleaning', }) # --- From 2302a3bb33aae60e924b6672dd0a6a1260f45287 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Sat, 17 May 2025 20:18:14 +0200 Subject: [PATCH 0555/1175] Add missing device condition translations to lock component (#145104) --- homeassistant/components/lock/strings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index fd2854b7932..46788e5a310 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -9,7 +9,11 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked", - "is_open": "{entity_name} is open" + "is_open": "{entity_name} is open", + "is_jammed": "{entity_name} is jammed", + "is_locking": "{entity_name} is locking", + "is_unlocking": "{entity_name} is unlocking", + "is_opening": "{entity_name} is opening" }, "trigger_type": { "locked": "{entity_name} locked", From 67b3428b07d3082aecb7356b91abf247822184c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 17 May 2025 20:19:31 +0200 Subject: [PATCH 0556/1175] Add Steam closet keep fresh mode to SmartThings (#145107) --- .../components/smartthings/binary_sensor.py | 8 ++++ .../components/smartthings/icons.json | 6 +++ .../components/smartthings/strings.json | 6 +++ .../components/smartthings/switch.py | 6 +++ .../snapshots/test_binary_sensor.ambr | 47 +++++++++++++++++++ .../smartthings/snapshots/test_switch.ambr | 47 +++++++++++++++++++ 6 files changed, 120 insertions(+) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 74d561f08ac..ea8db71c481 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -80,6 +80,14 @@ CAPABILITY_TO_SENSORS: dict[ entity_category=EntityCategory.DIAGNOSTIC, ) }, + Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: { + Attribute.OPERATING_STATE: SmartThingsBinarySensorEntityDescription( + key=Attribute.OPERATING_STATE, + translation_key="keep_fresh_mode_active", + is_on_key="running", + entity_category=EntityCategory.DIAGNOSTIC, + ) + }, Capability.FILTER_STATUS: { Attribute.FILTER_STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.FILTER_STATUS, diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 54dee9b29d2..bac2692de92 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -18,6 +18,9 @@ "state": { "on": "mdi:lock" } + }, + "keep_fresh_mode": { + "default": "mdi:creation" } }, "button": { @@ -100,6 +103,9 @@ "off": "mdi:tumble-dryer-off" } }, + "keep_fresh_mode": { + "default": "mdi:creation" + }, "ice_maker": { "default": "mdi:delete-variant" } diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 4005e769bc5..64dacca40ee 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -39,6 +39,9 @@ "dryer_wrinkle_prevent_active": { "name": "Wrinkle prevent active" }, + "keep_fresh_mode_active": { + "name": "Keep fresh mode active" + }, "filter_status": { "name": "Filter status" }, @@ -552,6 +555,9 @@ }, "sabbath_mode": { "name": "Sabbath mode" + }, + "keep_fresh_mode": { + "name": "Keep fresh mode" } }, "water_heater": { diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index f610a97f16e..42d9778428e 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -92,6 +92,12 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio translation_key="sabbath_mode", status_attribute=Attribute.STATUS, ), + Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: SmartThingsSwitchEntityDescription( + key=Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE, + translation_key="keep_fresh_mode", + status_attribute=Attribute.STATUS, + entity_category=EntityCategory.CONFIG, + ), } diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 61cecdbd364..583c256042e 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -1190,6 +1190,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_keep_fresh_mode_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.airdresser_keep_fresh_mode_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Keep fresh mode active', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'keep_fresh_mode_active', + 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetKeepFreshMode_operatingState_operatingState', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_keep_fresh_mode_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirDresser Keep fresh mode active', + }), + 'context': , + 'entity_id': 'binary_sensor.airdresser_keep_fresh_mode_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 060f1d3a374..fc55c4d535f 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -234,6 +234,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_keep_fresh_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airdresser_keep_fresh_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': 'Keep fresh mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'keep_fresh_mode', + 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetKeepFreshMode_status_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_keep_fresh_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirDresser Keep fresh mode', + }), + 'context': , + 'entity_id': 'switch.airdresser_keep_fresh_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 5619042fe71b2f7a61e832c88dd953cb71679866 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 17 May 2025 20:39:17 +0200 Subject: [PATCH 0557/1175] Add Steam closet auto cycle link to SmartThings (#145111) --- .../components/smartthings/icons.json | 6 +++ .../components/smartthings/strings.json | 3 ++ .../components/smartthings/switch.py | 9 +++- .../smartthings/snapshots/test_switch.ambr | 47 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index bac2692de92..3ec13c3adac 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -108,6 +108,12 @@ }, "ice_maker": { "default": "mdi:delete-variant" + }, + "auto_cycle_link": { + "default": "mdi:link-off", + "state": { + "on": "mdi:link" + } } } } diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 64dacca40ee..a1149d6083c 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -556,6 +556,9 @@ "sabbath_mode": { "name": "Sabbath mode" }, + "auto_cycle_link": { + "name": "Auto cycle link" + }, "keep_fresh_mode": { "name": "Keep fresh mode" } diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 42d9778428e..39809c7538a 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -71,7 +71,14 @@ CAPABILITY_TO_COMMAND_SWITCHES: dict[ status_attribute=Attribute.DRYER_WRINKLE_PREVENT, command=Command.SET_DRYER_WRINKLE_PREVENT, entity_category=EntityCategory.CONFIG, - ) + ), + Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK: SmartThingsCommandSwitchEntityDescription( + key=Capability.SAMSUNG_CE_STEAM_CLOSET_AUTO_CYCLE_LINK, + translation_key="auto_cycle_link", + status_attribute=Attribute.STEAM_CLOSET_AUTO_CYCLE_LINK, + command=Command.SET_STEAM_CLOSET_AUTO_CYCLE_LINK, + entity_category=EntityCategory.CONFIG, + ), } CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescription] = { Capability.SAMSUNG_CE_WASHER_BUBBLE_SOAK: SmartThingsSwitchEntityDescription( diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index fc55c4d535f..1b41476c315 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -234,6 +234,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airdresser_auto_cycle_link', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto cycle link', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_cycle_link', + 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetAutoCycleLink_steamClosetAutoCycleLink_steamClosetAutoCycleLink', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirDresser Auto cycle link', + }), + 'context': , + 'entity_id': 'switch.airdresser_auto_cycle_link', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[da_wm_sc_000001][switch.airdresser_keep_fresh_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ebed38c1dcbde70f1ede37eb1921e5c031537e10 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 17 May 2025 21:03:24 +0200 Subject: [PATCH 0558/1175] Add Steam closet sanitize to SmartThings (#145110) --- .../components/smartthings/icons.json | 3 ++ .../components/smartthings/strings.json | 3 ++ .../components/smartthings/switch.py | 6 +++ .../smartthings/snapshots/test_switch.ambr | 47 +++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 3ec13c3adac..f0c688b2ddc 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -109,6 +109,9 @@ "ice_maker": { "default": "mdi:delete-variant" }, + "sanitize": { + "default": "mdi:lotion" + }, "auto_cycle_link": { "default": "mdi:link-off", "state": { diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index a1149d6083c..2c77f7b9fe0 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -559,6 +559,9 @@ "auto_cycle_link": { "name": "Auto cycle link" }, + "sanitize": { + "name": "Sanitize" + }, "keep_fresh_mode": { "name": "Keep fresh mode" } diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 39809c7538a..61ebc56699b 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -99,6 +99,12 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio translation_key="sabbath_mode", status_attribute=Attribute.STATUS, ), + Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE: SmartThingsSwitchEntityDescription( + key=Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE, + translation_key="sanitize", + status_attribute=Attribute.STATUS, + entity_category=EntityCategory.CONFIG, + ), Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: SmartThingsSwitchEntityDescription( key=Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE, translation_key="keep_fresh_mode", diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 1b41476c315..be9253dd388 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -234,6 +234,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airdresser_sanitize', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sanitize', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sanitize', + 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetSanitizeMode_status_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirDresser Sanitize', + }), + 'context': , + 'entity_id': 'switch.airdresser_sanitize', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From a169d6ca97d91e301af989a9145ab57ab661ab01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 17 May 2025 17:57:28 -0400 Subject: [PATCH 0559/1175] Bump cryptography to 45.0.1 and pyopenssl to 25.1.0 (#145121) --- homeassistant/package_constraints.txt | 8 ++------ pyproject.toml | 4 ++-- requirements.txt | 4 ++-- script/gen_requirements_all.py | 4 ---- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ed9466073dd..7cd0a56c337 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ cached-ipaddress==0.10.0 certifi>=2021.5.30 ciso8601==2.3.2 cronsim==2.6 -cryptography==44.0.1 +cryptography==45.0.1 dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 @@ -55,7 +55,7 @@ psutil-home-assistant==0.0.1 PyJWT==2.10.1 pymicro-vad==1.0.1 PyNaCl==1.5.0 -pyOpenSSL==25.0.0 +pyOpenSSL==25.1.0 pyserial==3.5 pyspeex-noise==1.0.2 python-slugify==8.0.4 @@ -143,10 +143,6 @@ pubnub!=6.4.0 # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 -# pyOpenSSL 24.0.0 or later required to avoid import errors when -# cryptography 42.0.0 is installed with botocore -pyOpenSSL>=24.0.0 - # protobuf must be in package constraints for the wheel # builder to build binary wheels protobuf==5.29.2 diff --git a/pyproject.toml b/pyproject.toml index 68954726b56..bab4f92bc23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,10 +82,10 @@ dependencies = [ "numpy==2.2.2", "PyJWT==2.10.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==44.0.1", + "cryptography==45.0.1", "Pillow==11.2.1", "propcache==0.3.1", - "pyOpenSSL==25.0.0", + "pyOpenSSL==25.1.0", "orjson==3.10.18", "packaging>=23.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index 25f977d455f..a4ab40f2538 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,10 +34,10 @@ lru-dict==1.3.0 mutagen==1.47.0 numpy==2.2.2 PyJWT==2.10.1 -cryptography==44.0.1 +cryptography==45.0.1 Pillow==11.2.1 propcache==0.3.1 -pyOpenSSL==25.0.0 +pyOpenSSL==25.1.0 orjson==3.10.18 packaging>=23.1 psutil-home-assistant==0.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 307a9c42d53..f2e423536e8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -172,10 +172,6 @@ pubnub!=6.4.0 # https://github.com/dahlia/iso4217/issues/16 iso4217!=1.10.20220401 -# pyOpenSSL 24.0.0 or later required to avoid import errors when -# cryptography 42.0.0 is installed with botocore -pyOpenSSL>=24.0.0 - # protobuf must be in package constraints for the wheel # builder to build binary wheels protobuf==5.29.2 From f07265ece451dbb40f5aed274278c560695114ce Mon Sep 17 00:00:00 2001 From: XiaoXianNv-boot <76765956+XiaoXianNv-boot@users.noreply.github.com> Date: Sun, 18 May 2025 07:30:04 +0800 Subject: [PATCH 0560/1175] Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration (#144295) * Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration * Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration * Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration * Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration * Fix failed tests * Fix failed tests * Cleanup unused helper option * ruff --------- Co-authored-by: jbouwh --- homeassistant/components/mqtt/update.py | 5 +---- tests/components/mqtt/common.py | 5 ++--- tests/components/mqtt/test_update.py | 16 +++------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 145f0a2562c..5591e5d801d 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -105,10 +105,7 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend.""" - if self._attr_entity_picture is not None: - return self._attr_entity_picture - - return super().entity_picture + return self._attr_entity_picture @staticmethod def config_schema() -> VolSchemaType: diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index f0952e7f821..9bf1c236de6 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -1875,7 +1875,6 @@ async def help_test_entity_icon_and_entity_picture( mqtt_mock_entry: MqttMockHAClientGenerator, domain: str, config: ConfigType, - default_entity_picture: str | None = None, ) -> None: """Test entity picture and icon.""" await mqtt_mock_entry() @@ -1895,7 +1894,7 @@ async def help_test_entity_icon_and_entity_picture( state = hass.states.get(entity_id) assert entity_id is not None and state assert state.attributes.get("icon") is None - assert state.attributes.get("entity_picture") == default_entity_picture + assert state.attributes.get("entity_picture") is None # Discover an entity with an entity picture set unique_id = "veryunique2" @@ -1922,7 +1921,7 @@ async def help_test_entity_icon_and_entity_picture( state = hass.states.get(entity_id) assert entity_id is not None and state assert state.attributes.get("icon") == "mdi:emoji-happy-outline" - assert state.attributes.get("entity_picture") == default_entity_picture + assert state.attributes.get("entity_picture") is None async def help_test_publishing_with_custom_encoding( diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index 87eb381db03..335bf9cb4da 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -211,10 +211,7 @@ async def test_value_template( assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" - assert ( - state.attributes.get("entity_picture") - == "https://brands.home-assistant.io/_/mqtt/icon.png" - ) + assert state.attributes.get("entity_picture") is None async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0.0"}') @@ -324,10 +321,7 @@ async def test_value_template_float( assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9" assert state.attributes.get("latest_version") == "1.9" - assert ( - state.attributes.get("entity_picture") - == "https://brands.home-assistant.io/_/mqtt/icon.png" - ) + assert state.attributes.get("entity_picture") is None async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0"}') @@ -949,9 +943,5 @@ async def test_entity_icon_and_entity_picture( domain = update.DOMAIN config = DEFAULT_CONFIG await help_test_entity_icon_and_entity_picture( - hass, - mqtt_mock_entry, - domain, - config, - default_entity_picture="https://brands.home-assistant.io/_/mqtt/icon.png", + hass, mqtt_mock_entry, domain, config ) From 6fd9857666b1b700f7a7e71b23d4ddb480573f00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 18 May 2025 01:00:24 -0400 Subject: [PATCH 0561/1175] OpenAI Conversation split out chat log processing (#145129) --- .../openai_conversation/conversation.py | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 126a4713fb5..d55ffc2df0c 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -274,7 +274,7 @@ class OpenAIConversationEntity( user_input: conversation.ConversationInput, chat_log: conversation.ChatLog, ) -> conversation.ConversationResult: - """Call the API.""" + """Process the user input and call the API.""" options = self.entry.options try: @@ -287,6 +287,24 @@ class OpenAIConversationEntity( except conversation.ConverseError as err: return err.as_conversation_result() + await self._async_handle_chat_log(chat_log) + + intent_response = intent.IntentResponse(language=user_input.language) + assert type(chat_log.content[-1]) is conversation.AssistantContent + intent_response.async_set_speech(chat_log.content[-1].content or "") + return conversation.ConversationResult( + response=intent_response, + conversation_id=chat_log.conversation_id, + continue_conversation=chat_log.continue_conversation, + ) + + async def _async_handle_chat_log( + self, + chat_log: conversation.ChatLog, + ) -> None: + """Generate an answer for the chat log.""" + options = self.entry.options + tools: list[ToolParam] | None = None if chat_log.llm_api: tools = [ @@ -357,7 +375,7 @@ class OpenAIConversationEntity( raise HomeAssistantError("Error talking to OpenAI") from err async for content in chat_log.async_add_delta_content_stream( - user_input.agent_id, _transform_stream(chat_log, result, messages) + self.entity_id, _transform_stream(chat_log, result, messages) ): if not isinstance(content, conversation.AssistantContent): messages.extend(_convert_content_to_param(content)) @@ -365,15 +383,6 @@ class OpenAIConversationEntity( if not chat_log.unresponded_tool_results: break - intent_response = intent.IntentResponse(language=user_input.language) - assert type(chat_log.content[-1]) is conversation.AssistantContent - intent_response.async_set_speech(chat_log.content[-1].content or "") - return conversation.ConversationResult( - response=intent_response, - conversation_id=chat_log.conversation_id, - continue_conversation=chat_log.continue_conversation, - ) - async def _async_entry_update_listener( self, hass: HomeAssistant, entry: ConfigEntry ) -> None: From 2f4d0ede0f71cfb76a75277eef8c78cd50a3416c Mon Sep 17 00:00:00 2001 From: markhannon Date: Sun, 18 May 2025 15:13:23 +1000 Subject: [PATCH 0562/1175] Bump zcc-helper to 3.5.2 (#144926) --- homeassistant/components/zimi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json index d0dd3e09e06..3e019d2f053 100644 --- a/homeassistant/components/zimi/manifest.json +++ b/homeassistant/components/zimi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/zimi", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["zcc-helper==3.5"] + "requirements": ["zcc-helper==3.5.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 593ce825b20..9bd0a9ddf21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3153,7 +3153,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5 +zcc-helper==3.5.2 # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3858e46d78e..c82ac20f337 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2552,7 +2552,7 @@ yt-dlp[default]==2025.03.31 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5 +zcc-helper==3.5.2 # homeassistant.components.zeroconf zeroconf==0.147.0 From 888f17c50400d03a8fa24ce17032a14101018231 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 03:11:13 -0400 Subject: [PATCH 0563/1175] Bump google-maps-routing to 0.6.15 (#145130) --- homeassistant/components/google_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index 6d69c908d59..74c015c5345 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "iot_class": "cloud_polling", "loggers": ["google", "homeassistant.helpers.location"], - "requirements": ["google-maps-routing==0.6.14"] + "requirements": ["google-maps-routing==0.6.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bd0a9ddf21..e2516da1681 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1045,7 +1045,7 @@ google-cloud-texttospeech==2.25.1 google-genai==1.7.0 # homeassistant.components.google_travel_time -google-maps-routing==0.6.14 +google-maps-routing==0.6.15 # homeassistant.components.nest google-nest-sdm==7.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c82ac20f337..98d18a93345 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -896,7 +896,7 @@ google-cloud-texttospeech==2.25.1 google-genai==1.7.0 # homeassistant.components.google_travel_time -google-maps-routing==0.6.14 +google-maps-routing==0.6.15 # homeassistant.components.nest google-nest-sdm==7.1.4 From 705a987547a51261514fa4929b42287fb99bace3 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Sun, 18 May 2025 11:00:21 +0200 Subject: [PATCH 0564/1175] Fix enum values for program phases by appliance type on Miele appliances (#144916) --- homeassistant/components/miele/const.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index e6de990043d..338e8138352 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -299,7 +299,7 @@ STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER = { 5910: "remote_controlled", 65535: "not_running", } -STATE_PROGRAM_PHASE_MICROWAVE_OVEN_COMBO = { +STATE_PROGRAM_PHASE_STEAM_OVEN = { 0: "not_running", 3863: "steam_reduction", 7938: "process_running", @@ -322,12 +322,19 @@ STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = { MieleAppliance.DISHWASHER_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER, MieleAppliance.DISHWASHER_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER, MieleAppliance.OVEN: STATE_PROGRAM_PHASE_OVEN, - MieleAppliance.OVEN_MICROWAVE: STATE_PROGRAM_PHASE_MICROWAVE_OVEN_COMBO, - MieleAppliance.STEAM_OVEN: STATE_PROGRAM_PHASE_OVEN, + MieleAppliance.OVEN_MICROWAVE: STATE_PROGRAM_PHASE_MICROWAVE, + MieleAppliance.STEAM_OVEN: STATE_PROGRAM_PHASE_STEAM_OVEN, + MieleAppliance.STEAM_OVEN_COMBI: STATE_PROGRAM_PHASE_OVEN + | STATE_PROGRAM_PHASE_STEAM_OVEN, + MieleAppliance.STEAM_OVEN_MICRO: STATE_PROGRAM_PHASE_MICROWAVE + | STATE_PROGRAM_PHASE_STEAM_OVEN, + MieleAppliance.STEAM_OVEN_MK2: STATE_PROGRAM_PHASE_OVEN + | STATE_PROGRAM_PHASE_STEAM_OVEN, MieleAppliance.DIALOG_OVEN: STATE_PROGRAM_PHASE_OVEN, MieleAppliance.MICROWAVE: STATE_PROGRAM_PHASE_MICROWAVE, MieleAppliance.COFFEE_SYSTEM: STATE_PROGRAM_PHASE_COFFEE_SYSTEM, MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER, + MieleAppliance.DISH_WARMER: STATE_PROGRAM_PHASE_WARMING_DRAWER, } From 3eb0c8ddff5c3156464083b35450777e4c2dbfe9 Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Sun, 18 May 2025 16:46:11 +0200 Subject: [PATCH 0565/1175] Add Pterodactyl binary sensor tests (#142401) * Add binary sensor tests * Wait for background tasks as well in test_binary_sensor_update_failure * Fix module docstring * Use snapshot_platform, move constants to const.py, do not use snapshot for testing state updates * Use JSON fixtures * Use helper for loading JSON fixtures, remove unneeded mock in setup_integration * Move mocks to pytest markers where possible --- tests/components/pterodactyl/__init__.py | 15 +++ tests/components/pterodactyl/conftest.py | 119 +++--------------- tests/components/pterodactyl/const.py | 12 ++ .../pterodactyl/fixtures/server_1_data.json | 39 ++++++ .../pterodactyl/fixtures/server_2_data.json | 39 ++++++ .../fixtures/server_list_data.json | 60 +++++++++ .../fixtures/utilization_data.json | 12 ++ .../snapshots/test_binary_sensor.ambr | 97 ++++++++++++++ .../pterodactyl/test_binary_sensor.py | 89 +++++++++++++ .../pterodactyl/test_config_flow.py | 10 +- 10 files changed, 383 insertions(+), 109 deletions(-) create mode 100644 tests/components/pterodactyl/const.py create mode 100644 tests/components/pterodactyl/fixtures/server_1_data.json create mode 100644 tests/components/pterodactyl/fixtures/server_2_data.json create mode 100644 tests/components/pterodactyl/fixtures/server_list_data.json create mode 100644 tests/components/pterodactyl/fixtures/utilization_data.json create mode 100644 tests/components/pterodactyl/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/pterodactyl/test_binary_sensor.py diff --git a/tests/components/pterodactyl/__init__.py b/tests/components/pterodactyl/__init__.py index a5b28d67ae3..0142399ec42 100644 --- a/tests/components/pterodactyl/__init__.py +++ b/tests/components/pterodactyl/__init__.py @@ -1 +1,16 @@ """Tests for the Pterodactyl integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up Pterodactyl mock config entry.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/pterodactyl/conftest.py b/tests/components/pterodactyl/conftest.py index 62326e79207..c395410b6ae 100644 --- a/tests/components/pterodactyl/conftest.py +++ b/tests/components/pterodactyl/conftest.py @@ -9,108 +9,9 @@ import pytest from homeassistant.components.pterodactyl.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_URL -from tests.common import MockConfigEntry +from .const import TEST_API_KEY, TEST_URL -TEST_URL = "https://192.168.0.1:8080/" -TEST_API_KEY = "TestClientApiKey" -TEST_USER_INPUT = { - CONF_URL: TEST_URL, - CONF_API_KEY: TEST_API_KEY, -} -TEST_SERVER_LIST_DATA = { - "meta": {"pagination": {"total": 2, "count": 2, "per_page": 50, "current_page": 1}}, - "data": [ - { - "object": "server", - "attributes": { - "server_owner": True, - "identifier": "1", - "internal_id": 1, - "uuid": "1-1-1-1-1", - "name": "Test Server 1", - "node": "default_node", - "description": "Description of Test Server 1", - "limits": { - "memory": 2048, - "swap": 1024, - "disk": 10240, - "io": 500, - "cpu": 100, - "threads": None, - "oom_disabled": True, - }, - "invocation": "java -jar test_server1.jar", - "docker_image": "test_docker_image_1", - "egg_features": ["java_version"], - }, - }, - { - "object": "server", - "attributes": { - "server_owner": True, - "identifier": "2", - "internal_id": 2, - "uuid": "2-2-2-2-2", - "name": "Test Server 2", - "node": "default_node", - "description": "Description of Test Server 2", - "limits": { - "memory": 2048, - "swap": 1024, - "disk": 10240, - "io": 500, - "cpu": 100, - "threads": None, - "oom_disabled": True, - }, - "invocation": "java -jar test_server_2.jar", - "docker_image": "test_docker_image2", - "egg_features": ["java_version"], - }, - }, - ], -} -TEST_SERVER = { - "server_owner": True, - "identifier": "1", - "internal_id": 1, - "uuid": "1-1-1-1-1", - "name": "Test Server 1", - "node": "default_node", - "is_node_under_maintenance": False, - "sftp_details": {"ip": "192.168.0.1", "port": 2022}, - "description": "", - "limits": { - "memory": 2048, - "swap": 1024, - "disk": 10240, - "io": 500, - "cpu": 100, - "threads": None, - "oom_disabled": True, - }, - "invocation": "java -jar test.jar", - "docker_image": "test_docker_image", - "egg_features": ["eula", "java_version", "pid_limit"], - "feature_limits": {"databases": 0, "allocations": 0, "backups": 3}, - "status": None, - "is_suspended": False, - "is_installing": False, - "is_transferring": False, - "relationships": {"allocations": {...}, "variables": {...}}, -} -TEST_SERVER_UTILIZATION = { - "current_state": "running", - "is_suspended": False, - "resources": { - "memory_bytes": 1111, - "cpu_absolute": 22, - "disk_bytes": 3333, - "network_rx_bytes": 44, - "network_tx_bytes": 55, - "uptime": 6666, - }, -} +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture @@ -139,17 +40,25 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_pterodactyl(): +def mock_pterodactyl() -> Generator[AsyncMock]: """Mock the Pterodactyl API.""" with patch( "homeassistant.components.pterodactyl.api.PterodactylClient", autospec=True ) as mock: + server_list_data = load_json_object_fixture("server_list_data.json", DOMAIN) + server_1_data = load_json_object_fixture("server_1_data.json", DOMAIN) + server_2_data = load_json_object_fixture("server_2_data.json", DOMAIN) + utilization_data = load_json_object_fixture("utilization_data.json", DOMAIN) + mock.return_value.client.servers.list_servers.return_value = PaginatedResponse( - mock.return_value, "client", TEST_SERVER_LIST_DATA + mock.return_value, "client", server_list_data ) - mock.return_value.client.servers.get_server.return_value = TEST_SERVER + mock.return_value.client.servers.get_server.side_effect = [ + server_1_data, + server_2_data, + ] mock.return_value.client.servers.get_server_utilization.return_value = ( - TEST_SERVER_UTILIZATION + utilization_data ) yield mock.return_value diff --git a/tests/components/pterodactyl/const.py b/tests/components/pterodactyl/const.py new file mode 100644 index 00000000000..f6684a82fc5 --- /dev/null +++ b/tests/components/pterodactyl/const.py @@ -0,0 +1,12 @@ +"""Constants for Pterodactyl tests.""" + +from homeassistant.const import CONF_API_KEY, CONF_URL + +TEST_URL = "https://192.168.0.1:8080/" + +TEST_API_KEY = "TestClientApiKey" + +TEST_USER_INPUT = { + CONF_URL: TEST_URL, + CONF_API_KEY: TEST_API_KEY, +} diff --git a/tests/components/pterodactyl/fixtures/server_1_data.json b/tests/components/pterodactyl/fixtures/server_1_data.json new file mode 100644 index 00000000000..c780d55b318 --- /dev/null +++ b/tests/components/pterodactyl/fixtures/server_1_data.json @@ -0,0 +1,39 @@ +{ + "server_owner": true, + "identifier": "1", + "internal_id": 1, + "uuid": "1-1-1-1-1", + "name": "Test Server 1", + "node": "default_node", + "is_node_under_maintenance": false, + "sftp_details": { + "ip": "192.168.0.1", + "port": 2022 + }, + "description": "", + "limits": { + "memory": 2048, + "swap": 1024, + "disk": 10240, + "io": 500, + "cpu": 100, + "threads": null, + "oom_disabled": true + }, + "invocation": "java -jar test1.jar", + "docker_image": "test_docker_image1", + "egg_features": ["eula", "java_version", "pid_limit"], + "feature_limits": { + "databases": 0, + "allocations": 0, + "backups": 3 + }, + "status": null, + "is_suspended": false, + "is_installing": false, + "is_transferring": false, + "relationships": { + "allocations": {}, + "variables": {} + } +} diff --git a/tests/components/pterodactyl/fixtures/server_2_data.json b/tests/components/pterodactyl/fixtures/server_2_data.json new file mode 100644 index 00000000000..b240ff62ced --- /dev/null +++ b/tests/components/pterodactyl/fixtures/server_2_data.json @@ -0,0 +1,39 @@ +{ + "server_owner": true, + "identifier": "2", + "internal_id": 2, + "uuid": "2-2-2-2-2", + "name": "Test Server 2", + "node": "default_node", + "is_node_under_maintenance": false, + "sftp_details": { + "ip": "192.168.0.2", + "port": 2022 + }, + "description": "", + "limits": { + "memory": 4096, + "swap": 2048, + "disk": 20480, + "io": 1000, + "cpu": 200, + "threads": null, + "oom_disabled": true + }, + "invocation": "java -jar test2.jar", + "docker_image": "test_docker_image2", + "egg_features": ["eula", "java_version", "pid_limit"], + "feature_limits": { + "databases": 1, + "allocations": 1, + "backups": 5 + }, + "status": null, + "is_suspended": false, + "is_installing": false, + "is_transferring": false, + "relationships": { + "allocations": {}, + "variables": {} + } +} diff --git a/tests/components/pterodactyl/fixtures/server_list_data.json b/tests/components/pterodactyl/fixtures/server_list_data.json new file mode 100644 index 00000000000..d8796ad533e --- /dev/null +++ b/tests/components/pterodactyl/fixtures/server_list_data.json @@ -0,0 +1,60 @@ +{ + "meta": { + "pagination": { + "total": 2, + "count": 2, + "per_page": 50, + "current_page": 1 + } + }, + "data": [ + { + "object": "server", + "attributes": { + "server_owner": true, + "identifier": "1", + "internal_id": 1, + "uuid": "1-1-1-1-1", + "name": "Test Server 1", + "node": "default_node", + "description": "Description of Test Server 1", + "limits": { + "memory": 2048, + "swap": 1024, + "disk": 10240, + "io": 500, + "cpu": 100, + "threads": null, + "oom_disabled": true + }, + "invocation": "java -jar test1.jar", + "docker_image": "test_docker_image_1", + "egg_features": ["java_version"] + } + }, + { + "object": "server", + "attributes": { + "server_owner": true, + "identifier": "2", + "internal_id": 2, + "uuid": "2-2-2-2-2", + "name": "Test Server 2", + "node": "default_node", + "description": "Description of Test Server 2", + "limits": { + "memory": 2048, + "swap": 1024, + "disk": 10240, + "io": 500, + "cpu": 100, + "threads": null, + "oom_disabled": true + }, + "invocation": "java -jar test2.jar", + "docker_image": "test_docker_image2", + "egg_features": ["java_version"] + } + } + ] +} diff --git a/tests/components/pterodactyl/fixtures/utilization_data.json b/tests/components/pterodactyl/fixtures/utilization_data.json new file mode 100644 index 00000000000..6b71cb44635 --- /dev/null +++ b/tests/components/pterodactyl/fixtures/utilization_data.json @@ -0,0 +1,12 @@ +{ + "current_state": "running", + "is_suspended": false, + "resources": { + "memory_bytes": 1111, + "cpu_absolute": 22, + "disk_bytes": 3333, + "network_rx_bytes": 44, + "network_tx_bytes": 55, + "uptime": 6666 + } +} diff --git a/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..9bd7abc830b --- /dev/null +++ b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_binary_sensor[binary_sensor.test_server_1_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_server_1_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': 'Status', + 'platform': 'pterodactyl', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': '1-1-1-1-1_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_server_1_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Server 1 Status', + }), + 'context': , + 'entity_id': 'binary_sensor.test_server_1_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor[binary_sensor.test_server_2_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_server_2_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': 'Status', + 'platform': 'pterodactyl', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': '2-2-2-2-2_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor[binary_sensor.test_server_2_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Test Server 2 Status', + }), + 'context': , + 'entity_id': 'binary_sensor.test_server_2_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/pterodactyl/test_binary_sensor.py b/tests/components/pterodactyl/test_binary_sensor.py new file mode 100644 index 00000000000..4bacd30e011 --- /dev/null +++ b/tests/components/pterodactyl/test_binary_sensor.py @@ -0,0 +1,89 @@ +"""Tests for the binary sensor platform of the Pterodactyl integration.""" + +from collections.abc import Generator +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from requests.exceptions import ConnectionError +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("mock_pterodactyl") +async def test_binary_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test binary sensor.""" + with patch( + "homeassistant.components.pterodactyl._PLATFORMS", [Platform.BINARY_SENSOR] + ): + mock_config_entry = await setup_integration(hass, mock_config_entry) + + assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2 + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.usefixtures("mock_pterodactyl") +async def test_binary_sensor_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test binary sensor update.""" + await setup_integration(hass, mock_config_entry) + + freezer.tick(timedelta(seconds=90)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2 + assert ( + hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state + == STATE_ON + ) + assert ( + hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state + == STATE_ON + ) + + +async def test_binary_sensor_update_failure( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pterodactyl: Generator[AsyncMock], + freezer: FrozenDateTimeFactory, +) -> None: + """Test failed binary sensor update.""" + await setup_integration(hass, mock_config_entry) + + mock_pterodactyl.client.servers.get_server.side_effect = ConnectionError( + "Simulated connection error" + ) + + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(hass.states.async_all(Platform.BINARY_SENSOR)) == 2 + assert ( + hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_1_status").state + == STATE_UNAVAILABLE + ) + assert ( + hass.states.get(f"{Platform.BINARY_SENSOR}.test_server_2_status").state + == STATE_UNAVAILABLE + ) diff --git a/tests/components/pterodactyl/test_config_flow.py b/tests/components/pterodactyl/test_config_flow.py index 88247085083..8837fbe753b 100644 --- a/tests/components/pterodactyl/test_config_flow.py +++ b/tests/components/pterodactyl/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Pterodactyl config flow.""" -from pydactyl import PterodactylClient +from collections.abc import Generator +from unittest.mock import AsyncMock + from pydactyl.exceptions import BadRequestError, PterodactylApiError import pytest from requests.exceptions import HTTPError @@ -12,7 +14,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import TEST_API_KEY, TEST_URL, TEST_USER_INPUT +from .const import TEST_API_KEY, TEST_URL, TEST_USER_INPUT from tests.common import MockConfigEntry @@ -59,7 +61,7 @@ async def test_recovery_after_error( hass: HomeAssistant, exception_type: Exception, expected_error: str, - mock_pterodactyl: PterodactylClient, + mock_pterodactyl: Generator[AsyncMock], ) -> None: """Test recovery after an error.""" result = await hass.config_entries.flow.async_init( @@ -143,7 +145,7 @@ async def test_reauth_recovery_after_error( exception_type: Exception, expected_error: str, mock_config_entry: MockConfigEntry, - mock_pterodactyl: PterodactylClient, + mock_pterodactyl: Generator[AsyncMock], ) -> None: """Test recovery after an error during re-authentication.""" mock_config_entry.add_to_hass(hass) From 2aba4f261f38f50fa9f9f373f31991dc3eccb48b Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 18 May 2025 16:48:44 +0200 Subject: [PATCH 0566/1175] Add has_entity_name attribute to LCN entities (#145045) * Add _attr_has_entity_name * Fix tests --- homeassistant/components/lcn/entity.py | 1 + .../lcn/snapshots/test_binary_sensor.ambr | 36 ++++---- .../lcn/snapshots/test_climate.ambr | 12 +-- .../components/lcn/snapshots/test_cover.ambr | 48 +++++------ .../components/lcn/snapshots/test_light.ambr | 36 ++++---- .../components/lcn/snapshots/test_scene.ambr | 24 +++--- .../components/lcn/snapshots/test_sensor.ambr | 48 +++++------ .../components/lcn/snapshots/test_switch.ambr | 84 +++++++++---------- tests/components/lcn/test_binary_sensor.py | 12 ++- tests/components/lcn/test_climate.py | 54 +++++++----- tests/components/lcn/test_cover.py | 8 +- tests/components/lcn/test_init.py | 4 +- tests/components/lcn/test_light.py | 6 +- tests/components/lcn/test_scene.py | 6 +- tests/components/lcn/test_sensor.py | 8 +- tests/components/lcn/test_switch.py | 12 +-- 16 files changed, 208 insertions(+), 191 deletions(-) diff --git a/homeassistant/components/lcn/entity.py b/homeassistant/components/lcn/entity.py index 24897287449..a1940fc7ac3 100644 --- a/homeassistant/components/lcn/entity.py +++ b/homeassistant/components/lcn/entity.py @@ -23,6 +23,7 @@ class LcnEntity(Entity): """Parent class for all entities associated with the LCN component.""" _attr_should_poll = False + _attr_has_entity_name = True device_connection: DeviceConnectionType def __init__( diff --git a/tests/components/lcn/snapshots/test_binary_sensor.ambr b/tests/components/lcn/snapshots/test_binary_sensor.ambr index 383c9038d78..e3f7c9ab404 100644 --- a/tests/components/lcn/snapshots/test_binary_sensor.ambr +++ b/tests/components/lcn/snapshots/test_binary_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_binary_sensor[binary_sensor.binary_sensor1-entry] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_binary_sensor1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.binary_sensor1', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.testmodule_binary_sensor1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -33,20 +33,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_binary_sensor[binary_sensor.binary_sensor1-state] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_binary_sensor1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Binary_Sensor1', + 'friendly_name': 'TestModule Binary_Sensor1', }), 'context': , - 'entity_id': 'binary_sensor.binary_sensor1', + 'entity_id': 'binary_sensor.testmodule_binary_sensor1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_binary_sensor[binary_sensor.sensor_keylock-entry] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_sensor_keylock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -59,8 +59,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.sensor_keylock', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.testmodule_sensor_keylock', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -80,20 +80,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_binary_sensor[binary_sensor.sensor_keylock-state] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_sensor_keylock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sensor_KeyLock', + 'friendly_name': 'TestModule Sensor_KeyLock', }), 'context': , - 'entity_id': 'binary_sensor.sensor_keylock', + 'entity_id': 'binary_sensor.testmodule_sensor_keylock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_binary_sensor[binary_sensor.sensor_lockregulator1-entry] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_sensor_lockregulator1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -106,8 +106,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.sensor_lockregulator1', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.testmodule_sensor_lockregulator1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -127,13 +127,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_binary_sensor[binary_sensor.sensor_lockregulator1-state] +# name: test_setup_lcn_binary_sensor[binary_sensor.testmodule_sensor_lockregulator1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sensor_LockRegulator1', + 'friendly_name': 'TestModule Sensor_LockRegulator1', }), 'context': , - 'entity_id': 'binary_sensor.sensor_lockregulator1', + 'entity_id': 'binary_sensor.testmodule_sensor_lockregulator1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_climate.ambr b/tests/components/lcn/snapshots/test_climate.ambr index bd371c02492..7393a9a8421 100644 --- a/tests/components/lcn/snapshots/test_climate.ambr +++ b/tests/components/lcn/snapshots/test_climate.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_climate[climate.climate1-entry] +# name: test_setup_lcn_climate[climate.testmodule_climate1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19,8 +19,8 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.climate1', - 'has_entity_name': False, + 'entity_id': 'climate.testmodule_climate1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -40,11 +40,11 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_climate[climate.climate1-state] +# name: test_setup_lcn_climate[climate.testmodule_climate1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_temperature': None, - 'friendly_name': 'Climate1', + 'friendly_name': 'TestModule Climate1', 'hvac_modes': list([ , , @@ -55,7 +55,7 @@ 'temperature': None, }), 'context': , - 'entity_id': 'climate.climate1', + 'entity_id': 'climate.testmodule_climate1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_cover.ambr b/tests/components/lcn/snapshots/test_cover.ambr index 4d1356e3c92..722261f1432 100644 --- a/tests/components/lcn/snapshots/test_cover.ambr +++ b/tests/components/lcn/snapshots/test_cover.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_cover[cover.cover_outputs-entry] +# name: test_setup_lcn_cover[cover.testmodule_cover_outputs-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'cover', 'entity_category': None, - 'entity_id': 'cover.cover_outputs', - 'has_entity_name': False, + 'entity_id': 'cover.testmodule_cover_outputs', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -33,22 +33,22 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_cover[cover.cover_outputs-state] +# name: test_setup_lcn_cover[cover.testmodule_cover_outputs-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'assumed_state': True, - 'friendly_name': 'Cover_Outputs', + 'friendly_name': 'TestModule Cover_Outputs', 'supported_features': , }), 'context': , - 'entity_id': 'cover.cover_outputs', + 'entity_id': 'cover.testmodule_cover_outputs', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'open', }) # --- -# name: test_setup_lcn_cover[cover.cover_relays-entry] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,8 +61,8 @@ 'disabled_by': None, 'domain': 'cover', 'entity_category': None, - 'entity_id': 'cover.cover_relays', - 'has_entity_name': False, + 'entity_id': 'cover.testmodule_cover_relays', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -82,22 +82,22 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_cover[cover.cover_relays-state] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'assumed_state': True, - 'friendly_name': 'Cover_Relays', + 'friendly_name': 'TestModule Cover_Relays', 'supported_features': , }), 'context': , - 'entity_id': 'cover.cover_relays', + 'entity_id': 'cover.testmodule_cover_relays', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'open', }) # --- -# name: test_setup_lcn_cover[cover.cover_relays_bs4-entry] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays_bs4-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,8 +110,8 @@ 'disabled_by': None, 'domain': 'cover', 'entity_category': None, - 'entity_id': 'cover.cover_relays_bs4', - 'has_entity_name': False, + 'entity_id': 'cover.testmodule_cover_relays_bs4', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -131,22 +131,22 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_cover[cover.cover_relays_bs4-state] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays_bs4-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'assumed_state': True, - 'friendly_name': 'Cover_Relays_BS4', + 'friendly_name': 'TestModule Cover_Relays_BS4', 'supported_features': , }), 'context': , - 'entity_id': 'cover.cover_relays_bs4', + 'entity_id': 'cover.testmodule_cover_relays_bs4', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'open', }) # --- -# name: test_setup_lcn_cover[cover.cover_relays_module-entry] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays_module-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -159,8 +159,8 @@ 'disabled_by': None, 'domain': 'cover', 'entity_category': None, - 'entity_id': 'cover.cover_relays_module', - 'has_entity_name': False, + 'entity_id': 'cover.testmodule_cover_relays_module', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -180,15 +180,15 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_cover[cover.cover_relays_module-state] +# name: test_setup_lcn_cover[cover.testmodule_cover_relays_module-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'assumed_state': True, - 'friendly_name': 'Cover_Relays_Module', + 'friendly_name': 'TestModule Cover_Relays_Module', 'supported_features': , }), 'context': , - 'entity_id': 'cover.cover_relays_module', + 'entity_id': 'cover.testmodule_cover_relays_module', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_light.ambr b/tests/components/lcn/snapshots/test_light.ambr index 5bfd00fb0d7..0a9086d1efb 100644 --- a/tests/components/lcn/snapshots/test_light.ambr +++ b/tests/components/lcn/snapshots/test_light.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_light[light.light_output1-entry] +# name: test_setup_lcn_light[light.testmodule_light_output1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -16,8 +16,8 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.light_output1', - 'has_entity_name': False, + 'entity_id': 'light.testmodule_light_output1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -37,26 +37,26 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_light[light.light_output1-state] +# name: test_setup_lcn_light[light.testmodule_light_output1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'brightness': None, 'color_mode': None, - 'friendly_name': 'Light_Output1', + 'friendly_name': 'TestModule Light_Output1', 'supported_color_modes': list([ , ]), 'supported_features': , }), 'context': , - 'entity_id': 'light.light_output1', + 'entity_id': 'light.testmodule_light_output1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_light[light.light_output2-entry] +# name: test_setup_lcn_light[light.testmodule_light_output2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -73,8 +73,8 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.light_output2', - 'has_entity_name': False, + 'entity_id': 'light.testmodule_light_output2', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -94,25 +94,25 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_light[light.light_output2-state] +# name: test_setup_lcn_light[light.testmodule_light_output2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': None, - 'friendly_name': 'Light_Output2', + 'friendly_name': 'TestModule Light_Output2', 'supported_color_modes': list([ , ]), 'supported_features': , }), 'context': , - 'entity_id': 'light.light_output2', + 'entity_id': 'light.testmodule_light_output2', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_light[light.light_relay1-entry] +# name: test_setup_lcn_light[light.testmodule_light_relay1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -129,8 +129,8 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.light_relay1', - 'has_entity_name': False, + 'entity_id': 'light.testmodule_light_relay1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -150,18 +150,18 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_light[light.light_relay1-state] +# name: test_setup_lcn_light[light.testmodule_light_relay1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'color_mode': None, - 'friendly_name': 'Light_Relay1', + 'friendly_name': 'TestModule Light_Relay1', 'supported_color_modes': list([ , ]), 'supported_features': , }), 'context': , - 'entity_id': 'light.light_relay1', + 'entity_id': 'light.testmodule_light_relay1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_scene.ambr b/tests/components/lcn/snapshots/test_scene.ambr index 6dac4868437..9196e7d8ae0 100644 --- a/tests/components/lcn/snapshots/test_scene.ambr +++ b/tests/components/lcn/snapshots/test_scene.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_scene[scene.romantic-entry] +# name: test_setup_lcn_scene[scene.testmodule_romantic-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'scene', 'entity_category': None, - 'entity_id': 'scene.romantic', - 'has_entity_name': False, + 'entity_id': 'scene.testmodule_romantic', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -33,20 +33,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_scene[scene.romantic-state] +# name: test_setup_lcn_scene[scene.testmodule_romantic-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Romantic', + 'friendly_name': 'TestModule Romantic', }), 'context': , - 'entity_id': 'scene.romantic', + 'entity_id': 'scene.testmodule_romantic', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_scene[scene.romantic_transition-entry] +# name: test_setup_lcn_scene[scene.testmodule_romantic_transition-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -59,8 +59,8 @@ 'disabled_by': None, 'domain': 'scene', 'entity_category': None, - 'entity_id': 'scene.romantic_transition', - 'has_entity_name': False, + 'entity_id': 'scene.testmodule_romantic_transition', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -80,13 +80,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_scene[scene.romantic_transition-state] +# name: test_setup_lcn_scene[scene.testmodule_romantic_transition-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Romantic Transition', + 'friendly_name': 'TestModule Romantic Transition', }), 'context': , - 'entity_id': 'scene.romantic_transition', + 'entity_id': 'scene.testmodule_romantic_transition', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_sensor.ambr b/tests/components/lcn/snapshots/test_sensor.ambr index 1e172dda7e9..60586a45058 100644 --- a/tests/components/lcn/snapshots/test_sensor.ambr +++ b/tests/components/lcn/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_sensor[sensor.sensor_led6-entry] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_led6-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.sensor_led6', - 'has_entity_name': False, + 'entity_id': 'sensor.testmodule_sensor_led6', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -33,20 +33,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_led6-state] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_led6-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sensor_Led6', + 'friendly_name': 'TestModule Sensor_Led6', }), 'context': , - 'entity_id': 'sensor.sensor_led6', + 'entity_id': 'sensor.testmodule_sensor_led6', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_logicop1-entry] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_logicop1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -59,8 +59,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.sensor_logicop1', - 'has_entity_name': False, + 'entity_id': 'sensor.testmodule_sensor_logicop1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -80,20 +80,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_logicop1-state] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_logicop1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Sensor_LogicOp1', + 'friendly_name': 'TestModule Sensor_LogicOp1', }), 'context': , - 'entity_id': 'sensor.sensor_logicop1', + 'entity_id': 'sensor.testmodule_sensor_logicop1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_setpoint1-entry] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_setpoint1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -106,8 +106,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.sensor_setpoint1', - 'has_entity_name': False, + 'entity_id': 'sensor.testmodule_sensor_setpoint1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -127,22 +127,22 @@ 'unit_of_measurement': , }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_setpoint1-state] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_setpoint1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Sensor_Setpoint1', + 'friendly_name': 'TestModule Sensor_Setpoint1', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.sensor_setpoint1', + 'entity_id': 'sensor.testmodule_sensor_setpoint1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_var1-entry] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_var1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -155,8 +155,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.sensor_var1', - 'has_entity_name': False, + 'entity_id': 'sensor.testmodule_sensor_var1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -176,15 +176,15 @@ 'unit_of_measurement': , }) # --- -# name: test_setup_lcn_sensor[sensor.sensor_var1-state] +# name: test_setup_lcn_sensor[sensor.testmodule_sensor_var1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Sensor_Var1', + 'friendly_name': 'TestModule Sensor_Var1', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.sensor_var1', + 'entity_id': 'sensor.testmodule_sensor_var1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/snapshots/test_switch.ambr b/tests/components/lcn/snapshots/test_switch.ambr index 7ba943a671f..b37dd3303db 100644 --- a/tests/components/lcn/snapshots/test_switch.ambr +++ b/tests/components/lcn/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_setup_lcn_switch[switch.switch_group5-entry] +# name: test_setup_lcn_switch[switch.testgroup_switch_group5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,8 +12,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_group5', - 'has_entity_name': False, + 'entity_id': 'switch.testgroup_switch_group5', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -33,20 +33,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_group5-state] +# name: test_setup_lcn_switch[switch.testgroup_switch_group5-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Group5', + 'friendly_name': 'TestGroup Switch_Group5', }), 'context': , - 'entity_id': 'switch.switch_group5', + 'entity_id': 'switch.testgroup_switch_group5', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_keylock1-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_keylock1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -59,8 +59,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_keylock1', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_keylock1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -80,20 +80,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_keylock1-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_keylock1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_KeyLock1', + 'friendly_name': 'TestModule Switch_KeyLock1', }), 'context': , - 'entity_id': 'switch.switch_keylock1', + 'entity_id': 'switch.testmodule_switch_keylock1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_output1-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_output1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -106,8 +106,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_output1', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_output1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -127,20 +127,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_output1-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_output1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Output1', + 'friendly_name': 'TestModule Switch_Output1', }), 'context': , - 'entity_id': 'switch.switch_output1', + 'entity_id': 'switch.testmodule_switch_output1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_output2-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_output2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -153,8 +153,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_output2', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_output2', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -174,20 +174,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_output2-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_output2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Output2', + 'friendly_name': 'TestModule Switch_Output2', }), 'context': , - 'entity_id': 'switch.switch_output2', + 'entity_id': 'switch.testmodule_switch_output2', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_regulator1-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_regulator1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -200,8 +200,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_regulator1', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_regulator1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -221,20 +221,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_regulator1-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_regulator1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Regulator1', + 'friendly_name': 'TestModule Switch_Regulator1', }), 'context': , - 'entity_id': 'switch.switch_regulator1', + 'entity_id': 'switch.testmodule_switch_regulator1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_relay1-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_relay1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -247,8 +247,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_relay1', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_relay1', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -268,20 +268,20 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_relay1-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_relay1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Relay1', + 'friendly_name': 'TestModule Switch_Relay1', }), 'context': , - 'entity_id': 'switch.switch_relay1', + 'entity_id': 'switch.testmodule_switch_relay1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_setup_lcn_switch[switch.switch_relay2-entry] +# name: test_setup_lcn_switch[switch.testmodule_switch_relay2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -294,8 +294,8 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.switch_relay2', - 'has_entity_name': False, + 'entity_id': 'switch.testmodule_switch_relay2', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -315,13 +315,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_setup_lcn_switch[switch.switch_relay2-state] +# name: test_setup_lcn_switch[switch.testmodule_switch_relay2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Switch_Relay2', + 'friendly_name': 'TestModule Switch_Relay2', }), 'context': , - 'entity_id': 'switch.switch_relay2', + 'entity_id': 'switch.testmodule_switch_relay2', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/lcn/test_binary_sensor.py b/tests/components/lcn/test_binary_sensor.py index 7d636f546c4..7e828dbc588 100644 --- a/tests/components/lcn/test_binary_sensor.py +++ b/tests/components/lcn/test_binary_sensor.py @@ -22,9 +22,9 @@ from .conftest import MockConfigEntry, init_integration from tests.common import snapshot_platform -BINARY_SENSOR_LOCKREGULATOR1 = "binary_sensor.sensor_lockregulator1" -BINARY_SENSOR_SENSOR1 = "binary_sensor.binary_sensor1" -BINARY_SENSOR_KEYLOCK = "binary_sensor.sensor_keylock" +BINARY_SENSOR_LOCKREGULATOR1 = "binary_sensor.testmodule_sensor_lockregulator1" +BINARY_SENSOR_SENSOR1 = "binary_sensor.testmodule_binary_sensor1" +BINARY_SENSOR_KEYLOCK = "binary_sensor.testmodule_sensor_keylock" async def test_setup_lcn_binary_sensor( @@ -140,7 +140,11 @@ async def test_unload_config_entry(hass: HomeAssistant, entry: MockConfigEntry) @pytest.mark.parametrize( - "entity_id", ["binary_sensor.sensor_lockregulator1", "binary_sensor.sensor_keylock"] + "entity_id", + [ + "binary_sensor.testmodule_sensor_lockregulator1", + "binary_sensor.testmodule_sensor_keylock", + ], ) async def test_create_issue( hass: HomeAssistant, diff --git a/tests/components/lcn/test_climate.py b/tests/components/lcn/test_climate.py index 7bac7cc9e81..ceb6f9524d1 100644 --- a/tests/components/lcn/test_climate.py +++ b/tests/components/lcn/test_climate.py @@ -52,7 +52,7 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - await init_integration(hass, entry) with patch.object(MockModuleConnection, "lock_regulator") as lock_regulator: - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") state.state = HVACMode.OFF # command failed @@ -61,13 +61,16 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_HVAC_MODE: HVACMode.HEAT}, + { + ATTR_ENTITY_ID: "climate.testmodule_climate1", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, blocking=True, ) lock_regulator.assert_awaited_with(0, False) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state != HVACMode.HEAT @@ -78,13 +81,16 @@ async def test_set_hvac_mode_heat(hass: HomeAssistant, entry: MockConfigEntry) - await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_HVAC_MODE: HVACMode.HEAT}, + { + ATTR_ENTITY_ID: "climate.testmodule_climate1", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, blocking=True, ) lock_regulator.assert_awaited_with(0, False) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state == HVACMode.HEAT @@ -94,7 +100,7 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> await init_integration(hass, entry) with patch.object(MockModuleConnection, "lock_regulator") as lock_regulator: - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") state.state = HVACMode.HEAT # command failed @@ -103,13 +109,16 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_HVAC_MODE: HVACMode.OFF}, + { + ATTR_ENTITY_ID: "climate.testmodule_climate1", + ATTR_HVAC_MODE: HVACMode.OFF, + }, blocking=True, ) lock_regulator.assert_awaited_with(0, True, -1) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state != HVACMode.OFF @@ -120,13 +129,16 @@ async def test_set_hvac_mode_off(hass: HomeAssistant, entry: MockConfigEntry) -> await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_HVAC_MODE: HVACMode.OFF}, + { + ATTR_ENTITY_ID: "climate.testmodule_climate1", + ATTR_HVAC_MODE: HVACMode.OFF, + }, blocking=True, ) lock_regulator.assert_awaited_with(0, True, -1) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state == HVACMode.OFF @@ -136,7 +148,7 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N await init_integration(hass, entry) with patch.object(MockModuleConnection, "var_abs") as var_abs: - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") state.state = HVACMode.HEAT # wrong temperature set via service call with high/low attributes @@ -147,7 +159,7 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE, { - ATTR_ENTITY_ID: "climate.climate1", + ATTR_ENTITY_ID: "climate.testmodule_climate1", ATTR_TARGET_TEMP_LOW: 24.5, ATTR_TARGET_TEMP_HIGH: 25.5, }, @@ -163,13 +175,13 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_TEMPERATURE: 25.5}, + {ATTR_ENTITY_ID: "climate.testmodule_climate1", ATTR_TEMPERATURE: 25.5}, blocking=True, ) var_abs.assert_awaited_with(Var.R1VARSETPOINT, 25.5, VarUnit.CELSIUS) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.attributes[ATTR_TEMPERATURE] != 25.5 @@ -180,13 +192,13 @@ async def test_set_temperature(hass: HomeAssistant, entry: MockConfigEntry) -> N await hass.services.async_call( DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.climate1", ATTR_TEMPERATURE: 25.5}, + {ATTR_ENTITY_ID: "climate.testmodule_climate1", ATTR_TEMPERATURE: 25.5}, blocking=True, ) var_abs.assert_awaited_with(Var.R1VARSETPOINT, 25.5, VarUnit.CELSIUS) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.attributes[ATTR_TEMPERATURE] == 25.5 @@ -207,7 +219,7 @@ async def test_pushed_current_temperature_status_change( await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state == HVACMode.HEAT assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.5 @@ -230,7 +242,7 @@ async def test_pushed_setpoint_status_change( await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state == HVACMode.HEAT assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None @@ -253,7 +265,7 @@ async def test_pushed_lock_status_change( await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state is not None assert state.state == HVACMode.OFF assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None @@ -272,7 +284,7 @@ async def test_pushed_wrong_input( await device_connection.async_process_input(Unknown("input")) await hass.async_block_till_done() - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None assert state.attributes[ATTR_TEMPERATURE] is None @@ -285,5 +297,5 @@ async def test_unload_config_entry( await init_integration(hass, entry) await hass.config_entries.async_unload(entry.entry_id) - state = hass.states.get("climate.climate1") + state = hass.states.get("climate.testmodule_climate1") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index f2dd71757c9..1ac4ea6f664 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -36,10 +36,10 @@ from .conftest import MockConfigEntry, MockModuleConnection, init_integration from tests.common import snapshot_platform -COVER_OUTPUTS = "cover.cover_outputs" -COVER_RELAYS = "cover.cover_relays" -COVER_RELAYS_BS4 = "cover.cover_relays_bs4" -COVER_RELAYS_MODULE = "cover.cover_relays_MODULE" +COVER_OUTPUTS = "cover.testmodule_cover_outputs" +COVER_RELAYS = "cover.testmodule_cover_relays" +COVER_RELAYS_BS4 = "cover.testmodule_cover_relays_bs4" +COVER_RELAYS_MODULE = "cover.testmodule_cover_relays_module" async def test_setup_lcn_cover( diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index da967782539..5634449bf22 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -178,8 +178,8 @@ async def test_migrate_2_1(hass: HomeAssistant, snapshot: SnapshotAssertion) -> @pytest.mark.parametrize( ("entity_id", "replace"), [ - ("climate.climate1", ("-r1varsetpoint", "-var1.r1varsetpoint")), - ("scene.romantic", ("-00", "-0.0")), + ("climate.testmodule_climate1", ("-r1varsetpoint", "-var1.r1varsetpoint")), + ("scene.testmodule_romantic", ("-00", "-0.0")), ], ) @patch("homeassistant.components.lcn.PchkConnectionManager", MockPchkConnectionManager) diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index 4251d997724..00c2341631e 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -29,9 +29,9 @@ from .conftest import MockConfigEntry, MockModuleConnection, init_integration from tests.common import snapshot_platform -LIGHT_OUTPUT1 = "light.light_output1" -LIGHT_OUTPUT2 = "light.light_output2" -LIGHT_RELAY1 = "light.light_relay1" +LIGHT_OUTPUT1 = "light.testmodule_light_output1" +LIGHT_OUTPUT2 = "light.testmodule_light_output2" +LIGHT_RELAY1 = "light.testmodule_light_relay1" async def test_setup_lcn_light( diff --git a/tests/components/lcn/test_scene.py b/tests/components/lcn/test_scene.py index 27e7864df41..aaf17f292c1 100644 --- a/tests/components/lcn/test_scene.py +++ b/tests/components/lcn/test_scene.py @@ -43,11 +43,11 @@ async def test_scene_activate( await hass.services.async_call( DOMAIN_SCENE, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "scene.romantic"}, + {ATTR_ENTITY_ID: "scene.testmodule_romantic"}, blocking=True, ) - state = hass.states.get("scene.romantic") + state = hass.states.get("scene.testmodule_romantic") assert state is not None activate_scene.assert_awaited_with( @@ -60,5 +60,5 @@ async def test_unload_config_entry(hass: HomeAssistant, entry: MockConfigEntry) await init_integration(hass, entry) await hass.config_entries.async_unload(entry.entry_id) - state = hass.states.get("scene.romantic") + state = hass.states.get("scene.testmodule_romantic") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_sensor.py b/tests/components/lcn/test_sensor.py index 18335f4b073..85f5b62bf91 100644 --- a/tests/components/lcn/test_sensor.py +++ b/tests/components/lcn/test_sensor.py @@ -16,10 +16,10 @@ from .conftest import MockConfigEntry, init_integration from tests.common import snapshot_platform -SENSOR_VAR1 = "sensor.sensor_var1" -SENSOR_SETPOINT1 = "sensor.sensor_setpoint1" -SENSOR_LED6 = "sensor.sensor_led6" -SENSOR_LOGICOP1 = "sensor.sensor_logicop1" +SENSOR_VAR1 = "sensor.testmodule_sensor_var1" +SENSOR_SETPOINT1 = "sensor.testmodule_sensor_setpoint1" +SENSOR_LED6 = "sensor.testmodule_sensor_led6" +SENSOR_LOGICOP1 = "sensor.testmodule_sensor_logicop1" async def test_setup_lcn_sensor( diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index 15b156aac43..0c0067c8875 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -30,12 +30,12 @@ from .conftest import MockConfigEntry, MockModuleConnection, init_integration from tests.common import snapshot_platform -SWITCH_OUTPUT1 = "switch.switch_output1" -SWITCH_OUTPUT2 = "switch.switch_output2" -SWITCH_RELAY1 = "switch.switch_relay1" -SWITCH_RELAY2 = "switch.switch_relay2" -SWITCH_REGULATOR1 = "switch.switch_regulator1" -SWITCH_KEYLOCKK1 = "switch.switch_keylock1" +SWITCH_OUTPUT1 = "switch.testmodule_switch_output1" +SWITCH_OUTPUT2 = "switch.testmodule_switch_output2" +SWITCH_RELAY1 = "switch.testmodule_switch_relay1" +SWITCH_RELAY2 = "switch.testmodule_switch_relay2" +SWITCH_REGULATOR1 = "switch.testmodule_switch_regulator1" +SWITCH_KEYLOCKK1 = "switch.testmodule_switch_keylock1" async def test_setup_lcn_switch( From 906b3901fb4cec15913e946a3cb7e8bcd1016cca Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Sun, 18 May 2025 16:52:27 +0200 Subject: [PATCH 0567/1175] Add select platform to eheimdigital (#145031) * Add select platform to eheimdigital * Review * Review * Fix tests --- .../components/eheimdigital/__init__.py | 1 + .../components/eheimdigital/select.py | 102 +++++++++++++ .../components/eheimdigital/strings.json | 10 ++ .../eheimdigital/snapshots/test_select.ambr | 59 ++++++++ tests/components/eheimdigital/test_select.py | 136 ++++++++++++++++++ 5 files changed, 308 insertions(+) create mode 100644 homeassistant/components/eheimdigital/select.py create mode 100644 tests/components/eheimdigital/snapshots/test_select.ambr create mode 100644 tests/components/eheimdigital/test_select.py diff --git a/homeassistant/components/eheimdigital/__init__.py b/homeassistant/components/eheimdigital/__init__.py index 881396ea4af..bc8bbded186 100644 --- a/homeassistant/components/eheimdigital/__init__.py +++ b/homeassistant/components/eheimdigital/__init__.py @@ -13,6 +13,7 @@ PLATFORMS = [ Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.TIME, diff --git a/homeassistant/components/eheimdigital/select.py b/homeassistant/components/eheimdigital/select.py new file mode 100644 index 00000000000..9311eb01ecc --- /dev/null +++ b/homeassistant/components/eheimdigital/select.py @@ -0,0 +1,102 @@ +"""EHEIM Digital select entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Generic, TypeVar, override + +from eheimdigital.classic_vario import EheimDigitalClassicVario +from eheimdigital.device import EheimDigitalDevice +from eheimdigital.types import FilterMode + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator +from .entity import EheimDigitalEntity + +PARALLEL_UPDATES = 0 + +_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True) + + +@dataclass(frozen=True, kw_only=True) +class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]): + """Class describing EHEIM Digital select entities.""" + + value_fn: Callable[[_DeviceT_co], str | None] + set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]] + + +CLASSICVARIO_DESCRIPTIONS: tuple[ + EheimDigitalSelectDescription[EheimDigitalClassicVario], ... +] = ( + EheimDigitalSelectDescription[EheimDigitalClassicVario]( + key="filter_mode", + translation_key="filter_mode", + value_fn=( + lambda device: device.filter_mode.name.lower() + if device.filter_mode is not None + else None + ), + set_value_fn=( + lambda device, value: device.set_filter_mode(FilterMode[value.upper()]) + ), + options=[name.lower() for name in FilterMode.__members__], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: EheimDigitalConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the callbacks for the coordinator so select entities can be added as devices are found.""" + coordinator = entry.runtime_data + + def async_setup_device_entities( + device_address: dict[str, EheimDigitalDevice], + ) -> None: + """Set up the number entities for one or multiple devices.""" + entities: list[EheimDigitalSelect[EheimDigitalDevice]] = [] + for device in device_address.values(): + if isinstance(device, EheimDigitalClassicVario): + entities.extend( + EheimDigitalSelect[EheimDigitalClassicVario]( + coordinator, device, description + ) + for description in CLASSICVARIO_DESCRIPTIONS + ) + + async_add_entities(entities) + + coordinator.add_platform_callback(async_setup_device_entities) + async_setup_device_entities(coordinator.hub.devices) + + +class EheimDigitalSelect( + EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co] +): + """Represent an EHEIM Digital select entity.""" + + entity_description: EheimDigitalSelectDescription[_DeviceT_co] + + def __init__( + self, + coordinator: EheimDigitalUpdateCoordinator, + device: _DeviceT_co, + description: EheimDigitalSelectDescription[_DeviceT_co], + ) -> None: + """Initialize an EHEIM Digital select entity.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = f"{self._device_address}_{description.key}" + + @override + async def async_select_option(self, option: str) -> None: + return await self.entity_description.set_value_fn(self._device, option) + + @override + def _async_update_attrs(self) -> None: + self._attr_current_option = self.entity_description.value_fn(self._device) diff --git a/homeassistant/components/eheimdigital/strings.json b/homeassistant/components/eheimdigital/strings.json index f6f6b74a72e..89f802c9d6d 100644 --- a/homeassistant/components/eheimdigital/strings.json +++ b/homeassistant/components/eheimdigital/strings.json @@ -67,6 +67,16 @@ "name": "System LED brightness" } }, + "select": { + "filter_mode": { + "name": "Filter mode", + "state": { + "manual": "Manual", + "pulse": "Pulse", + "bio": "Bio" + } + } + }, "sensor": { "current_speed": { "name": "Current speed" diff --git a/tests/components/eheimdigital/snapshots/test_select.ambr b/tests/components/eheimdigital/snapshots/test_select.ambr new file mode 100644 index 00000000000..5416f5a2d78 --- /dev/null +++ b/tests/components/eheimdigital/snapshots/test_select.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_setup[select.mock_classicvario_filter_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'pulse', + 'bio', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_classicvario_filter_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': 'Filter mode', + 'platform': 'eheimdigital', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'filter_mode', + 'unique_id': '00:00:00:00:00:03_filter_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[select.mock_classicvario_filter_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock classicVARIO Filter mode', + 'options': list([ + 'manual', + 'pulse', + 'bio', + ]), + }), + 'context': , + 'entity_id': 'select.mock_classicvario_filter_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/eheimdigital/test_select.py b/tests/components/eheimdigital/test_select.py new file mode 100644 index 00000000000..89ec91b62a0 --- /dev/null +++ b/tests/components/eheimdigital/test_select.py @@ -0,0 +1,136 @@ +"""Tests for the select module.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from eheimdigital.types import FilterMode +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("classic_vario_mock") +async def test_setup( + hass: HomeAssistant, + eheimdigital_hub_mock: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test select platform setup.""" + mock_config_entry.add_to_hass(hass) + + with ( + patch("homeassistant.components.eheimdigital.PLATFORMS", [Platform.SELECT]), + patch( + "homeassistant.components.eheimdigital.coordinator.asyncio.Event", + new=AsyncMock, + ), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + for device in eheimdigital_hub_mock.return_value.devices: + await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( + device, eheimdigital_hub_mock.return_value.devices[device].device_type + ) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("classic_vario_mock") +@pytest.mark.parametrize( + ("device_name", "entity_list"), + [ + ( + "classic_vario_mock", + [ + ( + "select.mock_classicvario_filter_mode", + "manual", + "set_filter_mode", + (FilterMode.MANUAL,), + ), + ], + ), + ], +) +async def test_set_value( + hass: HomeAssistant, + eheimdigital_hub_mock: MagicMock, + mock_config_entry: MockConfigEntry, + device_name: str, + entity_list: list[tuple[str, float, str, tuple[FilterMode]]], + request: pytest.FixtureRequest, +) -> None: + """Test setting a value.""" + device: MagicMock = request.getfixturevalue(device_name) + await init_integration(hass, mock_config_entry) + + await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( + device.mac_address, device.device_type + ) + + await hass.async_block_till_done() + + for item in entity_list: + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: item[0], ATTR_OPTION: item[1]}, + blocking=True, + ) + calls = [call for call in device.mock_calls if call[0] == item[2]] + assert len(calls) == 1 and calls[0][1] == item[3] + + +@pytest.mark.usefixtures("classic_vario_mock", "heater_mock") +@pytest.mark.parametrize( + ("device_name", "entity_list"), + [ + ( + "classic_vario_mock", + [ + ( + "select.mock_classicvario_filter_mode", + "filter_mode", + FilterMode.BIO, + ), + ], + ), + ], +) +async def test_state_update( + hass: HomeAssistant, + eheimdigital_hub_mock: MagicMock, + mock_config_entry: MockConfigEntry, + device_name: str, + entity_list: list[tuple[str, str, FilterMode]], + request: pytest.FixtureRequest, +) -> None: + """Test state updates.""" + device: MagicMock = request.getfixturevalue(device_name) + await init_integration(hass, mock_config_entry) + + await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( + device.mac_address, device.device_type + ) + + await hass.async_block_till_done() + + for item in entity_list: + setattr(device, item[1], item[2]) + await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() + assert (state := hass.states.get(item[0])) + assert state.state == item[2].name.lower() From aa4c41abe84353163feb84adfc8dbc01b28b03d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Sun, 18 May 2025 17:23:21 +0200 Subject: [PATCH 0568/1175] Postpone update in WMSPro after service call (#144836) * Reduce stress on WMS WebControl pro with higher scan interval Avoid delays and connection issues due to overloaded hub. Fixes #133832 and #134413 * Schedule an entity state update after performing an action Avoid delaying immediate status updates, e.g. on/off changes. * Replace scheduled state updates with delayed action completion Suggested-by: joostlek --- homeassistant/components/wmspro/cover.py | 8 +++++++- homeassistant/components/wmspro/light.py | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wmspro/cover.py b/homeassistant/components/wmspro/cover.py index 97ce540dc0b..0d9ccb8547d 100644 --- a/homeassistant/components/wmspro/cover.py +++ b/homeassistant/components/wmspro/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta from typing import Any @@ -17,7 +18,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import WebControlProConfigEntry from .entity import WebControlProGenericEntity -SCAN_INTERVAL = timedelta(seconds=5) +ACTION_DELAY = 0.5 +SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 @@ -57,6 +59,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): """Move the cover to a specific position.""" action = self._dest.action(self._drive_action_desc) await action(percentage=100 - kwargs[ATTR_POSITION]) + await asyncio.sleep(ACTION_DELAY) @property def is_closed(self) -> bool | None: @@ -67,11 +70,13 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): """Open the cover.""" action = self._dest.action(self._drive_action_desc) await action(percentage=0) + await asyncio.sleep(ACTION_DELAY) async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" action = self._dest.action(self._drive_action_desc) await action(percentage=100) + await asyncio.sleep(ACTION_DELAY) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device if in motion.""" @@ -80,6 +85,7 @@ class WebControlProCover(WebControlProGenericEntity, CoverEntity): WMS_WebControl_pro_API_actionType.Stop, ) await action() + await asyncio.sleep(ACTION_DELAY) class WebControlProAwning(WebControlProCover): diff --git a/homeassistant/components/wmspro/light.py b/homeassistant/components/wmspro/light.py index 754e537c34a..d828c8a26e8 100644 --- a/homeassistant/components/wmspro/light.py +++ b/homeassistant/components/wmspro/light.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta from typing import Any @@ -16,7 +17,8 @@ from . import WebControlProConfigEntry from .const import BRIGHTNESS_SCALE from .entity import WebControlProGenericEntity -SCAN_INTERVAL = timedelta(seconds=5) +ACTION_DELAY = 0.5 +SCAN_INTERVAL = timedelta(seconds=15) PARALLEL_UPDATES = 1 @@ -55,11 +57,13 @@ class WebControlProLight(WebControlProGenericEntity, LightEntity): """Turn the light on.""" action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch) await action(onOffState=True) + await asyncio.sleep(ACTION_DELAY) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" action = self._dest.action(WMS_WebControl_pro_API_actionDescription.LightSwitch) await action(onOffState=False) + await asyncio.sleep(ACTION_DELAY) class WebControlProDimmer(WebControlProLight): @@ -88,3 +92,4 @@ class WebControlProDimmer(WebControlProLight): await action( percentage=brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS]) ) + await asyncio.sleep(ACTION_DELAY) From 3ff095cc518a521ee2a4e233af393ea2d02cac99 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Sun, 18 May 2025 17:25:09 +0200 Subject: [PATCH 0569/1175] Add Homee alarm-control-panel platform (#140041) * Add alarm control panel * Add alarm control panel tests * add disarm function * reuse state setting code * change sleeping to night * review change 1 * fix review comments * fix review comments --- homeassistant/components/homee/__init__.py | 1 + .../components/homee/alarm_control_panel.py | 138 ++++++++++++++++++ homeassistant/components/homee/entity.py | 22 ++- homeassistant/components/homee/strings.json | 8 + tests/components/homee/fixtures/homee.json | 135 +++++++++++++++++ .../snapshots/test_alarm_control_panel.ambr | 52 +++++++ .../homee/test_alarm_control_panel.py | 96 ++++++++++++ 7 files changed, 444 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/homee/alarm_control_panel.py create mode 100644 tests/components/homee/fixtures/homee.json create mode 100644 tests/components/homee/snapshots/test_alarm_control_panel.ambr create mode 100644 tests/components/homee/test_alarm_control_panel.py diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index 579704aea44..654bdde6211 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -15,6 +15,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, diff --git a/homeassistant/components/homee/alarm_control_panel.py b/homeassistant/components/homee/alarm_control_panel.py new file mode 100644 index 00000000000..fd7371b31e4 --- /dev/null +++ b/homeassistant/components/homee/alarm_control_panel.py @@ -0,0 +1,138 @@ +"""The Homee alarm control panel platform.""" + +from dataclasses import dataclass + +from pyHomee.const import AttributeChangedBy, AttributeType +from pyHomee.model import HomeeAttribute + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityDescription, + AlarmControlPanelEntityFeature, + AlarmControlPanelState, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import DOMAIN, HomeeConfigEntry +from .entity import HomeeEntity +from .helpers import get_name_for_enum + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class HomeeAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription): + """A class that describes Homee alarm control panel entities.""" + + code_arm_required: bool = False + state_list: list[AlarmControlPanelState] + + +ALARM_DESCRIPTIONS = { + AttributeType.HOMEE_MODE: HomeeAlarmControlPanelEntityDescription( + key="homee_mode", + code_arm_required=False, + state_list=[ + AlarmControlPanelState.ARMED_HOME, + AlarmControlPanelState.ARMED_NIGHT, + AlarmControlPanelState.ARMED_AWAY, + AlarmControlPanelState.ARMED_VACATION, + ], + ) +} + + +def get_supported_features( + state_list: list[AlarmControlPanelState], +) -> AlarmControlPanelEntityFeature: + """Return supported features based on the state list.""" + supported_features = AlarmControlPanelEntityFeature(0) + if AlarmControlPanelState.ARMED_HOME in state_list: + supported_features |= AlarmControlPanelEntityFeature.ARM_HOME + if AlarmControlPanelState.ARMED_AWAY in state_list: + supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY + if AlarmControlPanelState.ARMED_NIGHT in state_list: + supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT + if AlarmControlPanelState.ARMED_VACATION in state_list: + supported_features |= AlarmControlPanelEntityFeature.ARM_VACATION + return supported_features + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HomeeConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Add the Homee platform for the alarm control panel component.""" + + async_add_entities( + HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type]) + for node in config_entry.runtime_data.nodes + for attribute in node.attributes + if attribute.type in ALARM_DESCRIPTIONS and attribute.editable + ) + + +class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity): + """Representation of a Homee alarm control panel.""" + + entity_description: HomeeAlarmControlPanelEntityDescription + + def __init__( + self, + attribute: HomeeAttribute, + entry: HomeeConfigEntry, + description: HomeeAlarmControlPanelEntityDescription, + ) -> None: + """Initialize a Homee alarm control panel entity.""" + super().__init__(attribute, entry) + self.entity_description = description + self._attr_code_arm_required = description.code_arm_required + self._attr_supported_features = get_supported_features(description.state_list) + self._attr_translation_key = description.key + + @property + def alarm_state(self) -> AlarmControlPanelState: + """Return current state.""" + return self.entity_description.state_list[int(self._attribute.current_value)] + + @property + def changed_by(self) -> str: + """Return by whom or what the entity was last changed.""" + changed_by_name = get_name_for_enum( + AttributeChangedBy, self._attribute.changed_by + ) + return f"{changed_by_name} - {self._attribute.changed_by_id}" + + async def _async_set_alarm_state(self, state: AlarmControlPanelState) -> None: + """Set the alarm state.""" + if state in self.entity_description.state_list: + await self.async_set_homee_value( + self.entity_description.state_list.index(state) + ) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + # Since disarm is always present in the UI, we raise an error. + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="disarm_not_supported", + ) + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + await self._async_set_alarm_state(AlarmControlPanelState.ARMED_HOME) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm night command.""" + await self._async_set_alarm_state(AlarmControlPanelState.ARMED_NIGHT) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + await self._async_set_alarm_state(AlarmControlPanelState.ARMED_AWAY) + + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Send arm vacation command.""" + await self._async_set_alarm_state(AlarmControlPanelState.ARMED_VACATION) diff --git a/homeassistant/components/homee/entity.py b/homeassistant/components/homee/entity.py index 165a655d82b..4c85f52bb28 100644 --- a/homeassistant/components/homee/entity.py +++ b/homeassistant/components/homee/entity.py @@ -27,14 +27,20 @@ class HomeeEntity(Entity): ) self._entry = entry node = entry.runtime_data.get_node_by_id(attribute.node_id) - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}") - }, - name=node.name, - model=get_name_for_enum(NodeProfile, node.profile), - via_device=(DOMAIN, entry.runtime_data.settings.uid), - ) + # Homee hub itself has node-id -1 + if node.id == -1: + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.runtime_data.settings.uid)}, + ) + else: + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}") + }, + name=node.name, + model=get_name_for_enum(NodeProfile, node.profile), + via_device=(DOMAIN, entry.runtime_data.settings.uid), + ) self._host_connected = entry.runtime_data.connected diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index c53a1c2d3e2..092fca0c0ac 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -26,6 +26,11 @@ } }, "entity": { + "alarm_control_panel": { + "homee_mode": { + "name": "Status" + } + }, "binary_sensor": { "blackout_alarm": { "name": "Blackout" @@ -370,6 +375,9 @@ "connection_closed": { "message": "Could not connect to homee while setting attribute." }, + "disarm_not_supported": { + "message": "Disarm is not supported by homee." + }, "invalid_preset_mode": { "message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'." } diff --git a/tests/components/homee/fixtures/homee.json b/tests/components/homee/fixtures/homee.json new file mode 100644 index 00000000000..763e594c2fa --- /dev/null +++ b/tests/components/homee/fixtures/homee.json @@ -0,0 +1,135 @@ +{ + "id": -1, + "name": "homee", + "profile": 1, + "image": "default", + "favorite": 0, + "order": 0, + "protocol": 0, + "routing": 0, + "state": 1, + "state_changed": 16, + "added": 16, + "history": 1, + "cube_type": 0, + "note": "", + "services": 0, + "phonetic_name": "", + "owner": 0, + "security": 0, + "attributes": [ + { + "id": 1, + "node_id": -1, + "instance": 0, + "minimum": 0, + "maximum": 200, + "current_value": 0.0, + "target_value": 0.0, + "last_value": 2.0, + "unit": "n/a", + "step_value": 1.0, + "editable": 1, + "type": 205, + "state": 1, + "last_changed": 1735815716, + "changed_by": 2, + "changed_by_id": 4, + "based_on": 1, + "data": "", + "name": "", + "options": { + "history": { + "day": 182, + "week": 26, + "month": 6, + "stepped": true + } + } + }, + { + "id": 18, + "node_id": -1, + "instance": 0, + "minimum": 0, + "maximum": 100, + "current_value": 15.0, + "target_value": 15.0, + "last_value": 15.0, + "unit": "%", + "step_value": 0.1, + "editable": 0, + "type": 311, + "state": 1, + "last_changed": 1739390161, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "", + "options": { + "history": { + "day": 1, + "week": 26, + "month": 6 + } + } + }, + { + "id": 19, + "node_id": -1, + "instance": 0, + "minimum": 0, + "maximum": 100, + "current_value": 5.0, + "target_value": 5.0, + "last_value": 10.0, + "unit": "%", + "step_value": 0.1, + "editable": 0, + "type": 312, + "state": 1, + "last_changed": 1739390161, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "", + "options": { + "history": { + "day": 1, + "week": 26, + "month": 6 + } + } + }, + { + "id": 20, + "node_id": -1, + "instance": 0, + "minimum": 0, + "maximum": 100, + "current_value": 10.0, + "target_value": 10.0, + "last_value": 10.0, + "unit": "%", + "step_value": 0.1, + "editable": 0, + "type": 313, + "state": 1, + "last_changed": 1739390161, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "", + "options": { + "history": { + "day": 1, + "week": 26, + "month": 6 + } + } + } + ] +} diff --git a/tests/components/homee/snapshots/test_alarm_control_panel.ambr b/tests/components/homee/snapshots/test_alarm_control_panel.ambr new file mode 100644 index 00000000000..59a22f74080 --- /dev/null +++ b/tests/components/homee/snapshots/test_alarm_control_panel.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_alarm_control_panel_snapshot[alarm_control_panel.testhomee_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'alarm_control_panel', + 'entity_category': None, + 'entity_id': 'alarm_control_panel.testhomee_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': 'homee', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'homee_mode', + 'unique_id': '00055511EECC--1-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_alarm_control_panel_snapshot[alarm_control_panel.testhomee_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'changed_by': 'user - 4', + 'code_arm_required': False, + 'code_format': None, + 'friendly_name': 'TestHomee Status', + 'supported_features': , + }), + 'context': , + 'entity_id': 'alarm_control_panel.testhomee_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'armed_home', + }) +# --- diff --git a/tests/components/homee/test_alarm_control_panel.py b/tests/components/homee/test_alarm_control_panel.py new file mode 100644 index 00000000000..dafe74660ac --- /dev/null +++ b/tests/components/homee/test_alarm_control_panel.py @@ -0,0 +1,96 @@ +"""Test Homee alarm control panels.""" + +from unittest.mock import MagicMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, + SERVICE_ALARM_DISARM, +) +from homeassistant.components.homee.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er + +from . import build_mock_node, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def setup_alarm_control_panel( + hass: HomeAssistant, mock_homee: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Setups the integration for select tests.""" + mock_homee.nodes = [build_mock_node("homee.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + await setup_integration(hass, mock_config_entry) + + +@pytest.mark.parametrize( + ("service", "state"), + [ + (SERVICE_ALARM_ARM_HOME, 0), + (SERVICE_ALARM_ARM_NIGHT, 1), + (SERVICE_ALARM_ARM_AWAY, 2), + (SERVICE_ALARM_ARM_VACATION, 3), + ], +) +async def test_alarm_control_panel_services( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + service: str, + state: int, +) -> None: + """Test alarm control panel services.""" + await setup_alarm_control_panel(hass, mock_homee, mock_config_entry) + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.testhomee_status"}, + blocking=True, + ) + mock_homee.set_value.assert_called_once_with(-1, 1, state) + + +async def test_alarm_control_panel_service_disarm_error( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that disarm service calls no action.""" + await setup_alarm_control_panel(hass, mock_homee, mock_config_entry) + + with pytest.raises(ServiceValidationError) as exc_info: + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: "alarm_control_panel.testhomee_status"}, + blocking=True, + ) + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == "disarm_not_supported" + + +async def test_alarm_control_panel_snapshot( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the alarm-control_panel snapshots.""" + with patch( + "homeassistant.components.homee.PLATFORMS", [Platform.ALARM_CONTROL_PANEL] + ): + await setup_alarm_control_panel(hass, mock_homee, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 3f59b1c3769d670b8c5c59e3947a05a4fe57422b Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 19 May 2025 01:59:19 +0800 Subject: [PATCH 0570/1175] Add YoLink new device types support 5009 & 5029 (#144323) * Leak Stop * Fix as suggested. --- homeassistant/components/yolink/__init__.py | 4 +- .../components/yolink/binary_sensor.py | 10 +++- homeassistant/components/yolink/const.py | 3 ++ .../components/yolink/coordinator.py | 6 ++- homeassistant/components/yolink/sensor.py | 37 ++++++++++--- homeassistant/components/yolink/strings.json | 12 +++++ homeassistant/components/yolink/valve.py | 52 +++++++++++++++++-- 7 files changed, 112 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 7ba7433f53f..3dd5aa7c974 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from . import api -from .const import DOMAIN, YOLINK_EVENT +from .const import ATTR_LORA_INFO, DOMAIN, YOLINK_EVENT from .coordinator import YoLinkCoordinator from .device_trigger import CONF_LONG_PRESS, CONF_SHORT_PRESS from .services import async_register_services @@ -72,6 +72,8 @@ class YoLinkHomeMessageListener(MessageListener): if device_coordinator is None: return device_coordinator.dev_online = True + if (loraInfo := msg_data.get(ATTR_LORA_INFO)) is not None: + device_coordinator.dev_net_type = loraInfo.get("devNetType") device_coordinator.async_set_updated_data(msg_data) # handling events if ( diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index e5200c66afd..7f965650354 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -11,6 +11,7 @@ from yolink.const import ( ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_WATER_METER_CONTROLLER, ) @@ -51,6 +52,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_WATER_METER_CONTROLLER, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ] @@ -96,8 +98,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( state_key="alarm", device_class=BinarySensorDeviceClass.MOISTURE, value=lambda state: state.get("leak") if state is not None else None, + # This property will be lost during valve operation. + should_update_entity=lambda value: value is not None, exists_fn=lambda device: ( - device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER + device.device_type + in [ + ATTR_DEVICE_WATER_METER_CONTROLLER, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ] ), ), YoLinkBinarySensorEntityDescription( diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 960bf8568d4..9556c1bbd82 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -12,6 +12,7 @@ ATTR_VOLUME = "volume" ATTR_TEXT_MESSAGE = "message" ATTR_REPEAT = "repeat" ATTR_TONE = "tone" +ATTR_LORA_INFO = "loraInfo" YOLINK_EVENT = f"{DOMAIN}_event" YOLINK_OFFLINE_TIME = 32400 @@ -37,5 +38,7 @@ DEV_MODEL_SWITCH_YS5708_UC = "YS5708-UC" DEV_MODEL_SWITCH_YS5708_EC = "YS5708-EC" DEV_MODEL_SWITCH_YS5709_UC = "YS5709-UC" DEV_MODEL_SWITCH_YS5709_EC = "YS5709-EC" +DEV_MODEL_LEAK_STOP_YS5009 = "YS5009" +DEV_MODEL_LEAK_STOP_YS5029 = "YS5029" DEV_MODEL_WATER_METER_YS5018_EC = "YS5018-EC" DEV_MODEL_WATER_METER_YS5018_UC = "YS5018-UC" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index 8fd450df4a5..7d5323663de 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_DEVICE_STATE, DOMAIN, YOLINK_OFFLINE_TIME +from .const import ATTR_DEVICE_STATE, ATTR_LORA_INFO, DOMAIN, YOLINK_OFFLINE_TIME _LOGGER = logging.getLogger(__name__) @@ -47,6 +47,7 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): self.device = device self.paired_device = paired_device self.dev_online = True + self.dev_net_type = None async def _async_update_data(self) -> dict: """Fetch device state.""" @@ -83,5 +84,8 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): ) raise UpdateFailed from yl_client_err if device_state is not None: + dev_lora_info = device_state.get(ATTR_LORA_INFO) + if dev_lora_info is not None: + self.dev_net_type = dev_lora_info.get("devNetType") return device_state return {} diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 511b7718e26..6572566f8ee 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -15,6 +15,7 @@ from yolink.const import ( ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_MULTI_OUTLET, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_OUTLET, ATTR_DEVICE_POWER_FAILURE_ALARM, ATTR_DEVICE_SIREN, @@ -95,6 +96,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, ATTR_DEVICE_WATER_METER_CONTROLLER, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, @@ -116,6 +118,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, ATTR_DEVICE_WATER_METER_CONTROLLER, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ] MCU_DEV_TEMPERATURE_SENSOR = [ @@ -211,14 +214,14 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( translation_key="power_failure_alarm", device_class=SensorDeviceClass.ENUM, options=["normal", "alert", "off"], - exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM, ), YoLinkSensorEntityDescription( key="mute", translation_key="power_failure_alarm_mute", device_class=SensorDeviceClass.ENUM, options=["muted", "unmuted"], - exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM, value=lambda value: "muted" if value is True else "unmuted", ), YoLinkSensorEntityDescription( @@ -226,7 +229,7 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( translation_key="power_failure_alarm_volume", device_class=SensorDeviceClass.ENUM, options=["low", "medium", "high"], - exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM, value=cvt_volume, ), YoLinkSensorEntityDescription( @@ -234,14 +237,14 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( translation_key="power_failure_alarm_beep", device_class=SensorDeviceClass.ENUM, options=["enabled", "disabled"], - exists_fn=lambda device: device.device_type in ATTR_DEVICE_POWER_FAILURE_ALARM, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_POWER_FAILURE_ALARM, value=lambda value: "enabled" if value is True else "disabled", ), YoLinkSensorEntityDescription( key="waterDepth", device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.METERS, - exists_fn=lambda device: device.device_type in ATTR_DEVICE_WATER_DEPTH_SENSOR, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_WATER_DEPTH_SENSOR, ), YoLinkSensorEntityDescription( key="meter_reading", @@ -251,7 +254,29 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, should_update_entity=lambda value: value is not None, exists_fn=lambda device: ( - device.device_type in ATTR_DEVICE_WATER_METER_CONTROLLER + device.device_type == ATTR_DEVICE_WATER_METER_CONTROLLER + ), + ), + YoLinkSensorEntityDescription( + key="meter_1_reading", + translation_key="water_meter_1_reading", + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL_INCREASING, + should_update_entity=lambda value: value is not None, + exists_fn=lambda device: ( + device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER + ), + ), + YoLinkSensorEntityDescription( + key="meter_2_reading", + translation_key="water_meter_2_reading", + device_class=SensorDeviceClass.WATER, + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + state_class=SensorStateClass.TOTAL_INCREASING, + should_update_entity=lambda value: value is not None, + exists_fn=lambda device: ( + device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER ), ), YoLinkSensorEntityDescription( diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 825f9e3e619..d38ea248c31 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -90,6 +90,12 @@ }, "water_meter_reading": { "name": "Water meter reading" + }, + "water_meter_1_reading": { + "name": "Water meter 1 reading" + }, + "water_meter_2_reading": { + "name": "Water meter 2 reading" } }, "number": { @@ -100,6 +106,12 @@ "valve": { "meter_valve_state": { "name": "Valve state" + }, + "meter_valve_1_state": { + "name": "Valve 1" + }, + "meter_valve_2_state": { + "name": "Valve 2" } } }, diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index 26ce72a53d1..0e8a5e61855 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -6,7 +6,11 @@ from collections.abc import Callable from dataclasses import dataclass from yolink.client_request import ClientRequest -from yolink.const import ATTR_DEVICE_WATER_METER_CONTROLLER +from yolink.const import ( + ATTR_DEVICE_MODEL_A, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ATTR_DEVICE_WATER_METER_CONTROLLER, +) from yolink.device import YoLinkDevice from homeassistant.components.valve import ( @@ -30,6 +34,7 @@ class YoLinkValveEntityDescription(ValveEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True value: Callable = lambda state: state + channel_index: int | None = None DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( @@ -42,9 +47,32 @@ DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( == ATTR_DEVICE_WATER_METER_CONTROLLER and not device.device_model_name.startswith(DEV_MODEL_WATER_METER_YS5007), ), + YoLinkValveEntityDescription( + key="valve_1_state", + translation_key="meter_valve_1_state", + device_class=ValveDeviceClass.WATER, + value=lambda value: value != "open" if value is not None else None, + exists_fn=lambda device: ( + device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER + ), + channel_index=0, + ), + YoLinkValveEntityDescription( + key="valve_2_state", + translation_key="meter_valve_2_state", + device_class=ValveDeviceClass.WATER, + value=lambda value: value != "open" if value is not None else None, + exists_fn=lambda device: ( + device.device_type == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER + ), + channel_index=1, + ), ) -DEVICE_TYPE = [ATTR_DEVICE_WATER_METER_CONTROLLER] +DEVICE_TYPE = [ + ATTR_DEVICE_WATER_METER_CONTROLLER, + ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, +] async def async_setup_entry( @@ -102,7 +130,17 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity): async def _async_invoke_device(self, state: str) -> None: """Call setState api to change valve state.""" - await self.call_device(ClientRequest("setState", {"valve": state})) + if ( + self.coordinator.device.device_type + == ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER + ): + channel_index = self.entity_description.channel_index + if channel_index is not None: + await self.call_device( + ClientRequest("setState", {"valves": {str(channel_index): state}}) + ) + else: + await self.call_device(ClientRequest("setState", {"valve": state})) self._attr_is_closed = state == "close" self.async_write_ha_state() @@ -113,3 +151,11 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity): async def async_close_valve(self) -> None: """Close valve.""" await self._async_invoke_device("close") + + @property + def available(self) -> bool: + """Return true is device is available.""" + if self.coordinator.dev_net_type is not None: + # When the device operates in Class A mode, it cannot be controlled. + return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A + return super().available From 520c9646560cce3581d902a5e672637f9f5faa45 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 18 May 2025 20:50:33 +0200 Subject: [PATCH 0571/1175] Remove deprecated aux heat from elkm1 (#145148) --- homeassistant/components/elkm1/climate.py | 36 --------------------- homeassistant/components/elkm1/strings.json | 13 -------- 2 files changed, 49 deletions(-) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 55af0cfa29c..59d3aa9605a 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -20,10 +20,8 @@ from homeassistant.components.climate import ( from homeassistant.const import PRECISION_WHOLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from . import ElkM1ConfigEntry -from .const import DOMAIN from .entity import ElkEntity, create_elk_entities SUPPORT_HVAC = [ @@ -78,7 +76,6 @@ class ElkThermostat(ElkEntity, ClimateEntity): _attr_precision = PRECISION_WHOLE _attr_supported_features = ( ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON @@ -128,11 +125,6 @@ class ElkThermostat(ElkEntity, ClimateEntity): """Return the current humidity.""" return self._element.humidity - @property - def is_aux_heat(self) -> bool: - """Return if aux heater is on.""" - return self._element.mode == ThermostatMode.EMERGENCY_HEAT - @property def fan_mode(self) -> str | None: """Return the fan setting.""" @@ -151,34 +143,6 @@ class ElkThermostat(ElkEntity, ClimateEntity): thermostat_mode, fan_mode = HASS_TO_ELK_HVAC_MODES[hvac_mode] self._elk_set(thermostat_mode, fan_mode) - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - async_create_issue( - self.hass, - DOMAIN, - "migrate_aux_heat", - breaks_in_ha_version="2025.4.0", - is_fixable=True, - is_persistent=True, - translation_key="migrate_aux_heat", - severity=IssueSeverity.WARNING, - ) - self._elk_set(ThermostatMode.EMERGENCY_HEAT, None) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - async_create_issue( - self.hass, - DOMAIN, - "migrate_aux_heat", - breaks_in_ha_version="2025.4.0", - is_fixable=True, - is_persistent=True, - translation_key="migrate_aux_heat", - severity=IssueSeverity.WARNING, - ) - self._elk_set(ThermostatMode.HEAT, None) - async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode] diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index b50c1817838..19967612b0f 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -189,18 +189,5 @@ "name": "Sensor zone trigger", "description": "Triggers zone." } - }, - "issues": { - "migrate_aux_heat": { - "title": "Migration of Elk-M1 set_aux_heat action", - "fix_flow": { - "step": { - "confirm": { - "description": "The Elk-M1 `set_aux_heat` action has been migrated. A new emergency heat switch entity is available for each thermostat.\n\nUpdate any automations to use the new emergency heat switch entity. When this is done, select **Submit** to fix this issue.", - "title": "[%key:component::elkm1::issues::migrate_aux_heat::title%]" - } - } - } - } } } From a576f7baf33256a820f57a4bf2f61aacddb270ea Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 18 May 2025 21:28:15 +0200 Subject: [PATCH 0572/1175] Add Immich integration (#145125) * add immich integration * bump aioimmich==0.3.1 * rework to require an url as input and pare it afterwards * fix doc strings * remove name attribute from deviceinfo as it is default behaviour * add translated uom for count sensors * explicitly pass in the config_entry in coordinator * fix url in strings to uppercase * use data_updates attribute instead of data * remove left over * match entries only by host * remove quotes * import SOURCE_USER directly, instead of config_entries * split happy and sad flow tests * remove unneccessary async_block_till_done() calls * replace url example by "full URL" * bump aioimmich==0.4.0 * bump aioimmich==0.5.0 * allow multiple users for same immich instance * Fix tests * limit entities when user has no admin rights * Fix tests * Fix tests --------- Co-authored-by: Joostlek --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/immich/__init__.py | 56 +++ .../components/immich/config_flow.py | 174 +++++++ homeassistant/components/immich/const.py | 7 + .../components/immich/coordinator.py | 74 +++ homeassistant/components/immich/entity.py | 27 ++ homeassistant/components/immich/icons.json | 15 + homeassistant/components/immich/manifest.json | 11 + .../components/immich/quality_scale.yaml | 76 +++ homeassistant/components/immich/sensor.py | 147 ++++++ homeassistant/components/immich/strings.json | 73 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/immich/__init__.py | 13 + tests/components/immich/conftest.py | 136 ++++++ tests/components/immich/const.py | 24 + .../immich/snapshots/test_sensor.ambr | 444 ++++++++++++++++++ tests/components/immich/test_config_flow.py | 244 ++++++++++ tests/components/immich/test_sensor.py | 45 ++ 23 files changed, 1592 insertions(+) create mode 100644 homeassistant/components/immich/__init__.py create mode 100644 homeassistant/components/immich/config_flow.py create mode 100644 homeassistant/components/immich/const.py create mode 100644 homeassistant/components/immich/coordinator.py create mode 100644 homeassistant/components/immich/entity.py create mode 100644 homeassistant/components/immich/icons.json create mode 100644 homeassistant/components/immich/manifest.json create mode 100644 homeassistant/components/immich/quality_scale.yaml create mode 100644 homeassistant/components/immich/sensor.py create mode 100644 homeassistant/components/immich/strings.json create mode 100644 tests/components/immich/__init__.py create mode 100644 tests/components/immich/conftest.py create mode 100644 tests/components/immich/const.py create mode 100644 tests/components/immich/snapshots/test_sensor.ambr create mode 100644 tests/components/immich/test_config_flow.py create mode 100644 tests/components/immich/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 5648bbe3dd2..1ae56cd74d8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -270,6 +270,7 @@ homeassistant.components.image_processing.* homeassistant.components.image_upload.* homeassistant.components.imap.* homeassistant.components.imgw_pib.* +homeassistant.components.immich.* homeassistant.components.incomfort.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/CODEOWNERS b/CODEOWNERS index b8d7ea952ee..bbbfb9394e2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -710,6 +710,8 @@ build.json @home-assistant/supervisor /tests/components/imeon_inverter/ @Imeon-Energy /homeassistant/components/imgw_pib/ @bieniu /tests/components/imgw_pib/ @bieniu +/homeassistant/components/immich/ @mib1185 +/tests/components/immich/ @mib1185 /homeassistant/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery /homeassistant/components/incomfort/ @jbouwh diff --git a/homeassistant/components/immich/__init__.py b/homeassistant/components/immich/__init__.py new file mode 100644 index 00000000000..18782ec6fd3 --- /dev/null +++ b/homeassistant/components/immich/__init__.py @@ -0,0 +1,56 @@ +"""The Immich integration.""" + +from __future__ import annotations + +from aioimmich import Immich +from aioimmich.const import CONNECT_ERRORS +from aioimmich.exceptions import ImmichUnauthorizedError + +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import ImmichConfigEntry, ImmichDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool: + """Set up Immich from a config entry.""" + + session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) + immich = Immich( + session, + entry.data[CONF_API_KEY], + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_SSL], + ) + + try: + user_info = await immich.users.async_get_my_user() + except ImmichUnauthorizedError as err: + raise ConfigEntryAuthFailed from err + except CONNECT_ERRORS as err: + raise ConfigEntryNotReady from err + + coordinator = ImmichDataUpdateCoordinator(hass, entry, immich, user_info.is_admin) + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/immich/config_flow.py b/homeassistant/components/immich/config_flow.py new file mode 100644 index 00000000000..69fae3ff1eb --- /dev/null +++ b/homeassistant/components/immich/config_flow.py @@ -0,0 +1,174 @@ +"""Config flow for the Immich integration.""" + +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from aioimmich import Immich +from aioimmich.const import CONNECT_ERRORS +from aioimmich.exceptions import ImmichUnauthorizedError +from aioimmich.users.models import ImmichUser +import voluptuous as vol +from yarl import URL + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import DEFAULT_VERIFY_SSL, DOMAIN + + +class InvalidUrl(HomeAssistantError): + """Error to indicate invalid URL.""" + + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.URL) + ), + vol.Required(CONF_API_KEY): TextSelector( + config=TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } +) + + +def _parse_url(url: str) -> tuple[str, int, bool]: + """Parse the URL and return host, port, and ssl.""" + parsed_url = URL(url) + if ( + (host := parsed_url.host) is None + or (port := parsed_url.port) is None + or (scheme := parsed_url.scheme) is None + ): + raise InvalidUrl + return host, port, scheme == "https" + + +async def check_user_info( + hass: HomeAssistant, host: str, port: int, ssl: bool, verify_ssl: bool, api_key: str +) -> ImmichUser: + """Test connection and fetch own user info.""" + session = async_get_clientsession(hass, verify_ssl) + immich = Immich(session, api_key, host, port, ssl) + return await immich.users.async_get_my_user() + + +class ImmichConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Immich.""" + + VERSION = 1 + + _name: str + _current_data: Mapping[str, Any] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + (host, port, ssl) = _parse_url(user_input[CONF_URL]) + except InvalidUrl: + errors[CONF_URL] = "invalid_url" + else: + try: + my_user_info = await check_user_info( + self.hass, + host, + port, + ssl, + user_input[CONF_VERIFY_SSL], + user_input[CONF_API_KEY], + ) + except ImmichUnauthorizedError: + errors["base"] = "invalid_auth" + except CONNECT_ERRORS: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(my_user_info.user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=my_user_info.name, + data={ + CONF_HOST: host, + CONF_PORT: port, + CONF_SSL: ssl, + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Trigger a reauthentication flow.""" + self._current_data = entry_data + self._name = entry_data[CONF_HOST] + + 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 = {} + + if user_input is not None: + try: + my_user_info = await check_user_info( + self.hass, + self._current_data[CONF_HOST], + self._current_data[CONF_PORT], + self._current_data[CONF_SSL], + self._current_data[CONF_VERIFY_SSL], + user_input[CONF_API_KEY], + ) + except ImmichUnauthorizedError: + errors["base"] = "invalid_auth" + except CONNECT_ERRORS: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(my_user_info.user_id) + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort( + self._get_reauth_entry(), data_updates=user_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + description_placeholders={"name": self._name}, + errors=errors, + ) diff --git a/homeassistant/components/immich/const.py b/homeassistant/components/immich/const.py new file mode 100644 index 00000000000..47180967a67 --- /dev/null +++ b/homeassistant/components/immich/const.py @@ -0,0 +1,7 @@ +"""Constants for the Immich integration.""" + +DOMAIN = "immich" + +DEFAULT_PORT = 2283 +DEFAULT_USE_SSL = False +DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/immich/coordinator.py b/homeassistant/components/immich/coordinator.py new file mode 100644 index 00000000000..e1904a62e24 --- /dev/null +++ b/homeassistant/components/immich/coordinator.py @@ -0,0 +1,74 @@ +"""Coordinator for the Immich integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging + +from aioimmich import Immich +from aioimmich.const import CONNECT_ERRORS +from aioimmich.exceptions import ImmichUnauthorizedError +from aioimmich.server.models import ( + ImmichServerAbout, + ImmichServerStatistics, + ImmichServerStorage, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class ImmichData: + """Data class for storing data from the API.""" + + server_about: ImmichServerAbout + server_storage: ImmichServerStorage + server_usage: ImmichServerStatistics | None + + +type ImmichConfigEntry = ConfigEntry[ImmichDataUpdateCoordinator] + + +class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]): + """Class to manage fetching IMGW-PIB data API.""" + + config_entry: ImmichConfigEntry + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, api: Immich, is_admin: bool + ) -> None: + """Initialize the data update coordinator.""" + self.api = api + self.is_admin = is_admin + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self) -> ImmichData: + """Update data via internal method.""" + try: + server_about = await self.api.server.async_get_about_info() + server_storage = await self.api.server.async_get_storage_info() + server_usage = ( + await self.api.server.async_get_server_statistics() + if self.is_admin + else None + ) + except ImmichUnauthorizedError as err: + raise ConfigEntryAuthFailed from err + except CONNECT_ERRORS as err: + raise UpdateFailed from err + + return ImmichData(server_about, server_storage, server_usage) diff --git a/homeassistant/components/immich/entity.py b/homeassistant/components/immich/entity.py new file mode 100644 index 00000000000..f99f8872ce5 --- /dev/null +++ b/homeassistant/components/immich/entity.py @@ -0,0 +1,27 @@ +"""Base entity for the Immich integration.""" + +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ImmichDataUpdateCoordinator + + +class ImmichEntity(CoordinatorEntity[ImmichDataUpdateCoordinator]): + """Define immich base entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ImmichDataUpdateCoordinator, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer="Immich", + sw_version=coordinator.data.server_about.version, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/immich/icons.json b/homeassistant/components/immich/icons.json new file mode 100644 index 00000000000..15bac6370a6 --- /dev/null +++ b/homeassistant/components/immich/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "disk_usage": { + "default": "mdi:database" + }, + "photos_count": { + "default": "mdi:file-image" + }, + "videos_count": { + "default": "mdi:file-video" + } + } + } +} diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json new file mode 100644 index 00000000000..bb8cbe720fd --- /dev/null +++ b/homeassistant/components/immich/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "immich", + "name": "Immich", + "codeowners": ["@mib1185"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/immich", + "iot_class": "local_polling", + "loggers": ["aioimmich"], + "quality_scale": "silver", + "requirements": ["aioimmich==0.5.0"] +} diff --git a/homeassistant/components/immich/quality_scale.yaml b/homeassistant/components/immich/quality_scale.yaml new file mode 100644 index 00000000000..e89127871e2 --- /dev/null +++ b/homeassistant/components/immich/quality_scale.yaml @@ -0,0 +1,76 @@ +rules: + # Bronze + action-setup: + status: done + comment: No integration specific actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: done + comment: No integration specific actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: done + comment: No integration specific actions + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: done + test-coverage: done + + # Gold + devices: done + diagnostics: todo + discovery-update-info: + status: exempt + comment: Service can't be discovered + discovery: + status: exempt + comment: Service can't be discovered + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: + status: exempt + comment: Only one device entry per config entry + entity-category: todo + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: todo + icon-translations: done + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: No repair issues needed + stale-devices: + status: exempt + comment: Only one device entry per config entry + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: done diff --git a/homeassistant/components/immich/sensor.py b/homeassistant/components/immich/sensor.py new file mode 100644 index 00000000000..f8eeed2935a --- /dev/null +++ b/homeassistant/components/immich/sensor.py @@ -0,0 +1,147 @@ +"""Sensor platform for the Immich integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, UnitOfInformation +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import ImmichConfigEntry, ImmichData, ImmichDataUpdateCoordinator +from .entity import ImmichEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class ImmichSensorEntityDescription(SensorEntityDescription): + """Immich sensor entity description.""" + + value: Callable[[ImmichData], StateType] + is_suitable: Callable[[ImmichData], bool] = lambda _: True + + +SENSOR_TYPES: tuple[ImmichSensorEntityDescription, ...] = ( + ImmichSensorEntityDescription( + key="disk_size", + translation_key="disk_size", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_storage.disk_size_raw, + ), + ImmichSensorEntityDescription( + key="disk_available", + translation_key="disk_available", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_storage.disk_available_raw, + ), + ImmichSensorEntityDescription( + key="disk_use", + translation_key="disk_use", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_storage.disk_use_raw, + entity_registry_enabled_default=False, + ), + ImmichSensorEntityDescription( + key="disk_usage", + translation_key="disk_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_storage.disk_usage_percentage, + entity_registry_enabled_default=False, + ), + ImmichSensorEntityDescription( + key="photos_count", + translation_key="photos_count", + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_usage.photos if data.server_usage else None, + is_suitable=lambda data: data.server_usage is not None, + ), + ImmichSensorEntityDescription( + key="videos_count", + translation_key="videos_count", + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.server_usage.videos if data.server_usage else None, + is_suitable=lambda data: data.server_usage is not None, + ), + ImmichSensorEntityDescription( + key="usage_by_photos", + translation_key="usage_by_photos", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda d: d.server_usage.usage_photos if d.server_usage else None, + is_suitable=lambda data: data.server_usage is not None, + entity_registry_enabled_default=False, + ), + ImmichSensorEntityDescription( + key="usage_by_videos", + translation_key="usage_by_videos", + native_unit_of_measurement=UnitOfInformation.BYTES, + suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES, + suggested_display_precision=1, + device_class=SensorDeviceClass.DATA_SIZE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda d: d.server_usage.usage_videos if d.server_usage else None, + is_suitable=lambda data: data.server_usage is not None, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ImmichConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Add immich server state sensors.""" + coordinator = entry.runtime_data + async_add_entities( + ImmichSensorEntity(coordinator, description) + for description in SENSOR_TYPES + if description.is_suitable(coordinator.data) + ) + + +class ImmichSensorEntity(ImmichEntity, SensorEntity): + """Define Immich sensor entity.""" + + entity_description: ImmichSensorEntityDescription + + def __init__( + self, + coordinator: ImmichDataUpdateCoordinator, + description: ImmichSensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}" + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/immich/strings.json b/homeassistant/components/immich/strings.json new file mode 100644 index 00000000000..875eb79f50b --- /dev/null +++ b/homeassistant/components/immich/strings.json @@ -0,0 +1,73 @@ +{ + "common": { + "data_desc_url": "The full URL of your immich instance.", + "data_desc_api_key": "API key to connect to your immich instance.", + "data_desc_ssl_verify": "Whether to verify the SSL certificate when SSL encryption is used to connect to your immich instance." + }, + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "url": "[%key:component::immich::common::data_desc_url%]", + "api_key": "[%key:component::immich::common::data_desc_api_key%]", + "verify_ssl": "[%key:component::immich::common::data_desc_ssl_verify%]" + } + }, + "reauth_confirm": { + "description": "Update the API key for {name}.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::immich::common::data_desc_api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_url": "The provided URL is invalid.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unique_id_mismatch": "The provided API key does not match the configured user.", + "already_configured": "This user is already configured for this immich instance." + } + }, + "entity": { + "sensor": { + "disk_size": { + "name": "Disk size" + }, + "disk_available": { + "name": "Disk available" + }, + "disk_use": { + "name": "Disk used" + }, + "disk_usage": { + "name": "Disk usage" + }, + "photos_count": { + "name": "Photos count", + "unit_of_measurement": "photos" + }, + "videos_count": { + "name": "Videos count", + "unit_of_measurement": "videos" + }, + "usage_by_photos": { + "name": "Disk used by photos" + }, + "usage_by_videos": { + "name": "Disk used by videos" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e4815c82543..1b7536ed4b9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -288,6 +288,7 @@ FLOWS = { "imap", "imeon_inverter", "imgw_pib", + "immich", "improv_ble", "incomfort", "inkbird", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 85f9ae5e8a9..ccb67025091 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2959,6 +2959,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "immich": { + "name": "Immich", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "improv_ble": { "name": "Improv via BLE", "integration_type": "device", diff --git a/mypy.ini b/mypy.ini index 518d1953fb3..cf3314f515c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2456,6 +2456,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.immich.*] +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.incomfort.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index e2516da1681..4038533b7cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,6 +276,9 @@ aiohue==4.7.4 # homeassistant.components.imap aioimaplib==2.0.1 +# homeassistant.components.immich +aioimmich==0.5.0 + # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98d18a93345..b80fc33107e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,6 +261,9 @@ aiohue==4.7.4 # homeassistant.components.imap aioimaplib==2.0.1 +# homeassistant.components.immich +aioimmich==0.5.0 + # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/tests/components/immich/__init__.py b/tests/components/immich/__init__.py new file mode 100644 index 00000000000..604ab84d68d --- /dev/null +++ b/tests/components/immich/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Immich integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py new file mode 100644 index 00000000000..2c9483c3955 --- /dev/null +++ b/tests/components/immich/conftest.py @@ -0,0 +1,136 @@ +"""Common fixtures for the Immich tests.""" + +from collections.abc import AsyncGenerator, Generator +from datetime import datetime +from unittest.mock import AsyncMock, patch + +from aioimmich import ImmichServer, ImmichUsers +from aioimmich.server.models import ( + ImmichServerAbout, + ImmichServerStatistics, + ImmichServerStorage, +) +from aioimmich.users.models import AvatarColor, ImmichUser, UserStatus +import pytest + +from homeassistant.components.immich.const import DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.immich.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "localhost", + CONF_PORT: 80, + CONF_SSL: False, + CONF_API_KEY: "api_key", + CONF_VERIFY_SSL: True, + }, + unique_id="e7ef5713-9dab-4bd4-b899-715b0ca4379e", + title="Someone", + ) + + +@pytest.fixture +def mock_immich_server() -> AsyncMock: + """Mock the Immich server.""" + mock = AsyncMock(spec=ImmichServer) + mock.async_get_about_info.return_value = ImmichServerAbout( + "v1.132.3", + "some_url", + False, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ) + mock.async_get_storage_info.return_value = ImmichServerStorage( + "294.2 GiB", + "142.9 GiB", + "136.3 GiB", + 315926315008, + 153400434688, + 146402975744, + 48.56, + ) + mock.async_get_server_statistics.return_value = ImmichServerStatistics( + 27038, 1836, 119525451912, 54291170551, 65234281361 + ) + return mock + + +@pytest.fixture +def mock_immich_user() -> AsyncMock: + """Mock the Immich server.""" + mock = AsyncMock(spec=ImmichUsers) + mock.async_get_my_user.return_value = ImmichUser( + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "user@immich.local", + "user", + "", + AvatarColor.PRIMARY, + datetime.fromisoformat("2025-05-11T10:07:46.866Z"), + "user", + False, + True, + datetime.fromisoformat("2025-05-11T10:07:46.866Z"), + None, + None, + "", + None, + None, + UserStatus.ACTIVE, + ) + return mock + + +@pytest.fixture +async def mock_immich( + mock_immich_server: AsyncMock, mock_immich_user: AsyncMock +) -> AsyncGenerator[AsyncMock]: + """Mock the Immich API.""" + with ( + patch("homeassistant.components.immich.Immich", autospec=True) as mock_immich, + patch("homeassistant.components.immich.config_flow.Immich", new=mock_immich), + ): + client = mock_immich.return_value + client.server = mock_immich_server + client.users = mock_immich_user + yield client + + +@pytest.fixture +async def mock_non_admin_immich(mock_immich: AsyncMock) -> AsyncMock: + """Mock the Immich API.""" + mock_immich.users.async_get_my_user.return_value.is_admin = False + return mock_immich diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py new file mode 100644 index 00000000000..2779a02be55 --- /dev/null +++ b/tests/components/immich/const.py @@ -0,0 +1,24 @@ +"""Constants for the Immich integration tests.""" + +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_VERIFY_SSL, +) + +MOCK_USER_DATA = { + CONF_URL: "http://localhost", + CONF_API_KEY: "abcdef0123456789", + CONF_VERIFY_SSL: False, +} + +MOCK_CONFIG_ENTRY_DATA = { + CONF_HOST: "localhost", + CONF_API_KEY: "abcdef0123456789", + CONF_PORT: 80, + CONF_SSL: False, + CONF_VERIFY_SSL: False, +} diff --git a/tests/components/immich/snapshots/test_sensor.ambr b/tests/components/immich/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..7284f98f681 --- /dev/null +++ b/tests/components/immich/snapshots/test_sensor.ambr @@ -0,0 +1,444 @@ +# serializer version: 1 +# name: test_sensors[sensor.someone_disk_available-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_available', + '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': 'Disk available', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'disk_available', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_available', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.someone_disk_available-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Someone Disk available', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.someone_disk_available', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '136.34839630127', + }) +# --- +# name: test_sensors[sensor.someone_disk_size-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_size', + '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': 'Disk size', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'disk_size', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_size', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.someone_disk_size-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Someone Disk size', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.someone_disk_size', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '294.229309082031', + }) +# --- +# name: test_sensors[sensor.someone_disk_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_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': 'Disk usage', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'disk_usage', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_usage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.someone_disk_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Someone Disk usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.someone_disk_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.56', + }) +# --- +# name: test_sensors[sensor.someone_disk_used-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_used', + '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': 'Disk used', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'disk_use', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_use', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.someone_disk_used-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Someone Disk used', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.someone_disk_used', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '142.865287780762', + }) +# --- +# name: test_sensors[sensor.someone_disk_used_by_photos-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_used_by_photos', + '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': 'Disk used by photos', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'usage_by_photos', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_usage_by_photos', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.someone_disk_used_by_photos-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Someone Disk used by photos', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.someone_disk_used_by_photos', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.5625927364454', + }) +# --- +# name: test_sensors[sensor.someone_disk_used_by_videos-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_disk_used_by_videos', + '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': 'Disk used by videos', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'usage_by_videos', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_usage_by_videos', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.someone_disk_used_by_videos-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Someone Disk used by videos', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.someone_disk_used_by_videos', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60.754158870317', + }) +# --- +# name: test_sensors[sensor.someone_photos_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_photos_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Photos count', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'photos_count', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_photos_count', + 'unit_of_measurement': 'photos', + }) +# --- +# name: test_sensors[sensor.someone_photos_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Someone Photos count', + 'state_class': , + 'unit_of_measurement': 'photos', + }), + 'context': , + 'entity_id': 'sensor.someone_photos_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27038', + }) +# --- +# name: test_sensors[sensor.someone_videos_count-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.someone_videos_count', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Videos count', + 'platform': 'immich', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'videos_count', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_videos_count', + 'unit_of_measurement': 'videos', + }) +# --- +# name: test_sensors[sensor.someone_videos_count-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Someone Videos count', + 'state_class': , + 'unit_of_measurement': 'videos', + }), + 'context': , + 'entity_id': 'sensor.someone_videos_count', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1836', + }) +# --- diff --git a/tests/components/immich/test_config_flow.py b/tests/components/immich/test_config_flow.py new file mode 100644 index 00000000000..e26cb4df5a1 --- /dev/null +++ b/tests/components/immich/test_config_flow.py @@ -0,0 +1,244 @@ +"""Test the Immich config flow.""" + +from unittest.mock import AsyncMock, Mock + +from aiohttp import ClientError +from aioimmich.exceptions import ImmichUnauthorizedError +import pytest + +from homeassistant.components.immich.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_USER_DATA + +from tests.common import MockConfigEntry + + +async def test_step_user( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_immich: Mock +) -> None: + """Test a user initiated config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_DATA, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "user" + assert result["data"] == MOCK_CONFIG_ENTRY_DATA + assert result["result"].unique_id == "e7ef5713-9dab-4bd4-b899-715b0ca4379e" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + ( + ImmichUnauthorizedError( + { + "message": "Invalid API key", + "error": "Unauthenticated", + "statusCode": 401, + "correlationId": "abcdefg", + } + ), + "invalid_auth", + ), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_step_user_error_handling( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_immich: Mock, + exception: Exception, + error: str, +) -> None: + """Test a user initiated config flow with errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + mock_immich.users.async_get_my_user.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_DATA, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": error} + + mock_immich.users.async_get_my_user.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_DATA, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_step_user_invalid_url( + hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_immich: Mock +) -> None: + """Test a user initiated config flow with errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {**MOCK_USER_DATA, CONF_URL: "hts://invalid"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_URL: "invalid_url"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_DATA, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_user_already_configured( + hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry +) -> None: + """Test starting a flow by user when already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_DATA, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + 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: "other_fake_api_key", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_API_KEY] == "other_fake_api_key" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + ( + ImmichUnauthorizedError( + { + "message": "Invalid API key", + "error": "Unauthenticated", + "statusCode": 401, + "correlationId": "abcdefg", + } + ), + "invalid_auth", + ), + (ClientError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_reauth_flow_error_handling( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reauthentication flow with errors.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_immich.users.async_get_my_user.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "other_fake_api_key", + }, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": error} + + mock_immich.users.async_get_my_user.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "other_fake_api_key", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_API_KEY] == "other_fake_api_key" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow_mismatch( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauthentication flow with mis-matching unique id.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_immich.users.async_get_my_user.return_value.user_id = "other_user_id" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "other_fake_api_key", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" diff --git a/tests/components/immich/test_sensor.py b/tests/components/immich/test_sensor.py new file mode 100644 index 00000000000..ceebba7b8be --- /dev/null +++ b/tests/components/immich/test_sensor.py @@ -0,0 +1,45 @@ +"""Test the Immich sensor platform.""" + +from unittest.mock import Mock, 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, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Immich sensor platform.""" + + with patch("homeassistant.components.immich.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_admin_sensors( + hass: HomeAssistant, + mock_non_admin_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the integration doesn't create admin sensors if not admin.""" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.mock_title_photos_count") is None + assert hass.states.get("sensor.mock_title_videos_count") is None + assert hass.states.get("sensor.mock_title_disk_used_by_photos") is None + assert hass.states.get("sensor.mock_title_disk_used_by_videos") is None From 4c10502b0e04088f41d409e2de6806b4f82cb8af Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sun, 18 May 2025 21:44:53 +0200 Subject: [PATCH 0573/1175] Update `denonavr` to `1.1.1` (#145155) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 3cf2e5b5bda..c5a1b9aeb63 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.1.0"], + "requirements": ["denonavr==1.1.1"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index 4038533b7cf..08341c0c9ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -776,7 +776,7 @@ deluge-client==1.10.2 demetriek==1.2.0 # homeassistant.components.denonavr -denonavr==1.1.0 +denonavr==1.1.1 # homeassistant.components.devialet devialet==1.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b80fc33107e..fba8a391b73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ deluge-client==1.10.2 demetriek==1.2.0 # homeassistant.components.denonavr -denonavr==1.1.0 +denonavr==1.1.1 # homeassistant.components.devialet devialet==1.5.7 From d9cfab4c8e4b28e8930ac20558bd692403c6a40e Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sun, 18 May 2025 15:45:11 -0400 Subject: [PATCH 0574/1175] Bump sense-energy to 0.13.8 (#145156) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index fc54fb50064..3e9d6c81881 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_push", "loggers": ["sense_energy"], "quality_scale": "internal", - "requirements": ["sense-energy==0.13.7"] + "requirements": ["sense-energy==0.13.8"] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 0a21dbf4cc3..33106f0fd1b 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "iot_class": "cloud_polling", "loggers": ["sense_energy"], - "requirements": ["sense-energy==0.13.7"] + "requirements": ["sense-energy==0.13.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 08341c0c9ba..b2742d896cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2713,7 +2713,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.7 +sense-energy==0.13.8 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fba8a391b73..c43316e64ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2196,7 +2196,7 @@ securetar==2025.2.1 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense-energy==0.13.7 +sense-energy==0.13.8 # homeassistant.components.sensirion_ble sensirion-ble==0.1.1 From 78ac8ba841290fb193b079a11213ed35b475afe7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 18 May 2025 22:14:22 +0200 Subject: [PATCH 0575/1175] Remove deprecated aux heat from Nexia (#145147) --- homeassistant/components/nexia/climate.py | 39 --------------------- homeassistant/components/nexia/strings.json | 13 ------- 2 files changed, 52 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index e9637a16ae0..52ff87e11c7 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -34,7 +34,6 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import VolDictType from .const import ( @@ -42,7 +41,6 @@ from .const import ( ATTR_DEHUMIDIFY_SETPOINT, ATTR_HUMIDIFY_SETPOINT, ATTR_RUN_MODE, - DOMAIN, ) from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatZoneEntity @@ -183,8 +181,6 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): self._attr_supported_features = NEXIA_SUPPORTED if self._has_humidify_support or self._has_dehumidify_support: self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY - if self._has_emergency_heat: - self._attr_supported_features |= ClimateEntityFeature.AUX_HEAT self._attr_preset_modes = zone.get_presets() self._attr_fan_modes = thermostat.get_fan_modes() self._attr_hvac_modes = HVAC_MODES @@ -387,11 +383,6 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): ) self._signal_zone_update() - @property - def is_aux_heat(self) -> bool: - """Emergency heat state.""" - return self._thermostat.is_emergency_heat_active() - @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" @@ -414,36 +405,6 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): await self._zone.set_preset(preset_mode) self._signal_zone_update() - async def async_turn_aux_heat_off(self) -> None: - """Turn Aux Heat off.""" - async_create_issue( - self.hass, - DOMAIN, - "migrate_aux_heat", - breaks_in_ha_version="2025.4.0", - is_fixable=True, - is_persistent=True, - translation_key="migrate_aux_heat", - severity=IssueSeverity.WARNING, - ) - await self._thermostat.set_emergency_heat(False) - self._signal_thermostat_update() - - async def async_turn_aux_heat_on(self) -> None: - """Turn Aux Heat on.""" - async_create_issue( - self.hass, - DOMAIN, - "migrate_aux_heat", - breaks_in_ha_version="2025.4.0", - is_fixable=True, - is_persistent=True, - translation_key="migrate_aux_heat", - severity=IssueSeverity.WARNING, - ) - await self._thermostat.set_emergency_heat(True) - self._signal_thermostat_update() - async def async_turn_off(self) -> None: """Turn off the zone.""" await self.async_set_hvac_mode(HVACMode.OFF) diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json index 6dbfe552e35..d8ec2112fe4 100644 --- a/homeassistant/components/nexia/strings.json +++ b/homeassistant/components/nexia/strings.json @@ -118,18 +118,5 @@ } } } - }, - "issues": { - "migrate_aux_heat": { - "title": "Migration of Nexia set_aux_heat action", - "fix_flow": { - "step": { - "confirm": { - "description": "The Nexia `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat.\n\nUpdate any automations to use the new Emergency heat switch entity. When this is done, select **Submit** to fix this issue.", - "title": "[%key:component::nexia::issues::migrate_aux_heat::title%]" - } - } - } - } } } From c1fcd8ea7f8b8ffbe7439dd7d63a718f1122ba23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20M=C3=BCller?= Date: Sun, 18 May 2025 22:26:02 +0200 Subject: [PATCH 0576/1175] Fix Nanoleaf light state propagation after change from home asisstant (#144291) * Fix Nanoleaf light state propagation after change from home asisstant * Add tests to check if nanoleaf light is triggering async_write_ha_state * Fix pylint for test case * Fix use coordinator.async_refresh instead of async_write_ha_state * Fix tests --------- Co-authored-by: Joostlek --- homeassistant/components/nanoleaf/light.py | 2 + tests/components/nanoleaf/__init__.py | 12 +++ tests/components/nanoleaf/conftest.py | 49 +++++++++++ .../nanoleaf/snapshots/test_light.ambr | 84 +++++++++++++++++++ tests/components/nanoleaf/test_light.py | 68 +++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 tests/components/nanoleaf/conftest.py create mode 100644 tests/components/nanoleaf/snapshots/test_light.ambr create mode 100644 tests/components/nanoleaf/test_light.py diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 6d42110d53e..214b63d6668 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -125,8 +125,10 @@ class NanoleafLight(NanoleafEntity, LightEntity): await self._nanoleaf.turn_on() if brightness: await self._nanoleaf.set_brightness(int(brightness / 2.55)) + await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" transition: float | None = kwargs.get(ATTR_TRANSITION) await self._nanoleaf.turn_off(None if transition is None else int(transition)) + await self.coordinator.async_refresh() diff --git a/tests/components/nanoleaf/__init__.py b/tests/components/nanoleaf/__init__.py index ee614fad173..0e6d571e320 100644 --- a/tests/components/nanoleaf/__init__.py +++ b/tests/components/nanoleaf/__init__.py @@ -1 +1,13 @@ """Tests for the Nanoleaf integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/nanoleaf/conftest.py b/tests/components/nanoleaf/conftest.py new file mode 100644 index 00000000000..5dae7727eec --- /dev/null +++ b/tests/components/nanoleaf/conftest.py @@ -0,0 +1,49 @@ +"""Common fixtures for Nanoleaf tests.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.nanoleaf import DOMAIN +from homeassistant.const import CONF_HOST, CONF_TOKEN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a Nanoleaf config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "10.0.0.10", + CONF_TOKEN: "1234567890abcdef", + }, + ) + + +@pytest.fixture +async def mock_nanoleaf() -> AsyncGenerator[AsyncMock]: + """Mock a Nanoleaf device.""" + with patch( + "homeassistant.components.nanoleaf.Nanoleaf", autospec=True + ) as mock_nanoleaf: + client = mock_nanoleaf.return_value + client.model = "NO_TOUCH" + client.host = "10.0.0.10" + client.serial_no = "ABCDEF123456" + client.color_temperature_max = 4500 + client.color_temperature_min = 1200 + client.is_on = False + client.brightness = 50 + client.color_temperature = 2700 + client.hue = 120 + client.saturation = 50 + client.color_mode = "hs" + client.effect = "Rainbow" + client.effects_list = ["Rainbow", "Sunset", "Nemo"] + client.firmware_version = "4.0.0" + client.name = "Nanoleaf" + client.manufacturer = "Nanoleaf" + yield client diff --git a/tests/components/nanoleaf/snapshots/test_light.ambr b/tests/components/nanoleaf/snapshots/test_light.ambr new file mode 100644 index 00000000000..277c24a7365 --- /dev/null +++ b/tests/components/nanoleaf/snapshots/test_light.ambr @@ -0,0 +1,84 @@ +# serializer version: 1 +# name: test_entities[light.nanoleaf-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'effect_list': list([ + 'Rainbow', + 'Sunset', + 'Nemo', + ]), + 'max_color_temp_kelvin': 4500, + 'max_mireds': 833, + 'min_color_temp_kelvin': 1200, + 'min_mireds': 222, + 'supported_color_modes': list([ + , + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.nanoleaf', + 'has_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': 'nanoleaf', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'light', + 'unique_id': 'ABCDEF123456', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[light.nanoleaf-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': None, + 'color_temp': None, + 'color_temp_kelvin': None, + 'effect': None, + 'effect_list': list([ + 'Rainbow', + 'Sunset', + 'Nemo', + ]), + 'friendly_name': 'Nanoleaf', + 'hs_color': None, + 'max_color_temp_kelvin': 4500, + 'max_mireds': 833, + 'min_color_temp_kelvin': 1200, + 'min_mireds': 222, + 'rgb_color': None, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.nanoleaf', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/nanoleaf/test_light.py b/tests/components/nanoleaf/test_light.py new file mode 100644 index 00000000000..bd852ea81e4 --- /dev/null +++ b/tests/components/nanoleaf/test_light.py @@ -0,0 +1,68 @@ +"""Tests for the Nanoleaf light platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.light import ATTR_EFFECT_LIST, DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_nanoleaf: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.nanoleaf.PLATFORMS", [Platform.LIGHT]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +async def test_turning_on_or_off_writes_state( + hass: HomeAssistant, + mock_nanoleaf: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, +) -> None: + """Test turning on or off the light writes the state.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("light.nanoleaf").attributes[ATTR_EFFECT_LIST] == [ + "Rainbow", + "Sunset", + "Nemo", + ] + + mock_nanoleaf.effects_list = ["Rainbow", "Sunset", "Nemo", "Something Else"] + + await hass.services.async_call( + LIGHT_DOMAIN, + service, + { + ATTR_ENTITY_ID: "light.nanoleaf", + }, + blocking=True, + ) + assert hass.states.get("light.nanoleaf").attributes[ATTR_EFFECT_LIST] == [ + "Rainbow", + "Sunset", + "Nemo", + "Something Else", + ] From 3ecde49dca2baeef4acebd2d11f83c2cf248db96 Mon Sep 17 00:00:00 2001 From: generically-named <85384565+generically-named@users.noreply.github.com> Date: Mon, 19 May 2025 06:03:27 +0930 Subject: [PATCH 0577/1175] Add energy/water forecast for Miele integration (#144822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add energy/water forecast & fix drying_step error Adding the energy forecast and water forecast entities that are present in the HACS version of this integration but absent in the HA Core implantation. Also fixed the state_drying_step sensor which wasn't handling casting of the API value to an int correctly due to the API sometimes giving a None value. * Fix formatting issues from previous commit * Fix missing translation_key line 202 * Remove icon entries * Update icons.json * Update strings.json * Update strings.json (correcting mixed up energy/water forecast names) * Update homeassistant/components/miele/strings.json Co-authored-by: Åke Strandberg * Update homeassistant/components/miele/strings.json Co-authored-by: Åke Strandberg * Fix tests --------- Co-authored-by: Åke Strandberg Co-authored-by: Joostlek --- homeassistant/components/miele/icons.json | 6 ++ homeassistant/components/miele/sensor.py | 40 ++++++++ homeassistant/components/miele/strings.json | 6 ++ .../miele/snapshots/test_sensor.ambr | 96 +++++++++++++++++++ 4 files changed, 148 insertions(+) diff --git a/homeassistant/components/miele/icons.json b/homeassistant/components/miele/icons.json index d38a2862e89..1806fe688d6 100644 --- a/homeassistant/components/miele/icons.json +++ b/homeassistant/components/miele/icons.json @@ -87,6 +87,12 @@ }, "remaining_time": { "default": "mdi:clock-end" + }, + "energy_forecast": { + "default": "mdi:lightning-bolt-outline" + }, + "water_forecast": { + "default": "mdi:water-outline" } }, "switch": { diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index d09f16ee9a0..d5085ae606f 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -16,6 +16,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( + PERCENTAGE, REVOLUTIONS_PER_MINUTE, EntityCategory, UnitOfEnergy, @@ -258,6 +259,27 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, ), ), + MieleSensorDefinition( + types=( + MieleAppliance.WASHING_MACHINE, + MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL, + MieleAppliance.TUMBLE_DRYER, + MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL, + MieleAppliance.DISHWASHER, + MieleAppliance.WASHER_DRYER, + ), + description=MieleSensorDescription( + key="energy_forecast", + translation_key="energy_forecast", + value_fn=( + lambda value: value.energy_forecast * 100 + if value.energy_forecast is not None + else None + ), + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ), MieleSensorDefinition( types=( MieleAppliance.WASHING_MACHINE, @@ -274,6 +296,24 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( entity_category=EntityCategory.DIAGNOSTIC, ), ), + MieleSensorDefinition( + types=( + MieleAppliance.WASHING_MACHINE, + MieleAppliance.DISHWASHER, + MieleAppliance.WASHER_DRYER, + ), + description=MieleSensorDescription( + key="water_forecast", + translation_key="water_forecast", + value_fn=( + lambda value: value.water_forecast * 100 + if value.water_forecast is not None + else None + ), + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ), MieleSensorDefinition( types=( MieleAppliance.WASHING_MACHINE, diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index d0d8e14cf10..2cbc4f2f5f4 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -910,6 +910,12 @@ }, "core_target_temperature": { "name": "Core target temperature" + }, + "energy_forecast": { + "name": "Energy forecast" + }, + "water_forecast": { + "name": "Water forecast" } }, "switch": { diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 40072a8303a..aadcdb1118d 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1159,6 +1159,54 @@ 'state': '0.0', }) # --- +# name: test_sensor_states[platforms0][sensor.washing_machine_energy_forecast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_energy_forecast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy forecast', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_forecast', + 'unique_id': 'Dummy_Appliance_3-energy_forecast', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[platforms0][sensor.washing_machine_energy_forecast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Energy forecast', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.washing_machine_energy_forecast', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- # name: test_sensor_states[platforms0][sensor.washing_machine_program-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1638,3 +1686,51 @@ 'state': '0.0', }) # --- +# name: test_sensor_states[platforms0][sensor.washing_machine_water_forecast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_water_forecast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water forecast', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_forecast', + 'unique_id': 'Dummy_Appliance_3-water_forecast', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states[platforms0][sensor.washing_machine_water_forecast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Water forecast', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.washing_machine_water_forecast', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- From 3d83c6299b1a4b4217fda7904e79e2398be0e517 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 18 May 2025 22:51:42 +0200 Subject: [PATCH 0578/1175] Enable RFDEBUG on RFLink "Enable debug logging" (#138571) * Enable RFDEBUG on "Enable debug logging" * fix checks * fix checks * one more lap * fix test * wait to init rflink In my dev env this is not needed * use hass.async_create_task(handle_logging_changed()) instead async_at_started(hass, handle_logging_changed) * revert unneeded async_block_till_done * Remove the startup management There's a race condition at startup that can't be managed nicely --- homeassistant/components/rflink/__init__.py | 24 ++++++++++++++++- tests/components/rflink/test_init.py | 29 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 85195fb1581..d83a242ac71 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -16,8 +16,16 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGGING_CHANGED, +) +from homeassistant.core import ( + CoreState, + Event, + HassJob, + HomeAssistant, + ServiceCall, + callback, ) -from homeassistant.core import CoreState, HassJob, HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -41,6 +49,7 @@ from .entity import RflinkCommand from .utils import identify_event_type _LOGGER = logging.getLogger(__name__) +LIB_LOGGER = logging.getLogger("rflink") CONF_IGNORE_DEVICES = "ignore_devices" CONF_RECONNECT_INTERVAL = "reconnect_interval" @@ -277,4 +286,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.async_create_task(connect(), eager_start=False) async_dispatcher_connect(hass, SIGNAL_EVENT, event_callback) + + async def handle_logging_changed(_: Event) -> None: + """Handle logging changed event.""" + if LIB_LOGGER.isEnabledFor(logging.DEBUG): + await RflinkCommand.send_command("rfdebug", "on") + _LOGGER.info("RFDEBUG enabled") + else: + await RflinkCommand.send_command("rfdebug", "off") + _LOGGER.info("RFDEBUG disabled") + + # Listen to EVENT_LOGGING_CHANGED to manage the RFDEBUG + hass.bus.async_listen(EVENT_LOGGING_CHANGED, handle_logging_changed) + return True diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 1caae302748..d702cd44718 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -1,5 +1,6 @@ """Common functions for RFLink component tests and generic platform tests.""" +import logging from unittest.mock import Mock import pytest @@ -21,6 +22,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, + EVENT_LOGGING_CHANGED, SERVICE_STOP_COVER, SERVICE_TURN_OFF, ) @@ -556,3 +558,30 @@ async def test_unique_id( temperature_entry = entity_registry.async_get("sensor.temperature_device") assert temperature_entry assert temperature_entry.unique_id == "my_temperature_device_unique_id" + + +async def test_enable_debug_logs( + hass: HomeAssistant, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that changing debug level enables RFDEBUG.""" + + domain = RFLINK_DOMAIN + config = {RFLINK_DOMAIN: {CONF_HOST: "10.10.0.1", CONF_PORT: 1234}} + + # setup mocking rflink module + _, mock_create, _, _ = await mock_rflink(hass, config, domain, monkeypatch) + + logging.getLogger("rflink").setLevel(logging.DEBUG) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + await hass.async_block_till_done() + + assert "RFDEBUG enabled" in caplog.text + assert "RFDEBUG disabled" not in caplog.text + + logging.getLogger("rflink").setLevel(logging.INFO) + hass.bus.async_fire(EVENT_LOGGING_CHANGED) + await hass.async_block_till_done() + + assert "RFDEBUG disabled" in caplog.text From 541b969d3b4df38ef18d93e5d48b5cbd8d6dca92 Mon Sep 17 00:00:00 2001 From: wuede Date: Sun, 18 May 2025 23:00:36 +0200 Subject: [PATCH 0579/1175] Netatmo: do not fail on schedule updates (#142933) * do not fail on schedule updates * add test to check that the store data remains unchanged --- homeassistant/components/netatmo/climate.py | 29 ++++++++++++--------- tests/components/netatmo/test_climate.py | 28 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 2e3d8c6bcb8..f8f89ffd06b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -248,19 +248,22 @@ class NetatmoThermostat(NetatmoRoomEntity, ClimateEntity): 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.home.entity_id].get( - data["schedule_id"] - ), - "name", - None, - ) - self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( - self._selected_schedule - ) - self.async_write_ha_state() - self.data_handler.async_force_update(self._signal_name) + if data["event_type"] == EVENT_TYPE_SCHEDULE: + # handle schedule change + if "schedule_id" in data: + self._selected_schedule = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( + data["schedule_id"] + ), + "name", + None, + ) + self._attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = ( + self._selected_schedule + ) + self.async_write_ha_state() + self.data_handler.async_force_update(self._signal_name) + # ignore other schedule events return home = data["home"] diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 18c811fd76b..45216e415a5 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -66,6 +66,34 @@ async def test_entity( ) +async def test_schedule_update_webhook_event( + hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock +) -> None: + """Test schedule update webhook event without schedule_id.""" + + with selected_platforms([Platform.CLIMATE]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.livingroom" + + # Save initial state + initial_state = hass.states.get(climate_entity_livingroom) + + # Create a schedule update event without a schedule_id (the event is sent when temperature sets of a schedule are changed) + response = { + "home_id": "91763b24c43d3e344f424e8b", + "event_type": "schedule", + "push_type": "home_event_changed", + } + await simulate_webhook(hass, webhook_id, response) + + # State should be unchanged + assert hass.states.get(climate_entity_livingroom) == initial_state + + async def test_webhook_event_handling_thermostats( hass: HomeAssistant, config_entry: MockConfigEntry, netatmo_auth: AsyncMock ) -> None: From ff5ed82de8b6c91292718f2f3df816c06f4ac9ed Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 18 May 2025 23:01:02 +0200 Subject: [PATCH 0580/1175] Add Kaiser Nienhaus virtual motionblinds integration (#145096) * Add Kaiser Nienhaus virtual motionblinds integration * fix typo --- homeassistant/components/kaiser_nienhaus/__init__.py | 1 + homeassistant/components/kaiser_nienhaus/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 homeassistant/components/kaiser_nienhaus/__init__.py create mode 100644 homeassistant/components/kaiser_nienhaus/manifest.json diff --git a/homeassistant/components/kaiser_nienhaus/__init__.py b/homeassistant/components/kaiser_nienhaus/__init__.py new file mode 100644 index 00000000000..0aef3a37342 --- /dev/null +++ b/homeassistant/components/kaiser_nienhaus/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Kaiser Nienhaus.""" diff --git a/homeassistant/components/kaiser_nienhaus/manifest.json b/homeassistant/components/kaiser_nienhaus/manifest.json new file mode 100644 index 00000000000..ec52e03acd4 --- /dev/null +++ b/homeassistant/components/kaiser_nienhaus/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "kaiser_nienhaus", + "name": "Kaiser Nienhaus", + "integration_type": "virtual", + "supported_by": "motion_blinds" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ccb67025091..66addc2f5b5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3187,6 +3187,11 @@ "config_flow": true, "iot_class": "local_polling" }, + "kaiser_nienhaus": { + "name": "Kaiser Nienhaus", + "integration_type": "virtual", + "supported_by": "motion_blinds" + }, "kaiterra": { "name": "Kaiterra", "integration_type": "hub", From 2ba2248f672fdfebf35ddcc41d1665929611780d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 18 May 2025 23:03:13 +0200 Subject: [PATCH 0581/1175] Remove deprecated aux heat from econet (#145149) --- homeassistant/components/econet/climate.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 56a98c8d630..69ca3a827ec 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -148,11 +148,6 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity): if target_temp_low or target_temp_high: self._econet.set_set_point(None, target_temp_high, target_temp_low) - @property - def is_aux_heat(self) -> bool: - """Return true if aux heater.""" - return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT - @property def hvac_mode(self) -> HVACMode: """Return hvac operation ie. heat, cool, mode. From 075a41c69aca45c438b9810f0ec5bba14ada0e49 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Sun, 18 May 2025 22:37:06 +0100 Subject: [PATCH 0582/1175] Fix album and artist returning "None" rather than None for Squeezebox media player. (#144971) * fix * snapshot update * cast type --- homeassistant/components/squeezebox/media_player.py | 10 +++++----- .../squeezebox/snapshots/test_media_player.ambr | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index c7c7b79fa89..873bedd13fb 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -6,7 +6,7 @@ from collections.abc import Callable from datetime import datetime import json import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from pysqueezebox import Server, async_discover import voluptuous as vol @@ -330,22 +330,22 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): @property def media_title(self) -> str | None: """Title of current playing media.""" - return str(self._player.title) + return cast(str | None, self._player.title) @property def media_channel(self) -> str | None: """Channel (e.g. webradio name) of current playing media.""" - return str(self._player.remote_title) + return cast(str | None, self._player.remote_title) @property def media_artist(self) -> str | None: """Artist of current playing media.""" - return str(self._player.artist) + return cast(str | None, self._player.artist) @property def media_album_name(self) -> str | None: """Album of current playing media.""" - return str(self._player.album) + return cast(str | None, self._player.album) @property def repeat(self) -> RepeatMode: diff --git a/tests/components/squeezebox/snapshots/test_media_player.ambr b/tests/components/squeezebox/snapshots/test_media_player.ambr index c0633035a84..7540a448882 100644 --- a/tests/components/squeezebox/snapshots/test_media_player.ambr +++ b/tests/components/squeezebox/snapshots/test_media_player.ambr @@ -78,12 +78,8 @@ 'group_members': list([ ]), 'is_volume_muted': True, - 'media_album_name': 'None', - 'media_artist': 'None', - 'media_channel': 'None', 'media_duration': 1, 'media_position': 1, - 'media_title': 'None', 'query_result': dict({ }), 'repeat': , From eb4d561b9636e869cb4c8216e0ca97e9dcc12b41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 May 2025 20:10:38 -0400 Subject: [PATCH 0583/1175] Bump grpcio to 1.72.0 and protobuf to 6.30.2 (#143633) --- homeassistant/package_constraints.txt | 8 ++++---- script/gen_requirements_all.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cd0a56c337..7cd961c5da5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,9 +88,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.71.0 -grpcio-status==1.71.0 -grpcio-reflection==1.71.0 +grpcio==1.72.0 +grpcio-status==1.72.0 +grpcio-reflection==1.72.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 @@ -145,7 +145,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==5.29.2 +protobuf==6.30.2 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f2e423536e8..87f7edaa892 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -117,9 +117,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.71.0 -grpcio-status==1.71.0 -grpcio-reflection==1.71.0 +grpcio==1.72.0 +grpcio-status==1.72.0 +grpcio-reflection==1.72.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 @@ -174,7 +174,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==5.29.2 +protobuf==6.30.2 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From ce71f6444cbbb6304b9205fee1333114e36ef8bb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 May 2025 08:40:22 +0200 Subject: [PATCH 0584/1175] Sort and simplify DeletedDeviceEntry (#145171) * Sort and simplify DeletedDeviceEntry * Fix sort * Fix sort --- homeassistant/helpers/device_registry.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a80e74e7eb2..161e1205d4f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -397,11 +397,11 @@ class DeletedDeviceEntry: config_entries: set[str] = attr.ib() config_entries_subentries: dict[str, set[str | None]] = attr.ib() connections: set[tuple[str, str]] = attr.ib() - identifiers: set[tuple[str, str]] = attr.ib() + created_at: datetime = attr.ib() id: str = attr.ib() + identifiers: set[tuple[str, str]] = attr.ib() + modified_at: datetime = attr.ib() orphaned_timestamp: float | None = attr.ib() - created_at: datetime = attr.ib(factory=utcnow) - modified_at: datetime = attr.ib(factory=utcnow) _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) def to_device_entry( @@ -440,8 +440,8 @@ class DeletedDeviceEntry: "created_at": self.created_at, "identifiers": list(self.identifiers), "id": self.id, - "orphaned_timestamp": self.orphaned_timestamp, "modified_at": self.modified_at, + "orphaned_timestamp": self.orphaned_timestamp, } ) ) @@ -1244,6 +1244,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): created_at=device.created_at, identifiers=device.identifiers, id=device.id, + modified_at=utcnow(), orphaned_timestamp=None, ) for other_device in list(self.devices.values()): From aa3cbf2473a50085945d5dbf9936ef40923d5fca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 09:10:01 +0200 Subject: [PATCH 0585/1175] Cleanup unused string in samsungtv (#145174) --- homeassistant/components/samsungtv/strings.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index 431c9bd3ec6..6251e65b2f8 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -79,9 +79,6 @@ }, "encrypted_mode_auth_failed": { "message": "Token and session ID are required in encrypted mode." - }, - "failed_to_determine_connection_method": { - "message": "Failed to determine connection method, make sure the device is on." } } } From 030681a443f55272778066719d06ef552c1eda46 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 19 May 2025 11:14:22 +0300 Subject: [PATCH 0586/1175] Jewish calendar: use const in action code (#145007) * Use const defines in code * Added exception raises * Revert "Added exception raises" This reverts commit e8849e586c83b45ecfd374986edb0d8c64b263e4. --- homeassistant/components/jewish_calendar/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/service.py b/homeassistant/components/jewish_calendar/service.py index 53d324d6efa..06d537b168d 100644 --- a/homeassistant/components/jewish_calendar/service.py +++ b/homeassistant/components/jewish_calendar/service.py @@ -55,16 +55,16 @@ def async_setup_services(hass: HomeAssistant) -> None: async def get_omer_count(call: ServiceCall) -> ServiceResponse: """Return the Omer blessing for a given date.""" - date = call.data.get("date", dt_util.now().date()) + date = call.data.get(ATTR_DATE, dt_util.now().date()) after_sunset = ( call.data[ATTR_AFTER_SUNSET] - if "date" in call.data + if ATTR_DATE in call.data else is_after_sunset(hass) ) hebrew_date = HebrewDate.from_gdate( date + datetime.timedelta(days=int(after_sunset)) ) - nusach = Nusach[call.data["nusach"].upper()] + nusach = Nusach[call.data[ATTR_NUSACH].upper()] set_language(call.data[CONF_LANGUAGE]) omer = Omer(date=hebrew_date, nusach=nusach) return { From fa5a7aea7ebeab3629a34c9c3685b621b854a1a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 10:14:37 +0200 Subject: [PATCH 0587/1175] Bump github/codeql-action from 3.28.17 to 3.28.18 (#145173) 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 7cc5ae34bee..818aa813208 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,11 @@ jobs: uses: actions/checkout@v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@v3.28.17 + uses: github/codeql-action/init@v3.28.18 with: languages: python - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.28.17 + uses: github/codeql-action/analyze@v3.28.18 with: category: "/language:python" From e46ca416976f9b5a2d58404980562eefba224651 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 May 2025 04:22:47 -0400 Subject: [PATCH 0588/1175] Bump aioesphomeapi to 31.1.0 (#145170) --- 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 833fa47337f..d5faacfd1b0 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==31.0.1", + "aioesphomeapi==31.1.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.15.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index b2742d896cb..37126e04d80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.0.1 +aioesphomeapi==31.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c43316e64ea..aab9206a6a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -229,7 +229,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.0.1 +aioesphomeapi==31.1.0 # homeassistant.components.flo aioflo==2021.11.0 From 5f2425f421df3a85c349e6df12fced0b62b9167c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 19 May 2025 10:24:08 +0200 Subject: [PATCH 0589/1175] Bump hass-nabucasa from 0.100.0 to 0.101.0 (#145172) --- 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 91423007b74..faee244a074 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -13,6 +13,6 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["acme", "hass_nabucasa", "snitun"], - "requirements": ["hass-nabucasa==0.100.0"], + "requirements": ["hass-nabucasa==0.101.0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7cd961c5da5..63622cb8d81 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ fnv-hash-fast==1.5.0 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 habluetooth==3.48.2 -hass-nabucasa==0.100.0 +hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250516.0 diff --git a/pyproject.toml b/pyproject.toml index bab4f92bc23..183ef236ef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "ha-ffmpeg==3.2.2", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.100.0", + "hass-nabucasa==0.101.0", # hassil is indirectly imported from onboarding via the import chain # onboarding->cloud->assist_pipeline->conversation->hassil. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its diff --git a/requirements.txt b/requirements.txt index a4ab40f2538..7d15999bb38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ ciso8601==2.3.2 cronsim==2.6 fnv-hash-fast==1.5.0 ha-ffmpeg==3.2.2 -hass-nabucasa==0.100.0 +hass-nabucasa==0.101.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 37126e04d80..4700667f63e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ habiticalib==0.3.7 habluetooth==3.48.2 # homeassistant.components.cloud -hass-nabucasa==0.100.0 +hass-nabucasa==0.101.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aab9206a6a7..f1ee3fe8dd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -963,7 +963,7 @@ habiticalib==0.3.7 habluetooth==3.48.2 # homeassistant.components.cloud -hass-nabucasa==0.100.0 +hass-nabucasa==0.101.0 # homeassistant.components.conversation hassil==2.2.3 From 2bb0843c309cbf01fb5821f8ede4d1ef2c2fad44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:27:07 +0200 Subject: [PATCH 0590/1175] Add ability to mark type hints as compulsory on specific functions (#139730) --- pylint/plugins/hass_enforce_type_hints.py | 44 ++++++++++++++++------- tests/pylint/test_enforce_type_hints.py | 4 +-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3e18aacaa93..9855f688622 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -50,6 +50,9 @@ class TypeHintMatch: kwargs_type: str | None = None """kwargs_type is for the special case `**kwargs`""" has_async_counterpart: bool = False + """`function_name` and `async_function_name` share arguments and return type""" + mandatory: bool = False + """bypass ignore_missing_annotations""" def need_to_check_function(self, node: nodes.FunctionDef) -> bool: """Confirm if function should be checked.""" @@ -184,6 +187,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { }, return_type="bool", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="async_setup_entry", @@ -192,6 +196,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_remove_entry", @@ -200,6 +205,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_unload_entry", @@ -208,6 +214,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_migrate_entry", @@ -216,6 +223,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_remove_config_entry_device", @@ -225,6 +233,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 2: "DeviceEntry", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_reset_platform", @@ -233,6 +242,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "str", }, return_type=None, + mandatory=True, ), ], "__any_platform__": [ @@ -246,6 +256,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="async_setup_entry", @@ -255,6 +266,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 2: "AddConfigEntryEntitiesCallback", }, return_type=None, + mandatory=True, ), ], "application_credentials": [ @@ -3195,8 +3207,11 @@ class HassTypeHintChecker(BaseChecker): self._class_matchers.reverse() - def _ignore_function( - self, node: nodes.FunctionDef, annotations: list[nodes.NodeNG | None] + def _ignore_function_match( + self, + node: nodes.FunctionDef, + annotations: list[nodes.NodeNG | None], + match: TypeHintMatch, ) -> bool: """Check if we can skip the function validation.""" return ( @@ -3204,6 +3219,8 @@ class HassTypeHintChecker(BaseChecker): not self._in_test_module # some modules have checks forced and self._module_platform not in _FORCE_ANNOTATION_PLATFORMS + # some matches have checks forced + and not match.mandatory # other modules are only checked ignore_missing_annotations and self.linter.config.ignore_missing_annotations and node.returns is None @@ -3246,7 +3263,7 @@ class HassTypeHintChecker(BaseChecker): continue annotations = _get_all_annotations(function_node) - if self._ignore_function(function_node, annotations): + if self._ignore_function_match(function_node, annotations, match): continue self._check_function(function_node, match, annotations) @@ -3255,8 +3272,6 @@ class HassTypeHintChecker(BaseChecker): def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Apply relevant type hint checks on a FunctionDef node.""" annotations = _get_all_annotations(node) - if self._ignore_function(node, annotations): - return # Check method or function matchers. if node.is_method(): @@ -3277,14 +3292,15 @@ class HassTypeHintChecker(BaseChecker): matchers = self._function_matchers # Check that common arguments are correctly typed. - for arg_name, expected_type in _COMMON_ARGUMENTS.items(): - arg_node, annotation = _get_named_annotation(node, arg_name) - if arg_node and not _is_valid_type(expected_type, annotation): - self.add_message( - "hass-argument-type", - node=arg_node, - args=(arg_name, expected_type, node.name), - ) + if not self.linter.config.ignore_missing_annotations: + for arg_name, expected_type in _COMMON_ARGUMENTS.items(): + arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and not _is_valid_type(expected_type, annotation): + self.add_message( + "hass-argument-type", + node=arg_node, + args=(arg_name, expected_type, node.name), + ) for match in matchers: if not match.need_to_check_function(node): @@ -3299,6 +3315,8 @@ class HassTypeHintChecker(BaseChecker): match: TypeHintMatch, annotations: list[nodes.NodeNG | None], ) -> None: + if self._ignore_function_match(node, annotations, match): + return # Check that all positional arguments are correctly annotated. if match.arg_types: for key, expected_type in match.arg_types.items(): diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index c9748cc61f8..9179a545256 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -99,7 +99,7 @@ def test_regex_a_or_b( "code", [ """ - async def setup( #@ + async def async_turn_on( #@ arg1, arg2 ): pass @@ -115,7 +115,7 @@ def test_ignore_no_annotations( func_node = astroid.extract_node( code, - "homeassistant.components.pylint_test", + "homeassistant.components.pylint_test.light", ) type_hint_checker.visit_module(func_node.parent) From 9ff9d9230ef015d5087e549c796641a9e1604431 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 19 May 2025 10:40:03 +0200 Subject: [PATCH 0591/1175] Fix test results parsing error (#145077) --- .github/workflows/ci.yaml | 32 ++++++++++++++++++++++++---- tests/components/backup/test_util.py | 5 +++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 53de33b99e4..4a202a0c9d5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -944,7 +944,8 @@ jobs: bluez \ ffmpeg \ libturbojpeg \ - libgammu-dev + libgammu-dev \ + libxml2-utils - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} @@ -1020,6 +1021,11 @@ jobs: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true + - name: Beautify test results + # For easier identification of parsing errors + run: | + xmllint --format "junit.xml" > "junit.xml-tmp" + mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() uses: actions/upload-artifact@v4.6.2 @@ -1070,7 +1076,8 @@ jobs: bluez \ ffmpeg \ libturbojpeg \ - libmariadb-dev-compat + libmariadb-dev-compat \ + libxml2-utils - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} @@ -1154,6 +1161,11 @@ jobs: steps.pytest-partial.outputs.mariadb }} path: coverage.xml overwrite: true + - name: Beautify test results + # For easier identification of parsing errors + run: | + xmllint --format "junit.xml" > "junit.xml-tmp" + mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() uses: actions/upload-artifact@v4.6.2 @@ -1202,7 +1214,8 @@ jobs: sudo apt-get -y install \ bluez \ ffmpeg \ - libturbojpeg + libturbojpeg \ + libxml2-utils sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo apt-get -y install \ postgresql-server-dev-14 @@ -1290,6 +1303,11 @@ jobs: steps.pytest-partial.outputs.postgresql }} path: coverage.xml overwrite: true + - name: Beautify test results + # For easier identification of parsing errors + run: | + xmllint --format "junit.xml" > "junit.xml-tmp" + mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() uses: actions/upload-artifact@v4.6.2 @@ -1357,7 +1375,8 @@ jobs: bluez \ ffmpeg \ libturbojpeg \ - libgammu-dev + libgammu-dev \ + libxml2-utils - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} @@ -1436,6 +1455,11 @@ jobs: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true + - name: Beautify test results + # For easier identification of parsing errors + run: | + xmllint --format "junit.xml" > "junit.xml-tmp" + mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() uses: actions/upload-artifact@v4.6.2 diff --git a/tests/components/backup/test_util.py b/tests/components/backup/test_util.py index 229e25c312d..af37a3b88a6 100644 --- a/tests/components/backup/test_util.py +++ b/tests/components/backup/test_util.py @@ -112,6 +112,11 @@ from tests.common import get_fixture_path ), ), ], + ids=[ + "no addons and no metadata", + "with addons and metadata", + "only metadata", + ], ) def test_read_backup(backup_json_content: bytes, expected_backup: AgentBackup) -> None: """Test reading a backup.""" From a3aae6822908c2d0815a448e78f72ba4ab6a7096 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 19 May 2025 10:41:22 +0200 Subject: [PATCH 0592/1175] Add athmospheric pressure capability to SmartThings (#145103) --- .../components/smartthings/sensor.py | 11 + tests/components/smartthings/conftest.py | 1 + .../fixtures/device_status/lumi.json | 56 +++++ .../smartthings/fixtures/devices/lumi.json | 75 +++++++ .../smartthings/snapshots/test_init.ambr | 33 +++ .../smartthings/snapshots/test_sensor.ambr | 208 ++++++++++++++++++ 6 files changed, 384 insertions(+) create mode 100644 tests/components/smartthings/fixtures/device_status/lumi.json create mode 100644 tests/components/smartthings/fixtures/devices/lumi.json diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index e5fe6ef1fd6..6c8c78b4d32 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -26,6 +26,7 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfMass, UnitOfPower, + UnitOfPressure, UnitOfTemperature, UnitOfVolume, ) @@ -200,6 +201,15 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, + Capability.ATMOSPHERIC_PRESSURE_MEASUREMENT: { + Attribute.ATMOSPHERIC_PRESSURE: [ + SmartThingsSensorEntityDescription( + key=Attribute.ATMOSPHERIC_PRESSURE, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ) + ] + }, Capability.AUDIO_VOLUME: { Attribute.VOLUME: [ SmartThingsSensorEntityDescription( @@ -1071,6 +1081,7 @@ UNITS = { "lux": LIGHT_LUX, "mG": None, "μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + "kPa": UnitOfPressure.KPA, } diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 7a2945d4c02..ab6c6031d5e 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -160,6 +160,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "aux_ac", "hw_q80r_soundbar", "gas_meter", + "lumi", ] ) def device_fixture( diff --git a/tests/components/smartthings/fixtures/device_status/lumi.json b/tests/components/smartthings/fixtures/device_status/lumi.json new file mode 100644 index 00000000000..dc01671f4d9 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/lumi.json @@ -0,0 +1,56 @@ +{ + "components": { + "main": { + "configuration": {}, + "relativeHumidityMeasurement": { + "humidity": { + "value": 27.24, + "unit": "%", + "timestamp": "2025-05-11T23:31:11.979Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": { + "minimum": -58.0, + "maximum": 482.0 + }, + "unit": "F", + "timestamp": "2025-05-07T14:34:47.868Z" + }, + "temperature": { + "value": 76.0, + "unit": "F", + "timestamp": "2025-05-11T23:31:11.904Z" + } + }, + "atmosphericPressureMeasurement": { + "atmosphericPressure": { + "value": 100, + "unit": "kPa", + "timestamp": "2025-05-11T23:31:11.979Z" + } + }, + "battery": { + "quantity": { + "value": null + }, + "battery": { + "value": 100, + "unit": "%", + "timestamp": "2025-05-11T23:11:16.463Z" + }, + "type": { + "value": null + } + }, + "legendabsolute60149.atmosPressure": { + "atmosPressure": { + "value": 1004, + "unit": "mBar", + "timestamp": "2025-05-11T23:31:11.979Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/lumi.json b/tests/components/smartthings/fixtures/devices/lumi.json new file mode 100644 index 00000000000..2a5b90adfa1 --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/lumi.json @@ -0,0 +1,75 @@ +{ + "items": [ + { + "deviceId": "692ea4e9-2022-4ed8-8a57-1b884a59cc38", + "name": "temp-humid-press-therm-battery-05", + "label": "Outdoor Temp", + "manufacturerName": "SmartThingsCommunity", + "presentationId": "cea6ca21-a702-3c43-8fe5-a7872c7a963f", + "deviceManufacturerCode": "LUMI", + "locationId": "96fe7a00-c7f6-440a-940e-77aa81a9af4b", + "ownerId": "eabfbf0b-ba3f-40f5-8dcb-8aaba788f8e3", + "roomId": "1eca2d6d-d15d-4f0e-9e32-8709acb9b3fe", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "atmosphericPressureMeasurement", + "version": 1 + }, + { + "id": "legendabsolute60149.atmosPressure", + "version": 1 + }, + { + "id": "relativeHumidityMeasurement", + "version": 1 + }, + { + "id": "configuration", + "version": 1 + }, + { + "id": "battery", + "version": 1 + } + ], + "categories": [ + { + "name": "Thermostat", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2024-06-12T21:27:55.959Z", + "parentDeviceId": "61f28b8c-b975-415a-9197-fbc4e441e77a", + "profile": { + "id": "fa7886ec-6139-3357-8f4a-07a66491c173" + }, + "zigbee": { + "eui": "00158D000967924A", + "networkId": "4B01", + "driverId": "c09c02d7-d05d-4bf4-831b-207a1adeae2f", + "executingLocally": true, + "hubId": "61f28b8c-b975-415a-9197-fbc4e441e77a", + "provisioningState": "NONFUNCTIONAL", + "fingerprintType": "ZIGBEE_MANUFACTURER", + "fingerprintId": "lumi.weather" + }, + "type": "ZIGBEE", + "restrictionTier": 0, + "allowed": null, + "executionContext": "LOCAL", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index e96615f3120..58b89099b11 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1685,6 +1685,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[lumi] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '692ea4e9-2022-4ed8-8a57-1b884a59cc38', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'model_id': None, + 'name': 'Outdoor Temp', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_devices[multipurpose_sensor] DeviceRegistryEntrySnapshot({ 'area_id': 'theater', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 26805a83799..2884ded50af 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -10877,6 +10877,214 @@ 'state': '37', }) # --- +# name: test_all_entities[lumi][sensor.outdoor_temp_atmospheric_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.outdoor_temp_atmospheric_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': 'Atmospheric pressure', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_atmosphericPressureMeasurement_atmosphericPressure_atmosphericPressure', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_atmospheric_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'atmospheric_pressure', + 'friendly_name': 'Outdoor Temp Atmospheric pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.outdoor_temp_atmospheric_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_battery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.outdoor_temp_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_battery_battery_battery', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Outdoor Temp Battery', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.outdoor_temp_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.outdoor_temp_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_relativeHumidityMeasurement_humidity_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Outdoor Temp Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.outdoor_temp_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '27.24', + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.outdoor_temp_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[lumi][sensor.outdoor_temp_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Outdoor Temp Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.outdoor_temp_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.4', + }) +# --- # name: test_all_entities[multipurpose_sensor][sensor.deck_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 177afea5ad383bf345085029dc5ad97ff8cc4f3a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:57:22 +0200 Subject: [PATCH 0593/1175] Use runtime_data in huisbaasje (#144953) --- .../components/huisbaasje/__init__.py | 19 ++++++------------- homeassistant/components/huisbaasje/const.py | 2 -- .../components/huisbaasje/coordinator.py | 6 ++++-- homeassistant/components/huisbaasje/sensor.py | 10 +++------- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index e2414566fcb..7eca8141dc3 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -4,19 +4,18 @@ import logging from energyflip import EnergyFlip, EnergyFlipException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from .const import DATA_COORDINATOR, DOMAIN, FETCH_TIMEOUT, SOURCE_TYPES -from .coordinator import EnergyFlipUpdateCoordinator +from .const import FETCH_TIMEOUT, SOURCE_TYPES +from .coordinator import EnergyFlipConfigEntry, EnergyFlipUpdateCoordinator PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: EnergyFlipConfigEntry) -> bool: """Set up EnergyFlip from a config entry.""" # Create the EnergyFlip client energyflip = EnergyFlip( @@ -39,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Load the client in the data of home assistant - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} + entry.runtime_data = coordinator # Offload the loading of entities to the platform await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -47,13 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: EnergyFlipConfigEntry) -> bool: """Unload a config entry.""" # Forward the unloading of the entry to the platform - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - # If successful, unload the EnergyFlip client - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 2738289343f..a2dc39cb565 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -9,8 +9,6 @@ from energyflip.const import ( SOURCE_TYPE_GAS, ) -DATA_COORDINATOR = "coordinator" - DOMAIN = "huisbaasje" """Interval in seconds between polls to EnergyFlip.""" diff --git a/homeassistant/components/huisbaasje/coordinator.py b/homeassistant/components/huisbaasje/coordinator.py index 9467e1232c2..529f7916bc6 100644 --- a/homeassistant/components/huisbaasje/coordinator.py +++ b/homeassistant/components/huisbaasje/coordinator.py @@ -27,16 +27,18 @@ PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +type EnergyFlipConfigEntry = ConfigEntry[EnergyFlipUpdateCoordinator] + class EnergyFlipUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): """EnergyFlip data update coordinator.""" - config_entry: ConfigEntry + config_entry: EnergyFlipConfigEntry def __init__( self, hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnergyFlipConfigEntry, energyflip: EnergyFlip, ) -> None: """Initialize the Huisbaasje data coordinator.""" diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 9c471ff64ec..d6049e58550 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -20,7 +20,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ID, UnitOfEnergy, @@ -33,7 +32,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_RATE, SENSOR_TYPE_THIS_DAY, @@ -41,7 +39,7 @@ from .const import ( SENSOR_TYPE_THIS_WEEK, SENSOR_TYPE_THIS_YEAR, ) -from .coordinator import EnergyFlipUpdateCoordinator +from .coordinator import EnergyFlipConfigEntry, EnergyFlipUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -215,13 +213,11 @@ SENSORS_INFO = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: EnergyFlipConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator: EnergyFlipUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][ - DATA_COORDINATOR - ] + coordinator = config_entry.runtime_data user_id = config_entry.data[CONF_ID] async_add_entities( From f50afae1c3d6a22dc816a7534da1e1be62dc0ef2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:58:01 +0200 Subject: [PATCH 0594/1175] Use runtime_data in hvv_departures (#144951) --- .../components/hvv_departures/__init__.py | 11 ++++------- .../components/hvv_departures/binary_sensor.py | 6 +++--- .../components/hvv_departures/config_flow.py | 15 ++++++--------- homeassistant/components/hvv_departures/hub.py | 4 ++++ homeassistant/components/hvv_departures/sensor.py | 6 +++--- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 1104359111c..cfe76591688 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,17 +1,15 @@ """The HVV integration.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DOMAIN -from .hub import GTIHub +from .hub import GTIHub, HVVConfigEntry PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: HVVConfigEntry) -> bool: """Set up HVV from a config entry.""" hub = GTIHub( @@ -21,14 +19,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: aiohttp_client.async_get_clientsession(hass), ) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = hub + entry.runtime_data = hub await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: HVVConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index 622a8436e04..18598dd4c94 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -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 AddConfigEntryEntitiesCallback @@ -25,17 +24,18 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER +from .hub import HVVConfigEntry _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: HVVConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the binary_sensor platform.""" - hub = hass.data[DOMAIN][entry.entry_id] + hub = entry.runtime_data station_name = entry.data[CONF_STATION]["name"] station = entry.data[CONF_STATION] diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py index d76ccef7cab..63d457bf302 100644 --- a/homeassistant/components/hvv_departures/config_flow.py +++ b/homeassistant/components/hvv_departures/config_flow.py @@ -9,18 +9,13 @@ from pygti.auth import GTI_DEFAULT_HOST from pygti.exceptions import CannotConnect, InvalidAuth import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import CONF_FILTER, CONF_REAL_TIME, CONF_STATION, DOMAIN -from .hub import GTIHub +from .hub import GTIHub, HVVConfigEntry _LOGGER = logging.getLogger(__name__) @@ -137,7 +132,7 @@ class HVVDeparturesConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: HVVConfigEntry, ) -> OptionsFlowHandler: """Get options flow.""" return OptionsFlowHandler() @@ -146,6 +141,8 @@ class HVVDeparturesConfigFlow(ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(OptionsFlow): """Options flow handler.""" + config_entry: HVVConfigEntry + def __init__(self) -> None: """Initialize HVV Departures options flow.""" self.departure_filters: dict[str, Any] = {} @@ -157,7 +154,7 @@ class OptionsFlowHandler(OptionsFlow): errors = {} if not self.departure_filters: departure_list = {} - hub: GTIHub = self.hass.data[DOMAIN][self.config_entry.entry_id] + hub = self.config_entry.runtime_data try: departure_list = await hub.gti.departureList( diff --git a/homeassistant/components/hvv_departures/hub.py b/homeassistant/components/hvv_departures/hub.py index 7cffbed345c..31523b72ba1 100644 --- a/homeassistant/components/hvv_departures/hub.py +++ b/homeassistant/components/hvv_departures/hub.py @@ -2,6 +2,10 @@ from pygti.gti import GTI, Auth +from homeassistant.config_entries import ConfigEntry + +type HVVConfigEntry = ConfigEntry[GTIHub] + class GTIHub: """GTI Hub.""" diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 667893db8f2..1b10451f22d 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -8,7 +8,6 @@ from aiohttp import ClientConnectorError from pygti.exceptions import InvalidAuth from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ID, CONF_OFFSET from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client @@ -18,6 +17,7 @@ from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, utcnow from .const import ATTRIBUTION, CONF_REAL_TIME, CONF_STATION, DOMAIN, MANUFACTURER +from .hub import HVVConfigEntry MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MAX_LIST = 20 @@ -41,11 +41,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: HVVConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensor platform.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub = config_entry.runtime_data session = aiohttp_client.async_get_clientsession(hass) From da6c6c5201446abfc35ce0150d595525c42ca21d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:58:34 +0200 Subject: [PATCH 0595/1175] Use runtime_data in ialarm (#145178) --- homeassistant/components/ialarm/__init__.py | 19 +++++-------------- .../components/ialarm/alarm_control_panel.py | 12 ++++-------- homeassistant/components/ialarm/const.py | 2 -- .../components/ialarm/coordinator.py | 10 ++++++++-- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 2484a46f906..1604b37b967 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -6,18 +6,16 @@ import asyncio from pyialarm import IAlarm -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DATA_COORDINATOR, DOMAIN -from .coordinator import IAlarmDataUpdateCoordinator +from .coordinator import IAlarmConfigEntry, IAlarmDataUpdateCoordinator PLATFORMS = [Platform.ALARM_CONTROL_PANEL] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IAlarmConfigEntry) -> bool: """Set up iAlarm config.""" host: str = entry.data[CONF_HOST] port: int = entry.data[CONF_PORT] @@ -32,20 +30,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = IAlarmDataUpdateCoordinator(hass, entry, ialarm, mac) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - - hass.data[DOMAIN][entry.entry_id] = { - DATA_COORDINATOR: coordinator, - } + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IAlarmConfigEntry) -> bool: """Unload iAlarm config.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index e203f892c35..b2de9b3fefc 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -7,26 +7,22 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, AlarmControlPanelState, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DOMAIN -from .coordinator import IAlarmDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import IAlarmConfigEntry, IAlarmDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IAlarmConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up a iAlarm alarm control panel based on a config entry.""" - coordinator: IAlarmDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - async_add_entities([IAlarmPanel(coordinator)], False) + async_add_entities([IAlarmPanel(entry.runtime_data)], False) class IAlarmPanel( diff --git a/homeassistant/components/ialarm/const.py b/homeassistant/components/ialarm/const.py index 1b8074c34f0..01ce47e002a 100644 --- a/homeassistant/components/ialarm/const.py +++ b/homeassistant/components/ialarm/const.py @@ -4,8 +4,6 @@ from pyialarm import IAlarm from homeassistant.components.alarm_control_panel import AlarmControlPanelState -DATA_COORDINATOR = "ialarm" - DEFAULT_PORT = 18034 DOMAIN = "ialarm" diff --git a/homeassistant/components/ialarm/coordinator.py b/homeassistant/components/ialarm/coordinator.py index 61e87c36796..546e0b6b714 100644 --- a/homeassistant/components/ialarm/coordinator.py +++ b/homeassistant/components/ialarm/coordinator.py @@ -19,14 +19,20 @@ from .const import DOMAIN, IALARM_TO_HASS _LOGGER = logging.getLogger(__name__) +type IAlarmConfigEntry = ConfigEntry[IAlarmDataUpdateCoordinator] + class IAlarmDataUpdateCoordinator(DataUpdateCoordinator[None]): """Class to manage fetching iAlarm data.""" - config_entry: ConfigEntry + config_entry: IAlarmConfigEntry def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, ialarm: IAlarm, mac: str + self, + hass: HomeAssistant, + config_entry: IAlarmConfigEntry, + ialarm: IAlarm, + mac: str, ) -> None: """Initialize global iAlarm data updater.""" self.ialarm = ialarm From bd190b9b4cc7806f791542861b0525d7bea5c71c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:59:06 +0200 Subject: [PATCH 0596/1175] Use runtime_data in icloud (#145179) --- homeassistant/components/icloud/__init__.py | 17 +++++------------ homeassistant/components/icloud/account.py | 4 +++- .../components/icloud/device_tracker.py | 7 +++---- homeassistant/components/icloud/sensor.py | 7 +++---- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index e3c50cded16..13551ebece5 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -4,17 +4,15 @@ from __future__ import annotations from typing import Any -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.storage import Store -from .account import IcloudAccount +from .account import IcloudAccount, IcloudConfigEntry from .const import ( CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, CONF_WITH_FAMILY, - DOMAIN, PLATFORMS, STORAGE_KEY, STORAGE_VERSION, @@ -22,11 +20,9 @@ from .const import ( from .services import register_services -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool: """Set up an iCloud account from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] with_family = entry.data[CONF_WITH_FAMILY] @@ -51,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.async_add_executor_job(account.setup) - hass.data[DOMAIN][entry.unique_id] = account + entry.runtime_data = account await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -60,9 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.data[CONF_USERNAME]) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index e16d973277c..3006193a1ff 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -58,6 +58,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +type IcloudConfigEntry = ConfigEntry[IcloudAccount] + class IcloudAccount: """Representation of an iCloud account.""" @@ -71,7 +73,7 @@ class IcloudAccount: with_family: bool, max_interval: int, gps_accuracy_threshold: int, - config_entry: ConfigEntry, + config_entry: IcloudConfigEntry, ) -> None: """Initialize an iCloud account.""" self.hass = hass diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index ca194143852..e546d3034ae 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -5,13 +5,12 @@ from __future__ import annotations from typing import Any from homeassistant.components.device_tracker import TrackerEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .account import IcloudAccount, IcloudDevice +from .account import IcloudAccount, IcloudConfigEntry, IcloudDevice from .const import ( DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_LATITUDE, @@ -22,11 +21,11 @@ from .const import ( async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IcloudConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up device tracker for iCloud component.""" - account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] + account = entry.runtime_data tracked = set[str]() @callback diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 533605b8c7b..11690a0da59 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -5,7 +5,6 @@ from __future__ import annotations from typing import Any from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo @@ -13,17 +12,17 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level -from .account import IcloudAccount, IcloudDevice +from .account import IcloudAccount, IcloudConfigEntry, IcloudDevice from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: IcloudConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up device tracker for iCloud component.""" - account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] + account = entry.runtime_data tracked = set[str]() @callback From a34bce6202fe67da3558ef3a46942dbf73f1fa29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 10:59:46 +0200 Subject: [PATCH 0597/1175] Fix runtime_data in iqvia (#145181) --- homeassistant/components/iqvia/entity.py | 8 ++++---- homeassistant/components/iqvia/sensor.py | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iqvia/entity.py b/homeassistant/components/iqvia/entity.py index 1964a7cb039..04e92ef9c4d 100644 --- a/homeassistant/components/iqvia/entity.py +++ b/homeassistant/components/iqvia/entity.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_ZIP_CODE, DOMAIN, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK +from .const import CONF_ZIP_CODE, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK from .coordinator import IqviaConfigEntry, IqviaUpdateCoordinator @@ -44,9 +44,9 @@ class IQVIAEntity(CoordinatorEntity[IqviaUpdateCoordinator]): if self.entity_description.key == TYPE_ALLERGY_FORECAST: self.async_on_remove( - self.hass.data[DOMAIN][self._entry.entry_id][ - TYPE_ALLERGY_OUTLOOK - ].async_add_listener(self._handle_coordinator_update) + self._entry.runtime_data[TYPE_ALLERGY_OUTLOOK].async_add_listener( + self._handle_coordinator_update + ) ) self.update_from_latest_data() diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index c0401b27368..8b838d35ea1 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -17,7 +17,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( - DOMAIN, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, @@ -145,7 +144,7 @@ async def async_setup_entry( sensors.extend( [ IndexSensor( - hass.data[DOMAIN][entry.entry_id][ + entry.runtime_data[ API_CATEGORY_MAPPING.get(description.key, description.key) ], entry, @@ -207,9 +206,7 @@ class ForecastSensor(IQVIAEntity, SensorEntity): ) if self.entity_description.key == TYPE_ALLERGY_FORECAST: - outlook_coordinator = self.hass.data[DOMAIN][self._entry.entry_id][ - TYPE_ALLERGY_OUTLOOK - ] + outlook_coordinator = self._entry.runtime_data[TYPE_ALLERGY_OUTLOOK] if not outlook_coordinator.last_update_success: return From 717b84bab9a04323ec65ffc5b0d0d23fe35c1027 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 19 May 2025 17:01:30 +0800 Subject: [PATCH 0598/1175] Add battery entity for LockV2 in yolink (#145169) Add battery entity for LockV2 --- homeassistant/components/yolink/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 6572566f8ee..bc32d0eea83 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -12,6 +12,7 @@ from yolink.const import ( ATTR_DEVICE_FINGER, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_LOCK, + ATTR_DEVICE_LOCK_V2, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_MULTI_OUTLET, @@ -98,6 +99,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_LOCK, + ATTR_DEVICE_LOCK_V2, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_GARAGE_DOOR_CONTROLLER, @@ -114,6 +116,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_LOCK, + ATTR_DEVICE_LOCK_V2, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_WATER_DEPTH_SENSOR, From f27b2c4df1d7b7204fe80ad090afd41d582f5e3e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 May 2025 11:06:16 +0200 Subject: [PATCH 0599/1175] Improve device registry restore tests (#145186) --- tests/helpers/test_device_registry.py | 459 ++++++++++++++++++++------ 1 file changed, 366 insertions(+), 93 deletions(-) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 29edfb3fea7..45144627028 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -6,7 +6,7 @@ from datetime import datetime from functools import partial import time from typing import Any -from unittest.mock import patch +from unittest.mock import ANY, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -34,6 +34,32 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: return entry +@pytest.fixture +def mock_config_entry_with_subentries(hass: HomeAssistant) -> MockConfigEntry: + """Create a mock config entry and add it to hass.""" + entry = MockConfigEntry( + title=None, + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ), + ) + entry.add_to_hass(hass) + return entry + + async def test_get_or_create_returns_same_entry( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -3173,19 +3199,41 @@ async def test_cleanup_entity_registry_change( assert len(mock_call.mock_calls) == 2 +@pytest.mark.usefixtures("freezer") async def test_restore_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mock_config_entry: MockConfigEntry, + mock_config_entry_with_subentries: MockConfigEntry, ) -> None: """Make sure device id is stable.""" + entry_id = mock_config_entry_with_subentries.entry_id + subentry_id = "mock-subentry-id-1-1" update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) entry = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, + config_entry_id=entry_id, + config_subentry_id=subentry_id, + configuration_url="http://config_url_orig.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_orig", identifiers={("bridgeid", "0123")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_orig", + model="model_orig", + model_id="model_id_orig", + name="name_orig", + serial_number="serial_no_orig", + suggested_area="suggested_area_orig", + sw_version="version_orig", + via_device="via_device_id_orig", + ) + + # Apply user customizations + device_registry.async_update_device( + entry.id, + area_id="12345A", + disabled_by=dr.DeviceEntryDisabler.USER, + labels={"label1", "label2"}, + name_by_user="Test Friendly Name", ) assert len(device_registry.devices) == 1 @@ -3196,19 +3244,79 @@ async def test_restore_device( assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 1 + # This will create a new device entry2 = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, + config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, identifiers={("bridgeid", "4567")}, manufacturer="manufacturer", model="model", ) - entry3 = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, + assert entry2 == dr.DeviceEntry( + area_id=None, + config_entries={entry_id}, + config_entries_subentries={entry_id: {None}}, + configuration_url=None, + connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:cd:ef:12")}, + created_at=utcnow(), + disabled_by=None, + entry_type=None, + hw_version=None, + id=ANY, + identifiers={("bridgeid", "4567")}, + labels={}, manufacturer="manufacturer", model="model", + model_id=None, + modified_at=utcnow(), + name_by_user=None, + name=None, + primary_config_entry=entry_id, + serial_number=None, + suggested_area=None, + sw_version=None, + ) + # This will restore the original device + entry3 = device_registry.async_get_or_create( + config_entry_id=entry_id, + config_subentry_id=subentry_id, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=None, + hw_version="hw_version_new", + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + name="name_new", + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", + via_device="via_device_id_new", + ) + assert entry3 == dr.DeviceEntry( + area_id="suggested_area_new", + config_entries={entry_id}, + config_entries_subentries={entry_id: {subentry_id}}, + configuration_url="http://config_url_new.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=None, + entry_type=None, + hw_version="hw_version_new", + id=entry.id, + identifiers={("bridgeid", "0123")}, + labels={}, + manufacturer="manufacturer_new", + model="model_new", + model_id="model_id_new", + modified_at=utcnow(), + name_by_user=None, + name="name_new", + primary_config_entry=entry_id, + serial_number="serial_no_new", + suggested_area="suggested_area_new", + sw_version="version_new", ) assert entry.id == entry3.id @@ -3222,129 +3330,186 @@ async def test_restore_device( await hass.async_block_till_done() - assert len(update_events) == 4 + assert len(update_events) == 5 assert update_events[0].data == { "action": "create", "device_id": entry.id, } assert update_events[1].data == { - "action": "remove", + "action": "update", + "changes": { + "area_id": "suggested_area_orig", + "disabled_by": None, + "labels": set(), + "name_by_user": None, + }, "device_id": entry.id, } assert update_events[2].data == { + "action": "remove", + "device_id": entry.id, + } + assert update_events[3].data == { "action": "create", "device_id": entry2.id, } - assert update_events[3].data == { - "action": "create", - "device_id": entry3.id, - } - - -async def test_restore_simple_device( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_config_entry: MockConfigEntry, -) -> None: - """Make sure device id is stable.""" - update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) - entry = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - ) - - assert len(device_registry.devices) == 1 - assert len(device_registry.deleted_devices) == 0 - - device_registry.async_remove_device(entry.id) - - assert len(device_registry.devices) == 0 - assert len(device_registry.deleted_devices) == 1 - - entry2 = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, - identifiers={("bridgeid", "4567")}, - ) - entry3 = device_registry.async_get_or_create( - config_entry_id=mock_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - ) - - assert entry.id == entry3.id - assert entry.id != entry2.id - assert len(device_registry.devices) == 2 - assert len(device_registry.deleted_devices) == 0 - - await hass.async_block_till_done() - - assert len(update_events) == 4 - assert update_events[0].data == { - "action": "create", - "device_id": entry.id, - } - assert update_events[1].data == { - "action": "remove", - "device_id": entry.id, - } - assert update_events[2].data == { - "action": "create", - "device_id": entry2.id, - } - assert update_events[3].data == { + assert update_events[4].data == { "action": "create", "device_id": entry3.id, } +@pytest.mark.usefixtures("freezer") async def test_restore_shared_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Make sure device id is stable for shared devices.""" update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED) - config_entry_1 = MockConfigEntry() + config_entry_1 = MockConfigEntry( + subentries_data=( + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-1", + subentry_type="test", + title="Mock title", + unique_id="test", + ), + ), + ) config_entry_1.add_to_hass(hass) config_entry_2 = MockConfigEntry() config_entry_2.add_to_hass(hass) entry = device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + configuration_url="http://config_url_orig_1.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_orig_1", identifiers={("entry_123", "0123")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_orig_1", + model="model_orig_1", + model_id="model_id_orig_1", + name="name_orig_1", + serial_number="serial_no_orig_1", + suggested_area="suggested_area_orig_1", + sw_version="version_orig_1", + via_device="via_device_id_orig_1", ) assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 + # Add another config entry to the same device device_registry.async_get_or_create( config_entry_id=config_entry_2.entry_id, + configuration_url="http://config_url_orig_2.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=None, + hw_version="hw_version_orig_2", identifiers={("entry_234", "2345")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_orig_2", + model="model_orig_2", + model_id="model_id_orig_2", + name="name_orig_2", + serial_number="serial_no_orig_2", + suggested_area="suggested_area_orig_2", + sw_version="version_orig_2", + via_device="via_device_id_orig_2", ) assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 + # Apply user customizations + updated_device = device_registry.async_update_device( + entry.id, + area_id="12345A", + disabled_by=dr.DeviceEntryDisabler.USER, + labels={"label1", "label2"}, + name_by_user="Test Friendly Name", + ) + + # Check device entry before we remove it + assert updated_device == dr.DeviceEntry( + area_id="12345A", + config_entries={config_entry_1.entry_id, config_entry_2.entry_id}, + config_entries_subentries={ + config_entry_1.entry_id: {"mock-subentry-id-1-1"}, + config_entry_2.entry_id: {None}, + }, + configuration_url="http://config_url_orig_2.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=dr.DeviceEntryDisabler.USER, + entry_type=None, + hw_version="hw_version_orig_2", + id=entry.id, + identifiers={("entry_123", "0123"), ("entry_234", "2345")}, + labels={"label1", "label2"}, + manufacturer="manufacturer_orig_2", + model="model_orig_2", + model_id="model_id_orig_2", + modified_at=utcnow(), + name_by_user="Test Friendly Name", + name="name_orig_2", + primary_config_entry=config_entry_1.entry_id, + serial_number="serial_no_orig_2", + suggested_area="suggested_area_orig_2", + sw_version="version_orig_2", + ) + device_registry.async_remove_device(entry.id) assert len(device_registry.devices) == 0 assert len(device_registry.deleted_devices) == 1 + # config_entry_1 restores the original device, only the supplied config entry, + # config subentry, connections, and identifiers will be restored entry2 = device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + configuration_url="http://config_url_new_1.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_new_1", identifiers={("entry_123", "0123")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_new_1", + model="model_new_1", + model_id="model_id_new_1", + name="name_new_1", + serial_number="serial_no_new_1", + suggested_area="suggested_area_new_1", + sw_version="version_new_1", + via_device="via_device_id_new_1", + ) + + assert entry2 == dr.DeviceEntry( + area_id="suggested_area_new_1", + config_entries={config_entry_1.entry_id}, + config_entries_subentries={config_entry_1.entry_id: {"mock-subentry-id-1-1"}}, + configuration_url="http://config_url_new_1.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=None, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_new_1", + id=entry.id, + identifiers={("entry_123", "0123")}, + labels={}, + manufacturer="manufacturer_new_1", + model="model_new_1", + model_id="model_id_new_1", + modified_at=utcnow(), + name_by_user=None, + name="name_new_1", + primary_config_entry=config_entry_1.entry_id, + serial_number="serial_no_new_1", + suggested_area="suggested_area_new_1", + sw_version="version_new_1", ) - assert entry.id == entry2.id assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 @@ -3352,17 +3517,55 @@ async def test_restore_shared_device( assert isinstance(entry2.connections, set) assert isinstance(entry2.identifiers, set) + # Remove the device again device_registry.async_remove_device(entry.id) + # config_entry_2 restores the original device, only the supplied config entry, + # config subentry, connections, and identifiers will be restored entry3 = device_registry.async_get_or_create( config_entry_id=config_entry_2.entry_id, + configuration_url="http://config_url_new_2.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=None, + hw_version="hw_version_new_2", identifiers={("entry_234", "2345")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_new_2", + model="model_new_2", + model_id="model_id_new_2", + name="name_new_2", + serial_number="serial_no_new_2", + suggested_area="suggested_area_new_2", + sw_version="version_new_2", + via_device="via_device_id_new_2", + ) + + assert entry3 == dr.DeviceEntry( + area_id="suggested_area_new_2", + config_entries={config_entry_2.entry_id}, + config_entries_subentries={ + config_entry_2.entry_id: {None}, + }, + configuration_url="http://config_url_new_2.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=None, + entry_type=None, + hw_version="hw_version_new_2", + id=entry.id, + identifiers={("entry_234", "2345")}, + labels={}, + manufacturer="manufacturer_new_2", + model="model_new_2", + model_id="model_id_new_2", + modified_at=utcnow(), + name_by_user=None, + name="name_new_2", + primary_config_entry=config_entry_2.entry_id, + serial_number="serial_no_new_2", + suggested_area="suggested_area_new_2", + sw_version="version_new_2", ) - assert entry.id == entry3.id assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 @@ -3370,15 +3573,53 @@ async def test_restore_shared_device( assert isinstance(entry3.connections, set) assert isinstance(entry3.identifiers, set) + # Add config_entry_1 back to the restored device entry4 = device_registry.async_get_or_create( config_entry_id=config_entry_1.entry_id, + config_subentry_id="mock-subentry-id-1-1", + configuration_url="http://config_url_new_1.bla", connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_new_1", identifiers={("entry_123", "0123")}, - manufacturer="manufacturer", - model="model", + manufacturer="manufacturer_new_1", + model="model_new_1", + model_id="model_id_new_1", + name="name_new_1", + serial_number="serial_no_new_1", + suggested_area="suggested_area_new_1", + sw_version="version_new_1", + via_device="via_device_id_new_1", + ) + + assert entry4 == dr.DeviceEntry( + area_id="suggested_area_new_2", + config_entries={config_entry_1.entry_id, config_entry_2.entry_id}, + config_entries_subentries={ + config_entry_1.entry_id: {"mock-subentry-id-1-1"}, + config_entry_2.entry_id: {None}, + }, + configuration_url="http://config_url_new_1.bla", + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:ab:cd:ef")}, + created_at=utcnow(), + disabled_by=None, + entry_type=dr.DeviceEntryType.SERVICE, + hw_version="hw_version_new_1", + id=entry.id, + identifiers={("entry_123", "0123"), ("entry_234", "2345")}, + labels={}, + manufacturer="manufacturer_new_1", + model="model_new_1", + model_id="model_id_new_1", + modified_at=utcnow(), + name_by_user=None, + name="name_new_1", + primary_config_entry=config_entry_2.entry_id, + serial_number="serial_no_new_1", + suggested_area="suggested_area_new_1", + sw_version="version_new_1", ) - assert entry.id == entry4.id assert len(device_registry.devices) == 1 assert len(device_registry.deleted_devices) == 0 @@ -3388,7 +3629,7 @@ async def test_restore_shared_device( await hass.async_block_till_done() - assert len(update_events) == 7 + assert len(update_events) == 8 assert update_events[0].data == { "action": "create", "device_id": entry.id, @@ -3398,33 +3639,65 @@ async def test_restore_shared_device( "device_id": entry.id, "changes": { "config_entries": {config_entry_1.entry_id}, - "config_entries_subentries": {config_entry_1.entry_id: {None}}, + "config_entries_subentries": { + config_entry_1.entry_id: {"mock-subentry-id-1-1"} + }, + "configuration_url": "http://config_url_orig_1.bla", + "entry_type": dr.DeviceEntryType.SERVICE, + "hw_version": "hw_version_orig_1", "identifiers": {("entry_123", "0123")}, + "manufacturer": "manufacturer_orig_1", + "model": "model_orig_1", + "model_id": "model_id_orig_1", + "name": "name_orig_1", + "serial_number": "serial_no_orig_1", + "suggested_area": "suggested_area_orig_1", + "sw_version": "version_orig_1", }, } assert update_events[2].data == { - "action": "remove", + "action": "update", "device_id": entry.id, + "changes": { + "area_id": "suggested_area_orig_1", + "disabled_by": None, + "labels": set(), + "name_by_user": None, + }, } assert update_events[3].data == { - "action": "create", + "action": "remove", "device_id": entry.id, } assert update_events[4].data == { - "action": "remove", - "device_id": entry.id, - } - assert update_events[5].data == { "action": "create", "device_id": entry.id, } + assert update_events[5].data == { + "action": "remove", + "device_id": entry.id, + } assert update_events[6].data == { + "action": "create", + "device_id": entry.id, + } + assert update_events[7].data == { "action": "update", "device_id": entry.id, "changes": { "config_entries": {config_entry_2.entry_id}, "config_entries_subentries": {config_entry_2.entry_id: {None}}, + "configuration_url": "http://config_url_new_2.bla", + "entry_type": None, + "hw_version": "hw_version_new_2", "identifiers": {("entry_234", "2345")}, + "manufacturer": "manufacturer_new_2", + "model": "model_new_2", + "model_id": "model_id_new_2", + "name": "name_new_2", + "serial_number": "serial_no_new_2", + "suggested_area": "suggested_area_new_2", + "sw_version": "version_new_2", }, } From 8d83341308ba68b305095d1178082b8dc972a902 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 11:50:41 +0200 Subject: [PATCH 0600/1175] Mark type hint as compulsory for entity.available property (#145189) --- homeassistant/components/mediaroom/media_player.py | 2 +- homeassistant/components/osramlightify/light.py | 2 +- homeassistant/components/rmvtransport/sensor.py | 2 +- homeassistant/components/sony_projector/switch.py | 2 +- homeassistant/components/starline/button.py | 2 +- homeassistant/components/supervisord/sensor.py | 2 +- homeassistant/components/syncthing/sensor.py | 2 +- homeassistant/components/tfiac/climate.py | 2 +- homeassistant/components/versasense/sensor.py | 2 +- homeassistant/components/versasense/switch.py | 2 +- homeassistant/components/wiffi/binary_sensor.py | 2 +- homeassistant/components/wiffi/sensor.py | 4 ++-- homeassistant/components/xiaomi_miio/light.py | 4 ++-- homeassistant/components/xiaomi_miio/sensor.py | 4 ++-- homeassistant/components/xiaomi_miio/switch.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 1 + 16 files changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 4561c38ce80..bccbe9f66ac 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -165,7 +165,7 @@ class MediaroomDevice(MediaPlayerEntity): self._unique_id = None @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._available diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 25380810862..42af6c74e45 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -279,7 +279,7 @@ class Luminary(LightEntity): return self._device_attributes @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._available diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index c3217d9334e..92f4f5a0434 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -156,7 +156,7 @@ class RMVDepartureSensor(SensorEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._state is not None diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index f024c4ef4f7..c4d993cc22a 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -64,7 +64,7 @@ class SonyProjector(SwitchEntity): self._attributes = {} @property - def available(self): + def available(self) -> bool: """Return if projector is available.""" return self._available diff --git a/homeassistant/components/starline/button.py b/homeassistant/components/starline/button.py index fa46d2a3773..1d238e232b9 100644 --- a/homeassistant/components/starline/button.py +++ b/homeassistant/components/starline/button.py @@ -64,7 +64,7 @@ class StarlineButton(StarlineEntity, ButtonEntity): self.entity_description = description @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return super().available and self._device.online diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index c443e1e63df..c14eb6fb353 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -71,7 +71,7 @@ class SupervisorProcessSensor(SensorEntity): return self._info.get("statename") @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._available diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 697ea8aea6e..d6ad17969db 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -111,7 +111,7 @@ class FolderSensor(SensorEntity): return self._state["state"] @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._state is not None diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 9571597abe6..7fc6e2594c4 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -95,7 +95,7 @@ class TfiacClimate(ClimateEntity): self._available = True @property - def available(self): + def available(self) -> bool: """Return if the device is available.""" return self._available diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index 4c861bf5787..3956bd21fea 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -86,7 +86,7 @@ class VSensor(SensorEntity): return self._unit @property - def available(self): + def available(self) -> bool: """Return if the sensor is available.""" return self._available diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index 10bca79e536..828dbf6d9af 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -84,7 +84,7 @@ class VActuator(SwitchEntity): return self._is_on @property - def available(self): + def available(self) -> bool: """Return if the actuator is available.""" return self._available diff --git a/homeassistant/components/wiffi/binary_sensor.py b/homeassistant/components/wiffi/binary_sensor.py index 93fdb7cce1c..abb6dd11235 100644 --- a/homeassistant/components/wiffi/binary_sensor.py +++ b/homeassistant/components/wiffi/binary_sensor.py @@ -44,7 +44,7 @@ class BoolEntity(WiffiEntity, BinarySensorEntity): self.reset_expiration_date() @property - def available(self): + def available(self) -> bool: """Return true if value is valid.""" return self._attr_is_on is not None diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index 9afcc719c9b..f28c68dc31c 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -86,7 +86,7 @@ class NumberEntity(WiffiEntity, SensorEntity): self.reset_expiration_date() @property - def available(self): + def available(self) -> bool: """Return true if value is valid.""" return self._attr_native_value is not None @@ -116,7 +116,7 @@ class StringEntity(WiffiEntity, SensorEntity): self.reset_expiration_date() @property - def available(self): + def available(self) -> bool: """Return true if value is valid.""" return self._attr_native_value is not None diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 81f68306cbc..781ac0b4acd 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -271,7 +271,7 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): self._state_attrs = {} @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" return self._available @@ -1027,7 +1027,7 @@ class XiaomiGatewayLight(LightEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" return self._available diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 6f623c46af8..e837192ddd7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -928,7 +928,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): self.entity_description = description @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" return self._available @@ -1001,7 +1001,7 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): self._state = None @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" return self._available diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index e4b94aebc20..4469849eae7 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -789,7 +789,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): return self._icon @property - def available(self): + def available(self) -> bool: """Return true when state is known.""" return self._available diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 9855f688622..ddce048e4a6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -676,6 +676,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="available", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="assumed_state", From f11e040662c3dc9397a4f8ed708c05217d0aaeb1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 11:55:15 +0200 Subject: [PATCH 0601/1175] Mark all _FUNCTION_MATCH as mandatory in pylint plugin (#145194) --- pylint/plugins/hass_enforce_type_hints.py | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index ddce048e4a6..0e56e94d8cb 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -278,6 +278,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 2: "ClientCredential", }, return_type="AbstractOAuth2Implementation", + mandatory=True, ), TypeHintMatch( function_name="async_get_authorization_server", @@ -285,6 +286,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", }, return_type="AuthorizationServer", + mandatory=True, ), ], "backup": [ @@ -294,6 +296,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_post_backup", @@ -301,6 +304,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", }, return_type=None, + mandatory=True, ), ], "cast": [ @@ -311,6 +315,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "str", }, return_type="list[BrowseMedia]", + mandatory=True, ), TypeHintMatch( function_name="async_browse_media", @@ -321,6 +326,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "str", }, return_type=["BrowseMedia", "BrowseMedia | None"], + mandatory=True, ), TypeHintMatch( function_name="async_play_media", @@ -332,6 +338,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 4: "str", }, return_type="bool", + mandatory=True, ), ], "config_flow": [ @@ -341,6 +348,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", }, return_type="bool", + mandatory=True, ), ], "device_action": [ @@ -351,6 +359,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="ConfigType", + mandatory=True, ), TypeHintMatch( function_name="async_call_action_from_config", @@ -361,6 +370,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "Context | None", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_get_action_capabilities", @@ -369,6 +379,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="dict[str, Schema]", + mandatory=True, ), TypeHintMatch( function_name="async_get_actions", @@ -377,6 +388,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "str", }, return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + mandatory=True, ), ], "device_condition": [ @@ -387,6 +399,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="ConfigType", + mandatory=True, ), TypeHintMatch( function_name="async_condition_from_config", @@ -395,6 +408,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="ConditionCheckerType", + mandatory=True, ), TypeHintMatch( function_name="async_get_condition_capabilities", @@ -403,6 +417,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="dict[str, Schema]", + mandatory=True, ), TypeHintMatch( function_name="async_get_conditions", @@ -411,6 +426,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "str", }, return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + mandatory=True, ), ], "device_tracker": [ @@ -423,6 +439,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "DiscoveryInfoType | None", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_setup_scanner", @@ -433,6 +450,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "DiscoveryInfoType | None", }, return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="get_scanner", @@ -442,6 +460,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { }, return_type=["DeviceScanner", None], has_async_counterpart=True, + mandatory=True, ), ], "device_trigger": [ @@ -452,6 +471,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="ConfigType", + mandatory=True, ), TypeHintMatch( function_name="async_attach_trigger", @@ -462,6 +482,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "TriggerInfo", }, return_type="CALLBACK_TYPE", + mandatory=True, ), TypeHintMatch( function_name="async_get_trigger_capabilities", @@ -470,6 +491,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="dict[str, Schema]", + mandatory=True, ), TypeHintMatch( function_name="async_get_triggers", @@ -478,6 +500,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "str", }, return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + mandatory=True, ), ], "diagnostics": [ @@ -488,6 +511,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", }, return_type="Mapping[str, Any]", + mandatory=True, ), TypeHintMatch( function_name="async_get_device_diagnostics", @@ -497,6 +521,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 2: "DeviceEntry", }, return_type="Mapping[str, Any]", + mandatory=True, ), ], "notify": [ @@ -509,6 +534,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { }, return_type=["BaseNotificationService", None], has_async_counterpart=True, + mandatory=True, ), ], } From 07c3c3bba8f3096dafdc6a17033de52d93b840fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 11:56:05 +0200 Subject: [PATCH 0602/1175] Mark type hint as compulsory for entity.assumed_state property (#145187) --- homeassistant/components/raspyrfm/switch.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index b9506c3688c..a609ddb27d3 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -118,7 +118,7 @@ class RaspyRFMSwitch(SwitchEntity): return self._name @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True when the current state cannot be queried.""" return True diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 0e56e94d8cb..c5a79d166e2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -707,6 +707,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="assumed_state", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="force_update", From a1d6df6ce92dfc4a9f40a851bc7b9ab01b7f1986 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 19 May 2025 11:58:35 +0200 Subject: [PATCH 0603/1175] Remove deprecated aux heat from ephember (#145152) --- homeassistant/components/ephember/climate.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 3d82cfd7511..efdd106b34b 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -11,7 +11,6 @@ from pyephember2.pyephember2 import ( ZoneMode, zone_current_temperature, zone_is_active, - zone_is_boost_active, zone_is_hotwater, zone_mode, zone_name, @@ -102,7 +101,6 @@ class EphEmberThermostat(ClimateEntity): self._attr_name = self._zone_name if self._hot_water: - self._attr_supported_features = ClimateEntityFeature.AUX_HEAT self._attr_target_temperature_step = None else: self._attr_target_temperature_step = 0.5 @@ -144,22 +142,6 @@ class EphEmberThermostat(ClimateEntity): else: _LOGGER.error("Invalid operation mode provided %s", hvac_mode) - @property - def is_aux_heat(self) -> bool: - """Return true if aux heater.""" - - return zone_is_boost_active(self._zone) - - def turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - self._ember.activate_boost_by_name( - self._zone_name, zone_target_temperature(self._zone) - ) - - def turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - self._ember.deactivate_boost_by_name(self._zone_name) - def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: From 919684e20a2d9417f2dc0d6a8f6da48eb10fa9a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 May 2025 05:58:58 -0400 Subject: [PATCH 0604/1175] Minor cleanup for pipeline tts stream test (#145146) --- .../snapshots/test_pipeline.ambr | 2 +- .../assist_pipeline/test_pipeline.py | 29 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index 717823fe4e4..bbe08a2adbe 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_chat_log_tts_streaming[to_stream_tts0] +# name: test_chat_log_tts_streaming[to_stream_tts0-1] list([ dict({ 'data': dict({ diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index e318862a2f2..abf6572afc9 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1559,18 +1559,21 @@ async def test_pipeline_language_used_instead_of_conversation_language( @pytest.mark.parametrize( - "to_stream_tts", + ("to_stream_tts", "expected_chunks"), [ - [ - "hello,", - " ", - "how", - " ", - "are", - " ", - "you", - "?", - ] + ( + [ + "hello,", + " ", + "how", + " ", + "are", + " ", + "you", + "?", + ], + 1, + ), ], ) async def test_chat_log_tts_streaming( @@ -1582,6 +1585,7 @@ async def test_chat_log_tts_streaming( mock_tts_entity: MockTTSEntity, pipeline_data: assist_pipeline.pipeline.PipelineData, to_stream_tts: list[str], + expected_chunks: int, ) -> None: """Test that chat log events are streamed to the TTS entity.""" events: list[assist_pipeline.PipelineEvent] = [] @@ -1625,6 +1629,7 @@ async def test_chat_log_tts_streaming( ) mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio + mock_tts_entity.async_supports_streaming_input = Mock(return_value=True) with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", @@ -1692,7 +1697,7 @@ async def test_chat_log_tts_streaming( streamed_text = "".join(to_stream_tts) assert tts_result == streamed_text - assert len(received_tts) == 1 + assert len(received_tts) == expected_chunks assert "".join(received_tts) == streamed_text assert process_events(events) == snapshot From 92e570ffc1fbd6da2f55870dea981c8d1ece531e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 19 May 2025 12:01:54 +0200 Subject: [PATCH 0605/1175] Revert "Link Shelly device entry with Shelly BT scanner entry (#144626)" (#145177) This reverts commit b15c9ad130229bd4137f75fc8cd30b27392276d5. --- .../components/shelly/coordinator.py | 21 ++----------------- tests/components/shelly/test_coordinator.py | 18 ---------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index e4af35484c8..f980ba8f914 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -33,11 +33,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.device_registry import ( - CONNECTION_BLUETOOTH, - CONNECTION_NETWORK_MAC, - format_mac, -) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .bluetooth import async_connect_scanner @@ -164,11 +160,6 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( """Sleep period of the device.""" return self.config_entry.data.get(CONF_SLEEP_PERIOD, 0) - @property - def connections(self) -> set[tuple[str, str]]: - """Connections of the device.""" - return {(CONNECTION_NETWORK_MAC, self.mac)} - def async_setup(self, pending_platforms: list[Platform] | None = None) -> None: """Set up the coordinator.""" self._pending_platforms = pending_platforms @@ -176,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( device_entry = dev_reg.async_get_or_create( config_entry_id=self.config_entry.entry_id, name=self.name, - connections=self.connections, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, identifiers={(DOMAIN, self.mac)}, manufacturer="Shelly", model=get_shelly_model_name(self.model, self.sleep_period, self.device), @@ -532,14 +523,6 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """ return format_mac(bluetooth_mac_from_primary_mac(self.mac)).upper() - @property - def connections(self) -> set[tuple[str, str]]: - """Connections of the device.""" - connections = super().connections - if not self.sleep_period: - connections.add((CONNECTION_BLUETOOTH, self.bluetooth_source)) - return connections - async def async_device_online(self, source: str) -> None: """Handle device going online.""" if not self.sleep_period: diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index aae452538bb..cf7f82014a0 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -1078,21 +1078,3 @@ async def test_xmod_model_lookup( ) assert device assert device.model == xmod_model - - -async def test_device_entry_bt_address( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mock_rpc_device: Mock, -) -> None: - """Check if BT address is added to device entry connections.""" - entry = await init_integration(hass, 2) - - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))}, - ) - - assert device - assert len(device.connections) == 2 - assert (dr.CONNECTION_BLUETOOTH, "12:34:56:78:9A:BE") in device.connections From 77bab39ed0c7eab03a8e417ee982a6bfbfd17b22 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 12:05:33 +0200 Subject: [PATCH 0606/1175] Move downloader service to separate module (#145183) --- .../components/downloader/__init__.py | 148 +--------------- .../components/downloader/services.py | 159 ++++++++++++++++++ tests/components/downloader/test_init.py | 2 +- 3 files changed, 164 insertions(+), 145 deletions(-) create mode 100644 homeassistant/components/downloader/services.py diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 1a45886879a..c4fc8d2f500 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -2,32 +2,13 @@ from __future__ import annotations -from http import HTTPStatus import os -import re -import threading - -import requests -import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_register_admin_service -from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path +from homeassistant.core import HomeAssistant -from .const import ( - _LOGGER, - ATTR_FILENAME, - ATTR_OVERWRITE, - ATTR_SUBDIR, - ATTR_URL, - CONF_DOWNLOAD_DIR, - DOMAIN, - DOWNLOAD_COMPLETED_EVENT, - DOWNLOAD_FAILED_EVENT, - SERVICE_DOWNLOAD_FILE, -) +from .const import _LOGGER, CONF_DOWNLOAD_DIR +from .services import register_services async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -44,127 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - def download_file(service: ServiceCall) -> None: - """Start thread to download file specified in the URL.""" - - def do_download() -> None: - """Download the file.""" - try: - url = service.data[ATTR_URL] - - subdir = service.data.get(ATTR_SUBDIR) - - filename = service.data.get(ATTR_FILENAME) - - overwrite = service.data.get(ATTR_OVERWRITE) - - if subdir: - # Check the path - raise_if_invalid_path(subdir) - - final_path = None - - req = requests.get(url, stream=True, timeout=10) - - if req.status_code != HTTPStatus.OK: - _LOGGER.warning( - "Downloading '%s' failed, status_code=%d", url, req.status_code - ) - hass.bus.fire( - f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", - {"url": url, "filename": filename}, - ) - - else: - if filename is None and "content-disposition" in req.headers: - match = re.findall( - r"filename=(\S+)", req.headers["content-disposition"] - ) - - if match: - filename = match[0].strip("'\" ") - - if not filename: - filename = os.path.basename(url).strip() - - if not filename: - filename = "ha_download" - - # Check the filename - raise_if_invalid_filename(filename) - - # Do we want to download to subdir, create if needed - if subdir: - subdir_path = os.path.join(download_path, subdir) - - # Ensure subdir exist - os.makedirs(subdir_path, exist_ok=True) - - final_path = os.path.join(subdir_path, filename) - - else: - final_path = os.path.join(download_path, filename) - - path, ext = os.path.splitext(final_path) - - # If file exist append a number. - # We test filename, filename_2.. - if not overwrite: - tries = 1 - final_path = path + ext - while os.path.isfile(final_path): - tries += 1 - - final_path = f"{path}_{tries}.{ext}" - - _LOGGER.debug("%s -> %s", url, final_path) - - with open(final_path, "wb") as fil: - for chunk in req.iter_content(1024): - fil.write(chunk) - - _LOGGER.debug("Downloading of %s done", url) - hass.bus.fire( - f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", - {"url": url, "filename": filename}, - ) - - except requests.exceptions.ConnectionError: - _LOGGER.exception("ConnectionError occurred for %s", url) - hass.bus.fire( - f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", - {"url": url, "filename": filename}, - ) - - # Remove file if we started downloading but failed - if final_path and os.path.isfile(final_path): - os.remove(final_path) - except ValueError: - _LOGGER.exception("Invalid value") - hass.bus.fire( - f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", - {"url": url, "filename": filename}, - ) - - # Remove file if we started downloading but failed - if final_path and os.path.isfile(final_path): - os.remove(final_path) - - threading.Thread(target=do_download).start() - - async_register_admin_service( - hass, - DOMAIN, - SERVICE_DOWNLOAD_FILE, - download_file, - schema=vol.Schema( - { - vol.Optional(ATTR_FILENAME): cv.string, - vol.Optional(ATTR_SUBDIR): cv.string, - vol.Required(ATTR_URL): cv.url, - vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, - } - ), - ) + register_services(hass) return True diff --git a/homeassistant/components/downloader/services.py b/homeassistant/components/downloader/services.py new file mode 100644 index 00000000000..a8bcba605d9 --- /dev/null +++ b/homeassistant/components/downloader/services.py @@ -0,0 +1,159 @@ +"""Support for functionality to download files.""" + +from __future__ import annotations + +from http import HTTPStatus +import os +import re +import threading + +import requests +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path + +from .const import ( + _LOGGER, + ATTR_FILENAME, + ATTR_OVERWRITE, + ATTR_SUBDIR, + ATTR_URL, + CONF_DOWNLOAD_DIR, + DOMAIN, + DOWNLOAD_COMPLETED_EVENT, + DOWNLOAD_FAILED_EVENT, + SERVICE_DOWNLOAD_FILE, +) + + +def download_file(service: ServiceCall) -> None: + """Start thread to download file specified in the URL.""" + + entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0] + download_path = entry.data[CONF_DOWNLOAD_DIR] + + def do_download() -> None: + """Download the file.""" + try: + url = service.data[ATTR_URL] + + subdir = service.data.get(ATTR_SUBDIR) + + filename = service.data.get(ATTR_FILENAME) + + overwrite = service.data.get(ATTR_OVERWRITE) + + if subdir: + # Check the path + raise_if_invalid_path(subdir) + + final_path = None + + req = requests.get(url, stream=True, timeout=10) + + if req.status_code != HTTPStatus.OK: + _LOGGER.warning( + "Downloading '%s' failed, status_code=%d", url, req.status_code + ) + service.hass.bus.fire( + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", + {"url": url, "filename": filename}, + ) + + else: + if filename is None and "content-disposition" in req.headers: + match = re.findall( + r"filename=(\S+)", req.headers["content-disposition"] + ) + + if match: + filename = match[0].strip("'\" ") + + if not filename: + filename = os.path.basename(url).strip() + + if not filename: + filename = "ha_download" + + # Check the filename + raise_if_invalid_filename(filename) + + # Do we want to download to subdir, create if needed + if subdir: + subdir_path = os.path.join(download_path, subdir) + + # Ensure subdir exist + os.makedirs(subdir_path, exist_ok=True) + + final_path = os.path.join(subdir_path, filename) + + else: + final_path = os.path.join(download_path, filename) + + path, ext = os.path.splitext(final_path) + + # If file exist append a number. + # We test filename, filename_2.. + if not overwrite: + tries = 1 + final_path = path + ext + while os.path.isfile(final_path): + tries += 1 + + final_path = f"{path}_{tries}.{ext}" + + _LOGGER.debug("%s -> %s", url, final_path) + + with open(final_path, "wb") as fil: + for chunk in req.iter_content(1024): + fil.write(chunk) + + _LOGGER.debug("Downloading of %s done", url) + service.hass.bus.fire( + f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", + {"url": url, "filename": filename}, + ) + + except requests.exceptions.ConnectionError: + _LOGGER.exception("ConnectionError occurred for %s", url) + service.hass.bus.fire( + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", + {"url": url, "filename": filename}, + ) + + # Remove file if we started downloading but failed + if final_path and os.path.isfile(final_path): + os.remove(final_path) + except ValueError: + _LOGGER.exception("Invalid value") + service.hass.bus.fire( + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", + {"url": url, "filename": filename}, + ) + + # Remove file if we started downloading but failed + if final_path and os.path.isfile(final_path): + os.remove(final_path) + + threading.Thread(target=do_download).start() + + +def register_services(hass: HomeAssistant) -> None: + """Register the services for the downloader component.""" + async_register_admin_service( + hass, + DOMAIN, + SERVICE_DOWNLOAD_FILE, + download_file, + schema=vol.Schema( + { + vol.Optional(ATTR_FILENAME): cv.string, + vol.Optional(ATTR_SUBDIR): cv.string, + vol.Required(ATTR_URL): cv.url, + vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean, + } + ), + ) diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py index 70dfd227019..e74eb376b39 100644 --- a/tests/components/downloader/test_init.py +++ b/tests/components/downloader/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant.components.downloader import ( +from homeassistant.components.downloader.const import ( CONF_DOWNLOAD_DIR, DOMAIN, SERVICE_DOWNLOAD_FILE, From 68c3d5a15961303d03e57dac14d24f199c1dce2a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 19 May 2025 12:07:50 +0200 Subject: [PATCH 0607/1175] Add lamp capability for hood component in SmartThings (#145036) --- .../components/smartthings/select.py | 32 +++++++--- .../smartthings/snapshots/test_select.ambr | 58 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 4fcd7fd080f..39a49da2bbe 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -32,6 +32,8 @@ class SmartThingsSelectDescription(SelectEntityDescription): command: Command options_map: dict[str, str] | None = None default_options: list[str] | None = None + extra_components: list[str] | None = None + capability_ignore_list: list[Capability] | None = None CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { @@ -88,6 +90,8 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { command=Command.SET_BRIGHTNESS_LEVEL, options_map=LAMP_TO_HA, entity_category=EntityCategory.CONFIG, + extra_components=["hood"], + capability_ignore_list=[Capability.SAMSUNG_CE_CONNECTION_STATE], ), } @@ -100,12 +104,25 @@ async def async_setup_entry( """Add select entities for a config entry.""" entry_data = entry.runtime_data async_add_entities( - SmartThingsSelectEntity( - entry_data.client, device, CAPABILITIES_TO_SELECT[capability] - ) + SmartThingsSelectEntity(entry_data.client, device, description, component) + for capability, description in CAPABILITIES_TO_SELECT.items() for device in entry_data.devices.values() - for capability in device.status[MAIN] - if capability in CAPABILITIES_TO_SELECT + for component in device.status + if capability in device.status[component] + and ( + component == MAIN + or ( + description.extra_components is not None + and component in description.extra_components + ) + ) + and ( + description.capability_ignore_list is None + or any( + capability not in device.status[component] + for capability in description.capability_ignore_list + ) + ) ) @@ -119,14 +136,15 @@ class SmartThingsSelectEntity(SmartThingsEntity, SelectEntity): client: SmartThings, device: FullDevice, entity_description: SmartThingsSelectDescription, + component: str, ) -> None: """Initialize the instance.""" capabilities = {entity_description.key} if entity_description.requires_remote_control_status: capabilities.add(Capability.REMOTE_CONTROL_STATUS) - super().__init__(client, device, capabilities) + super().__init__(client, device, capabilities, component=component) self.entity_description = entity_description - self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{entity_description.key}_{entity_description.status_attribute}_{entity_description.status_attribute}" + self._attr_unique_id = f"{device.device.device_id}_{component}_{entity_description.key}_{entity_description.status_attribute}_{entity_description.status_attribute}" @property def options(self) -> list[str]: diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index b2c3234847e..c1093bbd209 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -1,4 +1,62 @@ # serializer version: 1 +# name: test_all_entities[da_ks_microwave_0101x][select.microwave_lamp-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.microwave_lamp', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lamp', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lamp', + 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_hood_samsungce.lamp_brightnessLevel_brightnessLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_microwave_0101x][select.microwave_lamp-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Microwave Lamp', + 'options': list([ + 'off', + 'low', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.microwave_lamp', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_all_entities[da_ks_oven_01061][select.oven_lamp-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From cb84e55c34dd6971f1e46ec841cfe9df9b924816 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 19 May 2025 12:09:27 +0200 Subject: [PATCH 0608/1175] Map auto to heat_cool for thermostat in SmartThings (#145098) --- homeassistant/components/smartthings/climate.py | 4 ++-- tests/components/smartthings/snapshots/test_climate.ambr | 4 ++-- tests/components/smartthings/test_climate.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 2c826697edd..d063316e233 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -31,7 +31,7 @@ from .entity import SmartThingsEntity ATTR_OPERATION_STATE = "operation_state" MODE_TO_STATE = { - "auto": HVACMode.AUTO, + "auto": HVACMode.HEAT_COOL, "cool": HVACMode.COOL, "eco": HVACMode.AUTO, "rush hour": HVACMode.AUTO, @@ -40,7 +40,7 @@ MODE_TO_STATE = { "off": HVACMode.OFF, } STATE_TO_MODE = { - HVACMode.AUTO: "auto", + HVACMode.HEAT_COOL: "auto", HVACMode.COOL: "cool", HVACMode.HEAT: "heat", HVACMode.OFF: "off", diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index b23e7024e05..6f4dd67d7f7 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -541,7 +541,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, @@ -589,7 +589,7 @@ 'hvac_modes': list([ , , - , + , ]), 'max_temp': 35.0, 'min_temp': 7.0, diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 8241e6de3b3..9e3fa22f55d 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -625,7 +625,7 @@ async def test_thermostat_set_hvac_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.AUTO}, + {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, blocking=True, ) devices.execute_device_command.assert_called_once_with( From 0fc81d6b3333988d615a0967addf61f58172bee4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 19 May 2025 12:23:04 +0200 Subject: [PATCH 0609/1175] Add diagnostics platform to Immich integration (#145162) * add diagnostics platform * also redact host --- .../components/immich/diagnostics.py | 26 ++++++++ .../components/immich/quality_scale.yaml | 2 +- .../immich/snapshots/test_diagnostics.ambr | 66 +++++++++++++++++++ tests/components/immich/test_diagnostics.py | 33 ++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/immich/diagnostics.py create mode 100644 tests/components/immich/snapshots/test_diagnostics.ambr create mode 100644 tests/components/immich/test_diagnostics.py diff --git a/homeassistant/components/immich/diagnostics.py b/homeassistant/components/immich/diagnostics.py new file mode 100644 index 00000000000..c44e24d8202 --- /dev/null +++ b/homeassistant/components/immich/diagnostics.py @@ -0,0 +1,26 @@ +"""Diagnostics support for immich.""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.core import HomeAssistant + +from .coordinator import ImmichConfigEntry + +TO_REDACT = {CONF_API_KEY, CONF_HOST} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ImmichConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = entry.runtime_data + + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "data": asdict(coordinator.data), + } diff --git a/homeassistant/components/immich/quality_scale.yaml b/homeassistant/components/immich/quality_scale.yaml index e89127871e2..053d51eb8c7 100644 --- a/homeassistant/components/immich/quality_scale.yaml +++ b/homeassistant/components/immich/quality_scale.yaml @@ -39,7 +39,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: Service can't be discovered diff --git a/tests/components/immich/snapshots/test_diagnostics.ambr b/tests/components/immich/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..3216de2fabd --- /dev/null +++ b/tests/components/immich/snapshots/test_diagnostics.ambr @@ -0,0 +1,66 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + 'server_about': dict({ + 'build': None, + 'build_image': None, + 'build_image_url': None, + 'build_url': None, + 'exiftool': None, + 'ffmpeg': None, + 'imagemagick': None, + 'libvips': None, + 'licensed': False, + 'nodejs': None, + 'repository': None, + 'repository_url': None, + 'source_commit': None, + 'source_ref': None, + 'source_url': None, + 'version': 'v1.132.3', + 'version_url': 'some_url', + }), + 'server_storage': dict({ + 'disk_available': '136.3 GiB', + 'disk_available_raw': 146402975744, + 'disk_size': '294.2 GiB', + 'disk_size_raw': 315926315008, + 'disk_usage_percentage': 48.56, + 'disk_use': '142.9 GiB', + 'disk_use_raw': 153400434688, + }), + 'server_usage': dict({ + 'photos': 27038, + 'usage': 119525451912, + 'usage_photos': 54291170551, + 'usage_videos': 65234281361, + 'videos': 1836, + }), + }), + 'entry': dict({ + 'data': dict({ + 'api_key': '**REDACTED**', + 'host': '**REDACTED**', + 'port': 80, + 'ssl': False, + 'verify_ssl': True, + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'immich', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Someone', + 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/immich/test_diagnostics.py b/tests/components/immich/test_diagnostics.py new file mode 100644 index 00000000000..f816aab8aae --- /dev/null +++ b/tests/components/immich/test_diagnostics.py @@ -0,0 +1,33 @@ +"""Tests for the Immich integration.""" + +from __future__ import annotations + +from unittest.mock import Mock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +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, + snapshot: SnapshotAssertion, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + + result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id")) From 08104eec56ac18dacc7563c33ebe2f8bbe88f3ef Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 19 May 2025 12:43:06 +0200 Subject: [PATCH 0610/1175] Fix Z-Wave unique id update during controller migration (#145185) --- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/api.py | 8 +- .../components/zwave_js/config_flow.py | 91 ++++- homeassistant/components/zwave_js/const.py | 2 +- tests/components/zwave_js/test_api.py | 4 +- tests/components/zwave_js/test_config_flow.py | 342 +++++++++++++++--- 6 files changed, 376 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 349baecc21d..6e76b2f89cf 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -105,6 +105,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, EVENT_VALUE_UPDATED, LIB_LOGGER, @@ -135,7 +136,6 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_DRIVER_EVENTS = "driver_events" -DRIVER_READY_TIMEOUT = 60 CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index f480c822a8c..5f6050b88e9 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -88,9 +88,9 @@ from .const import ( CONF_INSTALLER_MODE, DATA_CLIENT, DOMAIN, + DRIVER_READY_TIMEOUT, EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, - RESTORE_NVM_DRIVER_READY_TIMEOUT, USER_AGENT, ) from .helpers import ( @@ -189,8 +189,6 @@ STRATEGY = "strategy" # https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41 MINIMUM_QR_STRING_LENGTH = 52 -HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT = 60 - # Helper schemas PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( @@ -2866,7 +2864,7 @@ async def websocket_hard_reset_controller( await driver.async_hard_reset() with suppress(TimeoutError): - async with asyncio.timeout(HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() # When resetting the controller, the controller home id is also changed. @@ -3113,7 +3111,7 @@ async def websocket_restore_nvm( await controller.async_restore_nvm_base64(msg["data"]) with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e52a5e784e8..e442fb59cfc 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -65,7 +65,7 @@ from .const import ( CONF_USE_ADDON, DATA_CLIENT, DOMAIN, - RESTORE_NVM_DRIVER_READY_TIMEOUT, + DRIVER_READY_TIMEOUT, ) from .helpers import CannotConnect, async_get_version_info @@ -776,17 +776,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) @callback - def _async_update_entry( - self, updates: dict[str, Any], *, schedule_reload: bool = True - ) -> None: + def _async_update_entry(self, updates: dict[str, Any]) -> None: """Update the config entry with new data.""" config_entry = self._reconfigure_config_entry assert config_entry is not None self.hass.config_entries.async_update_entry( config_entry, data=config_entry.data | updates ) - if schedule_reload: - self.hass.config_entries.async_schedule_reload(config_entry.entry_id) + self.hass.config_entries.async_schedule_reload(config_entry.entry_id) async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None @@ -896,15 +893,63 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # Now that the old controller is gone, we can scan for serial ports again return await self.async_step_choose_serial_port() + try: + driver = self._get_driver() + except AbortFlow: + return self.async_abort(reason="config_entry_not_loaded") + + @callback + def set_driver_ready(event: dict) -> None: + "Set the driver ready event." + wait_driver_ready.set() + + wait_driver_ready = asyncio.Event() + + unsubscribe = driver.once("driver ready", set_driver_ready) + # reset the old controller try: - await self._get_driver().async_hard_reset() - except (AbortFlow, FailedCommand) as err: + await driver.async_hard_reset() + except FailedCommand as err: + unsubscribe() _LOGGER.error("Failed to reset controller: %s", err) return self.async_abort(reason="reset_failed") + # Update the unique id of the config entry + # to the new home id, which requires waiting for the driver + # to be ready before getting the new home id. + # If the backup restore, done later in the flow, fails, + # the config entry unique id should be the new home id + # after the controller reset. + try: + async with asyncio.timeout(DRIVER_READY_TIMEOUT): + await wait_driver_ready.wait() + except TimeoutError: + pass + finally: + unsubscribe() + config_entry = self._reconfigure_config_entry assert config_entry is not None + + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded, if the backup restore + # also fails. + _LOGGER.debug( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) + # Unload the config entry before asking the user to unplug the controller. await self.hass.config_entries.async_unload(config_entry.entry_id) @@ -1154,14 +1199,17 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): assert ws_address is not None version_info = self.version_info assert version_info is not None + config_entry = self._reconfigure_config_entry + assert config_entry is not None # We need to wait for the config entry to be reloaded, # before restoring the backup. # We will do this in the restore nvm progress task, # to get a nicer user experience. - self._async_update_entry( - { - "unique_id": str(version_info.home_id), + self.hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, CONF_URL: ws_address, CONF_USB_PATH: self.usb_path, CONF_S0_LEGACY_KEY: self.s0_legacy_key, @@ -1173,8 +1221,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USE_ADDON: True, CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, }, - schedule_reload=False, + unique_id=str(version_info.home_id), ) + return await self.async_step_restore_nvm() async def async_step_finish_addon_setup_reconfigure( @@ -1321,8 +1370,24 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): raise AbortFlow(f"Failed to restore network: {err}") from err else: with suppress(TimeoutError): - async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT): + async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + try: + version_info = await async_get_version_info( + self.hass, config_entry.data[CONF_URL] + ) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + _LOGGER.error( + "Failed to get server version, cannot update config entry " + "unique id with new home id, after controller reset" + ) + else: + self.hass.config_entries.async_update_entry( + config_entry, unique_id=str(version_info.home_id) + ) await self.hass.config_entries.async_reload(config_entry.entry_id) finally: for unsub in unsubs: diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 5792fca42a2..31cfb144e2a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -204,4 +204,4 @@ COVER_TILT_PROPERTY_KEYS: set[str | int | None] = { # Other constants -RESTORE_NVM_DRIVER_READY_TIMEOUT = 60 +DRIVER_READY_TIMEOUT = 60 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 7d4f9fe7a36..d2f0f205e8f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5191,7 +5191,7 @@ async def test_hard_reset_controller( client.async_send_command.side_effect = async_send_command_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.HARD_RESET_CONTROLLER_DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): await ws_client.send_json_auto_id( @@ -5663,7 +5663,7 @@ async def test_restore_nvm( client.async_send_command.side_effect = async_send_command_no_driver_ready with patch( - "homeassistant.components.zwave_js.api.RESTORE_NVM_DRIVER_READY_TIMEOUT", + "homeassistant.components.zwave_js.api.DRIVER_READY_TIMEOUT", new=0, ): # Send the subscription request diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 509fddb8704..7a2788a7b75 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -159,19 +159,6 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]: client.driver.controller.data["sdkVersion"] = original_sdk_version -@pytest.fixture(name="driver_ready_timeout") -def mock_driver_ready_timeout() -> Generator[None]: - """Mock migration nvm restore driver ready timeout.""" - with patch( - ( - "homeassistant.components.zwave_js.config_flow." - "RESTORE_NVM_DRIVER_READY_TIMEOUT" - ), - new=0, - ): - yield - - async def test_manual(hass: HomeAssistant) -> None: """Test we create an entry with manual step.""" @@ -867,8 +854,11 @@ async def test_usb_discovery_migration( restart_addon: AsyncMock, client: MagicMock, integration: MockConfigEntry, + get_server_version: AsyncMock, ) -> None: """Test usb discovery migration.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 addon_options["device"] = "/dev/ttyUSB0" entry = integration assert client.connect.call_count == 1 @@ -893,6 +883,13 @@ async def test_usb_discovery_migration( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -944,6 +941,7 @@ async def test_usb_discovery_migration( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -958,6 +956,8 @@ async def test_usb_discovery_migration( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -976,9 +976,10 @@ async def test_usb_discovery_migration( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") @@ -995,10 +996,9 @@ async def test_usb_discovery_migration( ] ], ) -async def test_usb_discovery_migration_driver_ready_timeout( +async def test_usb_discovery_migration_restore_driver_ready_timeout( hass: HomeAssistant, addon_options: dict[str, Any], - driver_ready_timeout: None, mock_usb_serial_by_id: MagicMock, set_addon_options: AsyncMock, restart_addon: AsyncMock, @@ -1030,6 +1030,13 @@ async def test_usb_discovery_migration_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -1092,21 +1099,25 @@ async def test_usb_discovery_migration_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 - await hass.async_block_till_done() - assert client.connect.call_count == 3 - assert entry.state is config_entries.ConfigEntryState.LOADED - assert client.driver.controller.async_restore_nvm.call_count == 1 - assert len(events) == 2 - assert events[0].data["progress"] == 0.25 - assert events[1].data["progress"] == 0.75 + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -3662,6 +3673,20 @@ async def test_reconfigure_migrate_low_sdk_version( ] ], ) +@pytest.mark.parametrize( + ( + "reset_server_version_side_effect", + "reset_unique_id", + "restore_server_version_side_effect", + "final_unique_id", + ), + [ + (None, "4321", None, "8765"), + (aiohttp.ClientError("Boom"), "1234", None, "8765"), + (None, "4321", aiohttp.ClientError("Boom"), "5678"), + (aiohttp.ClientError("Boom"), "1234", aiohttp.ClientError("Boom"), "5678"), + ], +) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client, @@ -3671,8 +3696,16 @@ async def test_reconfigure_migrate_with_addon( restart_addon, set_addon_options, get_addon_discovery_info, + get_server_version: AsyncMock, + reset_server_version_side_effect: Exception | None, + reset_unique_id: str, + restore_server_version_side_effect: Exception | None, + final_unique_id: str, ) -> None: """Test migration flow with add-on.""" + get_server_version.side_effect = reset_server_version_side_effect + version_info = get_server_version.return_value + version_info.home_id = 4321 entry = integration assert client.connect.call_count == 1 hass.config_entries.async_update_entry( @@ -3696,6 +3729,13 @@ async def test_reconfigure_migrate_with_addon( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3746,6 +3786,175 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "instruct_unplug" assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == reset_unique_id + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "choose_serial_port" + assert result["data_schema"].schema[CONF_USB_PATH] + + # Reset side effect before starting the add-on. + get_server_version.side_effect = None + version_info.home_id = 5678 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + assert set_addon_options.call_args == call( + "core_zwave_js", AddonsOptions(config={"device": "/test"}) + ) + + await hass.async_block_till_done() + + assert restart_addon.call_args == call("core_zwave_js") + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert entry.unique_id == "5678" + get_server_version.side_effect = restore_server_version_side_effect + version_info.home_id = 8765 + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 + + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "migration_successful" + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == final_unique_id + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_reconfigure_migrate_reset_driver_ready_timeout( + hass: HomeAssistant, + client, + supervisor, + integration, + addon_running, + restart_addon, + set_addon_options, + get_addon_discovery_info, + get_server_version: AsyncMock, +) -> None: + """Test migration flow with driver ready timeout after controller reset.""" + version_info = get_server_version.return_value + version_info.home_id = 4321 + entry = integration + assert client.connect.call_count == 1 + hass.config_entries.async_update_entry( + entry, + unique_id="1234", + data={ + "url": "ws://localhost:3000", + "use_addon": True, + "usb_path": "/dev/ttyUSB0", + }, + ) + + async def mock_backup_nvm_raw(): + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm backup progress", {"bytesRead": 100, "total": 200} + ) + return b"test_nvm_data" + + client.driver.controller.async_backup_nvm_raw = AsyncMock( + side_effect=mock_backup_nvm_raw + ) + + async def mock_reset_controller(): + await asyncio.sleep(0) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + + async def mock_restore_nvm(data: bytes): + client.driver.controller.emit( + "nvm convert progress", + {"event": "nvm convert progress", "bytesRead": 100, "total": 200}, + ) + await asyncio.sleep(0) + client.driver.controller.emit( + "nvm restore progress", + {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, + ) + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm) + + events = async_capture_events( + hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE + ) + + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_migrate"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "intent_migrate" + + with ( + patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ), + patch("pathlib.Path.write_bytes") as mock_file, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "backup_nvm" + + await hass.async_block_till_done() + assert client.driver.controller.async_backup_nvm_raw.call_count == 1 + assert mock_file.call_count == 1 + assert len(events) == 1 + assert events[0].data["progress"] == 0.5 + events.clear() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "instruct_unplug" + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry.unique_id == "4321" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -3770,6 +3979,8 @@ async def test_reconfigure_migrate_with_addon( assert restart_addon.call_args == call("core_zwave_js") + version_info.home_id = 5678 + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.SHOW_PROGRESS @@ -3788,9 +3999,10 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == "/test" - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert entry.unique_id == "5678" @pytest.mark.parametrize( @@ -3806,13 +4018,12 @@ async def test_reconfigure_migrate_with_addon( ] ], ) -async def test_reconfigure_migrate_driver_ready_timeout( +async def test_reconfigure_migrate_restore_driver_ready_timeout( hass: HomeAssistant, client, supervisor, integration, addon_running, - driver_ready_timeout: None, restart_addon, set_addon_options, get_addon_discovery_info, @@ -3841,6 +4052,13 @@ async def test_reconfigure_migrate_driver_ready_timeout( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + async def mock_restore_nvm(data: bytes): client.driver.controller.emit( "nvm convert progress", @@ -3912,21 +4130,25 @@ async def test_reconfigure_migrate_driver_ready_timeout( assert restart_addon.call_args == call("core_zwave_js") - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + with patch( + ("homeassistant.components.zwave_js.config_flow.DRIVER_READY_TIMEOUT"), + new=0, + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] is FlowResultType.SHOW_PROGRESS - assert result["step_id"] == "restore_nvm" - assert client.connect.call_count == 2 + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "restore_nvm" + assert client.connect.call_count == 2 - await hass.async_block_till_done() - assert client.connect.call_count == 3 - assert entry.state is config_entries.ConfigEntryState.LOADED - assert client.driver.controller.async_restore_nvm.call_count == 1 - assert len(events) == 2 - assert events[0].data["progress"] == 0.25 - assert events[1].data["progress"] == 0.75 + await hass.async_block_till_done() + assert client.connect.call_count == 3 + assert entry.state is config_entries.ConfigEntryState.LOADED + assert client.driver.controller.async_restore_nvm.call_count == 1 + assert len(events) == 2 + assert events[0].data["progress"] == 0.25 + assert events[1].data["progress"] == 0.75 - result = await hass.config_entries.flow.async_configure(result["flow_id"]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" @@ -4045,9 +4267,13 @@ async def test_reconfigure_migrate_start_addon_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) - client.driver.controller.async_restore_nvm = AsyncMock( - side_effect=FailedCommand("test_error", "unknown_error") - ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) result = await entry.start_reconfigure_flow(hass) @@ -4140,6 +4366,13 @@ async def test_reconfigure_migrate_restore_failure( client.driver.controller.async_backup_nvm_raw = AsyncMock( side_effect=mock_backup_nvm_raw ) + + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) client.driver.controller.async_restore_nvm = AsyncMock( side_effect=FailedCommand("test_error", "unknown_error") ) @@ -4292,7 +4525,7 @@ async def test_get_driver_failure_instruct_unplug( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "reset_failed" + assert result["reason"] == "config_entry_not_loaded" async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: @@ -4358,6 +4591,13 @@ async def test_choose_serial_port_usb_ports_failure( side_effect=mock_backup_nvm_raw ) + async def mock_reset_controller(): + client.driver.emit( + "driver ready", {"event": "driver ready", "source": "driver"} + ) + + client.driver.async_hard_reset = AsyncMock(side_effect=mock_reset_controller) + result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU From 7d96a2a6201461e8b6f3230f2eee6e2b326fea53 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 19 May 2025 12:46:38 +0200 Subject: [PATCH 0611/1175] [ci] Skip step if coverage is skipped (#145202) --- .github/workflows/ci.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4a202a0c9d5..af0bdc5c2df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1023,6 +1023,7 @@ jobs: overwrite: true - name: Beautify test results # For easier identification of parsing errors + if: needs.info.outputs.skip_coverage != 'true' run: | xmllint --format "junit.xml" > "junit.xml-tmp" mv "junit.xml-tmp" "junit.xml" @@ -1163,6 +1164,7 @@ jobs: overwrite: true - name: Beautify test results # For easier identification of parsing errors + if: needs.info.outputs.skip_coverage != 'true' run: | xmllint --format "junit.xml" > "junit.xml-tmp" mv "junit.xml-tmp" "junit.xml" @@ -1305,6 +1307,7 @@ jobs: overwrite: true - name: Beautify test results # For easier identification of parsing errors + if: needs.info.outputs.skip_coverage != 'true' run: | xmllint --format "junit.xml" > "junit.xml-tmp" mv "junit.xml-tmp" "junit.xml" @@ -1457,6 +1460,7 @@ jobs: overwrite: true - name: Beautify test results # For easier identification of parsing errors + if: needs.info.outputs.skip_coverage != 'true' run: | xmllint --format "junit.xml" > "junit.xml-tmp" mv "junit.xml-tmp" "junit.xml" From 241c89e885159701036d33f69676eea17e8b2f72 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 19 May 2025 13:11:07 +0200 Subject: [PATCH 0612/1175] Bump go2rtc-client to 0.1.3b0 (#145192) --- homeassistant/components/go2rtc/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index 07dbd3bd29b..09f7b3fd74c 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -8,6 +8,6 @@ "integration_type": "system", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["go2rtc-client==0.1.2"], + "requirements": ["go2rtc-client==0.1.3b0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63622cb8d81..fcb23c346a2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ cronsim==2.6 cryptography==45.0.1 dbus-fast==2.43.0 fnv-hash-fast==1.5.0 -go2rtc-client==0.1.2 +go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 habluetooth==3.48.2 hass-nabucasa==0.101.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4700667f63e..8de174fa84e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,7 +1023,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.2 +go2rtc-client==0.1.3b0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test.txt b/requirements_test.txt index aa989cdd0ed..40349402c4d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ astroid==3.3.10 coverage==7.6.12 freezegun==1.5.1 -go2rtc-client==0.1.2 +go2rtc-client==0.1.3b0 license-expression==30.4.1 mock-open==1.4.0 mypy-dev==1.16.0a8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1ee3fe8dd6..feba9bc8149 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ gios==6.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.2 +go2rtc-client==0.1.3b0 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 71e671ad9ac..5ca638ef487 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 78e3a2d0c65aac0ab9ba25a8101cea106d50f43c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 13:12:17 +0200 Subject: [PATCH 0613/1175] Mark all _CLASS_MATCH as mandatory in pylint plugin (#145200) --- pylint/plugins/hass_enforce_type_hints.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index c5a79d166e2..27ea23b0df3 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -549,6 +549,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="async_step_*", arg_types={}, return_type="FlowResult", + mandatory=True, ), ], ), @@ -561,6 +562,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 0: "ConfigEntry", }, return_type="OptionsFlow", + mandatory=True, ), TypeHintMatch( function_name="async_step_dhcp", @@ -568,6 +570,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "DhcpServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_hassio", @@ -575,6 +578,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "HassioServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_homekit", @@ -582,6 +586,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "ZeroconfServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_mqtt", @@ -589,6 +594,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "MqttServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_reauth", @@ -596,6 +602,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "Mapping[str, Any]", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_ssdp", @@ -603,6 +610,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "SsdpServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_usb", @@ -610,6 +618,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "UsbServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_zeroconf", @@ -617,11 +626,13 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "ZeroconfServiceInfo", }, return_type="ConfigFlowResult", + mandatory=True, ), TypeHintMatch( function_name="async_step_*", arg_types={}, return_type="ConfigFlowResult", + mandatory=True, ), ], ), @@ -632,6 +643,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="async_step_*", arg_types={}, return_type="ConfigFlowResult", + mandatory=True, ), ], ), @@ -642,6 +654,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="async_step_*", arg_types={}, return_type="SubentryFlowResult", + mandatory=True, ), ], ), From 8b22ab93c16cc482319d813f82043e0367c94e9a Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 19 May 2025 13:20:02 +0200 Subject: [PATCH 0614/1175] Bump velbusaio to 2025.5.0 (#145198) --- 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 2c05ae0301b..d64a1361987 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -14,7 +14,7 @@ "velbus-protocol" ], "quality_scale": "bronze", - "requirements": ["velbus-aio==2025.4.2"], + "requirements": ["velbus-aio==2025.5.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 8de174fa84e..1d00393b52c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3016,7 +3016,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.4.2 +velbus-aio==2025.5.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index feba9bc8149..4dbbf33fecd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2439,7 +2439,7 @@ vallox-websocket-api==5.3.0 vehicle==2.2.2 # homeassistant.components.velbus -velbus-aio==2025.4.2 +velbus-aio==2025.5.0 # homeassistant.components.venstar venstarcolortouch==0.19 From 7d25f68fa5e5d84651841074febb38f34d2f4d21 Mon Sep 17 00:00:00 2001 From: wuede Date: Mon, 19 May 2025 13:21:19 +0200 Subject: [PATCH 0615/1175] update pyatmo to version 9.2.0 (#145203) --- homeassistant/components/netatmo/data_handler.py | 2 +- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 283ccc3740e..0164d673619 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -236,7 +236,7 @@ class NetatmoDataHandler: **self.publisher[signal_name].kwargs ) - except (pyatmo.NoDevice, pyatmo.ApiError) as err: + except (pyatmo.NoDeviceError, pyatmo.ApiError) as err: _LOGGER.debug(err) has_error = True diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 84c8be1d0be..13beb1330e4 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pyatmo"], - "requirements": ["pyatmo==9.0.0"] + "requirements": ["pyatmo==9.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1d00393b52c..830d9c9220a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.0.0 +pyatmo==9.2.0 # homeassistant.components.apple_tv pyatv==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dbbf33fecd..d9afe540d04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1519,7 +1519,7 @@ pyasuswrt==0.1.21 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==9.0.0 +pyatmo==9.2.0 # homeassistant.components.apple_tv pyatv==0.16.0 From 484a54775840c31c5238af3be538ca7f0bc6ed80 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 14:55:48 +0300 Subject: [PATCH 0616/1175] Fix pylance warning on SnapshotAssertion import (#145206) --- tests/common.py | 2 +- tests/components/acaia/test_binary_sensor.py | 2 +- tests/components/acaia/test_button.py | 2 +- tests/components/acaia/test_diagnostics.py | 2 +- tests/components/acaia/test_init.py | 2 +- tests/components/acaia/test_sensor.py | 2 +- tests/components/accuweather/test_diagnostics.py | 2 +- tests/components/accuweather/test_sensor.py | 2 +- tests/components/advantage_air/test_climate.py | 2 +- tests/components/advantage_air/test_switch.py | 2 +- tests/components/aemet/test_diagnostics.py | 2 +- tests/components/airgradient/test_button.py | 2 +- tests/components/airgradient/test_diagnostics.py | 2 +- tests/components/airgradient/test_init.py | 2 +- tests/components/airgradient/test_number.py | 2 +- tests/components/airgradient/test_select.py | 2 +- tests/components/airgradient/test_sensor.py | 2 +- tests/components/airgradient/test_switch.py | 2 +- tests/components/airgradient/test_update.py | 2 +- tests/components/airly/test_diagnostics.py | 2 +- tests/components/airly/test_sensor.py | 2 +- tests/components/airnow/test_diagnostics.py | 2 +- tests/components/airtouch5/test_cover.py | 2 +- tests/components/airvisual/test_diagnostics.py | 2 +- tests/components/airvisual_pro/test_diagnostics.py | 2 +- tests/components/airzone/test_diagnostics.py | 2 +- tests/components/airzone_cloud/test_diagnostics.py | 2 +- tests/components/ambient_station/test_diagnostics.py | 2 +- tests/components/analytics/test_analytics.py | 2 +- tests/components/analytics_insights/test_sensor.py | 2 +- tests/components/aosmith/test_diagnostics.py | 2 +- tests/components/apcupsd/test_binary_sensor.py | 2 +- tests/components/apcupsd/test_init.py | 2 +- tests/components/apcupsd/test_sensor.py | 2 +- tests/components/aquacell/test_sensor.py | 2 +- tests/components/arve/test_sensor.py | 2 +- tests/components/august/test_diagnostics.py | 2 +- tests/components/autarco/test_diagnostics.py | 2 +- tests/components/autarco/test_sensor.py | 2 +- tests/components/axis/test_binary_sensor.py | 2 +- tests/components/axis/test_camera.py | 2 +- tests/components/axis/test_diagnostics.py | 2 +- tests/components/axis/test_hub.py | 2 +- tests/components/axis/test_light.py | 2 +- tests/components/axis/test_switch.py | 2 +- tests/components/backup/test_backup.py | 2 +- tests/components/backup/test_diagnostics.py | 2 +- tests/components/backup/test_onboarding.py | 2 +- tests/components/backup/test_sensors.py | 2 +- tests/components/backup/test_store.py | 2 +- tests/components/backup/test_websocket.py | 2 +- tests/components/balboa/test_binary_sensor.py | 2 +- tests/components/balboa/test_climate.py | 2 +- tests/components/balboa/test_event.py | 2 +- tests/components/balboa/test_fan.py | 2 +- tests/components/balboa/test_light.py | 2 +- tests/components/balboa/test_select.py | 2 +- tests/components/balboa/test_switch.py | 2 +- tests/components/balboa/test_time.py | 2 +- tests/components/bang_olufsen/test_diagnostics.py | 2 +- tests/components/blink/test_diagnostics.py | 2 +- tests/components/bluemaestro/test_sensor.py | 2 +- tests/components/blueprint/test_importer.py | 2 +- tests/components/braviatv/test_diagnostics.py | 2 +- tests/components/brother/test_diagnostics.py | 2 +- tests/components/brother/test_sensor.py | 2 +- tests/components/bsblan/test_diagnostics.py | 2 +- tests/components/cambridge_audio/test_diagnostics.py | 2 +- tests/components/cambridge_audio/test_init.py | 2 +- tests/components/cambridge_audio/test_media_browser.py | 2 +- tests/components/cambridge_audio/test_select.py | 2 +- tests/components/cambridge_audio/test_switch.py | 2 +- tests/components/ccm15/test_diagnostics.py | 2 +- tests/components/coinbase/test_diagnostics.py | 2 +- tests/components/comelit/test_climate.py | 2 +- tests/components/comelit/test_cover.py | 2 +- tests/components/comelit/test_diagnostics.py | 2 +- tests/components/comelit/test_humidifier.py | 2 +- tests/components/comelit/test_light.py | 2 +- tests/components/comelit/test_sensor.py | 2 +- tests/components/comelit/test_switch.py | 2 +- tests/components/conversation/test_default_agent.py | 2 +- tests/components/cookidoo/test_button.py | 2 +- tests/components/cookidoo/test_diagnostics.py | 2 +- tests/components/cpuspeed/test_diagnostics.py | 2 +- tests/components/deconz/test_alarm_control_panel.py | 2 +- tests/components/deconz/test_binary_sensor.py | 2 +- tests/components/deconz/test_button.py | 2 +- tests/components/deconz/test_climate.py | 2 +- tests/components/deconz/test_cover.py | 2 +- tests/components/deconz/test_diagnostics.py | 2 +- tests/components/deconz/test_fan.py | 2 +- tests/components/deconz/test_hub.py | 2 +- tests/components/deconz/test_light.py | 2 +- tests/components/deconz/test_number.py | 2 +- tests/components/deconz/test_scene.py | 2 +- tests/components/deconz/test_select.py | 2 +- tests/components/deconz/test_sensor.py | 2 +- tests/components/discovergy/test_diagnostics.py | 2 +- tests/components/discovergy/test_sensor.py | 2 +- tests/components/drop_connect/common.py | 2 +- tests/components/drop_connect/test_binary_sensor.py | 2 +- tests/components/drop_connect/test_sensor.py | 2 +- tests/components/dsmr_reader/test_diagnostics.py | 2 +- tests/components/ecovacs/test_binary_sensor.py | 2 +- tests/components/ecovacs/test_button.py | 2 +- tests/components/ecovacs/test_event.py | 2 +- tests/components/ecovacs/test_init.py | 2 +- tests/components/ecovacs/test_lawn_mower.py | 2 +- tests/components/ecovacs/test_number.py | 2 +- tests/components/ecovacs/test_select.py | 2 +- tests/components/ecovacs/test_sensor.py | 2 +- tests/components/ecovacs/test_switch.py | 2 +- tests/components/elmax/test_alarm_control_panel.py | 2 +- tests/components/elmax/test_binary_sensor.py | 2 +- tests/components/elmax/test_cover.py | 2 +- tests/components/elmax/test_switch.py | 2 +- tests/components/environment_canada/test_diagnostics.py | 2 +- tests/components/esphome/test_climate.py | 2 +- tests/components/esphome/test_diagnostics.py | 2 +- tests/components/evohome/test_climate.py | 2 +- tests/components/evohome/test_water_heater.py | 2 +- tests/components/fastdotcom/test_diagnostics.py | 2 +- tests/components/fibaro/test_diagnostics.py | 2 +- tests/components/flo/test_init.py | 2 +- tests/components/forecast_solar/test_diagnostics.py | 2 +- tests/components/forecast_solar/test_init.py | 2 +- tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritzbox/test_binary_sensor.py | 2 +- tests/components/fritzbox/test_button.py | 2 +- tests/components/fritzbox/test_climate.py | 2 +- tests/components/fritzbox/test_cover.py | 2 +- tests/components/fritzbox/test_light.py | 2 +- tests/components/fritzbox/test_sensor.py | 2 +- tests/components/fritzbox/test_switch.py | 2 +- tests/components/fronius/test_diagnostics.py | 2 +- tests/components/fronius/test_sensor.py | 2 +- tests/components/fujitsu_fglair/test_climate.py | 2 +- tests/components/fujitsu_fglair/test_sensor.py | 2 +- tests/components/fyta/test_binary_sensor.py | 2 +- tests/components/fyta/test_diagnostics.py | 2 +- tests/components/fyta/test_image.py | 2 +- tests/components/fyta/test_sensor.py | 2 +- tests/components/garages_amsterdam/test_binary_sensor.py | 2 +- tests/components/garages_amsterdam/test_sensor.py | 2 +- tests/components/gdacs/test_diagnostics.py | 2 +- tests/components/geniushub/test_binary_sensor.py | 2 +- tests/components/geniushub/test_climate.py | 2 +- tests/components/geniushub/test_sensor.py | 2 +- tests/components/geniushub/test_switch.py | 2 +- tests/components/geonetnz_quakes/test_diagnostics.py | 2 +- tests/components/gios/test_diagnostics.py | 2 +- tests/components/gios/test_sensor.py | 2 +- tests/components/glances/test_sensor.py | 2 +- tests/components/goodwe/test_diagnostics.py | 2 +- tests/components/google_assistant/test_diagnostics.py | 2 +- tests/components/hassio/test_backup.py | 2 +- tests/components/hassio/test_config.py | 2 +- tests/components/hassio/test_websocket_api.py | 2 +- tests/components/homekit_controller/test_diagnostics.py | 2 +- tests/components/honeywell/test_diagnostics.py | 2 +- tests/components/husqvarna_automower/test_binary_sensor.py | 2 +- tests/components/husqvarna_automower/test_button.py | 2 +- tests/components/husqvarna_automower/test_calendar.py | 2 +- tests/components/husqvarna_automower/test_device_tracker.py | 2 +- tests/components/husqvarna_automower/test_number.py | 2 +- tests/components/husqvarna_automower/test_sensor.py | 2 +- tests/components/husqvarna_automower/test_switch.py | 2 +- tests/components/igloohome/test_lock.py | 2 +- tests/components/igloohome/test_sensor.py | 2 +- tests/components/imgw_pib/test_diagnostics.py | 2 +- tests/components/imgw_pib/test_sensor.py | 2 +- tests/components/immich/test_diagnostics.py | 2 +- tests/components/immich/test_sensor.py | 2 +- tests/components/incomfort/test_binary_sensor.py | 2 +- tests/components/incomfort/test_climate.py | 2 +- tests/components/incomfort/test_sensor.py | 2 +- tests/components/incomfort/test_water_heater.py | 2 +- tests/components/intellifire/test_binary_sensor.py | 2 +- tests/components/intellifire/test_climate.py | 2 +- tests/components/intellifire/test_sensor.py | 2 +- tests/components/ipp/test_diagnostics.py | 2 +- tests/components/iqvia/test_diagnostics.py | 2 +- tests/components/israel_rail/test_sensor.py | 2 +- tests/components/jellyfin/test_diagnostics.py | 2 +- tests/components/knocki/test_event.py | 2 +- tests/components/knx/test_diagnostic.py | 2 +- tests/components/lamarzocco/test_binary_sensor.py | 2 +- tests/components/lamarzocco/test_button.py | 2 +- tests/components/lamarzocco/test_calendar.py | 2 +- tests/components/lamarzocco/test_diagnostics.py | 2 +- tests/components/lamarzocco/test_init.py | 2 +- tests/components/lamarzocco/test_number.py | 2 +- tests/components/lamarzocco/test_select.py | 2 +- tests/components/lamarzocco/test_sensor.py | 2 +- tests/components/lamarzocco/test_switch.py | 2 +- tests/components/lamarzocco/test_update.py | 2 +- tests/components/lametric/test_diagnostics.py | 2 +- tests/components/landisgyr_heat_meter/test_sensor.py | 2 +- tests/components/lektrico/test_binary_sensor.py | 2 +- tests/components/lektrico/test_button.py | 2 +- tests/components/lektrico/test_init.py | 2 +- tests/components/lektrico/test_number.py | 2 +- tests/components/lektrico/test_select.py | 2 +- tests/components/lektrico/test_sensor.py | 2 +- tests/components/lektrico/test_switch.py | 2 +- tests/components/letpot/test_binary_sensor.py | 2 +- tests/components/letpot/test_sensor.py | 2 +- tests/components/letpot/test_switch.py | 2 +- tests/components/letpot/test_time.py | 2 +- tests/components/lg_thinq/test_climate.py | 2 +- tests/components/lg_thinq/test_event.py | 2 +- tests/components/lg_thinq/test_number.py | 2 +- tests/components/lg_thinq/test_sensor.py | 2 +- tests/components/linear_garage_door/test_cover.py | 2 +- tests/components/linear_garage_door/test_diagnostics.py | 2 +- tests/components/linear_garage_door/test_light.py | 2 +- tests/components/linkplay/test_diagnostics.py | 2 +- tests/components/madvr/test_binary_sensor.py | 2 +- tests/components/madvr/test_diagnostics.py | 2 +- tests/components/madvr/test_remote.py | 2 +- tests/components/madvr/test_sensor.py | 2 +- tests/components/mastodon/test_diagnostics.py | 2 +- tests/components/matter/common.py | 2 +- tests/components/matter/test_binary_sensor.py | 2 +- tests/components/matter/test_button.py | 2 +- tests/components/matter/test_climate.py | 2 +- tests/components/matter/test_cover.py | 2 +- tests/components/matter/test_event.py | 2 +- tests/components/matter/test_fan.py | 2 +- tests/components/matter/test_light.py | 2 +- tests/components/matter/test_lock.py | 2 +- tests/components/matter/test_number.py | 2 +- tests/components/matter/test_select.py | 2 +- tests/components/matter/test_sensor.py | 2 +- tests/components/matter/test_switch.py | 2 +- tests/components/matter/test_vacuum.py | 2 +- tests/components/matter/test_valve.py | 2 +- tests/components/matter/test_water_heater.py | 2 +- tests/components/mealie/test_diagnostics.py | 2 +- tests/components/mealie/test_init.py | 2 +- tests/components/mealie/test_services.py | 2 +- tests/components/media_extractor/test_init.py | 2 +- tests/components/melcloud/test_diagnostics.py | 2 +- tests/components/melissa/test_climate.py | 2 +- tests/components/miele/test_binary_sensor.py | 2 +- tests/components/miele/test_button.py | 2 +- tests/components/miele/test_climate.py | 2 +- tests/components/miele/test_diagnostics.py | 2 +- tests/components/miele/test_fan.py | 2 +- tests/components/miele/test_init.py | 2 +- tests/components/miele/test_light.py | 2 +- tests/components/miele/test_sensor.py | 2 +- tests/components/miele/test_switch.py | 2 +- tests/components/miele/test_vacuum.py | 2 +- tests/components/minecraft_server/test_binary_sensor.py | 2 +- tests/components/minecraft_server/test_diagnostics.py | 2 +- tests/components/minecraft_server/test_sensor.py | 2 +- tests/components/modern_forms/test_diagnostics.py | 2 +- tests/components/moehlenhoff_alpha2/test_binary_sensor.py | 2 +- tests/components/moehlenhoff_alpha2/test_button.py | 2 +- tests/components/moehlenhoff_alpha2/test_climate.py | 2 +- tests/components/moehlenhoff_alpha2/test_sensor.py | 2 +- tests/components/monarch_money/test_sensor.py | 2 +- tests/components/monzo/test_sensor.py | 2 +- tests/components/motionblinds_ble/test_diagnostics.py | 2 +- tests/components/music_assistant/common.py | 2 +- tests/components/music_assistant/test_actions.py | 2 +- tests/components/music_assistant/test_media_player.py | 2 +- tests/components/myuplink/test_binary_sensor.py | 2 +- tests/components/myuplink/test_diagnostics.py | 2 +- tests/components/myuplink/test_init.py | 2 +- tests/components/myuplink/test_number.py | 2 +- tests/components/myuplink/test_select.py | 2 +- tests/components/myuplink/test_sensor.py | 2 +- tests/components/myuplink/test_switch.py | 2 +- tests/components/nam/test_diagnostics.py | 2 +- tests/components/nam/test_sensor.py | 2 +- tests/components/nanoleaf/test_light.py | 2 +- tests/components/nest/test_diagnostics.py | 2 +- tests/components/netatmo/common.py | 2 +- tests/components/netatmo/test_binary_sensor.py | 2 +- tests/components/netatmo/test_button.py | 2 +- tests/components/netatmo/test_camera.py | 2 +- tests/components/netatmo/test_climate.py | 2 +- tests/components/netatmo/test_cover.py | 2 +- tests/components/netatmo/test_diagnostics.py | 2 +- tests/components/netatmo/test_fan.py | 2 +- tests/components/netatmo/test_init.py | 2 +- tests/components/netatmo/test_light.py | 2 +- tests/components/netatmo/test_select.py | 2 +- tests/components/netatmo/test_sensor.py | 2 +- tests/components/netatmo/test_switch.py | 2 +- tests/components/nexia/test_diagnostics.py | 2 +- tests/components/nextdns/test_binary_sensor.py | 2 +- tests/components/nextdns/test_button.py | 2 +- tests/components/nextdns/test_diagnostics.py | 2 +- tests/components/nextdns/test_sensor.py | 2 +- tests/components/nextdns/test_switch.py | 2 +- tests/components/nibe_heatpump/test_climate.py | 2 +- tests/components/nibe_heatpump/test_coordinator.py | 2 +- tests/components/nibe_heatpump/test_number.py | 2 +- tests/components/nice_go/test_cover.py | 2 +- tests/components/nice_go/test_diagnostics.py | 2 +- tests/components/nice_go/test_light.py | 2 +- tests/components/niko_home_control/test_cover.py | 2 +- tests/components/niko_home_control/test_light.py | 2 +- tests/components/nuki/test_binary_sensor.py | 2 +- tests/components/nuki/test_lock.py | 2 +- tests/components/nuki/test_sensor.py | 2 +- tests/components/nws/test_diagnostics.py | 2 +- tests/components/nyt_games/test_init.py | 2 +- tests/components/nyt_games/test_sensor.py | 2 +- tests/components/omnilogic/test_sensor.py | 2 +- tests/components/omnilogic/test_switch.py | 2 +- tests/components/ondilo_ico/test_init.py | 2 +- tests/components/ondilo_ico/test_sensor.py | 2 +- tests/components/onedrive/test_diagnostics.py | 2 +- tests/components/onedrive/test_init.py | 2 +- tests/components/onedrive/test_sensor.py | 2 +- tests/components/onvif/test_diagnostics.py | 2 +- tests/components/opensky/test_sensor.py | 2 +- tests/components/openweathermap/test_sensor.py | 2 +- tests/components/openweathermap/test_weather.py | 2 +- tests/components/osoenergy/test_water_heater.py | 2 +- tests/components/overkiz/test_diagnostics.py | 2 +- tests/components/overseerr/test_diagnostics.py | 2 +- tests/components/overseerr/test_event.py | 2 +- tests/components/overseerr/test_init.py | 2 +- tests/components/overseerr/test_sensor.py | 2 +- tests/components/overseerr/test_services.py | 2 +- tests/components/p1_monitor/test_init.py | 2 +- tests/components/palazzetti/test_button.py | 2 +- tests/components/palazzetti/test_climate.py | 2 +- tests/components/palazzetti/test_diagnostics.py | 2 +- tests/components/palazzetti/test_init.py | 2 +- tests/components/palazzetti/test_number.py | 2 +- tests/components/palazzetti/test_sensor.py | 2 +- tests/components/pegel_online/test_diagnostics.py | 2 +- tests/components/pglab/test_sensor.py | 2 +- tests/components/philips_js/test_diagnostics.py | 2 +- tests/components/ping/test_binary_sensor.py | 2 +- tests/components/ping/test_sensor.py | 2 +- tests/components/plaato/test_binary_sensor.py | 2 +- tests/components/plaato/test_sensor.py | 2 +- tests/components/plugwise/test_diagnostics.py | 2 +- tests/components/powerfox/test_diagnostics.py | 2 +- tests/components/powerfox/test_sensor.py | 2 +- tests/components/rainmachine/test_binary_sensor.py | 2 +- tests/components/rainmachine/test_button.py | 2 +- tests/components/rainmachine/test_diagnostics.py | 2 +- tests/components/rainmachine/test_select.py | 2 +- tests/components/rainmachine/test_sensor.py | 2 +- tests/components/rainmachine/test_switch.py | 2 +- tests/components/rehlko/test_sensor.py | 2 +- tests/components/renault/test_diagnostics.py | 2 +- tests/components/renault/test_services.py | 2 +- tests/components/ridwell/test_diagnostics.py | 2 +- tests/components/roku/test_diagnostics.py | 2 +- tests/components/rova/test_init.py | 2 +- tests/components/rova/test_sensor.py | 2 +- tests/components/russound_rio/test_diagnostics.py | 2 +- tests/components/russound_rio/test_init.py | 2 +- tests/components/sabnzbd/test_binary_sensor.py | 2 +- tests/components/sabnzbd/test_button.py | 2 +- tests/components/sabnzbd/test_number.py | 2 +- tests/components/sabnzbd/test_sensor.py | 2 +- tests/components/sanix/test_sensor.py | 2 +- tests/components/sensorpush_cloud/test_sensor.py | 2 +- tests/components/seventeentrack/test_services.py | 2 +- tests/components/shelly/test_binary_sensor.py | 2 +- tests/components/shelly/test_button.py | 2 +- tests/components/shelly/test_climate.py | 2 +- tests/components/shelly/test_event.py | 2 +- tests/components/shelly/test_number.py | 2 +- tests/components/shelly/test_sensor.py | 2 +- tests/components/simplefin/test_binary_sensor.py | 2 +- tests/components/simplefin/test_sensor.py | 2 +- tests/components/slide_local/test_button.py | 2 +- tests/components/slide_local/test_cover.py | 2 +- tests/components/slide_local/test_diagnostics.py | 2 +- tests/components/slide_local/test_init.py | 2 +- tests/components/slide_local/test_switch.py | 2 +- tests/components/sma/test_diagnostics.py | 2 +- tests/components/sma/test_sensor.py | 2 +- tests/components/smarty/test_binary_sensor.py | 2 +- tests/components/smarty/test_button.py | 2 +- tests/components/smarty/test_fan.py | 2 +- tests/components/smarty/test_init.py | 2 +- tests/components/smarty/test_sensor.py | 2 +- tests/components/smarty/test_switch.py | 2 +- tests/components/smlight/test_diagnostics.py | 2 +- tests/components/solarlog/test_diagnostics.py | 2 +- tests/components/solarlog/test_sensor.py | 2 +- tests/components/sonos/test_media_browser.py | 2 +- tests/components/sonos/test_media_player.py | 2 +- tests/components/spotify/test_diagnostics.py | 2 +- tests/components/spotify/test_media_browser.py | 2 +- tests/components/spotify/test_media_player.py | 2 +- tests/components/squeezebox/test_media_player.py | 2 +- tests/components/statistics/test_config_flow.py | 2 +- tests/components/streamlabswater/test_binary_sensor.py | 2 +- tests/components/streamlabswater/test_sensor.py | 2 +- tests/components/suez_water/test_sensor.py | 2 +- tests/components/swiss_public_transport/test_sensor.py | 2 +- tests/components/switchbot/test_diagnostics.py | 2 +- tests/components/switchbot_cloud/test_sensor.py | 2 +- tests/components/syncthru/test_binary_sensor.py | 2 +- tests/components/syncthru/test_diagnostics.py | 2 +- tests/components/syncthru/test_sensor.py | 2 +- tests/components/systemmonitor/test_diagnostics.py | 2 +- tests/components/tado/test_diagnostics.py | 2 +- tests/components/tailscale/test_diagnostics.py | 2 +- tests/components/tankerkoenig/test_binary_sensor.py | 2 +- tests/components/tankerkoenig/test_diagnostics.py | 2 +- tests/components/tankerkoenig/test_sensor.py | 2 +- tests/components/tasmota/test_sensor.py | 2 +- tests/components/technove/test_binary_sensor.py | 2 +- tests/components/technove/test_sensor.py | 2 +- tests/components/tedee/test_binary_sensor.py | 2 +- tests/components/tedee/test_diagnostics.py | 2 +- tests/components/tedee/test_init.py | 2 +- tests/components/tedee/test_sensor.py | 2 +- tests/components/tesla_fleet/__init__.py | 2 +- tests/components/tesla_fleet/test_button.py | 2 +- tests/components/tesla_fleet/test_cover.py | 2 +- tests/components/tesla_fleet/test_lock.py | 2 +- tests/components/tesla_fleet/test_media_player.py | 2 +- tests/components/tesla_fleet/test_number.py | 2 +- tests/components/tesla_fleet/test_select.py | 2 +- tests/components/tesla_fleet/test_switch.py | 2 +- tests/components/tessie/common.py | 2 +- tests/components/tessie/test_binary_sensor.py | 2 +- tests/components/tessie/test_button.py | 2 +- tests/components/tessie/test_climate.py | 2 +- tests/components/tessie/test_cover.py | 2 +- tests/components/tessie/test_device_tracker.py | 2 +- tests/components/tessie/test_lock.py | 2 +- tests/components/tessie/test_media_player.py | 2 +- tests/components/tessie/test_number.py | 2 +- tests/components/tessie/test_select.py | 2 +- tests/components/tessie/test_sensor.py | 2 +- tests/components/tessie/test_switch.py | 2 +- tests/components/tessie/test_update.py | 2 +- tests/components/threshold/test_config_flow.py | 2 +- tests/components/tile/test_binary_sensor.py | 2 +- tests/components/tile/test_device_tracker.py | 2 +- tests/components/tile/test_diagnostics.py | 2 +- tests/components/tile/test_init.py | 2 +- tests/components/totalconnect/test_alarm_control_panel.py | 2 +- tests/components/totalconnect/test_binary_sensor.py | 2 +- tests/components/totalconnect/test_button.py | 2 +- tests/components/tplink/__init__.py | 2 +- tests/components/traccar_server/test_diagnostics.py | 2 +- tests/components/tractive/test_binary_sensor.py | 2 +- tests/components/tractive/test_device_tracker.py | 2 +- tests/components/tractive/test_diagnostics.py | 2 +- tests/components/tractive/test_sensor.py | 2 +- tests/components/tractive/test_switch.py | 2 +- tests/components/twinkly/test_diagnostics.py | 2 +- tests/components/twinkly/test_light.py | 2 +- tests/components/twinkly/test_select.py | 2 +- tests/components/unifi/test_button.py | 2 +- tests/components/unifi/test_device_tracker.py | 2 +- tests/components/unifi/test_image.py | 2 +- tests/components/unifi/test_sensor.py | 2 +- tests/components/unifi/test_switch.py | 2 +- tests/components/unifi/test_update.py | 2 +- tests/components/utility_meter/test_diagnostics.py | 2 +- tests/components/v2c/test_diagnostics.py | 2 +- tests/components/v2c/test_sensor.py | 2 +- tests/components/velbus/test_diagnostics.py | 2 +- tests/components/vesync/test_diagnostics.py | 2 +- tests/components/vesync/test_fan.py | 2 +- tests/components/vesync/test_light.py | 2 +- tests/components/vesync/test_sensor.py | 2 +- tests/components/vesync/test_switch.py | 2 +- tests/components/vodafone_station/test_button.py | 2 +- tests/components/vodafone_station/test_device_tracker.py | 2 +- tests/components/vodafone_station/test_diagnostics.py | 2 +- tests/components/vodafone_station/test_sensor.py | 2 +- tests/components/waqi/test_sensor.py | 2 +- tests/components/watttime/test_diagnostics.py | 2 +- tests/components/weatherflow_cloud/test_sensor.py | 2 +- tests/components/weatherflow_cloud/test_weather.py | 2 +- tests/components/weheat/test_binary_sensor.py | 2 +- tests/components/weheat/test_sensor.py | 2 +- tests/components/whirlpool/__init__.py | 2 +- tests/components/whirlpool/test_binary_sensor.py | 2 +- tests/components/whirlpool/test_climate.py | 2 +- tests/components/whirlpool/test_diagnostics.py | 2 +- tests/components/whirlpool/test_sensor.py | 2 +- tests/components/withings/test_diagnostics.py | 2 +- tests/components/withings/test_init.py | 2 +- tests/components/withings/test_sensor.py | 2 +- tests/components/wiz/test_diagnostics.py | 2 +- tests/components/wmspro/test_button.py | 2 +- tests/components/wmspro/test_cover.py | 2 +- tests/components/wmspro/test_diagnostics.py | 2 +- tests/components/wmspro/test_init.py | 2 +- tests/components/wmspro/test_light.py | 2 +- tests/components/wmspro/test_scene.py | 2 +- tests/components/wolflink/test_sensor.py | 2 +- tests/components/wyoming/test_conversation.py | 2 +- tests/components/wyoming/test_stt.py | 2 +- tests/components/wyoming/test_tts.py | 2 +- tests/components/yale/test_binary_sensor.py | 2 +- tests/components/yale/test_diagnostics.py | 2 +- tests/components/yale/test_lock.py | 2 +- tests/components/yale/test_sensor.py | 2 +- tests/components/youless/test_sensor.py | 2 +- tests/components/youtube/test_diagnostics.py | 2 +- tests/components/youtube/test_sensor.py | 2 +- tests/components/zeversolar/test_diagnostics.py | 2 +- tests/helpers/test_template.py | 2 +- tests/non_packaged_scripts/test_alexa_locales.py | 2 +- 516 files changed, 516 insertions(+), 516 deletions(-) diff --git a/tests/common.py b/tests/common.py index d439021a9df..a80027b2b7e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -32,7 +32,7 @@ from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F4 from annotatedyaml import load_yaml_dict, loader as yaml_loader import attr import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader diff --git a/tests/components/acaia/test_binary_sensor.py b/tests/components/acaia/test_binary_sensor.py index a7aa7034d8d..a03e18b40bc 100644 --- a/tests/components/acaia/test_binary_sensor.py +++ b/tests/components/acaia/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/acaia/test_button.py b/tests/components/acaia/test_button.py index f68f85e253d..171db32913d 100644 --- a/tests/components/acaia/test_button.py +++ b/tests/components/acaia/test_button.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ( diff --git a/tests/components/acaia/test_diagnostics.py b/tests/components/acaia/test_diagnostics.py index 77f6306b068..c628729ec66 100644 --- a/tests/components/acaia/test_diagnostics.py +++ b/tests/components/acaia/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Acaia integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/acaia/test_init.py b/tests/components/acaia/test_init.py index 8ad988d3b9b..d035630af56 100644 --- a/tests/components/acaia/test_init.py +++ b/tests/components/acaia/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.acaia.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/acaia/test_sensor.py b/tests/components/acaia/test_sensor.py index 2f5a851121c..79073937511 100644 --- a/tests/components/acaia/test_sensor.py +++ b/tests/components/acaia/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import PERCENTAGE, Platform from homeassistant.core import HomeAssistant, State diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py index bc97ae1fe14..3f8b54c1a10 100644 --- a/tests/components/accuweather/test_diagnostics.py +++ b/tests/components/accuweather/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 37ebe260f39..87737c2f40c 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -6,7 +6,7 @@ from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError from aiohttp.client_exceptions import ClientConnectorError from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.accuweather.const import ( UPDATE_INTERVAL_DAILY_FORECAST, diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index fc9aaade634..69094a80d30 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from advantage_air import ApiError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.advantage_air.climate import ADVANTAGE_AIR_MYAUTO from homeassistant.components.climate import ( diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index ecc652b3d9e..ea0bd558c8f 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/aemet/test_diagnostics.py b/tests/components/aemet/test_diagnostics.py index 6d007dd0465..a51d95f446e 100644 --- a/tests/components/aemet/test_diagnostics.py +++ b/tests/components/aemet/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.aemet.const import DOMAIN diff --git a/tests/components/airgradient/test_button.py b/tests/components/airgradient/test_button.py index 2440669b6e8..51fbd87ba67 100644 --- a/tests/components/airgradient/test_button.py +++ b/tests/components/airgradient/test_button.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS diff --git a/tests/components/airgradient/test_diagnostics.py b/tests/components/airgradient/test_diagnostics.py index 34a9bb7aab2..e8fb2581a99 100644 --- a/tests/components/airgradient/test_diagnostics.py +++ b/tests/components/airgradient/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index a121940f2bc..a253cb2888a 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/airgradient/test_number.py b/tests/components/airgradient/test_number.py index 2cbd72d033a..6fa1a7d3e07 100644 --- a/tests/components/airgradient/test_number.py +++ b/tests/components/airgradient/test_number.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.components.number import ( diff --git a/tests/components/airgradient/test_select.py b/tests/components/airgradient/test_select.py index b8ae2cefa4e..8782af4e46a 100644 --- a/tests/components/airgradient/test_select.py +++ b/tests/components/airgradient/test_select.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.components.select import ( diff --git a/tests/components/airgradient/test_sensor.py b/tests/components/airgradient/test_sensor.py index e3fed70839a..7679ba48546 100644 --- a/tests/components/airgradient/test_sensor.py +++ b/tests/components/airgradient/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from airgradient import AirGradientError, Measures from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/airgradient/test_switch.py b/tests/components/airgradient/test_switch.py index 475f38f554c..12b319379f6 100644 --- a/tests/components/airgradient/test_switch.py +++ b/tests/components/airgradient/test_switch.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from airgradient import AirGradientConnectionError, AirGradientError, Config from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN diff --git a/tests/components/airgradient/test_update.py b/tests/components/airgradient/test_update.py index 020a9a82a71..65614312b46 100644 --- a/tests/components/airgradient/test_update.py +++ b/tests/components/airgradient/test_update.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/airly/test_diagnostics.py b/tests/components/airly/test_diagnostics.py index 9a61bf5abee..13656f90a68 100644 --- a/tests/components/airly/test_diagnostics.py +++ b/tests/components/airly/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Airly diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 19f073496db..f45bbb65f6f 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -5,7 +5,7 @@ from http import HTTPStatus from unittest.mock import patch from airly.exceptions import AirlyError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/airnow/test_diagnostics.py b/tests/components/airnow/test_diagnostics.py index eb79dabe51a..5f3ccf5fbe0 100644 --- a/tests/components/airnow/test_diagnostics.py +++ b/tests/components/airnow/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/airtouch5/test_cover.py b/tests/components/airtouch5/test_cover.py index 57a344e8018..8c76ec4fb38 100644 --- a/tests/components/airtouch5/test_cover.py +++ b/tests/components/airtouch5/test_cover.py @@ -8,7 +8,7 @@ from airtouch5py.packets.zone_status import ( ZonePowerState, ZoneStatusZone, ) -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 0253f102c59..f5239ea7658 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -1,6 +1,6 @@ """Test AirVisual diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/airvisual_pro/test_diagnostics.py b/tests/components/airvisual_pro/test_diagnostics.py index 372b62eaf38..73893eb4bd2 100644 --- a/tests/components/airvisual_pro/test_diagnostics.py +++ b/tests/components/airvisual_pro/test_diagnostics.py @@ -1,6 +1,6 @@ """Test AirVisual Pro diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index bca75bca778..bd7bea13a48 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch from aioairzone.const import RAW_HVAC, RAW_VERSION, RAW_WEBSERVER -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.airzone.const import DOMAIN diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index d3e23fc7f4b..eb997ab1b73 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -14,7 +14,7 @@ from aioairzone_cloud.const import ( RAW_INSTALLATIONS_LIST, RAW_WEBSERVERS, ) -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.airzone_cloud.const import DOMAIN diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index 82db72eb9ca..14e4dd55f73 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Ambient PWS diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.ambient_station import AmbientStationConfigEntry diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ba7e46bdde7..e56df37fe44 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, Mock, PropertyMock, patch import aiohttp from awesomeversion import AwesomeVersion import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.matchers import path_type from homeassistant.components.analytics.analytics import Analytics diff --git a/tests/components/analytics_insights/test_sensor.py b/tests/components/analytics_insights/test_sensor.py index bf82e0c2d65..ce41afeb272 100644 --- a/tests/components/analytics_insights/test_sensor.py +++ b/tests/components/analytics_insights/test_sensor.py @@ -9,7 +9,7 @@ from python_homeassistant_analytics import ( HomeassistantAnalyticsConnectionError, HomeassistantAnalyticsNotModifiedError, ) -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/aosmith/test_diagnostics.py b/tests/components/aosmith/test_diagnostics.py index 9090ef5e7b7..d9fbed513bb 100644 --- a/tests/components/aosmith/test_diagnostics.py +++ b/tests/components/aosmith/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the A. O. Smith integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index d9d45830024..0bf1c00d2f3 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index e5c295ae1bf..e7328603a59 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -5,7 +5,7 @@ from collections import OrderedDict from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.apcupsd.const import DOMAIN from homeassistant.components.apcupsd.coordinator import UPDATE_INTERVAL diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index b14db49970b..4da17b1c128 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.apcupsd.coordinator import REQUEST_REFRESH_COOLDOWN from homeassistant.const import ( diff --git a/tests/components/aquacell/test_sensor.py b/tests/components/aquacell/test_sensor.py index 0c59dcc40e9..007040d9c79 100644 --- a/tests/components/aquacell/test_sensor.py +++ b/tests/components/aquacell/test_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/arve/test_sensor.py b/tests/components/arve/test_sensor.py index 541820fd7b6..77711632c56 100644 --- a/tests/components/arve/test_sensor.py +++ b/tests/components/arve/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index 0b00bde7b23..cdc538ca6bd 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -1,6 +1,6 @@ """Test august diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/autarco/test_diagnostics.py b/tests/components/autarco/test_diagnostics.py index 1d12a2c1894..461f65becdb 100644 --- a/tests/components/autarco/test_diagnostics.py +++ b/tests/components/autarco/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/autarco/test_sensor.py b/tests/components/autarco/test_sensor.py index c7e65baba70..9cdc93e98b0 100644 --- a/tests/components/autarco/test_sensor.py +++ b/tests/components/autarco/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from autarco import AutarcoConnectionError from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 766a51463a4..e13d77c73c8 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import Platform diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 9dcfbac4e7b..1f6f1bf44f8 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import camera from homeassistant.components.axis.const import CONF_STREAM_PROFILE diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index e96ba88c2cd..9107ef2e8a3 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Axis diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index b2f2d15d989..a7da7891d50 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -9,7 +9,7 @@ from unittest.mock import ANY, Mock, call, patch import axis as axislib import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index c33af5ec3a4..ccff3d06e2d 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -6,7 +6,7 @@ from unittest.mock import patch from axis.models.api import CONTEXT import pytest import respx -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 964cfdae64c..c0203bc3d4c 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import patch from axis.models.api import CONTEXT import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( diff --git a/tests/components/backup/test_backup.py b/tests/components/backup/test_backup.py index c9d797f4e30..5a33bf39390 100644 --- a/tests/components/backup/test_backup.py +++ b/tests/components/backup/test_backup.py @@ -10,7 +10,7 @@ from tarfile import TarError from unittest.mock import MagicMock, mock_open, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import DOMAIN, AgentBackup from homeassistant.core import HomeAssistant diff --git a/tests/components/backup/test_diagnostics.py b/tests/components/backup/test_diagnostics.py index a66b4a9a2ea..8f6c501ca86 100644 --- a/tests/components/backup/test_diagnostics.py +++ b/tests/components/backup/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests the diagnostics for Home Assistant Backup integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/backup/test_onboarding.py b/tests/components/backup/test_onboarding.py index 7dfd57ec60a..48e7252289a 100644 --- a/tests/components/backup/test_onboarding.py +++ b/tests/components/backup/test_onboarding.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import ANY, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import backup, onboarding from homeassistant.core import HomeAssistant diff --git a/tests/components/backup/test_sensors.py b/tests/components/backup/test_sensors.py index 6ff1aca7c6d..7320c037b21 100644 --- a/tests/components/backup/test_sensors.py +++ b/tests/components/backup/test_sensors.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import store from homeassistant.components.backup.const import DOMAIN diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index b078dcc2be7..97f6a4102f7 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index e6a59142ca2..2115533452e 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -7,7 +7,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, Mock, call, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import ( AddonInfo, diff --git a/tests/components/balboa/test_binary_sensor.py b/tests/components/balboa/test_binary_sensor.py index 5990c73bb68..8f3c7a4b21c 100644 --- a/tests/components/balboa/test_binary_sensor.py +++ b/tests/components/balboa/test_binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 9c23833518e..5cd5bc9091a 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from pybalboa import SpaControl from pybalboa.enums import HeatMode, OffLowMediumHighState import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, diff --git a/tests/components/balboa/test_event.py b/tests/components/balboa/test_event.py index 04f25f6cfa0..b5a10192c5c 100644 --- a/tests/components/balboa/test_event.py +++ b/tests/components/balboa/test_event.py @@ -6,7 +6,7 @@ from datetime import datetime from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.event import ATTR_EVENT_TYPE from homeassistant.const import STATE_UNKNOWN, Platform diff --git a/tests/components/balboa/test_fan.py b/tests/components/balboa/test_fan.py index 3eacb0d08c0..f9ab201b925 100644 --- a/tests/components/balboa/test_fan.py +++ b/tests/components/balboa/test_fan.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from pybalboa import SpaControl from pybalboa.enums import OffLowHighState, UnknownState import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ATTR_PERCENTAGE from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform diff --git a/tests/components/balboa/test_light.py b/tests/components/balboa/test_light.py index 01469416da5..5eb802f6fc9 100644 --- a/tests/components/balboa/test_light.py +++ b/tests/components/balboa/test_light.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from pybalboa import SpaControl from pybalboa.enums import OffOnState, UnknownState import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/balboa/test_select.py b/tests/components/balboa/test_select.py index da57ee8f22e..e44962b43b9 100644 --- a/tests/components/balboa/test_select.py +++ b/tests/components/balboa/test_select.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, call, patch from pybalboa import SpaControl from pybalboa.enums import LowHighRange import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/balboa/test_switch.py b/tests/components/balboa/test_switch.py index 4b6bae172f4..ed031bebe05 100644 --- a/tests/components/balboa/test_switch.py +++ b/tests/components/balboa/test_switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/balboa/test_time.py b/tests/components/balboa/test_time.py index 21778d08e2d..093e741bbf4 100644 --- a/tests/components/balboa/test_time.py +++ b/tests/components/balboa/test_time.py @@ -6,7 +6,7 @@ from datetime import time from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.time import ( ATTR_TIME, diff --git a/tests/components/bang_olufsen/test_diagnostics.py b/tests/components/bang_olufsen/test_diagnostics.py index a9415a222a8..fdc22390e64 100644 --- a/tests/components/bang_olufsen/test_diagnostics.py +++ b/tests/components/bang_olufsen/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/blink/test_diagnostics.py b/tests/components/blink/test_diagnostics.py index d527633d4c9..334ecfaa50c 100644 --- a/tests/components/blink/test_diagnostics.py +++ b/tests/components/blink/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/bluemaestro/test_sensor.py b/tests/components/bluemaestro/test_sensor.py index a75e390c781..40e8550cc9e 100644 --- a/tests/components/bluemaestro/test_sensor.py +++ b/tests/components/bluemaestro/test_sensor.py @@ -1,7 +1,7 @@ """Test the BlueMaestro sensors.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.bluemaestro.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 94036d208ab..c61be9e2b32 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -4,7 +4,7 @@ import json from pathlib import Path import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.blueprint import importer from homeassistant.core import HomeAssistant diff --git a/tests/components/braviatv/test_diagnostics.py b/tests/components/braviatv/test_diagnostics.py index a7bd1631788..2f6df722909 100644 --- a/tests/components/braviatv/test_diagnostics.py +++ b/tests/components/braviatv/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.braviatv.const import CONF_USE_PSK, DOMAIN diff --git a/tests/components/brother/test_diagnostics.py b/tests/components/brother/test_diagnostics.py index 117990b6470..493f2993555 100644 --- a/tests/components/brother/test_diagnostics.py +++ b/tests/components/brother/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 8069b27e307..28d08cd6b2f 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.brother.const import DOMAIN, UPDATE_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/bsblan/test_diagnostics.py b/tests/components/bsblan/test_diagnostics.py index aea53f8a1a2..c6b6c92e718 100644 --- a/tests/components/bsblan/test_diagnostics.py +++ b/tests/components/bsblan/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/cambridge_audio/test_diagnostics.py b/tests/components/cambridge_audio/test_diagnostics.py index 9c1a09c6318..42367a67876 100644 --- a/tests/components/cambridge_audio/test_diagnostics.py +++ b/tests/components/cambridge_audio/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/cambridge_audio/test_init.py b/tests/components/cambridge_audio/test_init.py index a058f7c8b6c..507a942c30f 100644 --- a/tests/components/cambridge_audio/test_init.py +++ b/tests/components/cambridge_audio/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock from aiostreammagic import StreamMagicError from aiostreammagic.models import CallbackType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cambridge_audio.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/cambridge_audio/test_media_browser.py b/tests/components/cambridge_audio/test_media_browser.py index da72cfab534..1e374566611 100644 --- a/tests/components/cambridge_audio/test_media_browser.py +++ b/tests/components/cambridge_audio/test_media_browser.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/cambridge_audio/test_select.py b/tests/components/cambridge_audio/test_select.py index 473c4027163..73359aaa2b7 100644 --- a/tests/components/cambridge_audio/test_select.py +++ b/tests/components/cambridge_audio/test_select.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, diff --git a/tests/components/cambridge_audio/test_switch.py b/tests/components/cambridge_audio/test_switch.py index 3192f198d1f..44f7379f22f 100644 --- a/tests/components/cambridge_audio/test_switch.py +++ b/tests/components/cambridge_audio/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, Platform diff --git a/tests/components/ccm15/test_diagnostics.py b/tests/components/ccm15/test_diagnostics.py index f6f0d75c4e3..ae876694c0c 100644 --- a/tests/components/ccm15/test_diagnostics.py +++ b/tests/components/ccm15/test_diagnostics.py @@ -1,7 +1,7 @@ """Test CCM15 diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ccm15.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT diff --git a/tests/components/coinbase/test_diagnostics.py b/tests/components/coinbase/test_diagnostics.py index 0e06c172c37..98936f47e48 100644 --- a/tests/components/coinbase/test_diagnostics.py +++ b/tests/components/coinbase/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index 059d7d27d77..e0b1e116f64 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -7,7 +7,7 @@ from aiocomelit.api import ComelitSerialBridgeObject from aiocomelit.const import CLIMATE, WATT from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/comelit/test_cover.py b/tests/components/comelit/test_cover.py index 7fb74911cc6..b09a2e6322c 100644 --- a/tests/components/comelit/test_cover.py +++ b/tests/components/comelit/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from aiocomelit.api import ComelitSerialBridgeObject from aiocomelit.const import COVER, WATT from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.comelit.const import SCAN_INTERVAL from homeassistant.components.cover import ( diff --git a/tests/components/comelit/test_diagnostics.py b/tests/components/comelit/test_diagnostics.py index cabcd0f4cac..8743c5b4b64 100644 --- a/tests/components/comelit/test_diagnostics.py +++ b/tests/components/comelit/test_diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/comelit/test_humidifier.py b/tests/components/comelit/test_humidifier.py index 448453aadef..f432c63e14c 100644 --- a/tests/components/comelit/test_humidifier.py +++ b/tests/components/comelit/test_humidifier.py @@ -7,7 +7,7 @@ from aiocomelit.api import ComelitSerialBridgeObject from aiocomelit.const import CLIMATE, WATT from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.comelit.const import DOMAIN, SCAN_INTERVAL from homeassistant.components.humidifier import ( diff --git a/tests/components/comelit/test_light.py b/tests/components/comelit/test_light.py index 7c3cd15c135..36a191c9ee3 100644 --- a/tests/components/comelit/test_light.py +++ b/tests/components/comelit/test_light.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, diff --git a/tests/components/comelit/test_sensor.py b/tests/components/comelit/test_sensor.py index 2b857f9c94a..1bf717ca894 100644 --- a/tests/components/comelit/test_sensor.py +++ b/tests/components/comelit/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from aiocomelit.api import AlarmDataObject, ComelitVedoAreaObject, ComelitVedoZoneObject from aiocomelit.const import AlarmAreaState, AlarmZoneState from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.comelit.const import SCAN_INTERVAL from homeassistant.const import STATE_UNKNOWN, Platform diff --git a/tests/components/comelit/test_switch.py b/tests/components/comelit/test_switch.py index 01efabf6b6f..31a4c4b144c 100644 --- a/tests/components/comelit/test_switch.py +++ b/tests/components/comelit/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index dca4653b480..f075f267111 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, patch from hassil.recognize import Intent, IntentData, MatchEntity, RecognizeResult import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion import yaml from homeassistant.components import conversation, cover, media_player, weather diff --git a/tests/components/cookidoo/test_button.py b/tests/components/cookidoo/test_button.py index 3e832ec9fe6..f96cbf4665d 100644 --- a/tests/components/cookidoo/test_button.py +++ b/tests/components/cookidoo/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from cookidoo_api import CookidooRequestException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/cookidoo/test_diagnostics.py b/tests/components/cookidoo/test_diagnostics.py index c253e1f6e09..1bd172f846f 100644 --- a/tests/components/cookidoo/test_diagnostics.py +++ b/tests/components/cookidoo/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/cpuspeed/test_diagnostics.py b/tests/components/cpuspeed/test_diagnostics.py index a596c7d62d9..e84235af3b0 100644 --- a/tests/components/cpuspeed/test_diagnostics.py +++ b/tests/components/cpuspeed/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index dbe75584df7..8e0b696c274 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pydeconz.models.sensor.ancillary_control import AncillaryControlPanel import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 59d31afb9fc..288be082f43 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, diff --git a/tests/components/deconz/test_button.py b/tests/components/deconz/test_button.py index c649dba5b00..4451d68c186 100644 --- a/tests/components/deconz/test_button.py +++ b/tests/components/deconz/test_button.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index e1000f0b4d6..723ff12ad37 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -4,7 +4,7 @@ from collections.abc import Callable from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 47f8083798e..99f78dd1a92 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -4,7 +4,7 @@ from collections.abc import Callable from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index 2abc6d83995..640e8947c17 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -1,7 +1,7 @@ """Test deCONZ diagnostics.""" from pydeconz.websocket import State -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 21809a138c6..a544f46e39d 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -4,7 +4,7 @@ from collections.abc import Callable from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PERCENTAGE, diff --git a/tests/components/deconz/test_hub.py b/tests/components/deconz/test_hub.py index 1b000828b85..f674a6ef6df 100644 --- a/tests/components/deconz/test_hub.py +++ b/tests/components/deconz/test_hub.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pydeconz.websocket import State import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.deconz.config_flow import DECONZ_MANUFACTURERURL from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 9ac15d4867b..6aacdf7011b 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py index 962c2c0a89b..dd2f26eec4b 100644 --- a/tests/components/deconz/test_number.py +++ b/tests/components/deconz/test_number.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index c1240b6881c..d03cbec28e0 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/deconz/test_select.py b/tests/components/deconz/test_select.py index c677853841c..5d79cb8cd50 100644 --- a/tests/components/deconz/test_select.py +++ b/tests/components/deconz/test_select.py @@ -10,7 +10,7 @@ from pydeconz.models.sensor.presence import ( PresenceConfigTriggerDistance, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 958cb3b793a..521ff3c7efb 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -5,7 +5,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN diff --git a/tests/components/discovergy/test_diagnostics.py b/tests/components/discovergy/test_diagnostics.py index 5c231c3d221..ca05edfe8c2 100644 --- a/tests/components/discovergy/test_diagnostics.py +++ b/tests/components/discovergy/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Discovergy diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/discovergy/test_sensor.py b/tests/components/discovergy/test_sensor.py index 814efb1ba57..20d8756ec44 100644 --- a/tests/components/discovergy/test_sensor.py +++ b/tests/components/discovergy/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from pydiscovergy.error import DiscovergyClientError, HTTPError, InvalidLogin import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/drop_connect/common.py b/tests/components/drop_connect/common.py index 9eb76f57dad..a695d85bab7 100644 --- a/tests/components/drop_connect/common.py +++ b/tests/components/drop_connect/common.py @@ -1,6 +1,6 @@ """Define common test values.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.drop_connect.const import ( CONF_COMMAND_TOPIC, diff --git a/tests/components/drop_connect/test_binary_sensor.py b/tests/components/drop_connect/test_binary_sensor.py index ab89e05d809..41de9d16958 100644 --- a/tests/components/drop_connect/test_binary_sensor.py +++ b/tests/components/drop_connect/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/drop_connect/test_sensor.py b/tests/components/drop_connect/test_sensor.py index c33f0aefe37..40f95c268b6 100644 --- a/tests/components/drop_connect/test_sensor.py +++ b/tests/components/drop_connect/test_sensor.py @@ -4,7 +4,7 @@ from collections.abc import Generator from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/dsmr_reader/test_diagnostics.py b/tests/components/dsmr_reader/test_diagnostics.py index 793fe1362b0..070d7d152ab 100644 --- a/tests/components/dsmr_reader/test_diagnostics.py +++ b/tests/components/dsmr_reader/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.dsmr_reader.const import DOMAIN diff --git a/tests/components/ecovacs/test_binary_sensor.py b/tests/components/ecovacs/test_binary_sensor.py index 16e2d3fefc5..0a39d3f2623 100644 --- a/tests/components/ecovacs/test_binary_sensor.py +++ b/tests/components/ecovacs/test_binary_sensor.py @@ -2,7 +2,7 @@ from deebot_client.events.water_info import MopAttachedEvent import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_button.py b/tests/components/ecovacs/test_button.py index 3021db62e6f..30a7db431d0 100644 --- a/tests/components/ecovacs/test_button.py +++ b/tests/components/ecovacs/test_button.py @@ -9,7 +9,7 @@ from deebot_client.commands.json import ( ) from deebot_client.events import LifeSpan import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.ecovacs.const import DOMAIN diff --git a/tests/components/ecovacs/test_event.py b/tests/components/ecovacs/test_event.py index 03fb79e083f..56a0298bef1 100644 --- a/tests/components/ecovacs/test_event.py +++ b/tests/components/ecovacs/test_event.py @@ -5,7 +5,7 @@ from datetime import timedelta from deebot_client.events import CleanJobStatus, ReportStatsEvent from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_init.py b/tests/components/ecovacs/test_init.py index 13b73d853d5..c0e5ce143c9 100644 --- a/tests/components/ecovacs/test_init.py +++ b/tests/components/ecovacs/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from deebot_client.exceptions import DeebotError, InvalidAuthenticationError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_lawn_mower.py b/tests/components/ecovacs/test_lawn_mower.py index 2c0abd0a49e..bab1495e16c 100644 --- a/tests/components/ecovacs/test_lawn_mower.py +++ b/tests/components/ecovacs/test_lawn_mower.py @@ -7,7 +7,7 @@ from deebot_client.commands.json import Charge, CleanV2 from deebot_client.events import StateEvent from deebot_client.models import CleanAction, State import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_number.py b/tests/components/ecovacs/test_number.py index 32bc8f90696..dd7308e18fd 100644 --- a/tests/components/ecovacs/test_number.py +++ b/tests/components/ecovacs/test_number.py @@ -6,7 +6,7 @@ from deebot_client.command import Command from deebot_client.commands.json import SetCutDirection, SetVolume from deebot_client.events import CutDirectionEvent, Event, VolumeEvent import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_select.py b/tests/components/ecovacs/test_select.py index 1e03bb18e28..c3025d99cfa 100644 --- a/tests/components/ecovacs/test_select.py +++ b/tests/components/ecovacs/test_select.py @@ -5,7 +5,7 @@ from deebot_client.commands.json import SetWaterInfo from deebot_client.event_bus import EventBus from deebot_client.events.water_info import WaterAmount, WaterAmountEvent import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import select from homeassistant.components.ecovacs.const import DOMAIN diff --git a/tests/components/ecovacs/test_sensor.py b/tests/components/ecovacs/test_sensor.py index 8222e9976d5..6c3900ccd19 100644 --- a/tests/components/ecovacs/test_sensor.py +++ b/tests/components/ecovacs/test_sensor.py @@ -14,7 +14,7 @@ from deebot_client.events import ( station, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/ecovacs/test_switch.py b/tests/components/ecovacs/test_switch.py index 040528debaa..23c802fa0ef 100644 --- a/tests/components/ecovacs/test_switch.py +++ b/tests/components/ecovacs/test_switch.py @@ -27,7 +27,7 @@ from deebot_client.events import ( TrueDetectEvent, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ecovacs.const import DOMAIN from homeassistant.components.ecovacs.controller import EcovacsController diff --git a/tests/components/elmax/test_alarm_control_panel.py b/tests/components/elmax/test_alarm_control_panel.py index 88fc0a33c51..f7e956708ab 100644 --- a/tests/components/elmax/test_alarm_control_panel.py +++ b/tests/components/elmax/test_alarm_control_panel.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.elmax.const import POLLING_SECONDS from homeassistant.const import Platform diff --git a/tests/components/elmax/test_binary_sensor.py b/tests/components/elmax/test_binary_sensor.py index f6cead79ee7..685cf1ff7c1 100644 --- a/tests/components/elmax/test_binary_sensor.py +++ b/tests/components/elmax/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/elmax/test_cover.py b/tests/components/elmax/test_cover.py index 9fa72432072..a42c9c17122 100644 --- a/tests/components/elmax/test_cover.py +++ b/tests/components/elmax/test_cover.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/elmax/test_switch.py b/tests/components/elmax/test_switch.py index ba6efee2184..b11fe447150 100644 --- a/tests/components/elmax/test_switch.py +++ b/tests/components/elmax/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/environment_canada/test_diagnostics.py b/tests/components/environment_canada/test_diagnostics.py index 7c35c33f93a..f46b89d20c2 100644 --- a/tests/components/environment_canada/test_diagnostics.py +++ b/tests/components/environment_canada/test_diagnostics.py @@ -2,7 +2,7 @@ from typing import Any -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.environment_canada.const import CONF_STATION from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE diff --git a/tests/components/esphome/test_climate.py b/tests/components/esphome/test_climate.py index 739c2119bf0..dd42ee97029 100644 --- a/tests/components/esphome/test_climate.py +++ b/tests/components/esphome/test_climate.py @@ -14,7 +14,7 @@ from aioesphomeapi import ( ClimateSwingMode, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 250cc8dbc49..84f2243a844 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -5,7 +5,7 @@ from unittest.mock import ANY from aioesphomeapi import APIClient import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components import bluetooth diff --git a/tests/components/evohome/test_climate.py b/tests/components/evohome/test_climate.py index b1b930c6382..171b910690b 100644 --- a/tests/components/evohome/test_climate.py +++ b/tests/components/evohome/test_climate.py @@ -9,7 +9,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/evohome/test_water_heater.py b/tests/components/evohome/test_water_heater.py index ca9a5ba6af8..c06f57b61ed 100644 --- a/tests/components/evohome/test_water_heater.py +++ b/tests/components/evohome/test_water_heater.py @@ -10,7 +10,7 @@ from unittest.mock import patch from evohomeasync2 import EvohomeClient from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.water_heater import ( ATTR_AWAY_MODE, diff --git a/tests/components/fastdotcom/test_diagnostics.py b/tests/components/fastdotcom/test_diagnostics.py index 7ea644665c7..36b29c8a9f1 100644 --- a/tests/components/fastdotcom/test_diagnostics.py +++ b/tests/components/fastdotcom/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fastdotcom.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER diff --git a/tests/components/fibaro/test_diagnostics.py b/tests/components/fibaro/test_diagnostics.py index c6148e0cc33..35b75a79ba9 100644 --- a/tests/components/fibaro/test_diagnostics.py +++ b/tests/components/fibaro/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import Mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fibaro import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py index c1983b898da..8dfa712ecb1 100644 --- a/tests/components/flo/test_init.py +++ b/tests/components/flo/test_init.py @@ -1,7 +1,7 @@ """Test init.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/forecast_solar/test_diagnostics.py b/tests/components/forecast_solar/test_diagnostics.py index 0e80fba7647..e29b4a468ab 100644 --- a/tests/components/forecast_solar/test_diagnostics.py +++ b/tests/components/forecast_solar/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Forecast.Solar integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/forecast_solar/test_init.py b/tests/components/forecast_solar/test_init.py index 481ec3c0c9d..680a30580cb 100644 --- a/tests/components/forecast_solar/test_init.py +++ b/tests/components/forecast_solar/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch from forecast_solar import ForecastSolarConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index cbcaa57dab4..84b06a3dd4a 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -2,7 +2,7 @@ from __future__ import annotations -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.fritz.const import DOMAIN diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 3eac2c24953..ae691f6107e 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch import pytest from requests.exceptions import HTTPError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py index 5280cd7cc83..ada50d7f16c 100644 --- a/tests/components/fritzbox/test_button.py +++ b/tests/components/fritzbox/test_button.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import Mock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index bdf9dba8b42..bf8ce5d8a5b 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, _Call, call, patch from freezegun.api import FrozenDateTimeFactory import pytest from requests.exceptions import HTTPError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py index a1332e9715b..75e11983f39 100644 --- a/tests/components/fritzbox/test_cover.py +++ b/tests/components/fritzbox/test_cover.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import Mock, call, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ATTR_POSITION, DOMAIN as COVER_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index d9a81bf8f21..7e6fa05d8cd 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import Mock, call, patch from requests.exceptions import HTTPError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fritzbox.const import ( COLOR_MODE, diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 7912aaf8d12..4d12e8750a3 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch import pytest from requests.exceptions import HTTPError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index cb6b563d344..d8894c0ae93 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch import pytest from requests.exceptions import HTTPError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN diff --git a/tests/components/fronius/test_diagnostics.py b/tests/components/fronius/test_diagnostics.py index ddef5b4a18c..cb6faf547e2 100644 --- a/tests/components/fronius/test_diagnostics.py +++ b/tests/components/fronius/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Fronius integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 63f36705c8f..be8cd43cf2b 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -2,7 +2,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fronius.const import DOMAIN from homeassistant.components.fronius.coordinator import ( diff --git a/tests/components/fujitsu_fglair/test_climate.py b/tests/components/fujitsu_fglair/test_climate.py index 676ff97f26a..4e9dc750af9 100644 --- a/tests/components/fujitsu_fglair/test_climate.py +++ b/tests/components/fujitsu_fglair/test_climate.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, diff --git a/tests/components/fujitsu_fglair/test_sensor.py b/tests/components/fujitsu_fglair/test_sensor.py index b8200f114ad..45d455200fb 100644 --- a/tests/components/fujitsu_fglair/test_sensor.py +++ b/tests/components/fujitsu_fglair/test_sensor.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/fyta/test_binary_sensor.py b/tests/components/fyta/test_binary_sensor.py index 9d6a4ae3b0e..aa5c45b6ebc 100644 --- a/tests/components/fyta/test_binary_sensor.py +++ b/tests/components/fyta/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from fyta_cli.fyta_exceptions import FytaConnectionError, FytaPlantError from fyta_cli.fyta_models import Plant import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fyta.const import DOMAIN as FYTA_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform diff --git a/tests/components/fyta/test_diagnostics.py b/tests/components/fyta/test_diagnostics.py index cfaa5484b82..1fb626756e5 100644 --- a/tests/components/fyta/test_diagnostics.py +++ b/tests/components/fyta/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.const import Platform diff --git a/tests/components/fyta/test_image.py b/tests/components/fyta/test_image.py index 4feb125bd15..93cca1a1c09 100644 --- a/tests/components/fyta/test_image.py +++ b/tests/components/fyta/test_image.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from fyta_cli.fyta_exceptions import FytaConnectionError, FytaPlantError from fyta_cli.fyta_models import Plant import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fyta.const import DOMAIN as FYTA_DOMAIN from homeassistant.components.image import ImageEntity diff --git a/tests/components/fyta/test_sensor.py b/tests/components/fyta/test_sensor.py index 07e3965e66f..e9835ff5dfc 100644 --- a/tests/components/fyta/test_sensor.py +++ b/tests/components/fyta/test_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from fyta_cli.fyta_exceptions import FytaConnectionError, FytaPlantError from fyta_cli.fyta_models import Plant import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fyta.const import DOMAIN as FYTA_DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/garages_amsterdam/test_binary_sensor.py b/tests/components/garages_amsterdam/test_binary_sensor.py index b7d0333f7e3..b610ad484e8 100644 --- a/tests/components/garages_amsterdam/test_binary_sensor.py +++ b/tests/components/garages_amsterdam/test_binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/garages_amsterdam/test_sensor.py b/tests/components/garages_amsterdam/test_sensor.py index bc36401ea47..5e573cf3100 100644 --- a/tests/components/garages_amsterdam/test_sensor.py +++ b/tests/components/garages_amsterdam/test_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/gdacs/test_diagnostics.py b/tests/components/gdacs/test_diagnostics.py index 3c6cf4080a6..8e8882ff6e7 100644 --- a/tests/components/gdacs/test_diagnostics.py +++ b/tests/components/gdacs/test_diagnostics.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/geniushub/test_binary_sensor.py b/tests/components/geniushub/test_binary_sensor.py index 682929eb696..6edeb317a55 100644 --- a/tests/components/geniushub/test_binary_sensor.py +++ b/tests/components/geniushub/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/geniushub/test_climate.py b/tests/components/geniushub/test_climate.py index d14e57b9552..d116f862b55 100644 --- a/tests/components/geniushub/test_climate.py +++ b/tests/components/geniushub/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/geniushub/test_sensor.py b/tests/components/geniushub/test_sensor.py index a75329ca7fc..6e3af621bcc 100644 --- a/tests/components/geniushub/test_sensor.py +++ b/tests/components/geniushub/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/geniushub/test_switch.py b/tests/components/geniushub/test_switch.py index 0e88562e381..905c32e0c35 100644 --- a/tests/components/geniushub/test_switch.py +++ b/tests/components/geniushub/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/geonetnz_quakes/test_diagnostics.py b/tests/components/geonetnz_quakes/test_diagnostics.py index db5e1300768..ffe570cb269 100644 --- a/tests/components/geonetnz_quakes/test_diagnostics.py +++ b/tests/components/geonetnz_quakes/test_diagnostics.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/gios/test_diagnostics.py b/tests/components/gios/test_diagnostics.py index a965e5550df..cc3df9e3593 100644 --- a/tests/components/gios/test_diagnostics.py +++ b/tests/components/gios/test_diagnostics.py @@ -1,6 +1,6 @@ """Test GIOS diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index d9096916106..fd343d16525 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -6,7 +6,7 @@ import json from unittest.mock import patch from gios import ApiError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.gios.const import DOMAIN from homeassistant.components.sensor import DOMAIN as PLATFORM diff --git a/tests/components/glances/test_sensor.py b/tests/components/glances/test_sensor.py index 8e0367a712c..71bb689f3ff 100644 --- a/tests/components/glances/test_sensor.py +++ b/tests/components/glances/test_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.glances.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE diff --git a/tests/components/goodwe/test_diagnostics.py b/tests/components/goodwe/test_diagnostics.py index 0a997edc594..fa90889e75e 100644 --- a/tests/components/goodwe/test_diagnostics.py +++ b/tests/components/goodwe/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.goodwe import CONF_MODEL_FAMILY, DOMAIN diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py index 1d68079563c..b75654edd1b 100644 --- a/tests/components/google_assistant/test_diagnostics.py +++ b/tests/components/google_assistant/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant import setup diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index af951fe8aa1..544b9bd5958 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -32,7 +32,7 @@ from aiohasupervisor.models.backups import LOCATION_CLOUD_BACKUP, LOCATION_LOCAL from aiohasupervisor.models.mounts import MountsInfo from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import ( DOMAIN as BACKUP_DOMAIN, diff --git a/tests/components/hassio/test_config.py b/tests/components/hassio/test_config.py index 86a97cc4a0a..4df8d2e81ac 100644 --- a/tests/components/hassio/test_config.py +++ b/tests/components/hassio/test_config.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from uuid import UUID import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.hassio.const import DATA_CONFIG_STORE, DOMAIN diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index cbf664d0e49..8c68e9bf705 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiohasupervisor import SupervisorError from aiohasupervisor.models import HomeAssistantUpdateOptions, StoreAddonUpdate import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.backup import BackupManagerError, ManagerBackup diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index f79c875385d..e5408aa5e0f 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -1,6 +1,6 @@ """Test homekit_controller diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.homekit_controller.const import KNOWN_DEVICES diff --git a/tests/components/honeywell/test_diagnostics.py b/tests/components/honeywell/test_diagnostics.py index 06c41d3d055..a857a7f633f 100644 --- a/tests/components/honeywell/test_diagnostics.py +++ b/tests/components/honeywell/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/husqvarna_automower/test_binary_sensor.py b/tests/components/husqvarna_automower/test_binary_sensor.py index 7812a684196..3d40da99dcb 100644 --- a/tests/components/husqvarna_automower/test_binary_sensor.py +++ b/tests/components/husqvarna_automower/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py index b76bc7c9d73..1674c356f73 100644 --- a/tests/components/husqvarna_automower/test_button.py +++ b/tests/components/husqvarna_automower/test_button.py @@ -7,7 +7,7 @@ from aioautomower.exceptions import ApiError from aioautomower.model import MowerAttributes from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL diff --git a/tests/components/husqvarna_automower/test_calendar.py b/tests/components/husqvarna_automower/test_calendar.py index 8138b8c139b..8f9a3e6a016 100644 --- a/tests/components/husqvarna_automower/test_calendar.py +++ b/tests/components/husqvarna_automower/test_calendar.py @@ -11,7 +11,7 @@ import zoneinfo from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, diff --git a/tests/components/husqvarna_automower/test_device_tracker.py b/tests/components/husqvarna_automower/test_device_tracker.py index 91f5e40b154..3ab5e55f2c7 100644 --- a/tests/components/husqvarna_automower/test_device_tracker.py +++ b/tests/components/husqvarna_automower/test_device_tracker.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/husqvarna_automower/test_number.py b/tests/components/husqvarna_automower/test_number.py index 005d294954c..227010e939d 100644 --- a/tests/components/husqvarna_automower/test_number.py +++ b/tests/components/husqvarna_automower/test_number.py @@ -7,7 +7,7 @@ from aioautomower.exceptions import ApiError from aioautomower.model import MowerAttributes from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import EXECUTION_TIME_DELAY from homeassistant.const import Platform diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 85d20178e73..3d4922781b4 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -7,7 +7,7 @@ import zoneinfo from aioautomower.model import MowerAttributes, MowerModes, MowerStates from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL from homeassistant.const import STATE_UNKNOWN, Platform diff --git a/tests/components/husqvarna_automower/test_switch.py b/tests/components/husqvarna_automower/test_switch.py index 06efb8c45c0..d6ca8ff36e2 100644 --- a/tests/components/husqvarna_automower/test_switch.py +++ b/tests/components/husqvarna_automower/test_switch.py @@ -9,7 +9,7 @@ from aioautomower.model import MowerAttributes, MowerModes, Zone from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import ( DOMAIN, diff --git a/tests/components/igloohome/test_lock.py b/tests/components/igloohome/test_lock.py index 324a4ab231a..621f9995190 100644 --- a/tests/components/igloohome/test_lock.py +++ b/tests/components/igloohome/test_lock.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/igloohome/test_sensor.py b/tests/components/igloohome/test_sensor.py index bfc60574450..21ea3efbf8e 100644 --- a/tests/components/igloohome/test_sensor.py +++ b/tests/components/igloohome/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/imgw_pib/test_diagnostics.py b/tests/components/imgw_pib/test_diagnostics.py index 14d4e7a5224..2b2568050f3 100644 --- a/tests/components/imgw_pib/test_diagnostics.py +++ b/tests/components/imgw_pib/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/imgw_pib/test_sensor.py b/tests/components/imgw_pib/test_sensor.py index a1920f38006..cb27f0f9b46 100644 --- a/tests/components/imgw_pib/test_sensor.py +++ b/tests/components/imgw_pib/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from imgw_pib import ApiError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.imgw_pib.const import DOMAIN, UPDATE_INTERVAL from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM diff --git a/tests/components/immich/test_diagnostics.py b/tests/components/immich/test_diagnostics.py index f816aab8aae..67b4bfa01d8 100644 --- a/tests/components/immich/test_diagnostics.py +++ b/tests/components/immich/test_diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import Mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/immich/test_sensor.py b/tests/components/immich/test_sensor.py index ceebba7b8be..510999f584e 100644 --- a/tests/components/immich/test_sensor.py +++ b/tests/components/immich/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/incomfort/test_binary_sensor.py b/tests/components/incomfort/test_binary_sensor.py index e90cc3ac391..e0716324de7 100644 --- a/tests/components/incomfort/test_binary_sensor.py +++ b/tests/components/incomfort/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from incomfortclient import FaultCode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/incomfort/test_climate.py b/tests/components/incomfort/test_climate.py index dbcf14e3bd7..a4c97d88e34 100644 --- a/tests/components/incomfort/test_climate.py +++ b/tests/components/incomfort/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import climate from homeassistant.components.incomfort.coordinator import InComfortData diff --git a/tests/components/incomfort/test_sensor.py b/tests/components/incomfort/test_sensor.py index df0db39a56c..78e7a52362b 100644 --- a/tests/components/incomfort/test_sensor.py +++ b/tests/components/incomfort/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/incomfort/test_water_heater.py b/tests/components/incomfort/test_water_heater.py index 082aecf6d49..35edb134ac9 100644 --- a/tests/components/incomfort/test_water_heater.py +++ b/tests/components/incomfort/test_water_heater.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform diff --git a/tests/components/intellifire/test_binary_sensor.py b/tests/components/intellifire/test_binary_sensor.py index a40f92b84d5..d8bce78263d 100644 --- a/tests/components/intellifire/test_binary_sensor.py +++ b/tests/components/intellifire/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/intellifire/test_climate.py b/tests/components/intellifire/test_climate.py index da1b2864791..6b4ad01f9d6 100644 --- a/tests/components/intellifire/test_climate.py +++ b/tests/components/intellifire/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import patch from freezegun import freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/intellifire/test_sensor.py b/tests/components/intellifire/test_sensor.py index 96e344d77fc..9b5d25c679a 100644 --- a/tests/components/intellifire/test_sensor.py +++ b/tests/components/intellifire/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun import freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/ipp/test_diagnostics.py b/tests/components/ipp/test_diagnostics.py index d78f066d788..3bd1fbc2e3e 100644 --- a/tests/components/ipp/test_diagnostics.py +++ b/tests/components/ipp/test_diagnostics.py @@ -1,7 +1,7 @@ """Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/iqvia/test_diagnostics.py b/tests/components/iqvia/test_diagnostics.py index 9d5639c311c..dc3d0cb8557 100644 --- a/tests/components/iqvia/test_diagnostics.py +++ b/tests/components/iqvia/test_diagnostics.py @@ -1,6 +1,6 @@ """Test IQVIA diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/israel_rail/test_sensor.py b/tests/components/israel_rail/test_sensor.py index 85b7328742f..08aed2bbc21 100644 --- a/tests/components/israel_rail/test_sensor.py +++ b/tests/components/israel_rail/test_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant diff --git a/tests/components/jellyfin/test_diagnostics.py b/tests/components/jellyfin/test_diagnostics.py index bd34e3a8e31..822d8dbc5bb 100644 --- a/tests/components/jellyfin/test_diagnostics.py +++ b/tests/components/jellyfin/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Jellyfin diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index 4f639e08773..27d8b93bf64 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from knocki import Event, EventType, Trigger, TriggerDetails import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.knocki.const import DOMAIN from homeassistant.const import STATE_UNKNOWN diff --git a/tests/components/knx/test_diagnostic.py b/tests/components/knx/test_diagnostic.py index 6d4bf7e6007..3f8bc805855 100644 --- a/tests/components/knx/test_diagnostic.py +++ b/tests/components/knx/test_diagnostic.py @@ -3,7 +3,7 @@ from typing import Any import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from homeassistant.components.knx.const import ( diff --git a/tests/components/lamarzocco/test_binary_sensor.py b/tests/components/lamarzocco/test_binary_sensor.py index 47e5a96ecbc..ef8c7e17d97 100644 --- a/tests/components/lamarzocco/test_binary_sensor.py +++ b/tests/components/lamarzocco/test_binary_sensor.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory from pylamarzocco.exceptions import RequestNotSuccessful import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lamarzocco/test_button.py b/tests/components/lamarzocco/test_button.py index 61b7ba77c22..2272829965b 100644 --- a/tests/components/lamarzocco/test_button.py +++ b/tests/components/lamarzocco/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pylamarzocco.exceptions import RequestNotSuccessful import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/lamarzocco/test_calendar.py b/tests/components/lamarzocco/test_calendar.py index 0d8db9bec89..8824de6d3f4 100644 --- a/tests/components/lamarzocco/test_calendar.py +++ b/tests/components/lamarzocco/test_calendar.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, diff --git a/tests/components/lamarzocco/test_diagnostics.py b/tests/components/lamarzocco/test_diagnostics.py index 762b33cc696..7aa0edcd0ad 100644 --- a/tests/components/lamarzocco/test_diagnostics.py +++ b/tests/components/lamarzocco/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the La Marzocco integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/lamarzocco/test_init.py b/tests/components/lamarzocco/test_init.py index 31510ad1426..1e56e540e2a 100644 --- a/tests/components/lamarzocco/test_init.py +++ b/tests/components/lamarzocco/test_init.py @@ -6,7 +6,7 @@ from pylamarzocco.const import FirmwareType, ModelName from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from pylamarzocco.models import WebSocketDetails import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lamarzocco.config_flow import CONF_MACHINE from homeassistant.components.lamarzocco.const import DOMAIN diff --git a/tests/components/lamarzocco/test_number.py b/tests/components/lamarzocco/test_number.py index e4be04f4ce4..b36f2944f4a 100644 --- a/tests/components/lamarzocco/test_number.py +++ b/tests/components/lamarzocco/test_number.py @@ -11,7 +11,7 @@ from pylamarzocco.const import ( ) from pylamarzocco.exceptions import RequestNotSuccessful import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/lamarzocco/test_select.py b/tests/components/lamarzocco/test_select.py index 78cb9e313dd..845eda69d5b 100644 --- a/tests/components/lamarzocco/test_select.py +++ b/tests/components/lamarzocco/test_select.py @@ -10,7 +10,7 @@ from pylamarzocco.const import ( ) from pylamarzocco.exceptions import RequestNotSuccessful import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/lamarzocco/test_sensor.py b/tests/components/lamarzocco/test_sensor.py index d3aba1ef370..183d3f2daa6 100644 --- a/tests/components/lamarzocco/test_sensor.py +++ b/tests/components/lamarzocco/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from pylamarzocco.const import ModelName import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lamarzocco/test_switch.py b/tests/components/lamarzocco/test_switch.py index b8e536e5c1b..0f1c4fd6ebb 100644 --- a/tests/components/lamarzocco/test_switch.py +++ b/tests/components/lamarzocco/test_switch.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from pylamarzocco.const import SmartStandByType from pylamarzocco.exceptions import RequestNotSuccessful import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 3dbc5e98bee..46e466a3acc 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -12,7 +12,7 @@ from pylamarzocco.const import ( from pylamarzocco.exceptions import RequestNotSuccessful from pylamarzocco.models import UpdateDetails import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/lametric/test_diagnostics.py b/tests/components/lametric/test_diagnostics.py index e1fcbafcb73..8f42682ccfc 100644 --- a/tests/components/lametric/test_diagnostics.py +++ b/tests/components/lametric/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the LaMetric integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 1578c67432d..60373fa6c94 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest import serial -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from ultraheat_api.response import HeatMeterResponse from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN diff --git a/tests/components/lektrico/test_binary_sensor.py b/tests/components/lektrico/test_binary_sensor.py index d49eac6cc23..05947ec1cda 100644 --- a/tests/components/lektrico/test_binary_sensor.py +++ b/tests/components/lektrico/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_button.py b/tests/components/lektrico/test_button.py index 7bd77848d21..65d85ec1250 100644 --- a/tests/components/lektrico/test_button.py +++ b/tests/components/lektrico/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_init.py b/tests/components/lektrico/test_init.py index 93068ffe531..996c4fed527 100644 --- a/tests/components/lektrico/test_init.py +++ b/tests/components/lektrico/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lektrico.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_number.py b/tests/components/lektrico/test_number.py index ade6515ca72..3250ac6af91 100644 --- a/tests/components/lektrico/test_number.py +++ b/tests/components/lektrico/test_number.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_select.py b/tests/components/lektrico/test_select.py index cb09c47535e..367517c59aa 100644 --- a/tests/components/lektrico/test_select.py +++ b/tests/components/lektrico/test_select.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_sensor.py b/tests/components/lektrico/test_sensor.py index 27be7ff1c11..d3c6d464b9b 100644 --- a/tests/components/lektrico/test_sensor.py +++ b/tests/components/lektrico/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lektrico/test_switch.py b/tests/components/lektrico/test_switch.py index cfa693d9e44..6b038a250b4 100644 --- a/tests/components/lektrico/test_switch.py +++ b/tests/components/lektrico/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/letpot/test_binary_sensor.py b/tests/components/letpot/test_binary_sensor.py index 03ce1bee1a5..43565914072 100644 --- a/tests/components/letpot/test_binary_sensor.py +++ b/tests/components/letpot/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/letpot/test_sensor.py b/tests/components/letpot/test_sensor.py index a527d062ca7..3ed4c6d9308 100644 --- a/tests/components/letpot/test_sensor.py +++ b/tests/components/letpot/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/letpot/test_switch.py b/tests/components/letpot/test_switch.py index 0ba1f556bc9..7eeafd78291 100644 --- a/tests/components/letpot/test_switch.py +++ b/tests/components/letpot/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from letpot.exceptions import LetPotConnectionException, LetPotException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( SERVICE_TOGGLE, diff --git a/tests/components/letpot/test_time.py b/tests/components/letpot/test_time.py index e65ea4532e1..dba51ce8497 100644 --- a/tests/components/letpot/test_time.py +++ b/tests/components/letpot/test_time.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from letpot.exceptions import LetPotConnectionException, LetPotException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.time import SERVICE_SET_VALUE from homeassistant.const import Platform diff --git a/tests/components/lg_thinq/test_climate.py b/tests/components/lg_thinq/test_climate.py index e53b1c5ff39..c79331dd638 100644 --- a/tests/components/lg_thinq/test_climate.py +++ b/tests/components/lg_thinq/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lg_thinq/test_event.py b/tests/components/lg_thinq/test_event.py index bea758cb943..398af1e8aad 100644 --- a/tests/components/lg_thinq/test_event.py +++ b/tests/components/lg_thinq/test_event.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lg_thinq/test_number.py b/tests/components/lg_thinq/test_number.py index e578e4eba7a..7c37ba3f5e0 100644 --- a/tests/components/lg_thinq/test_number.py +++ b/tests/components/lg_thinq/test_number.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/lg_thinq/test_sensor.py b/tests/components/lg_thinq/test_sensor.py index e1f1a7ed93d..e2c8e122eea 100644 --- a/tests/components/lg_thinq/test_sensor.py +++ b/tests/components/lg_thinq/test_sensor.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/linear_garage_door/test_cover.py b/tests/components/linear_garage_door/test_cover.py index be5ae8f35f7..caa590f3b3a 100644 --- a/tests/components/linear_garage_door/test_cover.py +++ b/tests/components/linear_garage_door/test_cover.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, diff --git a/tests/components/linear_garage_door/test_diagnostics.py b/tests/components/linear_garage_door/test_diagnostics.py index a00feed43ff..f51bb0a366c 100644 --- a/tests/components/linear_garage_door/test_diagnostics.py +++ b/tests/components/linear_garage_door/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/linear_garage_door/test_light.py b/tests/components/linear_garage_door/test_light.py index 351ddad813a..d462130dc91 100644 --- a/tests/components/linear_garage_door/test_light.py +++ b/tests/components/linear_garage_door/test_light.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, diff --git a/tests/components/linkplay/test_diagnostics.py b/tests/components/linkplay/test_diagnostics.py index de60b7ecb3a..332359b9769 100644 --- a/tests/components/linkplay/test_diagnostics.py +++ b/tests/components/linkplay/test_diagnostics.py @@ -5,7 +5,7 @@ from unittest.mock import patch from linkplay.bridge import LinkPlayMultiroom from linkplay.consts import API_ENDPOINT from linkplay.endpoint import LinkPlayApiEndpoint -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.linkplay.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/madvr/test_binary_sensor.py b/tests/components/madvr/test_binary_sensor.py index 9ddbc7b3afe..6db0471b338 100644 --- a/tests/components/madvr/test_binary_sensor.py +++ b/tests/components/madvr/test_binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/madvr/test_diagnostics.py b/tests/components/madvr/test_diagnostics.py index 453eaba8d94..4e355e82612 100644 --- a/tests/components/madvr/test_diagnostics.py +++ b/tests/components/madvr/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.const import Platform diff --git a/tests/components/madvr/test_remote.py b/tests/components/madvr/test_remote.py index 1ddbacdb6e9..e91c206bdd5 100644 --- a/tests/components/madvr/test_remote.py +++ b/tests/components/madvr/test_remote.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.remote import ( DOMAIN as REMOTE_DOMAIN, diff --git a/tests/components/madvr/test_sensor.py b/tests/components/madvr/test_sensor.py index dd1722913f2..029f32d552d 100644 --- a/tests/components/madvr/test_sensor.py +++ b/tests/components/madvr/test_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.madvr.sensor import get_temperature from homeassistant.const import STATE_UNKNOWN, Platform diff --git a/tests/components/mastodon/test_diagnostics.py b/tests/components/mastodon/test_diagnostics.py index c2de15d1a51..531543ee65d 100644 --- a/tests/components/mastodon/test_diagnostics.py +++ b/tests/components/mastodon/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/common.py b/tests/components/matter/common.py index 519b4c4027d..18c4760e473 100644 --- a/tests/components/matter/common.py +++ b/tests/components/matter/common.py @@ -10,7 +10,7 @@ from unittest.mock import MagicMock from matter_server.client.models.node import MatterNode from matter_server.common.helpers.util import dataclass_from_dict from matter_server.common.models import EventType, MatterNodeData -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index c20c5cb7f29..bea9c1ad237 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from matter_server.client.models.node import MatterNode from matter_server.common.models import EventType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.matter.binary_sensor import ( DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS, diff --git a/tests/components/matter/test_button.py b/tests/components/matter/test_button.py index cbf62dd80c7..2af2d40cb74 100644 --- a/tests/components/matter/test_button.py +++ b/tests/components/matter/test_button.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 037ec4e7626..7761d5d27da 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -6,7 +6,7 @@ from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ClimateEntityFeature, HVACAction, HVACMode from homeassistant.const import Platform diff --git a/tests/components/matter/test_cover.py b/tests/components/matter/test_cover.py index 224aabd9082..cdf7f6300be 100644 --- a/tests/components/matter/test_cover.py +++ b/tests/components/matter/test_cover.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import CoverEntityFeature, CoverState from homeassistant.const import Platform diff --git a/tests/components/matter/test_event.py b/tests/components/matter/test_event.py index 651c71a5dce..8098d4dd639 100644 --- a/tests/components/matter/test_event.py +++ b/tests/components/matter/test_event.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from matter_server.client.models.node import MatterNode from matter_server.common.models import EventType, MatterNodeEvent import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.event import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES from homeassistant.const import Platform diff --git a/tests/components/matter/test_fan.py b/tests/components/matter/test_fan.py index 6ed95b0ecc2..6c3acd1978d 100644 --- a/tests/components/matter/test_fan.py +++ b/tests/components/matter/test_fan.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, call from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_DIRECTION, diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index c49b47c9106..b600ededa6e 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ColorMode from homeassistant.const import Platform diff --git a/tests/components/matter/test_lock.py b/tests/components/matter/test_lock.py index bb03b296fc6..ab3995e6771 100644 --- a/tests/components/matter/test_lock.py +++ b/tests/components/matter/test_lock.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lock import LockEntityFeature, LockState from homeassistant.const import ATTR_CODE, STATE_UNKNOWN, Platform diff --git a/tests/components/matter/test_number.py b/tests/components/matter/test_number.py index 2a4eea1c324..c94b92dbc46 100644 --- a/tests/components/matter/test_number.py +++ b/tests/components/matter/test_number.py @@ -7,7 +7,7 @@ from matter_server.common import custom_clusters from matter_server.common.errors import MatterError from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 2403b4b1623..71999873135 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -6,7 +6,7 @@ from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 03ffa31125e..868c73a1dff 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import EntityCategory, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_switch.py b/tests/components/matter/test_switch.py index f294cd31a26..ecb65e625d9 100644 --- a/tests/components/matter/test_switch.py +++ b/tests/components/matter/test_switch.py @@ -8,7 +8,7 @@ from matter_server.client.models.node import MatterNode from matter_server.common.errors import MatterError from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index 1b33f6a2fe2..5bd90ee1109 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_valve.py b/tests/components/matter/test_valve.py index 9c4429dda65..36ab34cb64e 100644 --- a/tests/components/matter/test_valve.py +++ b/tests/components/matter/test_valve.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, call from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/matter/test_water_heater.py b/tests/components/matter/test_water_heater.py index eb2ea9eb40e..2785dc9c778 100644 --- a/tests/components/matter/test_water_heater.py +++ b/tests/components/matter/test_water_heater.py @@ -6,7 +6,7 @@ from chip.clusters import Objects as clusters from matter_server.client.models.node import MatterNode from matter_server.common.helpers.util import create_attribute_path_from_attribute import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.water_heater import ( STATE_ECO, diff --git a/tests/components/mealie/test_diagnostics.py b/tests/components/mealie/test_diagnostics.py index 88680da9784..43434d31107 100644 --- a/tests/components/mealie/test_diagnostics.py +++ b/tests/components/mealie/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index a45a67801df..7581363dee4 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from aiomealie import About, MealieAuthenticationError, MealieConnectionError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.mealie.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 63668379490..57c55159bdc 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -11,7 +11,7 @@ from aiomealie import ( ) from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.mealie.const import ( ATTR_CONFIG_ENTRY_ID, diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index 21fab6f875c..aa554720786 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -6,7 +6,7 @@ from typing import Any from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from yt_dlp import DownloadError from homeassistant.components.media_extractor.const import ( diff --git a/tests/components/melcloud/test_diagnostics.py b/tests/components/melcloud/test_diagnostics.py index 32ec94a54d1..e1c498e8704 100644 --- a/tests/components/melcloud/test_diagnostics.py +++ b/tests/components/melcloud/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.melcloud.const import DOMAIN diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index b305d629a91..c93f741413d 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, diff --git a/tests/components/miele/test_binary_sensor.py b/tests/components/miele/test_binary_sensor.py index d56128a1a76..db44ea554a4 100644 --- a/tests/components/miele/test_binary_sensor.py +++ b/tests/components/miele/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/miele/test_button.py b/tests/components/miele/test_button.py index f4331bc40c5..d3cfb2af999 100644 --- a/tests/components/miele/test_button.py +++ b/tests/components/miele/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientResponseError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index 29124eda893..bff55311f4b 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE diff --git a/tests/components/miele/test_diagnostics.py b/tests/components/miele/test_diagnostics.py index cf322b971c8..e613a4e512e 100644 --- a/tests/components/miele/test_diagnostics.py +++ b/tests/components/miele/test_diagnostics.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import paths from homeassistant.components.miele.const import DOMAIN diff --git a/tests/components/miele/test_fan.py b/tests/components/miele/test_fan.py index ce0a4936b41..47c7c4fb8ec 100644 --- a/tests/components/miele/test_fan.py +++ b/tests/components/miele/test_fan.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from aiohttp import ClientResponseError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON diff --git a/tests/components/miele/test_init.py b/tests/components/miele/test_init.py index 37ea5a57ed4..dae3d5ef79c 100644 --- a/tests/components/miele/test_init.py +++ b/tests/components/miele/test_init.py @@ -9,7 +9,7 @@ from aiohttp import ClientConnectionError from freezegun.api import FrozenDateTimeFactory from pymiele import OAUTH2_TOKEN import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.miele.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/miele/test_light.py b/tests/components/miele/test_light.py index 9da6f5c686a..c0cae688c1c 100644 --- a/tests/components/miele/test_light.py +++ b/tests/components/miele/test_light.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index f5d579fc963..7beb2fec8f1 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/miele/test_switch.py b/tests/components/miele/test_switch.py index 038dc781d40..d60708c24e1 100644 --- a/tests/components/miele/test_switch.py +++ b/tests/components/miele/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON diff --git a/tests/components/miele/test_vacuum.py b/tests/components/miele/test_vacuum.py index 81e29bb30b6..f1f0ae22930 100644 --- a/tests/components/miele/test_vacuum.py +++ b/tests/components/miele/test_vacuum.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientResponseError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.miele.const import PROCESS_ACTION, PROGRAM_ID from homeassistant.components.vacuum import ( diff --git a/tests/components/minecraft_server/test_binary_sensor.py b/tests/components/minecraft_server/test_binary_sensor.py index 77537a5e8e4..c87644961f2 100644 --- a/tests/components/minecraft_server/test_binary_sensor.py +++ b/tests/components/minecraft_server/test_binary_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from mcstatus import BedrockServer, JavaServer from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant diff --git a/tests/components/minecraft_server/test_diagnostics.py b/tests/components/minecraft_server/test_diagnostics.py index e72d0c5f8db..800af79e51c 100644 --- a/tests/components/minecraft_server/test_diagnostics.py +++ b/tests/components/minecraft_server/test_diagnostics.py @@ -5,7 +5,7 @@ from unittest.mock import patch from mcstatus import BedrockServer, JavaServer from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/minecraft_server/test_sensor.py b/tests/components/minecraft_server/test_sensor.py index a4cea239f7a..3502184df86 100644 --- a/tests/components/minecraft_server/test_sensor.py +++ b/tests/components/minecraft_server/test_sensor.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from mcstatus import BedrockServer, JavaServer from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant diff --git a/tests/components/modern_forms/test_diagnostics.py b/tests/components/modern_forms/test_diagnostics.py index 9eb2e4efa94..10a4c8385fa 100644 --- a/tests/components/modern_forms/test_diagnostics.py +++ b/tests/components/modern_forms/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the Modern Forms diagnostics platform.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/moehlenhoff_alpha2/test_binary_sensor.py b/tests/components/moehlenhoff_alpha2/test_binary_sensor.py index e650e9f9ba6..f9fbe60fb44 100644 --- a/tests/components/moehlenhoff_alpha2/test_binary_sensor.py +++ b/tests/components/moehlenhoff_alpha2/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/moehlenhoff_alpha2/test_button.py b/tests/components/moehlenhoff_alpha2/test_button.py index d4465746d53..09ffd1134ea 100644 --- a/tests/components/moehlenhoff_alpha2/test_button.py +++ b/tests/components/moehlenhoff_alpha2/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/moehlenhoff_alpha2/test_climate.py b/tests/components/moehlenhoff_alpha2/test_climate.py index a32f2b5bd4f..a9e46167693 100644 --- a/tests/components/moehlenhoff_alpha2/test_climate.py +++ b/tests/components/moehlenhoff_alpha2/test_climate.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/moehlenhoff_alpha2/test_sensor.py b/tests/components/moehlenhoff_alpha2/test_sensor.py index 931c744faea..6f89d8ce306 100644 --- a/tests/components/moehlenhoff_alpha2/test_sensor.py +++ b/tests/components/moehlenhoff_alpha2/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/monarch_money/test_sensor.py b/tests/components/monarch_money/test_sensor.py index aac1eaefb2d..1fe1b8cdb12 100644 --- a/tests/components/monarch_money/test_sensor.py +++ b/tests/components/monarch_money/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/monzo/test_sensor.py b/tests/components/monzo/test_sensor.py index a57466fdbd4..c4b55d11c36 100644 --- a/tests/components/monzo/test_sensor.py +++ b/tests/components/monzo/test_sensor.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from monzopy import InvalidMonzoAPIResponseError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.monzo.const import DOMAIN from homeassistant.components.monzo.sensor import ( diff --git a/tests/components/motionblinds_ble/test_diagnostics.py b/tests/components/motionblinds_ble/test_diagnostics.py index 878d2caa326..6d041a2df8b 100644 --- a/tests/components/motionblinds_ble/test_diagnostics.py +++ b/tests/components/motionblinds_ble/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Motionblinds Bluetooth diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/music_assistant/common.py b/tests/components/music_assistant/common.py index 6d7ef927c6e..a98ae82fbe1 100644 --- a/tests/components/music_assistant/common.py +++ b/tests/components/music_assistant/common.py @@ -19,7 +19,7 @@ from music_assistant_models.media_items import ( ) from music_assistant_models.player import Player from music_assistant_models.player_queue import PlayerQueue -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/music_assistant/test_actions.py b/tests/components/music_assistant/test_actions.py index ba8b1acdeac..0a469807de3 100644 --- a/tests/components/music_assistant/test_actions.py +++ b/tests/components/music_assistant/test_actions.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock from music_assistant_models.media_items import SearchResults import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.music_assistant.actions import ( SERVICE_GET_LIBRARY, diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 00ba6bc8093..288d49092e5 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -11,7 +11,7 @@ from music_assistant_models.enums import ( ) from music_assistant_models.media_items import Track import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import paths from homeassistant.components.media_player import ( diff --git a/tests/components/myuplink/test_binary_sensor.py b/tests/components/myuplink/test_binary_sensor.py index 160530bcdab..cf297a0a3f7 100644 --- a/tests/components/myuplink/test_binary_sensor.py +++ b/tests/components/myuplink/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/myuplink/test_diagnostics.py b/tests/components/myuplink/test_diagnostics.py index e0803eb76f0..1da81c5cf1f 100644 --- a/tests/components/myuplink/test_diagnostics.py +++ b/tests/components/myuplink/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the myuplink integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import paths from homeassistant.core import HomeAssistant diff --git a/tests/components/myuplink/test_init.py b/tests/components/myuplink/test_init.py index 320bf202024..891ba992772 100644 --- a/tests/components/myuplink/test_init.py +++ b/tests/components/myuplink/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from aiohttp import ClientConnectionError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.myuplink.const import DOMAIN, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/myuplink/test_number.py b/tests/components/myuplink/test_number.py index ef7b1749782..a488ae3972c 100644 --- a/tests/components/myuplink/test_number.py +++ b/tests/components/myuplink/test_number.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/myuplink/test_select.py b/tests/components/myuplink/test_select.py index f1797ebe5ad..f19aff60d26 100644 --- a/tests/components/myuplink/test_select.py +++ b/tests/components/myuplink/test_select.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/myuplink/test_sensor.py b/tests/components/myuplink/test_sensor.py index 98cdfc322da..9f0beebe995 100644 --- a/tests/components/myuplink/test_sensor.py +++ b/tests/components/myuplink/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/myuplink/test_switch.py b/tests/components/myuplink/test_switch.py index 82d381df7fc..628287b8fd8 100644 --- a/tests/components/myuplink/test_switch.py +++ b/tests/components/myuplink/test_switch.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from aiohttp import ClientError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/nam/test_diagnostics.py b/tests/components/nam/test_diagnostics.py index 7ed49a37e0a..b29e5e834b2 100644 --- a/tests/components/nam/test_diagnostics.py +++ b/tests/components/nam/test_diagnostics.py @@ -1,6 +1,6 @@ """Test NAM diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 6924af48f01..40cabfb49ae 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch from freezegun.api import FrozenDateTimeFactory from nettigo_air_monitor import ApiError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tenacity import RetryError from homeassistant.components.nam.const import DEFAULT_UPDATE_INTERVAL, DOMAIN diff --git a/tests/components/nanoleaf/test_light.py b/tests/components/nanoleaf/test_light.py index bd852ea81e4..3260c2e2609 100644 --- a/tests/components/nanoleaf/test_light.py +++ b/tests/components/nanoleaf/test_light.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ATTR_EFFECT_LIST, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index a072394a43d..74249a71a8b 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -4,7 +4,7 @@ from unittest.mock import patch from google_nest_sdm.exceptions import SubscriberException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.nest.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 9110f8c724f..06c56aa7e22 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -6,7 +6,7 @@ import json from typing import Any from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.webhook import async_handle_webhook from homeassistant.const import Platform diff --git a/tests/components/netatmo/test_binary_sensor.py b/tests/components/netatmo/test_binary_sensor.py index 7b841ba204e..91d2b3ad63b 100644 --- a/tests/components/netatmo/test_binary_sensor.py +++ b/tests/components/netatmo/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/netatmo/test_button.py b/tests/components/netatmo/test_button.py index bffecf7d83a..d526f508624 100644 --- a/tests/components/netatmo/test_button.py +++ b/tests/components/netatmo/test_button.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 32f20544043..706cf887539 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch import pyatmo import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import camera from homeassistant.components.camera import CameraState diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 45216e415a5..f3532c999e7 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from voluptuous.error import MultipleInvalid from homeassistant.components.climate import ( diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py index 9368a564afb..3aa67395cec 100644 --- a/tests/components/netatmo/test_cover.py +++ b/tests/components/netatmo/test_cover.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( ATTR_POSITION, diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py index 7a0bf11c652..dadec4a1eb2 100644 --- a/tests/components/netatmo/test_diagnostics.py +++ b/tests/components/netatmo/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import paths from homeassistant.core import HomeAssistant diff --git a/tests/components/netatmo/test_fan.py b/tests/components/netatmo/test_fan.py index 3dbc8b3a6f5..e80d3ae76fd 100644 --- a/tests/components/netatmo/test_fan.py +++ b/tests/components/netatmo/test_fan.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PRESET_MODE, diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index c1a687c6fa8..18d255ec6ee 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyatmo.const import ALL_SCOPES import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import cloud from homeassistant.components.netatmo import DOMAIN diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 0932395b8ec..16a3ac2aaeb 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index 458115f8f5c..6b9eb6f4451 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index e9e1ff4739e..95776d21f6a 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.netatmo import sensor from homeassistant.const import Platform diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py index 837f6201b1e..fd7b09daa4f 100644 --- a/tests/components/netatmo/test_switch.py +++ b/tests/components/netatmo/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/nexia/test_diagnostics.py b/tests/components/nexia/test_diagnostics.py index ff9696d1567..fc3a8d5ee98 100644 --- a/tests/components/nexia/test_diagnostics.py +++ b/tests/components/nexia/test_diagnostics.py @@ -1,6 +1,6 @@ """Test august diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py index 19cad755fb4..99e40af0dce 100644 --- a/tests/components/nextdns/test_binary_sensor.py +++ b/tests/components/nextdns/test_binary_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import patch from nextdns import ApiError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index 3d2422c34a7..0cb4a7cd0df 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -6,7 +6,7 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.nextdns.const import DOMAIN diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 3bb1fc3ee67..4a5e09908ec 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,6 +1,6 @@ """Test NextDNS diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index eddf5a1cc5a..43e823fbf38 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from nextdns import ApiError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index c85525ac457..1b0edb2c83c 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -7,7 +7,7 @@ from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectorError from nextdns import ApiError, InvalidApiKeyError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tenacity import RetryError from homeassistant.components.nextdns.const import DOMAIN diff --git a/tests/components/nibe_heatpump/test_climate.py b/tests/components/nibe_heatpump/test_climate.py index 073e142f7ff..91245503eb3 100644 --- a/tests/components/nibe_heatpump/test_climate.py +++ b/tests/components/nibe_heatpump/test_climate.py @@ -12,7 +12,7 @@ from nibe.coil_groups import ( ) from nibe.heatpump import Model import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/nibe_heatpump/test_coordinator.py b/tests/components/nibe_heatpump/test_coordinator.py index 2fade8e34d7..05c771ee420 100644 --- a/tests/components/nibe_heatpump/test_coordinator.py +++ b/tests/components/nibe_heatpump/test_coordinator.py @@ -7,7 +7,7 @@ from unittest.mock import patch from nibe.coil import Coil, CoilData from nibe.heatpump import Model import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nibe_heatpump/test_number.py b/tests/components/nibe_heatpump/test_number.py index 73fed9ee08a..dc7faf0a80e 100644 --- a/tests/components/nibe_heatpump/test_number.py +++ b/tests/components/nibe_heatpump/test_number.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from nibe.coil import CoilData from nibe.heatpump import Model import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/nice_go/test_cover.py b/tests/components/nice_go/test_cover.py index 542b1717d88..df708f64b8f 100644 --- a/tests/components/nice_go/test_cover.py +++ b/tests/components/nice_go/test_cover.py @@ -6,7 +6,7 @@ from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory from nice_go import ApiError, AuthFailedError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, diff --git a/tests/components/nice_go/test_diagnostics.py b/tests/components/nice_go/test_diagnostics.py index 5c8647f3d6e..283709aa167 100644 --- a/tests/components/nice_go/test_diagnostics.py +++ b/tests/components/nice_go/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/nice_go/test_light.py b/tests/components/nice_go/test_light.py index 2bc9de59b2b..5c43367f169 100644 --- a/tests/components/nice_go/test_light.py +++ b/tests/components/nice_go/test_light.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from aiohttp import ClientError from nice_go import ApiError, AuthFailedError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, diff --git a/tests/components/niko_home_control/test_cover.py b/tests/components/niko_home_control/test_cover.py index 5e9a17c3324..3941c60b5c8 100644 --- a/tests/components/niko_home_control/test_cover.py +++ b/tests/components/niko_home_control/test_cover.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.const import ( diff --git a/tests/components/niko_home_control/test_light.py b/tests/components/niko_home_control/test_light.py index a11f846bba6..476ea95cda8 100644 --- a/tests/components/niko_home_control/test_light.py +++ b/tests/components/niko_home_control/test_light.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( diff --git a/tests/components/nuki/test_binary_sensor.py b/tests/components/nuki/test_binary_sensor.py index 54fbc93c144..11507100aae 100644 --- a/tests/components/nuki/test_binary_sensor.py +++ b/tests/components/nuki/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nuki/test_lock.py b/tests/components/nuki/test_lock.py index 824d508f3dc..fc2d9d1cba8 100644 --- a/tests/components/nuki/test_lock.py +++ b/tests/components/nuki/test_lock.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nuki/test_sensor.py b/tests/components/nuki/test_sensor.py index dde803d573f..69a0aec56f7 100644 --- a/tests/components/nuki/test_sensor.py +++ b/tests/components/nuki/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/nws/test_diagnostics.py b/tests/components/nws/test_diagnostics.py index 55f7f3100a0..fecd74eb0f4 100644 --- a/tests/components/nws/test_diagnostics.py +++ b/tests/components/nws/test_diagnostics.py @@ -1,6 +1,6 @@ """Test NWS diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import nws from homeassistant.core import HomeAssistant diff --git a/tests/components/nyt_games/test_init.py b/tests/components/nyt_games/test_init.py index 2e1a8c92f90..ced155ac5a2 100644 --- a/tests/components/nyt_games/test_init.py +++ b/tests/components/nyt_games/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.nyt_games.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/nyt_games/test_sensor.py b/tests/components/nyt_games/test_sensor.py index f35caf20b57..5802b38dd83 100644 --- a/tests/components/nyt_games/test_sensor.py +++ b/tests/components/nyt_games/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from nyt_games import NYTGamesError, WordleStats import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.nyt_games.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE diff --git a/tests/components/omnilogic/test_sensor.py b/tests/components/omnilogic/test_sensor.py index 166eb7f87f2..ed7d781ab2d 100644 --- a/tests/components/omnilogic/test_sensor.py +++ b/tests/components/omnilogic/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/omnilogic/test_switch.py b/tests/components/omnilogic/test_switch.py index 1f9506380a2..adc8fe04763 100644 --- a/tests/components/omnilogic/test_switch.py +++ b/tests/components/omnilogic/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/ondilo_ico/test_init.py b/tests/components/ondilo_ico/test_init.py index 58b1e27987d..d93c5ce4df6 100644 --- a/tests/components/ondilo_ico/test_init.py +++ b/tests/components/ondilo_ico/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory from ondilo import OndiloError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/ondilo_ico/test_sensor.py b/tests/components/ondilo_ico/test_sensor.py index c944353724e..8785ca39880 100644 --- a/tests/components/ondilo_ico/test_sensor.py +++ b/tests/components/ondilo_ico/test_sensor.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import MagicMock, patch from ondilo import OndiloError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/onedrive/test_diagnostics.py b/tests/components/onedrive/test_diagnostics.py index f82d9925ee6..9be8455f287 100644 --- a/tests/components/onedrive/test_diagnostics.py +++ b/tests/components/onedrive/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the OneDrive integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/onedrive/test_init.py b/tests/components/onedrive/test_init.py index 952ca01e1cb..af12f66b60e 100644 --- a/tests/components/onedrive/test_init.py +++ b/tests/components/onedrive/test_init.py @@ -13,7 +13,7 @@ from onedrive_personal_sdk.exceptions import ( ) from onedrive_personal_sdk.models.items import AppRoot, Drive, File, Folder, ItemUpdate import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.onedrive.const import ( CONF_FOLDER_ID, diff --git a/tests/components/onedrive/test_sensor.py b/tests/components/onedrive/test_sensor.py index ea9d93a9a7b..18e8ad85ac2 100644 --- a/tests/components/onedrive/test_sensor.py +++ b/tests/components/onedrive/test_sensor.py @@ -9,7 +9,7 @@ from onedrive_personal_sdk.const import DriveType from onedrive_personal_sdk.exceptions import HttpRequestException from onedrive_personal_sdk.models.items import Drive import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant diff --git a/tests/components/onvif/test_diagnostics.py b/tests/components/onvif/test_diagnostics.py index ce8febe2341..ca2ba8e8c74 100644 --- a/tests/components/onvif/test_diagnostics.py +++ b/tests/components/onvif/test_diagnostics.py @@ -1,6 +1,6 @@ """Test ONVIF diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/opensky/test_sensor.py b/tests/components/opensky/test_sensor.py index 937540a42c1..54bab7e7ee6 100644 --- a/tests/components/opensky/test_sensor.py +++ b/tests/components/opensky/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from python_opensky import StatesResponse -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.opensky.const import ( DOMAIN, diff --git a/tests/components/openweathermap/test_sensor.py b/tests/components/openweathermap/test_sensor.py index 8cb8bd11c26..fdf21ec71fe 100644 --- a/tests/components/openweathermap/test_sensor.py +++ b/tests/components/openweathermap/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.openweathermap.const import ( OWM_MODE_FREE_CURRENT, diff --git a/tests/components/openweathermap/test_weather.py b/tests/components/openweathermap/test_weather.py index 9ac51afd6b3..0d7dfcad71f 100644 --- a/tests/components/openweathermap/test_weather.py +++ b/tests/components/openweathermap/test_weather.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.openweathermap.const import ( DOMAIN, diff --git a/tests/components/osoenergy/test_water_heater.py b/tests/components/osoenergy/test_water_heater.py index 851e710fa1c..fd27975c938 100644 --- a/tests/components/osoenergy/test_water_heater.py +++ b/tests/components/osoenergy/test_water_heater.py @@ -3,7 +3,7 @@ from unittest.mock import ANY, MagicMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.osoenergy.const import DOMAIN from homeassistant.components.osoenergy.water_heater import ( diff --git a/tests/components/overkiz/test_diagnostics.py b/tests/components/overkiz/test_diagnostics.py index 672370c2667..e052818daee 100644 --- a/tests/components/overkiz/test_diagnostics.py +++ b/tests/components/overkiz/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.overkiz.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/overseerr/test_diagnostics.py b/tests/components/overseerr/test_diagnostics.py index 28b97e9514f..394799a277c 100644 --- a/tests/components/overseerr/test_diagnostics.py +++ b/tests/components/overseerr/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/overseerr/test_event.py b/tests/components/overseerr/test_event.py index 3866ccc09ca..448cac7c5c1 100644 --- a/tests/components/overseerr/test_event.py +++ b/tests/components/overseerr/test_event.py @@ -7,7 +7,7 @@ from freezegun.api import FrozenDateTimeFactory from future.backports.datetime import timedelta import pytest from python_overseerr import OverseerrConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.overseerr import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/overseerr/test_init.py b/tests/components/overseerr/test_init.py index 6418e2103db..66e6a5c134c 100644 --- a/tests/components/overseerr/test_init.py +++ b/tests/components/overseerr/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch import pytest from python_overseerr import OverseerrAuthenticationError, OverseerrConnectionError from python_overseerr.models import WebhookNotificationOptions -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import cloud from homeassistant.components.cloud import CloudNotAvailable diff --git a/tests/components/overseerr/test_sensor.py b/tests/components/overseerr/test_sensor.py index 6689b1ebcc3..2350f1b0883 100644 --- a/tests/components/overseerr/test_sensor.py +++ b/tests/components/overseerr/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.overseerr import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/overseerr/test_services.py b/tests/components/overseerr/test_services.py index a0b87b5deef..3d7bcc3577f 100644 --- a/tests/components/overseerr/test_services.py +++ b/tests/components/overseerr/test_services.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock import pytest from python_overseerr import OverseerrConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.overseerr.const import ( ATTR_CONFIG_ENTRY_ID, diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py index 3b7426051d4..a8ce2646034 100644 --- a/tests/components/p1_monitor/test_init.py +++ b/tests/components/p1_monitor/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from p1monitor import P1MonitorConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/palazzetti/test_button.py b/tests/components/palazzetti/test_button.py index de0f26fe8aa..85fd63d45d5 100644 --- a/tests/components/palazzetti/test_button.py +++ b/tests/components/palazzetti/test_button.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from pypalazzetti.exceptions import CommunicationError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/palazzetti/test_climate.py b/tests/components/palazzetti/test_climate.py index 22bd04f234e..d2aa17e71b3 100644 --- a/tests/components/palazzetti/test_climate.py +++ b/tests/components/palazzetti/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from pypalazzetti.exceptions import CommunicationError, ValidationError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_FAN_MODE, diff --git a/tests/components/palazzetti/test_diagnostics.py b/tests/components/palazzetti/test_diagnostics.py index 80d021be511..e25ad7b9c6e 100644 --- a/tests/components/palazzetti/test_diagnostics.py +++ b/tests/components/palazzetti/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Palazzetti diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/palazzetti/test_init.py b/tests/components/palazzetti/test_init.py index 710144b2b7b..3002de1a0d2 100644 --- a/tests/components/palazzetti/test_init.py +++ b/tests/components/palazzetti/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/palazzetti/test_number.py b/tests/components/palazzetti/test_number.py index 8f09384c1b7..6483834e190 100644 --- a/tests/components/palazzetti/test_number.py +++ b/tests/components/palazzetti/test_number.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from pypalazzetti.exceptions import CommunicationError, ValidationError from pypalazzetti.fan import FanType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/palazzetti/test_sensor.py b/tests/components/palazzetti/test_sensor.py index c7d7317bb0b..55889692203 100644 --- a/tests/components/palazzetti/test_sensor.py +++ b/tests/components/palazzetti/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/pegel_online/test_diagnostics.py b/tests/components/pegel_online/test_diagnostics.py index 220f244b751..a5b08d4bae2 100644 --- a/tests/components/pegel_online/test_diagnostics.py +++ b/tests/components/pegel_online/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.pegel_online.const import CONF_STATION, DOMAIN diff --git a/tests/components/pglab/test_sensor.py b/tests/components/pglab/test_sensor.py index 75932dd036c..0991d6bd814 100644 --- a/tests/components/pglab/test_sensor.py +++ b/tests/components/pglab/test_sensor.py @@ -4,7 +4,7 @@ import json from freezegun import freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/philips_js/test_diagnostics.py b/tests/components/philips_js/test_diagnostics.py index d61546e52c3..0d8909c86be 100644 --- a/tests/components/philips_js/test_diagnostics.py +++ b/tests/components/philips_js/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from haphilipsjs.typing import ChannelListType, ContextType, FavoriteListType -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index 660b5ca31f1..93742ca9005 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from icmplib import Host import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.ping.const import CONF_IMPORTED_BY, DOMAIN diff --git a/tests/components/ping/test_sensor.py b/tests/components/ping/test_sensor.py index 5c4833aaf06..bdc8b7d28e4 100644 --- a/tests/components/ping/test_sensor.py +++ b/tests/components/ping/test_sensor.py @@ -1,7 +1,7 @@ """Test sensor platform of Ping.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/plaato/test_binary_sensor.py b/tests/components/plaato/test_binary_sensor.py index 73d378dd531..5542c79e8ea 100644 --- a/tests/components/plaato/test_binary_sensor.py +++ b/tests/components/plaato/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/plaato/test_sensor.py b/tests/components/plaato/test_sensor.py index e4574634c4b..63e9255faa0 100644 --- a/tests/components/plaato/test_sensor.py +++ b/tests/components/plaato/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index a2b0521d6e1..dbfd810d4dc 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/powerfox/test_diagnostics.py b/tests/components/powerfox/test_diagnostics.py index 7dc2c3c7263..220c809a5f9 100644 --- a/tests/components/powerfox/test_diagnostics.py +++ b/tests/components/powerfox/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/powerfox/test_sensor.py b/tests/components/powerfox/test_sensor.py index 547d8de202c..2dfc1227d77 100644 --- a/tests/components/powerfox/test_sensor.py +++ b/tests/components/powerfox/test_sensor.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from powerfox import PowerfoxConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/rainmachine/test_binary_sensor.py b/tests/components/rainmachine/test_binary_sensor.py index d428993da51..55736f118b3 100644 --- a/tests/components/rainmachine/test_binary_sensor.py +++ b/tests/components/rainmachine/test_binary_sensor.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/rainmachine/test_button.py b/tests/components/rainmachine/test_button.py index 629c325c79e..a9d4042bf8f 100644 --- a/tests/components/rainmachine/test_button.py +++ b/tests/components/rainmachine/test_button.py @@ -3,7 +3,7 @@ from typing import Any from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index ad5743957dd..65cf45810a3 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -1,7 +1,7 @@ """Test RainMachine diagnostics.""" from regenmaschine.errors import RainMachineError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/rainmachine/test_select.py b/tests/components/rainmachine/test_select.py index ca9ce2e644d..31768313c0b 100644 --- a/tests/components/rainmachine/test_select.py +++ b/tests/components/rainmachine/test_select.py @@ -3,7 +3,7 @@ from typing import Any from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/rainmachine/test_sensor.py b/tests/components/rainmachine/test_sensor.py index 3ff533b6da0..15bb87a8151 100644 --- a/tests/components/rainmachine/test_sensor.py +++ b/tests/components/rainmachine/test_sensor.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/rainmachine/test_switch.py b/tests/components/rainmachine/test_switch.py index 50e73a78efe..cc0552a15f1 100644 --- a/tests/components/rainmachine/test_switch.py +++ b/tests/components/rainmachine/test_switch.py @@ -4,7 +4,7 @@ from typing import Any from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rainmachine import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/rehlko/test_sensor.py b/tests/components/rehlko/test_sensor.py index ef3d9d1cf6a..ce361678a59 100644 --- a/tests/components/rehlko/test_sensor.py +++ b/tests/components/rehlko/test_sensor.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rehlko.coordinator import SCAN_INTERVAL_MINUTES from homeassistant.const import STATE_UNAVAILABLE, Platform diff --git a/tests/components/renault/test_diagnostics.py b/tests/components/renault/test_diagnostics.py index 233a32f7af8..1e238b15225 100644 --- a/tests/components/renault/test_diagnostics.py +++ b/tests/components/renault/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Renault diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.renault import DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 1762210ec6f..eef38c00f36 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -8,7 +8,7 @@ import pytest from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule, HvacSchedule -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.renault.const import DOMAIN from homeassistant.components.renault.services import ( diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index 45683bba903..bfdf7d8a9da 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Ridwell diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/roku/test_diagnostics.py b/tests/components/roku/test_diagnostics.py index 37e0d43a582..c352fa60b56 100644 --- a/tests/components/roku/test_diagnostics.py +++ b/tests/components/roku/test_diagnostics.py @@ -1,7 +1,7 @@ """Tests for the diagnostics data provided by the Roku integration.""" from rokuecp import Device as RokuDevice -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util diff --git a/tests/components/rova/test_init.py b/tests/components/rova/test_init.py index 2190e2f8ce3..5441a730bf6 100644 --- a/tests/components/rova/test_init.py +++ b/tests/components/rova/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest from requests import ConnectTimeout -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.rova import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/rova/test_sensor.py b/tests/components/rova/test_sensor.py index ae8b64363da..27a3c109ce3 100644 --- a/tests/components/rova/test_sensor.py +++ b/tests/components/rova/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/russound_rio/test_diagnostics.py b/tests/components/russound_rio/test_diagnostics.py index c6c5441128d..3d83ef12df1 100644 --- a/tests/components/russound_rio/test_diagnostics.py +++ b/tests/components/russound_rio/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/russound_rio/test_init.py b/tests/components/russound_rio/test_init.py index d654eea32bd..935b921b069 100644 --- a/tests/components/russound_rio/test_init.py +++ b/tests/components/russound_rio/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, Mock from aiorussound.models import CallbackType import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.russound_rio.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/sabnzbd/test_binary_sensor.py b/tests/components/sabnzbd/test_binary_sensor.py index 48a3c006488..e823ae6ba96 100644 --- a/tests/components/sabnzbd/test_binary_sensor.py +++ b/tests/components/sabnzbd/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/sabnzbd/test_button.py b/tests/components/sabnzbd/test_button.py index 199d8eb03a0..813d532a38b 100644 --- a/tests/components/sabnzbd/test_button.py +++ b/tests/components/sabnzbd/test_button.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from pysabnzbd import SabnzbdApiException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ( diff --git a/tests/components/sabnzbd/test_number.py b/tests/components/sabnzbd/test_number.py index 61f7ea45ab1..974c5435f15 100644 --- a/tests/components/sabnzbd/test_number.py +++ b/tests/components/sabnzbd/test_number.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory from pysabnzbd import SabnzbdApiException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/sabnzbd/test_sensor.py b/tests/components/sabnzbd/test_sensor.py index 31c0868a5a7..1e5e41efce0 100644 --- a/tests/components/sabnzbd/test_sensor.py +++ b/tests/components/sabnzbd/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/sanix/test_sensor.py b/tests/components/sanix/test_sensor.py index d9729ca3c25..f7fbfa61f3f 100644 --- a/tests/components/sanix/test_sensor.py +++ b/tests/components/sanix/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/sensorpush_cloud/test_sensor.py b/tests/components/sensorpush_cloud/test_sensor.py index c35d40f1bc2..775fb788836 100644 --- a/tests/components/sensorpush_cloud/test_sensor.py +++ b/tests/components/sensorpush_cloud/test_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry diff --git a/tests/components/seventeentrack/test_services.py b/tests/components/seventeentrack/test_services.py index bbd5644ad63..2147ce994e0 100644 --- a/tests/components/seventeentrack/test_services.py +++ b/tests/components/seventeentrack/test_services.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.seventeentrack import DOMAIN from homeassistant.components.seventeentrack.const import ( diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index ea3a7d5f3d2..fc79853f29e 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import Mock from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_MOTION from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER diff --git a/tests/components/shelly/test_button.py b/tests/components/shelly/test_button.py index 2057076d18b..8d355098463 100644 --- a/tests/components/shelly/test_button.py +++ b/tests/components/shelly/test_button.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from aioshelly.const import MODEL_BLU_GATEWAY_G3 from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.shelly.const import DOMAIN diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 81914bb6a90..eddd9ab6fd0 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -11,7 +11,7 @@ from aioshelly.const import ( ) from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index a5367408955..a3c96b6b247 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -6,7 +6,7 @@ from aioshelly.ble.const import BLE_SCRIPT_NAME from aioshelly.const import MODEL_I3 import pytest from pytest_unordered import unordered -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.event import ( ATTR_EVENT_TYPE, diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 8589d643b2b..e33b04721cc 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock from aioshelly.const import MODEL_BLU_GATEWAY_G3 from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_MAX, diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 7edd38a4b31..3bf63546419 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import Mock from aioshelly.const import MODEL_BLU_GATEWAY_G3 from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, diff --git a/tests/components/simplefin/test_binary_sensor.py b/tests/components/simplefin/test_binary_sensor.py index 40c6882153d..58b0319d71f 100644 --- a/tests/components/simplefin/test_binary_sensor.py +++ b/tests/components/simplefin/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/simplefin/test_sensor.py b/tests/components/simplefin/test_sensor.py index 495f249d4e1..b26cd620a69 100644 --- a/tests/components/simplefin/test_sensor.py +++ b/tests/components/simplefin/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest from simplefin4py.exceptions import SimpleFinAuthError, SimpleFinPaymentRequiredError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/slide_local/test_button.py b/tests/components/slide_local/test_button.py index c232affbb99..d4bf955ad58 100644 --- a/tests/components/slide_local/test_button.py +++ b/tests/components/slide_local/test_button.py @@ -9,7 +9,7 @@ from goslideapi.goslideapi import ( DigestAuthCalcError, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/slide_local/test_cover.py b/tests/components/slide_local/test_cover.py index e0e4a0741d8..793f9d9513d 100644 --- a/tests/components/slide_local/test_cover.py +++ b/tests/components/slide_local/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory from goslideapi.goslideapi import ClientConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( ATTR_POSITION, diff --git a/tests/components/slide_local/test_diagnostics.py b/tests/components/slide_local/test_diagnostics.py index 3e11af378c5..cebc4443882 100644 --- a/tests/components/slide_local/test_diagnostics.py +++ b/tests/components/slide_local/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.const import Platform diff --git a/tests/components/slide_local/test_init.py b/tests/components/slide_local/test_init.py index ec9a12f9eeb..27aba115cf8 100644 --- a/tests/components/slide_local/test_init.py +++ b/tests/components/slide_local/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from goslideapi.goslideapi import ClientConnectionError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform diff --git a/tests/components/slide_local/test_switch.py b/tests/components/slide_local/test_switch.py index 9d0d8274aa5..85f90974ce6 100644 --- a/tests/components/slide_local/test_switch.py +++ b/tests/components/slide_local/test_switch.py @@ -9,7 +9,7 @@ from goslideapi.goslideapi import ( DigestAuthCalcError, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/sma/test_diagnostics.py b/tests/components/sma/test_diagnostics.py index 6c1fe0dc5cb..fa65ca049be 100644 --- a/tests/components/sma/test_diagnostics.py +++ b/tests/components/sma/test_diagnostics.py @@ -1,6 +1,6 @@ """Test the SMA diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 92b8c12554c..8199e8fc163 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -4,7 +4,7 @@ from collections.abc import Generator from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/smarty/test_binary_sensor.py b/tests/components/smarty/test_binary_sensor.py index d28fb44e1ce..5bc81eceb38 100644 --- a/tests/components/smarty/test_binary_sensor.py +++ b/tests/components/smarty/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/smarty/test_button.py b/tests/components/smarty/test_button.py index 0a7b67f2be6..3bb8da82201 100644 --- a/tests/components/smarty/test_button.py +++ b/tests/components/smarty/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/smarty/test_fan.py b/tests/components/smarty/test_fan.py index 2c0135b7aa2..557a1977017 100644 --- a/tests/components/smarty/test_fan.py +++ b/tests/components/smarty/test_fan.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/smarty/test_init.py b/tests/components/smarty/test_init.py index 6468fd74507..27c4e0f5145 100644 --- a/tests/components/smarty/test_init.py +++ b/tests/components/smarty/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smarty.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/smarty/test_sensor.py b/tests/components/smarty/test_sensor.py index a534a2ebb0f..7ec44886952 100644 --- a/tests/components/smarty/test_sensor.py +++ b/tests/components/smarty/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/smarty/test_switch.py b/tests/components/smarty/test_switch.py index 1a6748e2d23..e90eb09fc39 100644 --- a/tests/components/smarty/test_switch.py +++ b/tests/components/smarty/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( diff --git a/tests/components/smlight/test_diagnostics.py b/tests/components/smlight/test_diagnostics.py index d0c756bfd87..778ef8e5811 100644 --- a/tests/components/smlight/test_diagnostics.py +++ b/tests/components/smlight/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smlight.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/solarlog/test_diagnostics.py b/tests/components/solarlog/test_diagnostics.py index bc0b020462d..b129f5265be 100644 --- a/tests/components/solarlog/test_diagnostics.py +++ b/tests/components/solarlog/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.const import Platform diff --git a/tests/components/solarlog/test_sensor.py b/tests/components/solarlog/test_sensor.py index 77aa0308cda..132220c6261 100644 --- a/tests/components/solarlog/test_sensor.py +++ b/tests/components/solarlog/test_sensor.py @@ -10,7 +10,7 @@ from solarlog_cli.solarlog_exceptions import ( SolarLogUpdateError, ) from solarlog_cli.solarlog_models import InverterData -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index ce6e103be58..669e9168297 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -3,7 +3,7 @@ from functools import partial import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.components.sonos.media_browser import ( diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 78d88a1ea98..aaaaac6a4ba 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from soco.data_structures import SearchResult from sonos_websocket.exception import SonosWebsocketError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, diff --git a/tests/components/spotify/test_diagnostics.py b/tests/components/spotify/test_diagnostics.py index 6744ca11a00..80ef136e779 100644 --- a/tests/components/spotify/test_diagnostics.py +++ b/tests/components/spotify/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/spotify/test_media_browser.py b/tests/components/spotify/test_media_browser.py index ff3404dcfe9..603bc70c7c5 100644 --- a/tests/components/spotify/test_media_browser.py +++ b/tests/components/spotify/test_media_browser.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import BrowseError from homeassistant.components.spotify import DOMAIN diff --git a/tests/components/spotify/test_media_player.py b/tests/components/spotify/test_media_player.py index 456af43d411..913034b9636 100644 --- a/tests/components/spotify/test_media_player.py +++ b/tests/components/spotify/test_media_player.py @@ -12,7 +12,7 @@ from spotifyaio import ( SpotifyConnectionError, SpotifyNotFoundError, ) -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index 824cc387139..bbdad374bcf 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, diff --git a/tests/components/statistics/test_config_flow.py b/tests/components/statistics/test_config_flow.py index 77ccba5ba4c..fd82e688ee0 100644 --- a/tests/components/statistics/test_config_flow.py +++ b/tests/components/statistics/test_config_flow.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.recorder import Recorder diff --git a/tests/components/streamlabswater/test_binary_sensor.py b/tests/components/streamlabswater/test_binary_sensor.py index 7beb088d498..e9f899409a2 100644 --- a/tests/components/streamlabswater/test_binary_sensor.py +++ b/tests/components/streamlabswater/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/streamlabswater/test_sensor.py b/tests/components/streamlabswater/test_sensor.py index 6afb71f3fd7..ddae5ba3a9f 100644 --- a/tests/components/streamlabswater/test_sensor.py +++ b/tests/components/streamlabswater/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/suez_water/test_sensor.py b/tests/components/suez_water/test_sensor.py index 950d5d8393d..f9e7ff1f9e6 100644 --- a/tests/components/suez_water/test_sensor.py +++ b/tests/components/suez_water/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.suez_water.const import DATA_REFRESH_INTERVAL from homeassistant.components.suez_water.coordinator import PySuezError diff --git a/tests/components/swiss_public_transport/test_sensor.py b/tests/components/swiss_public_transport/test_sensor.py index 6e832728277..4922941002e 100644 --- a/tests/components/swiss_public_transport/test_sensor.py +++ b/tests/components/swiss_public_transport/test_sensor.py @@ -8,7 +8,7 @@ from opendata_transport.exceptions import ( OpendataTransportError, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.swiss_public_transport.const import ( diff --git a/tests/components/switchbot/test_diagnostics.py b/tests/components/switchbot/test_diagnostics.py index e5974459e09..7b7617498fd 100644 --- a/tests/components/switchbot/test_diagnostics.py +++ b/tests/components/switchbot/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.switchbot.const import ( diff --git a/tests/components/switchbot_cloud/test_sensor.py b/tests/components/switchbot_cloud/test_sensor.py index 1008dd72b47..0927e3cf1ea 100644 --- a/tests/components/switchbot_cloud/test_sensor.py +++ b/tests/components/switchbot_cloud/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import patch from switchbot_api import Device -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/syncthru/test_binary_sensor.py b/tests/components/syncthru/test_binary_sensor.py index ae5f0b6a90c..7067f553807 100644 --- a/tests/components/syncthru/test_binary_sensor.py +++ b/tests/components/syncthru/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/syncthru/test_diagnostics.py b/tests/components/syncthru/test_diagnostics.py index f5988936328..3ff4bc8cc08 100644 --- a/tests/components/syncthru/test_diagnostics.py +++ b/tests/components/syncthru/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/syncthru/test_sensor.py b/tests/components/syncthru/test_sensor.py index 600e2962730..78641739c8f 100644 --- a/tests/components/syncthru/test_sensor.py +++ b/tests/components/syncthru/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py index 26e421e6574..f9bde984399 100644 --- a/tests/components/systemmonitor/test_diagnostics.py +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import Mock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/tado/test_diagnostics.py b/tests/components/tado/test_diagnostics.py index 3a4f04b0a4c..36d136d5d77 100644 --- a/tests/components/tado/test_diagnostics.py +++ b/tests/components/tado/test_diagnostics.py @@ -1,6 +1,6 @@ """Test the Tado component diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.tado.const import DOMAIN diff --git a/tests/components/tailscale/test_diagnostics.py b/tests/components/tailscale/test_diagnostics.py index 26ba611438c..7dcf94f8ce8 100644 --- a/tests/components/tailscale/test_diagnostics.py +++ b/tests/components/tailscale/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Tailscale integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tankerkoenig/test_binary_sensor.py b/tests/components/tankerkoenig/test_binary_sensor.py index c103f2d26ff..880eb0e2f8c 100644 --- a/tests/components/tankerkoenig/test_binary_sensor.py +++ b/tests/components/tankerkoenig/test_binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/tankerkoenig/test_diagnostics.py b/tests/components/tankerkoenig/test_diagnostics.py index e7b479a0c32..6e1c81fa2c4 100644 --- a/tests/components/tankerkoenig/test_diagnostics.py +++ b/tests/components/tankerkoenig/test_diagnostics.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/tankerkoenig/test_sensor.py b/tests/components/tankerkoenig/test_sensor.py index 788c1de7021..27c2324662c 100644 --- a/tests/components/tankerkoenig/test_sensor.py +++ b/tests/components/tankerkoenig/test_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tankerkoenig import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 78235f7ebf5..098cdbbf8d1 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -13,7 +13,7 @@ from hatasmota.utils import ( get_topic_tele_will, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.tasmota.const import DEFAULT_PREFIX diff --git a/tests/components/technove/test_binary_sensor.py b/tests/components/technove/test_binary_sensor.py index 93d4805cecb..cbc34534480 100644 --- a/tests/components/technove/test_binary_sensor.py +++ b/tests/components/technove/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from technove import TechnoVEError from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform diff --git a/tests/components/technove/test_sensor.py b/tests/components/technove/test_sensor.py index 9cf80a659eb..48c59c80197 100644 --- a/tests/components/technove/test_sensor.py +++ b/tests/components/technove/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from technove import Station, Status, TechnoVEError from homeassistant.components.technove.const import DOMAIN diff --git a/tests/components/tedee/test_binary_sensor.py b/tests/components/tedee/test_binary_sensor.py index ccfd12440ea..cc931bb0c7c 100644 --- a/tests/components/tedee/test_binary_sensor.py +++ b/tests/components/tedee/test_binary_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from aiotedee import TedeeLock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tedee/test_diagnostics.py b/tests/components/tedee/test_diagnostics.py index 1487645572f..2cb18407432 100644 --- a/tests/components/tedee/test_diagnostics.py +++ b/tests/components/tedee/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Tedee integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tedee/test_init.py b/tests/components/tedee/test_init.py index 71bf5262f00..7f1f52c7977 100644 --- a/tests/components/tedee/test_init.py +++ b/tests/components/tedee/test_init.py @@ -11,7 +11,7 @@ from aiotedee.exception import ( TedeeWebhookException, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN from homeassistant.components.webhook import async_generate_url diff --git a/tests/components/tedee/test_sensor.py b/tests/components/tedee/test_sensor.py index 3c03d340100..4c8a3775443 100644 --- a/tests/components/tedee/test_sensor.py +++ b/tests/components/tedee/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from aiotedee import TedeeLock from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tesla_fleet/__init__.py b/tests/components/tesla_fleet/__init__.py index 78159402bff..c51cd83ee66 100644 --- a/tests/components/tesla_fleet/__init__.py +++ b/tests/components/tesla_fleet/__init__.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.application_credentials import ( ClientCredential, diff --git a/tests/components/tesla_fleet/test_button.py b/tests/components/tesla_fleet/test_button.py index d43f7448379..9eb12961dfa 100644 --- a/tests/components/tesla_fleet/test_button.py +++ b/tests/components/tesla_fleet/test_button.py @@ -4,7 +4,7 @@ from copy import deepcopy from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import NotOnWhitelistFault from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS diff --git a/tests/components/tesla_fleet/test_cover.py b/tests/components/tesla_fleet/test_cover.py index 15d14f34a87..045e5cfabb9 100644 --- a/tests/components/tesla_fleet/test_cover.py +++ b/tests/components/tesla_fleet/test_cover.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.cover import ( diff --git a/tests/components/tesla_fleet/test_lock.py b/tests/components/tesla_fleet/test_lock.py index ac9a7b49b55..a8aec27100c 100644 --- a/tests/components/tesla_fleet/test_lock.py +++ b/tests/components/tesla_fleet/test_lock.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.lock import ( diff --git a/tests/components/tesla_fleet/test_media_player.py b/tests/components/tesla_fleet/test_media_player.py index b2900d96c80..3233246b8b5 100644 --- a/tests/components/tesla_fleet/test_media_player.py +++ b/tests/components/tesla_fleet/test_media_player.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.media_player import ( diff --git a/tests/components/tesla_fleet/test_number.py b/tests/components/tesla_fleet/test_number.py index 4ade98852c8..66734c27f6f 100644 --- a/tests/components/tesla_fleet/test_number.py +++ b/tests/components/tesla_fleet/test_number.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.number import ( diff --git a/tests/components/tesla_fleet/test_select.py b/tests/components/tesla_fleet/test_select.py index f06d67041c9..5aa05ab7976 100644 --- a/tests/components/tesla_fleet/test_select.py +++ b/tests/components/tesla_fleet/test_select.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode from tesla_fleet_api.exceptions import VehicleOffline diff --git a/tests/components/tesla_fleet/test_switch.py b/tests/components/tesla_fleet/test_switch.py index 022c3a0ab18..dcdf66b7cc1 100644 --- a/tests/components/tesla_fleet/test_switch.py +++ b/tests/components/tesla_fleet/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.exceptions import VehicleOffline from homeassistant.components.switch import ( diff --git a/tests/components/tessie/common.py b/tests/components/tessie/common.py index 37a38fffaa4..a78d91e3f48 100644 --- a/tests/components/tessie/common.py +++ b/tests/components/tessie/common.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError from aiohttp.client import RequestInfo -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tessie import PLATFORMS from homeassistant.components.tessie.const import DOMAIN, TessieStatus diff --git a/tests/components/tessie/test_binary_sensor.py b/tests/components/tessie/test_binary_sensor.py index 0ced8a6d8aa..26d343181fa 100644 --- a/tests/components/tessie/test_binary_sensor.py +++ b/tests/components/tessie/test_binary_sensor.py @@ -1,7 +1,7 @@ """Test the Tessie binary sensor platform.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tessie/test_button.py b/tests/components/tessie/test_button.py index c9cfca3288a..da5942c0fdd 100644 --- a/tests/components/tessie/test_button.py +++ b/tests/components/tessie/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID, Platform diff --git a/tests/components/tessie/test_climate.py b/tests/components/tessie/test_climate.py index bc688e1ca70..4a0134c1b58 100644 --- a/tests/components/tessie/test_climate.py +++ b/tests/components/tessie/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/tessie/test_cover.py b/tests/components/tessie/test_cover.py index 02a8f22b6ea..b71b1f44377 100644 --- a/tests/components/tessie/test_cover.py +++ b/tests/components/tessie/test_cover.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, diff --git a/tests/components/tessie/test_device_tracker.py b/tests/components/tessie/test_device_tracker.py index 08d96b7303e..01defd8844c 100644 --- a/tests/components/tessie/test_device_tracker.py +++ b/tests/components/tessie/test_device_tracker.py @@ -1,6 +1,6 @@ """Test the Tessie device tracker platform.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tessie/test_lock.py b/tests/components/tessie/test_lock.py index 1208bb17d55..f94614bd2bf 100644 --- a/tests/components/tessie/test_lock.py +++ b/tests/components/tessie/test_lock.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, diff --git a/tests/components/tessie/test_media_player.py b/tests/components/tessie/test_media_player.py index 008607b8018..27a4828b6bb 100644 --- a/tests/components/tessie/test_media_player.py +++ b/tests/components/tessie/test_media_player.py @@ -3,7 +3,7 @@ from datetime import timedelta from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL from homeassistant.const import Platform diff --git a/tests/components/tessie/test_number.py b/tests/components/tessie/test_number.py index 69bbe1c9087..8f1d0820ea9 100644 --- a/tests/components/tessie/test_number.py +++ b/tests/components/tessie/test_number.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/tessie/test_select.py b/tests/components/tessie/test_select.py index 64380d363fc..44a5e99b5c1 100644 --- a/tests/components/tessie/test_select.py +++ b/tests/components/tessie/test_select.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from tesla_fleet_api.const import EnergyExportMode, EnergyOperationMode from tesla_fleet_api.exceptions import UnsupportedVehicle diff --git a/tests/components/tessie/test_sensor.py b/tests/components/tessie/test_sensor.py index 92256d25eb1..144ec06723d 100644 --- a/tests/components/tessie/test_sensor.py +++ b/tests/components/tessie/test_sensor.py @@ -2,7 +2,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tessie/test_switch.py b/tests/components/tessie/test_switch.py index f58468edfb7..aaa9c769ff8 100644 --- a/tests/components/tessie/test_switch.py +++ b/tests/components/tessie/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/tessie/test_update.py b/tests/components/tessie/test_update.py index 8d098e9a966..3510632b62c 100644 --- a/tests/components/tessie/test_update.py +++ b/tests/components/tessie/test_update.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.update import ( ATTR_IN_PROGRESS, diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 5d9d22c3f81..3c27f09d396 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.threshold.const import DOMAIN diff --git a/tests/components/tile/test_binary_sensor.py b/tests/components/tile/test_binary_sensor.py index c8b4b9b8376..e5606baf5c7 100644 --- a/tests/components/tile/test_binary_sensor.py +++ b/tests/components/tile/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tile/test_device_tracker.py b/tests/components/tile/test_device_tracker.py index 105cae1a7d7..50718114aa6 100644 --- a/tests/components/tile/test_device_tracker.py +++ b/tests/components/tile/test_device_tracker.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index 87bc670d604..0c7e0001ff3 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/tile/test_init.py b/tests/components/tile/test_init.py index fba354ade17..28daac6ff5d 100644 --- a/tests/components/tile/test_init.py +++ b/tests/components/tile/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.tile.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 6ba067b8ae2..6f7d8163362 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -5,7 +5,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from total_connect_client.exceptions import ( AuthenticationError, ServiceUnavailable, diff --git a/tests/components/totalconnect/test_binary_sensor.py b/tests/components/totalconnect/test_binary_sensor.py index dc433129ac8..8910487ea58 100644 --- a/tests/components/totalconnect/test_binary_sensor.py +++ b/tests/components/totalconnect/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR, diff --git a/tests/components/totalconnect/test_button.py b/tests/components/totalconnect/test_button.py index 87764e55186..092b058e693 100644 --- a/tests/components/totalconnect/test_button.py +++ b/tests/components/totalconnect/test_button.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from total_connect_client.exceptions import FailedToBypassZone from homeassistant.components.button import DOMAIN as BUTTON, SERVICE_PRESS diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index ac5bb347765..c67f1495986 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -20,7 +20,7 @@ from kasa.smart.modules import Speaker from kasa.smart.modules.alarm import Alarm from kasa.smart.modules.clean import AreaUnit, Clean, ErrorCode, Status from kasa.smartcam.modules.camera import LOCAL_STREAMING_PORT, Camera -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.tplink.const import DOMAIN diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 738fea1a45d..711c812e6a3 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er diff --git a/tests/components/tractive/test_binary_sensor.py b/tests/components/tractive/test_binary_sensor.py index cd7ffbc3da3..283543d761d 100644 --- a/tests/components/tractive/test_binary_sensor.py +++ b/tests/components/tractive/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tractive/test_device_tracker.py b/tests/components/tractive/test_device_tracker.py index ff9c7ca88ef..6fdbc245662 100644 --- a/tests/components/tractive/test_device_tracker.py +++ b/tests/components/tractive/test_device_tracker.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.device_tracker import SourceType from homeassistant.const import Platform diff --git a/tests/components/tractive/test_diagnostics.py b/tests/components/tractive/test_diagnostics.py index ce07b4d6e2a..1dcba8e12dd 100644 --- a/tests/components/tractive/test_diagnostics.py +++ b/tests/components/tractive/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/tractive/test_sensor.py b/tests/components/tractive/test_sensor.py index b53cc3c4d64..30463cd0bd9 100644 --- a/tests/components/tractive/test_sensor.py +++ b/tests/components/tractive/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/tractive/test_switch.py b/tests/components/tractive/test_switch.py index cc7ce6cf81f..92e4676aef1 100644 --- a/tests/components/tractive/test_switch.py +++ b/tests/components/tractive/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch from aiotractive.exceptions import TractiveError -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( diff --git a/tests/components/twinkly/test_diagnostics.py b/tests/components/twinkly/test_diagnostics.py index d7ef4dd9b11..b1f75d005b9 100644 --- a/tests/components/twinkly/test_diagnostics.py +++ b/tests/components/twinkly/test_diagnostics.py @@ -1,7 +1,7 @@ """Tests for the diagnostics of the twinkly component.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index f8289cb95e3..670f9c4a381 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from ttls.client import TwinklyError from homeassistant.components.light import ( diff --git a/tests/components/twinkly/test_select.py b/tests/components/twinkly/test_select.py index 103fbe0f634..515ce3c2cb5 100644 --- a/tests/components/twinkly/test_select.py +++ b/tests/components/twinkly/test_select.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, STATE_UNAVAILABLE, Platform diff --git a/tests/components/unifi/test_button.py b/tests/components/unifi/test_button.py index 94343d12ba2..61bb9718be7 100644 --- a/tests/components/unifi/test_button.py +++ b/tests/components/unifi/test_button.py @@ -7,7 +7,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.unifi.const import CONF_SITE_ID diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 39b70344db7..73b986aed87 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -9,7 +9,7 @@ from aiounifi.models.event import EventKey from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.unifi.const import ( diff --git a/tests/components/unifi/test_image.py b/tests/components/unifi/test_image.py index dc37d7cb8b7..4f0c815ca0c 100644 --- a/tests/components/unifi/test_image.py +++ b/tests/components/unifi/test_image.py @@ -8,7 +8,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.image import DOMAIN as IMAGE_DOMAIN from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index ee8b102edaa..6b58f49f072 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -10,7 +10,7 @@ from aiounifi.models.device import DeviceState from aiounifi.models.message import MessageKey from freezegun.api import FrozenDateTimeFactory, freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index c8ee786895c..c336c4ef6db 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -7,7 +7,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 7bf4b9aec9d..3b54aa9ebe4 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from yarl import URL from homeassistant.components.unifi.const import CONF_SITE_ID diff --git a/tests/components/utility_meter/test_diagnostics.py b/tests/components/utility_meter/test_diagnostics.py index 8be5f949940..88521a91b7f 100644 --- a/tests/components/utility_meter/test_diagnostics.py +++ b/tests/components/utility_meter/test_diagnostics.py @@ -3,7 +3,7 @@ from aiohttp.test_utils import TestClient from freezegun import freeze_time import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.auth.models import Credentials diff --git a/tests/components/v2c/test_diagnostics.py b/tests/components/v2c/test_diagnostics.py index eafbd68e6fc..6371b2480e8 100644 --- a/tests/components/v2c/test_diagnostics.py +++ b/tests/components/v2c/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/v2c/test_sensor.py b/tests/components/v2c/test_sensor.py index 430f91647dd..11dcfe5e4a5 100644 --- a/tests/components/v2c/test_sensor.py +++ b/tests/components/v2c/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.v2c.sensor import _METER_ERROR_OPTIONS from homeassistant.const import Platform diff --git a/tests/components/velbus/test_diagnostics.py b/tests/components/velbus/test_diagnostics.py index af84115ff14..74a0b4911de 100644 --- a/tests/components/velbus/test_diagnostics.py +++ b/tests/components/velbus/test_diagnostics.py @@ -1,7 +1,7 @@ """Test Velbus diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/vesync/test_diagnostics.py b/tests/components/vesync/test_diagnostics.py index 25aa5337281..c2b789a932e 100644 --- a/tests/components/vesync/test_diagnostics.py +++ b/tests/components/vesync/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pyvesync.helpers import Helpers -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.matchers import path_type from homeassistant.components.vesync.const import DOMAIN diff --git a/tests/components/vesync/test_fan.py b/tests/components/vesync/test_fan.py index ccc8c5cd595..cf572e5b981 100644 --- a/tests/components/vesync/test_fan.py +++ b/tests/components/vesync/test_fan.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest import requests_mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ATTR_PRESET_MODE, DOMAIN as FAN_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON diff --git a/tests/components/vesync/test_light.py b/tests/components/vesync/test_light.py index 866e6b295bf..7300e28e406 100644 --- a/tests/components/vesync/test_light.py +++ b/tests/components/vesync/test_light.py @@ -2,7 +2,7 @@ import pytest import requests_mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/vesync/test_sensor.py b/tests/components/vesync/test_sensor.py index 04d759de584..d4e6abcdbab 100644 --- a/tests/components/vesync/test_sensor.py +++ b/tests/components/vesync/test_sensor.py @@ -2,7 +2,7 @@ import pytest import requests_mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/vesync/test_switch.py b/tests/components/vesync/test_switch.py index e5d5986b364..b0af5afc5d2 100644 --- a/tests/components/vesync/test_switch.py +++ b/tests/components/vesync/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest import requests_mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON diff --git a/tests/components/vodafone_station/test_button.py b/tests/components/vodafone_station/test_button.py index ade5eb78965..84df839cae0 100644 --- a/tests/components/vodafone_station/test_button.py +++ b/tests/components/vodafone_station/test_button.py @@ -9,7 +9,7 @@ from aiovodafone.exceptions import ( GenericLoginError, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.vodafone_station.const import DOMAIN diff --git a/tests/components/vodafone_station/test_device_tracker.py b/tests/components/vodafone_station/test_device_tracker.py index a94f4ad05c4..2c8c2065510 100644 --- a/tests/components/vodafone_station/test_device_tracker.py +++ b/tests/components/vodafone_station/test_device_tracker.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.vodafone_station.const import SCAN_INTERVAL from homeassistant.components.vodafone_station.coordinator import CONSIDER_HOME_SECONDS diff --git a/tests/components/vodafone_station/test_diagnostics.py b/tests/components/vodafone_station/test_diagnostics.py index 5a4a46ce693..fa74292bcbc 100644 --- a/tests/components/vodafone_station/test_diagnostics.py +++ b/tests/components/vodafone_station/test_diagnostics.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/vodafone_station/test_sensor.py b/tests/components/vodafone_station/test_sensor.py index 5f27b67e3dd..35c486a359f 100644 --- a/tests/components/vodafone_station/test_sensor.py +++ b/tests/components/vodafone_station/test_sensor.py @@ -6,7 +6,7 @@ from aiovodafone import CannotAuthenticate from aiovodafone.exceptions import AlreadyLogged, CannotConnect from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.vodafone_station.const import LINE_TYPES, SCAN_INTERVAL from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform diff --git a/tests/components/waqi/test_sensor.py b/tests/components/waqi/test_sensor.py index 0cd2aa67233..7fd8e214240 100644 --- a/tests/components/waqi/test_sensor.py +++ b/tests/components/waqi/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from aiowaqi import WAQIAirQuality, WAQIError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.waqi.const import DOMAIN diff --git a/tests/components/watttime/test_diagnostics.py b/tests/components/watttime/test_diagnostics.py index f4465a44d26..ff697d5119e 100644 --- a/tests/components/watttime/test_diagnostics.py +++ b/tests/components/watttime/test_diagnostics.py @@ -1,6 +1,6 @@ """Test WattTime diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/weatherflow_cloud/test_sensor.py b/tests/components/weatherflow_cloud/test_sensor.py index 4d6ff0c8c9f..13ac3910571 100644 --- a/tests/components/weatherflow_cloud/test_sensor.py +++ b/tests/components/weatherflow_cloud/test_sensor.py @@ -4,7 +4,7 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from weatherflow4py.models.rest.observation import ObservationStationREST from homeassistant.components.weatherflow_cloud import DOMAIN diff --git a/tests/components/weatherflow_cloud/test_weather.py b/tests/components/weatherflow_cloud/test_weather.py index 04da96df423..8da67b27060 100644 --- a/tests/components/weatherflow_cloud/test_weather.py +++ b/tests/components/weatherflow_cloud/test_weather.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/weheat/test_binary_sensor.py b/tests/components/weheat/test_binary_sensor.py index 5769fc9a1a8..69122a35ea9 100644 --- a/tests/components/weheat/test_binary_sensor.py +++ b/tests/components/weheat/test_binary_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from weheat.abstractions.discovery import HeatPumpDiscovery from homeassistant.const import Platform diff --git a/tests/components/weheat/test_sensor.py b/tests/components/weheat/test_sensor.py index eab571b09ed..b4d436cdaf1 100644 --- a/tests/components/weheat/test_sensor.py +++ b/tests/components/weheat/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from weheat.abstractions.discovery import HeatPumpDiscovery from homeassistant.const import Platform diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index 7d915b91116..ca96ff1f2a9 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform diff --git a/tests/components/whirlpool/test_binary_sensor.py b/tests/components/whirlpool/test_binary_sensor.py index bdd4c05c05d..e4539fa5d13 100644 --- a/tests/components/whirlpool/test_binary_sensor.py +++ b/tests/components/whirlpool/test_binary_sensor.py @@ -1,7 +1,7 @@ """Test the Whirlpool Binary Sensor domain.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index e9fb47d1c28..2c36c713546 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion import whirlpool from homeassistant.components.climate import ( diff --git a/tests/components/whirlpool/test_diagnostics.py b/tests/components/whirlpool/test_diagnostics.py index 192339156e1..6ffdc82289f 100644 --- a/tests/components/whirlpool/test_diagnostics.py +++ b/tests/components/whirlpool/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Blink diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 9aa88c26123..6e28539d661 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -4,7 +4,7 @@ from datetime import UTC, datetime, timedelta from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from whirlpool.washerdryer import MachineState from homeassistant.components.whirlpool.sensor import SCAN_INTERVAL diff --git a/tests/components/withings/test_diagnostics.py b/tests/components/withings/test_diagnostics.py index 51f54b2ab17..2b58d6d22cf 100644 --- a/tests/components/withings/test_diagnostics.py +++ b/tests/components/withings/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index d88af39488b..e71402b8a98 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -15,7 +15,7 @@ from aiowithings import ( ) from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components import cloud diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 20927c197a4..0b863721f85 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch from aiowithings import Goals from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.withings import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform diff --git a/tests/components/wiz/test_diagnostics.py b/tests/components/wiz/test_diagnostics.py index 07178d5e93b..14fbdbf916a 100644 --- a/tests/components/wiz/test_diagnostics.py +++ b/tests/components/wiz/test_diagnostics.py @@ -1,6 +1,6 @@ """Test WiZ diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/wmspro/test_button.py b/tests/components/wmspro/test_button.py index 2894399f9f9..980b347ea2b 100644 --- a/tests/components/wmspro/test_button.py +++ b/tests/components/wmspro/test_button.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/wmspro/test_cover.py b/tests/components/wmspro/test_cover.py index ba2ab796c7d..f28d7f849ef 100644 --- a/tests/components/wmspro/test_cover.py +++ b/tests/components/wmspro/test_cover.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.wmspro.const import DOMAIN from homeassistant.components.wmspro.cover import SCAN_INTERVAL diff --git a/tests/components/wmspro/test_diagnostics.py b/tests/components/wmspro/test_diagnostics.py index 24698cfc493..43313402f78 100644 --- a/tests/components/wmspro/test_diagnostics.py +++ b/tests/components/wmspro/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/wmspro/test_init.py b/tests/components/wmspro/test_init.py index 56857ae86ca..c0fab8e2c81 100644 --- a/tests/components/wmspro/test_init.py +++ b/tests/components/wmspro/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock import aiohttp import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.wmspro.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/wmspro/test_light.py b/tests/components/wmspro/test_light.py index 9f45a821884..749c1d9104b 100644 --- a/tests/components/wmspro/test_light.py +++ b/tests/components/wmspro/test_light.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wmspro.const import DOMAIN diff --git a/tests/components/wmspro/test_scene.py b/tests/components/wmspro/test_scene.py index a6b16e5bbc9..9a24d54fa76 100644 --- a/tests/components/wmspro/test_scene.py +++ b/tests/components/wmspro/test_scene.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.wmspro.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON diff --git a/tests/components/wolflink/test_sensor.py b/tests/components/wolflink/test_sensor.py index 8fc78f707d5..ad0325ec06e 100644 --- a/tests/components/wolflink/test_sensor.py +++ b/tests/components/wolflink/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/wyoming/test_conversation.py b/tests/components/wyoming/test_conversation.py index 7278a254d4a..d3c60f9d0c6 100644 --- a/tests/components/wyoming/test_conversation.py +++ b/tests/components/wyoming/test_conversation.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from wyoming.asr import Transcript from wyoming.handle import Handled, NotHandled from wyoming.intent import Entity, Intent, NotRecognized diff --git a/tests/components/wyoming/test_stt.py b/tests/components/wyoming/test_stt.py index bd83c31c561..cfbcf24d405 100644 --- a/tests/components/wyoming/test_stt.py +++ b/tests/components/wyoming/test_stt.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from wyoming.asr import Transcript from homeassistant.components import stt diff --git a/tests/components/wyoming/test_tts.py b/tests/components/wyoming/test_tts.py index c52b1391038..c658bff1d0c 100644 --- a/tests/components/wyoming/test_tts.py +++ b/tests/components/wyoming/test_tts.py @@ -7,7 +7,7 @@ from unittest.mock import patch import wave import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from wyoming.audio import AudioChunk, AudioStop from homeassistant.components import tts, wyoming diff --git a/tests/components/yale/test_binary_sensor.py b/tests/components/yale/test_binary_sensor.py index 16ec0ffbeb4..95434b1b2d2 100644 --- a/tests/components/yale/test_binary_sensor.py +++ b/tests/components/yale/test_binary_sensor.py @@ -3,7 +3,7 @@ import datetime from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( diff --git a/tests/components/yale/test_diagnostics.py b/tests/components/yale/test_diagnostics.py index e5fd6b1c1a7..8a18f9ee791 100644 --- a/tests/components/yale/test_diagnostics.py +++ b/tests/components/yale/test_diagnostics.py @@ -1,6 +1,6 @@ """Test yale diagnostics.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/yale/test_lock.py b/tests/components/yale/test_lock.py index 1a99cf967ba..50051913d5f 100644 --- a/tests/components/yale/test_lock.py +++ b/tests/components/yale/test_lock.py @@ -5,7 +5,7 @@ import datetime from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState diff --git a/tests/components/yale/test_sensor.py b/tests/components/yale/test_sensor.py index 5d724b4bb9d..1ee04bf1ee1 100644 --- a/tests/components/yale/test_sensor.py +++ b/tests/components/yale/test_sensor.py @@ -2,7 +2,7 @@ from typing import Any -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import core as ha from homeassistant.const import ( diff --git a/tests/components/youless/test_sensor.py b/tests/components/youless/test_sensor.py index 67dff314df7..e18ae678e42 100644 --- a/tests/components/youless/test_sensor.py +++ b/tests/components/youless/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/youtube/test_diagnostics.py b/tests/components/youtube/test_diagnostics.py index 3a5765b5890..99d8b9d5185 100644 --- a/tests/components/youtube/test_diagnostics.py +++ b/tests/components/youtube/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the YouTube integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.youtube.const import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/components/youtube/test_sensor.py b/tests/components/youtube/test_sensor.py index e883347c8db..1090b8c391a 100644 --- a/tests/components/youtube/test_sensor.py +++ b/tests/components/youtube/test_sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from youtubeaio.types import UnauthorizedError, YouTubeBackendError from homeassistant import config_entries diff --git a/tests/components/zeversolar/test_diagnostics.py b/tests/components/zeversolar/test_diagnostics.py index 0d7a919b023..b5a59b588fb 100644 --- a/tests/components/zeversolar/test_diagnostics.py +++ b/tests/components/zeversolar/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the Zeversolar integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.zeversolar import DOMAIN from homeassistant.core import HomeAssistant diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 43efe79e96f..4de8d47cc16 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -16,7 +16,7 @@ from freezegun import freeze_time import orjson import pytest from pytest_unordered import unordered -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion import voluptuous as vol from homeassistant import config_entries diff --git a/tests/non_packaged_scripts/test_alexa_locales.py b/tests/non_packaged_scripts/test_alexa_locales.py index ea139f7de8e..35a44fa74d4 100644 --- a/tests/non_packaged_scripts/test_alexa_locales.py +++ b/tests/non_packaged_scripts/test_alexa_locales.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest import requests_mock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from script.alexa_locales import SITE, run_script From 555215a848c4cece9cf6bfb13a6b0c1fc62a06a8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 15:05:08 +0300 Subject: [PATCH 0617/1175] Update quality_scale rules status for Comelit (#143592) --- homeassistant/components/comelit/quality_scale.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/comelit/quality_scale.yaml b/homeassistant/components/comelit/quality_scale.yaml index 09871838914..a74fab22484 100644 --- a/homeassistant/components/comelit/quality_scale.yaml +++ b/homeassistant/components/comelit/quality_scale.yaml @@ -55,10 +55,8 @@ rules: docs-known-limitations: status: exempt comment: no known limitations, yet - docs-supported-devices: - status: todo - comment: review and complete missing ones - docs-supported-functions: todo + docs-supported-devices: done + docs-supported-functions: done docs-troubleshooting: done docs-use-cases: done dynamic-devices: From e868b3e8ffe74a0b4caed343d936dbf9ee8f2319 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 May 2025 14:13:57 +0200 Subject: [PATCH 0618/1175] Sort and simplify DeletedRegistryEntry (#145207) --- homeassistant/helpers/entity_registry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 78a65acf290..abe0468ed17 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -406,11 +406,12 @@ class DeletedRegistryEntry: platform: str = attr.ib() config_entry_id: str | None = attr.ib() config_subentry_id: str | None = attr.ib() + created_at: datetime = attr.ib() domain: str = attr.ib(init=False, repr=False) id: str = attr.ib() + modified_at: datetime = attr.ib() orphaned_timestamp: float | None = attr.ib() - created_at: datetime = attr.ib(factory=utcnow) - modified_at: datetime = attr.ib(factory=utcnow) + _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False) @domain.default @@ -975,6 +976,7 @@ class EntityRegistry(BaseRegistry): created_at=entity.created_at, entity_id=entity_id, id=entity.id, + modified_at=utcnow(), orphaned_timestamp=orphaned_timestamp, platform=entity.platform, unique_id=entity.unique_id, From 0c0c61f9e0e264189a7637eb08abfc4a98fc0c13 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 15:16:12 +0300 Subject: [PATCH 0619/1175] Bump aiocomelit to 0.12.3 (#145209) --- homeassistant/components/comelit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/comelit/test_climate.py | 2 +- tests/components/comelit/test_humidifier.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index 58f347b4ba3..bea84c6b805 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aiocomelit"], "quality_scale": "bronze", - "requirements": ["aiocomelit==0.12.1"] + "requirements": ["aiocomelit==0.12.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 830d9c9220a..b5c2036ba13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -211,7 +211,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.1 +aiocomelit==0.12.3 # homeassistant.components.dhcp aiodhcpwatcher==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9afe540d04..f4c96646188 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,7 +199,7 @@ aiobafi6==0.9.0 aiobotocore==2.21.1 # homeassistant.components.comelit -aiocomelit==0.12.1 +aiocomelit==0.12.3 # homeassistant.components.dhcp aiodhcpwatcher==1.2.0 diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index e0b1e116f64..3337ba28769 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -84,7 +84,7 @@ async def test_climate_data_update( freezer: FrozenDateTimeFactory, mock_serial_bridge: AsyncMock, mock_serial_bridge_config_entry: MockConfigEntry, - val: list[Any, Any], + val: list[list[Any]], mode: HVACMode, temp: float, ) -> None: diff --git a/tests/components/comelit/test_humidifier.py b/tests/components/comelit/test_humidifier.py index f432c63e14c..a096a1c0eb4 100644 --- a/tests/components/comelit/test_humidifier.py +++ b/tests/components/comelit/test_humidifier.py @@ -91,7 +91,7 @@ async def test_humidifier_data_update( freezer: FrozenDateTimeFactory, mock_serial_bridge: AsyncMock, mock_serial_bridge_config_entry: MockConfigEntry, - val: list[Any, Any], + val: list[list[Any]], mode: str, humidity: float, ) -> None: From 9d050360c8446dab3caa0c782d9b34d2c9bdf6f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 14:18:35 +0200 Subject: [PATCH 0620/1175] Prevent import from syrupy.SnapshotAssertion (#145208) --- tests/components/apsystems/test_binary_sensor.py | 2 +- tests/components/apsystems/test_number.py | 2 +- tests/components/apsystems/test_sensor.py | 2 +- tests/components/apsystems/test_switch.py | 2 +- tests/components/august/test_binary_sensor.py | 2 +- tests/components/august/test_lock.py | 2 +- tests/components/co2signal/test_diagnostics.py | 2 +- tests/components/co2signal/test_sensor.py | 2 +- tests/components/easyenergy/test_diagnostics.py | 2 +- tests/components/mold_indicator/test_config_flow.py | 2 +- tests/components/ohme/test_button.py | 2 +- tests/components/ohme/test_diagnostics.py | 2 +- tests/components/ohme/test_init.py | 2 +- tests/components/ohme/test_number.py | 2 +- tests/components/ohme/test_select.py | 2 +- tests/components/ohme/test_sensor.py | 2 +- tests/components/ohme/test_switch.py | 2 +- tests/components/ohme/test_time.py | 2 +- tests/components/poolsense/test_binary_sensor.py | 2 +- tests/components/poolsense/test_sensor.py | 2 +- tests/components/rdw/test_diagnostics.py | 2 +- tests/components/smartthings/__init__.py | 2 +- tests/components/smartthings/test_binary_sensor.py | 2 +- tests/components/smartthings/test_button.py | 2 +- tests/components/smartthings/test_climate.py | 2 +- tests/components/smartthings/test_cover.py | 2 +- tests/components/smartthings/test_diagnostics.py | 2 +- tests/components/smartthings/test_event.py | 2 +- tests/components/smartthings/test_fan.py | 2 +- tests/components/smartthings/test_init.py | 2 +- tests/components/smartthings/test_light.py | 2 +- tests/components/smartthings/test_lock.py | 2 +- tests/components/smartthings/test_media_player.py | 2 +- tests/components/smartthings/test_number.py | 2 +- tests/components/smartthings/test_scene.py | 2 +- tests/components/smartthings/test_select.py | 2 +- tests/components/smartthings/test_sensor.py | 2 +- tests/components/smartthings/test_switch.py | 2 +- tests/components/smartthings/test_update.py | 2 +- tests/components/smartthings/test_valve.py | 2 +- tests/components/smartthings/test_water_heater.py | 2 +- tests/components/synology_dsm/test_config_flow.py | 2 +- tests/components/tag/test_init.py | 2 +- tests/ruff.toml | 1 + 44 files changed, 44 insertions(+), 43 deletions(-) diff --git a/tests/components/apsystems/test_binary_sensor.py b/tests/components/apsystems/test_binary_sensor.py index 0c6fbffc93c..88e482e3eaa 100644 --- a/tests/components/apsystems/test_binary_sensor.py +++ b/tests/components/apsystems/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/apsystems/test_number.py b/tests/components/apsystems/test_number.py index 912759b4a17..6cf054148bf 100644 --- a/tests/components/apsystems/test_number.py +++ b/tests/components/apsystems/test_number.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/apsystems/test_sensor.py b/tests/components/apsystems/test_sensor.py index 810ad3e7bdf..9a87e7ecf18 100644 --- a/tests/components/apsystems/test_sensor.py +++ b/tests/components/apsystems/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/apsystems/test_switch.py b/tests/components/apsystems/test_switch.py index afd889fe958..290cece126d 100644 --- a/tests/components/apsystems/test_switch.py +++ b/tests/components/apsystems/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index bcdd4d55330..563221635f8 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -4,7 +4,7 @@ import datetime from unittest.mock import Mock from freezegun.api import FrozenDateTimeFactory -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from yalexs.pubnub_async import AugustPubNub from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 065ffef91ff..a1ba83ecb01 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,7 +6,7 @@ from unittest.mock import Mock from aiohttp import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from yalexs.manager.activity import INITIAL_LOCK_RESYNC_TIME from yalexs.pubnub_async import AugustPubNub diff --git a/tests/components/co2signal/test_diagnostics.py b/tests/components/co2signal/test_diagnostics.py index 3d5e1a0580b..3ede845f01f 100644 --- a/tests/components/co2signal/test_diagnostics.py +++ b/tests/components/co2signal/test_diagnostics.py @@ -1,7 +1,7 @@ """Test the CO2Signal diagnostics.""" import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.core import HomeAssistant diff --git a/tests/components/co2signal/test_sensor.py b/tests/components/co2signal/test_sensor.py index fddda17f3ed..2154782f62d 100644 --- a/tests/components/co2signal/test_sensor.py +++ b/tests/components/co2signal/test_sensor.py @@ -11,7 +11,7 @@ from aioelectricitymaps import ( ) from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er diff --git a/tests/components/easyenergy/test_diagnostics.py b/tests/components/easyenergy/test_diagnostics.py index d0eb9de3b00..8b9d850d98c 100644 --- a/tests/components/easyenergy/test_diagnostics.py +++ b/tests/components/easyenergy/test_diagnostics.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock from easyenergy import EasyEnergyNoDataError import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.const import ATTR_ENTITY_ID diff --git a/tests/components/mold_indicator/test_config_flow.py b/tests/components/mold_indicator/test_config_flow.py index bb8362b5e0d..aca6e37ff92 100644 --- a/tests/components/mold_indicator/test_config_flow.py +++ b/tests/components/mold_indicator/test_config_flow.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.mold_indicator.const import ( diff --git a/tests/components/ohme/test_button.py b/tests/components/ohme/test_button.py index 1728563b2e9..70dab600b6d 100644 --- a/tests/components/ohme/test_button.py +++ b/tests/components/ohme/test_button.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory from ohme import ChargerStatus -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.const import ( diff --git a/tests/components/ohme/test_diagnostics.py b/tests/components/ohme/test_diagnostics.py index 6aab1262189..25ee5ae10db 100644 --- a/tests/components/ohme/test_diagnostics.py +++ b/tests/components/ohme/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/ohme/test_init.py b/tests/components/ohme/test_init.py index 0f4c7cd64ee..7d9d388867f 100644 --- a/tests/components/ohme/test_init.py +++ b/tests/components/ohme/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.ohme.const import DOMAIN from homeassistant.config_entries import ConfigEntryState diff --git a/tests/components/ohme/test_number.py b/tests/components/ohme/test_number.py index 9cfce2a850f..e162cd337ae 100644 --- a/tests/components/ohme/test_number.py +++ b/tests/components/ohme/test_number.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/ohme/test_select.py b/tests/components/ohme/test_select.py index 5aeebc1f477..1f0225fd70f 100644 --- a/tests/components/ohme/test_select.py +++ b/tests/components/ohme/test_select.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from ohme import ChargerMode -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/ohme/test_sensor.py b/tests/components/ohme/test_sensor.py index 8fc9edddcf9..b7c8f82aafc 100644 --- a/tests/components/ohme/test_sensor.py +++ b/tests/components/ohme/test_sensor.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory from ohme import ApiException import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/ohme/test_switch.py b/tests/components/ohme/test_switch.py index 8d82a5a3ea4..976b5cfcccd 100644 --- a/tests/components/ohme/test_switch.py +++ b/tests/components/ohme/test_switch.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, diff --git a/tests/components/ohme/test_time.py b/tests/components/ohme/test_time.py index 0562dfa124c..8c604e19086 100644 --- a/tests/components/ohme/test_time.py +++ b/tests/components/ohme/test_time.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.time import ( ATTR_TIME, diff --git a/tests/components/poolsense/test_binary_sensor.py b/tests/components/poolsense/test_binary_sensor.py index 4d10413c124..debf0faa52a 100644 --- a/tests/components/poolsense/test_binary_sensor.py +++ b/tests/components/poolsense/test_binary_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/poolsense/test_sensor.py b/tests/components/poolsense/test_sensor.py index 7f088eee6a3..bac5dd8c701 100644 --- a/tests/components/poolsense/test_sensor.py +++ b/tests/components/poolsense/test_sensor.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/tests/components/rdw/test_diagnostics.py b/tests/components/rdw/test_diagnostics.py index a5e8c72dba1..0f4a2279993 100644 --- a/tests/components/rdw/test_diagnostics.py +++ b/tests/components/rdw/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the diagnostics data provided by the RDW integration.""" -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant diff --git a/tests/components/smartthings/__init__.py b/tests/components/smartthings/__init__.py index f316db7bef8..3395f7f4673 100644 --- a/tests/components/smartthings/__init__.py +++ b/tests/components/smartthings/__init__.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, DeviceEvent, DeviceHealthEvent from pysmartthings.models import HealthStatus -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smartthings.const import MAIN from homeassistant.const import Platform diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 22ca94df81a..42534e5b691 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity diff --git a/tests/components/smartthings/test_button.py b/tests/components/smartthings/test_button.py index 5c5f98912e2..daacee7def1 100644 --- a/tests/components/smartthings/test_button.py +++ b/tests/components/smartthings/test_button.py @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory from pysmartthings import Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.smartthings import MAIN diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 9e3fa22f55d..ff8b5277e20 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, call from pysmartthings import Attribute, Capability, Command, Status from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 559c6821204..ad6fc762c3c 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command, Status from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, diff --git a/tests/components/smartthings/test_diagnostics.py b/tests/components/smartthings/test_diagnostics.py index b28a3a1aff5..4eba6593a7f 100644 --- a/tests/components/smartthings/test_diagnostics.py +++ b/tests/components/smartthings/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.smartthings.const import DOMAIN diff --git a/tests/components/smartthings/test_event.py b/tests/components/smartthings/test_event.py index b9a6fc8be86..96b66036906 100644 --- a/tests/components/smartthings/test_event.py +++ b/tests/components/smartthings/test_event.py @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory from pysmartthings import Attribute, Capability from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.event import ATTR_EVENT_TYPES from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 04196417690..36a453ff595 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PERCENTAGE, diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index fcb962449bf..0b8d2e1e632 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -13,7 +13,7 @@ from pysmartthings import ( Subscription, ) import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, HVACMode diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 46f8f3ae7a3..0aa818dd7f4 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, call from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 48e83f479fa..54932e1094e 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState from homeassistant.components.smartthings.const import MAIN diff --git a/tests/components/smartthings/test_media_player.py b/tests/components/smartthings/test_media_player.py index e3f3652c0ed..0fb53e642d4 100644 --- a/tests/components/smartthings/test_media_player.py +++ b/tests/components/smartthings/test_media_player.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command, Status from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, diff --git a/tests/components/smartthings/test_number.py b/tests/components/smartthings/test_number.py index fa485776c37..f9dfe4d3228 100644 --- a/tests/components/smartthings/test_number.py +++ b/tests/components/smartthings/test_number.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.number import ( ATTR_VALUE, diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 7ef287b9e96..5eb055f96f0 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, Platform diff --git a/tests/components/smartthings/test_select.py b/tests/components/smartthings/test_select.py index da27565ead5..3e1746331f9 100644 --- a/tests/components/smartthings/test_select.py +++ b/tests/components/smartthings/test_select.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.select import ( ATTR_OPTION, diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 04ad85ef02d..bfb203c1485 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 2be2c670faf..09f710366d0 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components import automation, script from homeassistant.components.automation import automations_with_entity diff --git a/tests/components/smartthings/test_update.py b/tests/components/smartthings/test_update.py index e4b360e0398..960e8bfb6d7 100644 --- a/tests/components/smartthings/test_update.py +++ b/tests/components/smartthings/test_update.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smartthings.const import MAIN from homeassistant.components.update import ( diff --git a/tests/components/smartthings/test_valve.py b/tests/components/smartthings/test_valve.py index 9d2cef65035..9aff2dc09be 100644 --- a/tests/components/smartthings/test_valve.py +++ b/tests/components/smartthings/test_valve.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smartthings import MAIN from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN, ValveState diff --git a/tests/components/smartthings/test_water_heater.py b/tests/components/smartthings/test_water_heater.py index 54df6aa12e6..a12280e5c92 100644 --- a/tests/components/smartthings/test_water_heater.py +++ b/tests/components/smartthings/test_water_heater.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, call from pysmartthings import Attribute, Capability, Command from pysmartthings.models import HealthStatus import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.smartthings import MAIN from homeassistant.components.water_heater import ( diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 932cf057d3d..f2aa6df802e 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -12,7 +12,7 @@ from synology_dsm.exceptions import ( SynologyDSMLoginInvalidException, SynologyDSMRequestException, ) -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index ac862e59f2d..25b1e116c04 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -5,7 +5,7 @@ from typing import Any from freezegun.api import FrozenDateTimeFactory import pytest -from syrupy import SnapshotAssertion +from syrupy.assertion import SnapshotAssertion from syrupy.filters import props from homeassistant.components.tag import DOMAIN, _create_entry, async_scan_tag diff --git a/tests/ruff.toml b/tests/ruff.toml index c56b8f68ffc..b22f39f1525 100644 --- a/tests/ruff.toml +++ b/tests/ruff.toml @@ -13,6 +13,7 @@ extend-ignore = [ [lint.flake8-tidy-imports.banned-api] "async_timeout".msg = "use asyncio.timeout instead" "pytz".msg = "use zoneinfo instead" +"syrupy.SnapshotAssertion".msg = "use syrupy.assertion.SnapshotAssertion instead" [lint.isort] known-first-party = [ From 0cf503d871d14ad57dd1017eafbfcc1f549033fb Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Mon, 19 May 2025 20:22:10 +0800 Subject: [PATCH 0621/1175] Add exception translation for switchbot device initialization (#144828) --- .../components/switchbot/__init__.py | 15 ++- .../components/switchbot/strings.json | 9 ++ tests/components/switchbot/__init__.py | 8 ++ tests/components/switchbot/test_init.py | 91 +++++++++++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/components/switchbot/test_init.py diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 1f41f494764..22119a5442e 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -24,6 +24,7 @@ from .const import ( CONF_RETRY_COUNT, CONNECTABLE_SUPPORTED_MODEL_TYPES, DEFAULT_RETRY_COUNT, + DOMAIN, ENCRYPTED_MODELS, HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL, SupportedModels, @@ -138,7 +139,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) -> ) if not ble_device: raise ConfigEntryNotReady( - f"Could not find Switchbot {sensor_type} with address {address}" + translation_domain=DOMAIN, + translation_key="device_not_found_error", + translation_placeholders={"sensor_type": sensor_type, "address": address}, ) cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) @@ -153,7 +156,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) -> ) except ValueError as error: raise ConfigEntryNotReady( - "Invalid encryption configuration provided" + translation_domain=DOMAIN, + translation_key="value_error", + translation_placeholders={"error": str(error)}, ) from error else: device = cls( @@ -174,7 +179,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: SwitchbotConfigEntry) -> ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): - raise ConfigEntryNotReady(f"{address} is not advertising state") + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="advertising_state_error", + translation_placeholders={"address": address}, + ) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) await hass.config_entries.async_forward_entry_setups( diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 41bc09dde1a..a5f502a261b 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -197,6 +197,15 @@ "exceptions": { "operation_error": { "message": "An error occurred while performing the action: {error}" + }, + "value_error": { + "message": "Switchbot device initialization failed because of incorrect configuration parameters: {error}" + }, + "advertising_state_error": { + "message": "{address} is not advertising state" + }, + "device_not_found_error": { + "message": "Could not find Switchbot {sensor_type} with address {address}" } } } diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 5ab9dc7df13..8ba242823f6 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -47,6 +47,14 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: return entry +def patch_async_ble_device_from_address(return_value: BluetoothServiceInfoBleak | None): + """Patch async ble device from address to return a given value.""" + return patch( + "homeassistant.components.bluetooth.async_ble_device_from_address", + return_value=return_value, + ) + + WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoHand", manufacturer_data={89: b"\xfd`0U\x92W"}, diff --git a/tests/components/switchbot/test_init.py b/tests/components/switchbot/test_init.py new file mode 100644 index 00000000000..8969557bc0f --- /dev/null +++ b/tests/components/switchbot/test_init.py @@ -0,0 +1,91 @@ +"""Test the switchbot init.""" + +from collections.abc import Callable +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.core import HomeAssistant + +from . import ( + HUBMINI_MATTER_SERVICE_INFO, + LOCK_SERVICE_INFO, + patch_async_ble_device_from_address, +) + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + ValueError("wrong model"), + "Switchbot device initialization failed because of incorrect configuration parameters: wrong model", + ), + ], +) +async def test_exception_handling_for_device_initialization( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + exception: Exception, + error_message: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test exception handling for lock initialization.""" + inject_bluetooth_service_info(hass, LOCK_SERVICE_INFO) + + entry = mock_entry_encrypted_factory(sensor_type="lock") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock.__init__", + side_effect=exception, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert error_message in caplog.text + + +async def test_setup_entry_without_ble_device( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test setup entry without ble device.""" + + entry = mock_entry_factory("hygrometer_co2") + entry.add_to_hass(hass) + + with patch_async_ble_device_from_address(None): + await hass.config_entries.async_setup(entry.entry_id) + + assert ( + "Could not find Switchbot hygrometer_co2 with address aa:bb:cc:dd:ee:ff" + in caplog.text + ) + + +async def test_coordinator_wait_ready_timeout( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the coordinator async_wait_ready timeout by calling it directly.""" + + inject_bluetooth_service_info(hass, HUBMINI_MATTER_SERVICE_INFO) + + entry = mock_entry_factory("hubmini_matter") + entry.add_to_hass(hass) + + timeout_mock = AsyncMock() + timeout_mock.__aenter__.side_effect = TimeoutError + timeout_mock.__aexit__.return_value = None + + with patch( + "homeassistant.components.switchbot.coordinator.asyncio.timeout", + return_value=timeout_mock, + ): + await hass.config_entries.async_setup(entry.entry_id) + + assert "aa:bb:cc:dd:ee:ff is not advertising state" in caplog.text From 880f5faeec7695a1b0491339f96769f59176c777 Mon Sep 17 00:00:00 2001 From: markhannon Date: Mon, 19 May 2025 22:24:25 +1000 Subject: [PATCH 0622/1175] Add cover entity to Zimi integration (#144330) --- homeassistant/components/zimi/__init__.py | 8 +- homeassistant/components/zimi/cover.py | 93 +++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zimi/cover.py diff --git a/homeassistant/components/zimi/__init__.py b/homeassistant/components/zimi/__init__.py index a184ba71a52..a00dd60ee5f 100644 --- a/homeassistant/components/zimi/__init__.py +++ b/homeassistant/components/zimi/__init__.py @@ -16,7 +16,13 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN from .helpers import async_connect_to_controller -PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zimi/cover.py b/homeassistant/components/zimi/cover.py new file mode 100644 index 00000000000..8f05e35e263 --- /dev/null +++ b/homeassistant/components/zimi/cover.py @@ -0,0 +1,93 @@ +"""Platform for cover integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ZimiConfigEntry +from .entity import ZimiEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ZimiConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Zimi Cover platform.""" + + api = config_entry.runtime_data + + doors = [ZimiCover(device, api) for device in api.doors] + + async_add_entities(doors) + + +class ZimiCover(ZimiEntity, CoverEntity): + """Representation of a Zimi cover.""" + + _attr_device_class = CoverDeviceClass.GARAGE + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.STOP + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover/door.""" + _LOGGER.debug("Sending close_cover() for %s", self.name) + await self._device.close_door() + + @property + def current_cover_position(self) -> int | None: + """Return the current cover/door position.""" + return self._device.percentage + + @property + def is_closed(self) -> bool | None: + """Return true if cover is closed.""" + return self._device.is_closed + + @property + def is_closing(self) -> bool | None: + """Return true if cover is closing.""" + return self._device.is_closing + + @property + def is_opening(self) -> bool | None: + """Return true if cover is opening.""" + return self._device.is_opening + + @property + def is_open(self) -> bool | None: + """Return true if cover is open.""" + return self._device.is_open + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover/door.""" + _LOGGER.debug("Sending open_cover() for %s", self.name) + await self._device.open_door() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Open the cover/door to a specified percentage.""" + if position := kwargs.get("position"): + _LOGGER.debug("Sending set_cover_position(%d) for %s", position, self.name) + await self._device.open_to_percentage(position) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + _LOGGER.debug( + "Stopping open_cover() by setting to current position for %s", self.name + ) + await self.async_set_cover_position(position=self.current_cover_position) From f6a0d630c38ac2439172f8b69762b149708afa1e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 19 May 2025 14:45:30 +0200 Subject: [PATCH 0623/1175] Fix typo in Ecovacs get_supported_entities (#145215) --- homeassistant/components/ecovacs/binary_sensor.py | 4 ++-- homeassistant/components/ecovacs/button.py | 8 ++++---- homeassistant/components/ecovacs/number.py | 4 ++-- homeassistant/components/ecovacs/select.py | 4 ++-- homeassistant/components/ecovacs/sensor.py | 4 ++-- homeassistant/components/ecovacs/switch.py | 4 ++-- homeassistant/components/ecovacs/util.py | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/ecovacs/binary_sensor.py b/homeassistant/components/ecovacs/binary_sensor.py index 73b21d4574d..7c85a63cc78 100644 --- a/homeassistant/components/ecovacs/binary_sensor.py +++ b/homeassistant/components/ecovacs/binary_sensor.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT -from .util import get_supported_entitites +from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -49,7 +49,7 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" async_add_entities( - get_supported_entitites( + get_supported_entities( config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS ) ) diff --git a/homeassistant/components/ecovacs/button.py b/homeassistant/components/ecovacs/button.py index 04eb0af02e6..ba1a0847408 100644 --- a/homeassistant/components/ecovacs/button.py +++ b/homeassistant/components/ecovacs/button.py @@ -16,13 +16,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry -from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS +from .const import SUPPORTED_LIFESPANS from .entity import ( EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EcovacsEntity, ) -from .util import get_supported_entitites +from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple( key=f"station_action_{action.name.lower()}", translation_key=f"station_action_{action.name.lower()}", ) - for action in SUPPORTED_STATION_ACTIONS + for action in StationAction ) @@ -85,7 +85,7 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities: list[EcovacsEntity] = get_supported_entitites( + entities: list[EcovacsEntity] = get_supported_entities( controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS ) entities.extend( diff --git a/homeassistant/components/ecovacs/number.py b/homeassistant/components/ecovacs/number.py index 7a74b02ceca..1fbf65aec65 100644 --- a/homeassistant/components/ecovacs/number.py +++ b/homeassistant/components/ecovacs/number.py @@ -25,7 +25,7 @@ from .entity import ( EcovacsEntity, EventT, ) -from .util import get_supported_entitites +from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -87,7 +87,7 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities: list[EcovacsEntity] = get_supported_entitites( + entities: list[EcovacsEntity] = get_supported_entities( controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS ) if entities: diff --git a/homeassistant/components/ecovacs/select.py b/homeassistant/components/ecovacs/select.py index 31292401343..deddb7e252a 100644 --- a/homeassistant/components/ecovacs/select.py +++ b/homeassistant/components/ecovacs/select.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import EcovacsConfigEntry from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT -from .util import get_name_key, get_supported_entitites +from .util import get_name_key, get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -59,7 +59,7 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities = get_supported_entitites( + entities = get_supported_entities( controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS ) if entities: diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index eab642119e4..67556606f3a 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -48,7 +48,7 @@ from .entity import ( EcovacsLegacyEntity, EventT, ) -from .util import get_name_key, get_options, get_supported_entitites +from .util import get_name_key, get_options, get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -211,7 +211,7 @@ async def async_setup_entry( """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities: list[EcovacsEntity] = get_supported_entitites( + entities: list[EcovacsEntity] = get_supported_entities( controller, EcovacsSensor, ENTITY_DESCRIPTIONS ) entities.extend( diff --git a/homeassistant/components/ecovacs/switch.py b/homeassistant/components/ecovacs/switch.py index dd379dbb199..d151b55ca1c 100644 --- a/homeassistant/components/ecovacs/switch.py +++ b/homeassistant/components/ecovacs/switch.py @@ -17,7 +17,7 @@ from .entity import ( EcovacsDescriptionEntity, EcovacsEntity, ) -from .util import get_supported_entitites +from .util import get_supported_entities @dataclass(kw_only=True, frozen=True) @@ -109,7 +109,7 @@ async def async_setup_entry( ) -> None: """Add entities for passed config_entry in HA.""" controller = config_entry.runtime_data - entities: list[EcovacsEntity] = get_supported_entitites( + entities: list[EcovacsEntity] = get_supported_entities( controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS ) if entities: diff --git a/homeassistant/components/ecovacs/util.py b/homeassistant/components/ecovacs/util.py index 0cfbf1e8f91..968ab92851b 100644 --- a/homeassistant/components/ecovacs/util.py +++ b/homeassistant/components/ecovacs/util.py @@ -32,7 +32,7 @@ def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str: ) -def get_supported_entitites( +def get_supported_entities( controller: EcovacsController, entity_class: type[EcovacsDescriptionEntity], descriptions: tuple[EcovacsCapabilityEntityDescription, ...], From 7c5090d627eb21e07db88f6126a156405ab3f0b2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 14:55:48 +0200 Subject: [PATCH 0624/1175] Add missing type hint in zestimate (#145218) --- homeassistant/components/zestimate/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index ec8850b187d..6b3b38bdde8 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -107,13 +107,13 @@ class ZestimateDataSensor(SensorEntity): attributes["address"] = self.address return attributes - def update(self): + def update(self) -> None: """Get the latest data and update the states.""" try: response = requests.get(_RESOURCE, params=self.params, timeout=5) data = response.content.decode("utf-8") - data_dict = xmltodict.parse(data).get(ZESTIMATE) + data_dict = xmltodict.parse(data)[ZESTIMATE] error_code = int(data_dict["message"]["code"]) if error_code != 0: _LOGGER.error("The API returned: %s", data_dict["message"]["text"]) From e64f76bebe1ce804622e40b5097778537ec1228e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 16:01:41 +0300 Subject: [PATCH 0625/1175] Add full test coverage for Comelit cover (#144761) --- homeassistant/components/comelit/cover.py | 10 ++----- tests/components/comelit/test_cover.py | 36 ++++++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index d430952fabf..d4eaa9223df 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -7,7 +7,7 @@ from typing import Any, cast from aiocomelit import ComelitSerialBridgeObject from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON -from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState +from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -68,16 +68,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity): def is_closed(self) -> bool | None: """Return if the cover is closed.""" - if self._last_state in [None, "unknown"]: - return None - - if self.device_status != STATE_COVER.index("stopped"): - return False - if self._last_action: return self._last_action == STATE_COVER.index("closing") - return self._last_state == CoverState.CLOSED + return None @property def is_closing(self) -> bool: diff --git a/tests/components/comelit/test_cover.py b/tests/components/comelit/test_cover.py index b09a2e6322c..5513f3c4e25 100644 --- a/tests/components/comelit/test_cover.py +++ b/tests/components/comelit/test_cover.py @@ -15,6 +15,7 @@ from homeassistant.components.cover import ( SERVICE_STOP_COVER, STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform @@ -94,7 +95,7 @@ async def test_cover_open( await hass.async_block_till_done() assert (state := hass.states.get(ENTITY_ID)) - assert state.state == STATE_UNKNOWN + assert state.state == STATE_OPEN async def test_cover_close( @@ -159,3 +160,36 @@ async def test_cover_stop_if_stopped( assert (state := hass.states.get(ENTITY_ID)) assert state.state == STATE_UNKNOWN + + +async def test_cover_restore_state( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test cover restore state on reload.""" + + mock_serial_bridge.reset_mock() + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_UNKNOWN + + # Open cover + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + mock_serial_bridge.set_device_status.assert_called() + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_OPENING + + await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_OPENING From 760f2d1959880abc436c4d8a5e0b0903f06fbace Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 16:27:41 +0300 Subject: [PATCH 0626/1175] Remove pylance warnings for Comelit tests (#145199) --- tests/components/comelit/conftest.py | 4 ++-- tests/components/comelit/test_climate.py | 2 +- tests/components/comelit/test_humidifier.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/comelit/conftest.py b/tests/components/comelit/conftest.py index 1e5e85cd26e..8ac77505590 100644 --- a/tests/components/comelit/conftest.py +++ b/tests/components/comelit/conftest.py @@ -57,7 +57,7 @@ def mock_serial_bridge() -> Generator[AsyncMock]: @pytest.fixture -def mock_serial_bridge_config_entry() -> Generator[MockConfigEntry]: +def mock_serial_bridge_config_entry() -> MockConfigEntry: """Mock a Comelit config entry for Comelit bridge.""" return MockConfigEntry( domain=COMELIT_DOMAIN, @@ -94,7 +94,7 @@ def mock_vedo() -> Generator[AsyncMock]: @pytest.fixture -def mock_vedo_config_entry() -> Generator[MockConfigEntry]: +def mock_vedo_config_entry() -> MockConfigEntry: """Mock a Comelit config entry for Comelit vedo.""" return MockConfigEntry( domain=COMELIT_DOMAIN, diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index 3337ba28769..1938211c9dd 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -139,7 +139,7 @@ async def test_climate_data_update_bad_data( status=0, human_status="off", type="climate", - val="bad_data", + val="bad_data", # type: ignore[arg-type] protected=0, zone="Living room", power=0.0, diff --git a/tests/components/comelit/test_humidifier.py b/tests/components/comelit/test_humidifier.py index a096a1c0eb4..c5ba89becfa 100644 --- a/tests/components/comelit/test_humidifier.py +++ b/tests/components/comelit/test_humidifier.py @@ -146,7 +146,7 @@ async def test_humidifier_data_update_bad_data( status=0, human_status="off", type="climate", - val="bad_data", + val="bad_data", # type: ignore[arg-type] protected=0, zone="Living room", power=0.0, From 8df447091d55ad46df44d2ff3dd2a7c7c3f7090b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 15:52:40 +0200 Subject: [PATCH 0627/1175] Add missing type hint in vlc (#145223) --- homeassistant/components/vlc/media_player.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index d1a481a99b1..7c8bdcf8a6e 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -70,7 +70,7 @@ class VlcDevice(MediaPlayerEntity): self._vlc = self._instance.media_player_new() self._attr_name = name - def update(self): + def update(self) -> None: """Get the latest details from the device.""" status = self._vlc.get_state() if status == vlc.State.Playing: @@ -88,8 +88,6 @@ class VlcDevice(MediaPlayerEntity): self._attr_volume_level = self._vlc.audio_get_volume() / 100 self._attr_is_volume_muted = self._vlc.audio_get_mute() == 1 - return True - def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" track_length = self._vlc.get_length() / 1000 From a38e033e13a25770502b3ff67c19e039c1013f38 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 15:53:41 +0200 Subject: [PATCH 0628/1175] Improve type hints in rtorrent (#145222) --- homeassistant/components/rtorrent/sensor.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 70fe7919edb..367542ca8c2 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import cast import xmlrpc.client import voluptuous as vol @@ -126,6 +127,9 @@ def format_speed(speed): return round(kb_spd, 2 if kb_spd < 0.1 else 1) +type RTorrentData = tuple[float, float, list, list, list, list, list] + + class RTorrentSensor(SensorEntity): """Representation of an rtorrent sensor.""" @@ -135,12 +139,12 @@ class RTorrentSensor(SensorEntity): """Initialize the sensor.""" self.entity_description = description self.client = rtorrent_client - self.data = None + self.data: RTorrentData | None = None self._attr_name = f"{client_name} {description.name}" self._attr_available = False - def update(self): + def update(self) -> None: """Get the latest data from rtorrent and updates the state.""" multicall = xmlrpc.client.MultiCall(self.client) multicall.throttle.global_up.rate() @@ -152,7 +156,7 @@ class RTorrentSensor(SensorEntity): multicall.d.multicall2("", "leeching", "d.down.rate=") try: - self.data = multicall() + self.data = cast(RTorrentData, multicall()) self._attr_available = True except (xmlrpc.client.ProtocolError, OSError) as ex: _LOGGER.error("Connection to rtorrent failed (%s)", ex) @@ -164,14 +168,16 @@ class RTorrentSensor(SensorEntity): all_torrents = self.data[2] stopped_torrents = self.data[3] complete_torrents = self.data[4] + up_torrents = self.data[5] + down_torrents = self.data[6] uploading_torrents = 0 - for up_torrent in self.data[5]: + for up_torrent in up_torrents: if up_torrent[0]: uploading_torrents += 1 downloading_torrents = 0 - for down_torrent in self.data[6]: + for down_torrent in down_torrents: if down_torrent[0]: downloading_torrents += 1 From 05795d0ad845243b10326e39fc06b655e42738c8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 15:56:27 +0200 Subject: [PATCH 0629/1175] Use _attr_native_value in repetier (#145219) --- homeassistant/components/repetier/sensor.py | 32 +++++++++------------ 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index d413c25c8d4..3903ab8adfb 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -78,7 +78,6 @@ class RepetierSensor(SensorEntity): self._attributes: dict = {} self._temp_id = temp_id self._printer_id = printer_id - self._state = None self._attr_name = name self._attr_available = False @@ -88,17 +87,12 @@ class RepetierSensor(SensorEntity): """Return sensor attributes.""" return self._attributes - @property - def native_value(self): - """Return sensor state.""" - return self._state - @callback def update_callback(self): """Get new data and update state.""" self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect update callbacks.""" self.async_on_remove( async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback) @@ -115,14 +109,14 @@ class RepetierSensor(SensorEntity): self._attr_available = True return data - def update(self): + def update(self) -> None: """Update the sensor.""" if (data := self._get_data()) is None: return state = data.pop("state") _LOGGER.debug("Printer %s State %s", self.name, state) self._attributes.update(data) - self._state = state + self._attr_native_value = state class RepetierTempSensor(RepetierSensor): @@ -131,11 +125,11 @@ class RepetierTempSensor(RepetierSensor): @property def native_value(self): """Return sensor state.""" - if self._state is None: + if self._attr_native_value is None: return None - return round(self._state, 2) + return round(self._attr_native_value, 2) - def update(self): + def update(self) -> None: """Update the sensor.""" if (data := self._get_data()) is None: return @@ -143,7 +137,7 @@ class RepetierTempSensor(RepetierSensor): temp_set = data["temp_set"] _LOGGER.debug("Printer %s Setpoint: %s, Temp: %s", self.name, temp_set, state) self._attributes.update(data) - self._state = state + self._attr_native_value = state class RepetierJobSensor(RepetierSensor): @@ -152,9 +146,9 @@ class RepetierJobSensor(RepetierSensor): @property def native_value(self): """Return sensor state.""" - if self._state is None: + if self._attr_native_value is None: return None - return round(self._state, 2) + return round(self._attr_native_value, 2) class RepetierJobEndSensor(RepetierSensor): @@ -162,7 +156,7 @@ class RepetierJobEndSensor(RepetierSensor): _attr_device_class = SensorDeviceClass.TIMESTAMP - def update(self): + def update(self) -> None: """Update the sensor.""" if (data := self._get_data()) is None: return @@ -171,7 +165,7 @@ class RepetierJobEndSensor(RepetierSensor): print_time = data["print_time"] from_start = data["from_start"] time_end = start + round(print_time, 0) - self._state = dt_util.utc_from_timestamp(time_end) + self._attr_native_value = dt_util.utc_from_timestamp(time_end) remaining = print_time - from_start remaining_secs = int(round(remaining, 0)) _LOGGER.debug( @@ -186,14 +180,14 @@ class RepetierJobStartSensor(RepetierSensor): _attr_device_class = SensorDeviceClass.TIMESTAMP - def update(self): + def update(self) -> None: """Update the sensor.""" if (data := self._get_data()) is None: return job_name = data["job_name"] start = data["start"] from_start = data["from_start"] - self._state = dt_util.utc_from_timestamp(start) + self._attr_native_value = dt_util.utc_from_timestamp(start) elapsed_secs = int(round(from_start, 0)) _LOGGER.debug( "Job %s elapsed %s", From e3d2f917e2e2f2a11cebdddd63c17510ba1679b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 15:58:34 +0200 Subject: [PATCH 0630/1175] Use shorthand attributes in yandex transport sensor (#145225) --- .../components/yandex_transport/sensor.py | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index f87d29fffed..e6ecc0ee0b8 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from aioymaps import CaptchaError, NoSessionError, YandexMapsRequester import voluptuous as vol @@ -71,6 +72,7 @@ class DiscoverYandexTransport(SensorEntity): """Implementation of yandex_transport sensor.""" _attr_attribution = "Data provided by maps.yandex.ru" + _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_icon = "mdi:bus" def __init__(self, requester: YandexMapsRequester, stop_id, routes, name) -> None: @@ -78,13 +80,15 @@ class DiscoverYandexTransport(SensorEntity): self.requester = requester self._stop_id = stop_id self._routes = routes - self._state = None - self._name = name - self._attrs = None + self._attr_name = name - async def async_update(self, *, tries=0): + async def async_update(self) -> None: """Get the latest data from maps.yandex.ru and update the states.""" - attrs = {} + await self._try_update(tries=0) + + async def _try_update(self, *, tries: int) -> None: + """Get the latest data from maps.yandex.ru and update the states.""" + attrs: dict[str, Any] = {} closer_time = None try: yandex_reply = await self.requester.get_stop_info(self._stop_id) @@ -108,7 +112,7 @@ class DiscoverYandexTransport(SensorEntity): if tries > 0: return await self.requester.set_new_session() - await self.async_update(tries=tries + 1) + await self._try_update(tries=tries + 1) return stop_name = data["name"] @@ -146,27 +150,9 @@ class DiscoverYandexTransport(SensorEntity): attrs[STOP_NAME] = stop_name if closer_time is None: - self._state = None + self._attr_native_value = None else: - self._state = dt_util.utc_from_timestamp(closer_time).replace(microsecond=0) - self._attrs = attrs - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return SensorDeviceClass.TIMESTAMP - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attrs + self._attr_native_value = dt_util.utc_from_timestamp(closer_time).replace( + microsecond=0 + ) + self._attr_extra_state_attributes = attrs From 85448ea903504da08ba97104053eddfcf802c0fe Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 19 May 2025 16:05:48 +0200 Subject: [PATCH 0631/1175] Fix Z-Wave config entry unique id after NVM restore (#145221) * Fix Z-Wave config entry unique id after NVM restore * Remove stale comment --- homeassistant/components/zwave_js/api.py | 21 ++++++++++++ tests/components/zwave_js/test_api.py | 43 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5f6050b88e9..c1a24b6ea65 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -3113,6 +3113,27 @@ async def websocket_restore_nvm( with suppress(TimeoutError): async with asyncio.timeout(DRIVER_READY_TIMEOUT): await wait_driver_ready.wait() + + # When restoring the NVM to the controller, the controller home id is also changed. + # The controller state in the client is stale after restoring the NVM, + # so get the new home id with a new client using the helper function. + # The client state will be refreshed by reloading the config entry, + # after the unique id of the config entry has been updated. + try: + version_info = await async_get_version_info(hass, entry.data[CONF_URL]) + except CannotConnect: + # Just log this error, as there's nothing to do about it here. + # The stale unique id needs to be handled by a repair flow, + # after the config entry has been reloaded. + LOGGER.error( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller NVM restore" + ) + else: + hass.config_entries.async_update_entry( + entry, unique_id=str(version_info.home_id) + ) + await hass.config_entries.async_reload(entry.entry_id) connection.send_message( diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index d2f0f205e8f..83a22cbee32 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -5567,8 +5567,12 @@ async def test_restore_nvm( integration, client, hass_ws_client: WebSocketGenerator, + get_server_version: AsyncMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the restore NVM websocket command.""" + entry = integration + assert entry.unique_id == "3245146787" ws_client = await hass_ws_client(hass) # Set up mocks for the controller events @@ -5648,6 +5652,45 @@ async def test_restore_nvm( }, require_schema=14, ) + assert entry.unique_id == "1234" + + client.async_send_command.reset_mock() + + # Test client connect error when getting the server version. + + get_server_version.side_effect = ClientError("Boom!") + + # Send the subscription request + await ws_client.send_json_auto_id( + { + "type": "zwave_js/restore_nvm", + "entry_id": entry.entry_id, + "data": "dGVzdA==", # base64 encoded "test" + } + ) + + # Verify the finished event first + msg = await ws_client.receive_json() + assert msg["type"] == "event" + assert msg["event"]["event"] == "finished" + + # Verify subscription success + msg = await ws_client.receive_json() + assert msg["type"] == "result" + assert msg["success"] is True + + assert client.async_send_command.call_count == 3 + assert client.async_send_command.call_args_list[0] == call( + { + "command": "controller.restore_nvm", + "nvmData": "dGVzdA==", + }, + require_schema=14, + ) + assert ( + "Failed to get server version, cannot update config entry" + "unique id with new home id, after controller NVM restore" + ) in caplog.text client.async_send_command.reset_mock() From 8938c109c25ae072452302fa31a436d4d102eb11 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 19 May 2025 16:09:23 +0200 Subject: [PATCH 0632/1175] Improve entity registry restore test (#145220) --- tests/helpers/test_entity_registry.py | 142 ++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 20 deletions(-) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 7df7bb398e8..671c2ddeb29 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util.dt import utc_from_timestamp, utcnow from tests.common import ( ANY, @@ -2440,10 +2440,11 @@ def test_migrate_entity_to_new_platform_error_handling( async def test_restore_entity( hass: HomeAssistant, + device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, freezer: FrozenDateTimeFactory, ) -> None: - """Make sure entity registry id is stable and entity_id is reused if possible.""" + """Make sure entity registry id is stable.""" update_events = async_capture_events(hass, er.EVENT_ENTITY_REGISTRY_UPDATED) config_entry = MockConfigEntry( domain="light", @@ -2455,11 +2456,44 @@ async def test_restore_entity( title="Mock title", unique_id="test", ), + config_entries.ConfigSubentryData( + data={}, + subentry_id="mock-subentry-id-1-2", + subentry_type="test", + title="Mock title", + unique_id="test", + ), ], ) config_entry.add_to_hass(hass) + device_entry_1 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry_2 = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "22:34:56:AB:CD:EF")}, + ) entry1 = entity_registry.async_get_or_create( - "light", "hue", "1234", config_entry=config_entry + "light", + "hue", + "1234", + capabilities={"key1": "value1"}, + config_entry=config_entry, + config_subentry_id="mock-subentry-id-1-1", + device_id=device_entry_1.id, + disabled_by=er.RegistryEntryDisabler.DEVICE, + entity_category=EntityCategory.DIAGNOSTIC, + get_initial_options=lambda: {"test_domain": {"key1": "value1"}}, + has_entity_name=True, + hidden_by=er.RegistryEntryHider.INTEGRATION, + original_device_class="device_class_1", + original_icon="original_icon_1", + original_name="original_name_1", + suggested_object_id="suggested_1", + supported_features=1, + translation_key="translation_key_1", + unit_of_measurement="unit_1", ) entry2 = entity_registry.async_get_or_create( "light", @@ -2469,8 +2503,22 @@ async def test_restore_entity( config_subentry_id="mock-subentry-id-1-1", ) + # Apply user customizations entry1 = entity_registry.async_update_entity( - entry1.entity_id, new_entity_id="light.custom_1" + entry1.entity_id, + aliases={"alias1", "alias2"}, + area_id="12345A", + categories={"scope1": "id", "scope2": "id"}, + device_class="device_class_user", + disabled_by=er.RegistryEntryDisabler.USER, + hidden_by=er.RegistryEntryHider.USER, + icon="icon_user", + labels={"label1", "label2"}, + name="Test Friendly Name", + new_entity_id="light.custom_1", + ) + entry1 = entity_registry.async_update_entity_options( + entry1.entity_id, "options_domain", {"key": "value"} ) entity_registry.async_remove(entry1.entity_id) @@ -2478,17 +2526,61 @@ async def test_restore_entity( assert len(entity_registry.entities) == 0 assert len(entity_registry.deleted_entities) == 2 - # Re-add entities + # Re-add entities, integration has changed entry1_restored = entity_registry.async_get_or_create( - "light", "hue", "1234", config_entry=config_entry + "light", + "hue", + "1234", + capabilities={"key2": "value2"}, + config_entry=config_entry, + config_subentry_id="mock-subentry-id-1-2", + device_id=device_entry_2.id, + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + entity_category=EntityCategory.CONFIG, + get_initial_options=lambda: {"test_domain": {"key2": "value2"}}, + has_entity_name=False, + hidden_by=None, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + suggested_object_id="suggested_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", ) entry2_restored = entity_registry.async_get_or_create("light", "hue", "5678") assert len(entity_registry.entities) == 2 assert len(entity_registry.deleted_entities) == 0 assert entry1 != entry1_restored - # entity_id is not restored - assert attr.evolve(entry1, entity_id="light.hue_1234") == entry1_restored + # entity_id and user customizations are not restored. new integration options are + # respected. + assert entry1_restored == er.RegistryEntry( + entity_id="light.suggested_2", + unique_id="1234", + platform="hue", + capabilities={"key2": "value2"}, + config_entry_id=config_entry.entry_id, + config_subentry_id="mock-subentry-id-1-2", + created_at=utcnow(), + device_class=None, + device_id=device_entry_2.id, + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + entity_category=EntityCategory.CONFIG, + has_entity_name=False, + hidden_by=None, + icon=None, + id=entry1.id, + modified_at=utcnow(), + name=None, + options={"test_domain": {"key2": "value2"}}, + original_device_class="device_class_2", + original_icon="original_icon_2", + original_name="original_name_2", + supported_features=2, + translation_key="translation_key_2", + unit_of_measurement="unit_2", + ) assert entry2 != entry2_restored # Config entry and subentry are not restored assert ( @@ -2534,23 +2626,33 @@ async def test_restore_entity( # Check the events await hass.async_block_till_done() - assert len(update_events) == 13 - assert update_events[0].data == {"action": "create", "entity_id": "light.hue_1234"} + assert len(update_events) == 14 + assert update_events[0].data == { + "action": "create", + "entity_id": "light.suggested_1", + } assert update_events[1].data == {"action": "create", "entity_id": "light.hue_5678"} assert update_events[2].data["action"] == "update" - assert update_events[3].data == {"action": "remove", "entity_id": "light.custom_1"} - assert update_events[4].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[3].data["action"] == "update" + assert update_events[4].data == {"action": "remove", "entity_id": "light.custom_1"} + assert update_events[5].data == {"action": "remove", "entity_id": "light.hue_5678"} # Restore entities the 1st time - assert update_events[5].data == {"action": "create", "entity_id": "light.hue_1234"} - assert update_events[6].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[7].data == {"action": "remove", "entity_id": "light.hue_1234"} - assert update_events[8].data == {"action": "remove", "entity_id": "light.hue_5678"} + assert update_events[6].data == { + "action": "create", + "entity_id": "light.suggested_2", + } + assert update_events[7].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[8].data == { + "action": "remove", + "entity_id": "light.suggested_2", + } + assert update_events[9].data == {"action": "remove", "entity_id": "light.hue_5678"} # Restore entities the 2nd time - assert update_events[9].data == {"action": "create", "entity_id": "light.hue_1234"} - assert update_events[10].data == {"action": "create", "entity_id": "light.hue_5678"} - assert update_events[11].data == {"action": "remove", "entity_id": "light.hue_1234"} + assert update_events[10].data == {"action": "create", "entity_id": "light.hue_1234"} + assert update_events[11].data == {"action": "create", "entity_id": "light.hue_5678"} + assert update_events[12].data == {"action": "remove", "entity_id": "light.hue_1234"} # Restore entities the 3rd time - assert update_events[12].data == {"action": "create", "entity_id": "light.hue_1234"} + assert update_events[13].data == {"action": "create", "entity_id": "light.hue_1234"} async def test_async_migrate_entry_delete_self( From 9c798cbb5da538178cd0284e3d11dacd2294c51b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 17:12:27 +0300 Subject: [PATCH 0633/1175] Add device reconfigure to Comelit config flow (#142866) * Add device reconfigure to Comelit config flow * tweak * tweak * update quality scale * apply review comment * apply review comment * review comment * complete test --- .../components/comelit/config_flow.py | 76 ++++++++++++---- .../components/comelit/quality_scale.yaml | 4 +- homeassistant/components/comelit/strings.json | 13 +++ tests/components/comelit/test_config_flow.py | 91 +++++++++++++++++++ 4 files changed, 161 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/comelit/config_flow.py b/homeassistant/components/comelit/config_flow.py index 10180236f79..5b09b582c66 100644 --- a/homeassistant/components/comelit/config_flow.py +++ b/homeassistant/components/comelit/config_flow.py @@ -28,20 +28,22 @@ DEFAULT_HOST = "192.168.1.252" DEFAULT_PIN = 111111 -def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: - """Return user form schema.""" - user_input = user_input or {} - return vol.Schema( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, - vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST), - } - ) - - +USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, + vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST), + } +) STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.positive_int}) +STEP_RECONFIGURE = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int, + } +) async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: @@ -87,13 +89,11 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=user_form_schema(user_input) - ) + return self.async_show_form(step_id="user", data_schema=USER_SCHEMA) self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) - errors = {} + errors: dict[str, str] = {} try: info = await validate_input(self.hass, user_input) @@ -108,21 +108,21 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=user_form_schema(user_input), errors=errors + step_id="user", data_schema=USER_SCHEMA, errors=errors ) async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle reauth flow.""" - self.context["title_placeholders"] = {"host": entry_data[CONF_HOST]} + self.context["title_placeholders"] = {CONF_HOST: entry_data[CONF_HOST]} return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle reauth confirm.""" - errors = {} + errors: dict[str, str] = {} reauth_entry = self._get_reauth_entry() entry_data = reauth_entry.data @@ -163,6 +163,42 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the device.""" + reconfigure_entry = self._get_reconfigure_entry() + if not user_input: + return self.async_show_form( + step_id="reconfigure", data_schema=STEP_RECONFIGURE + ) + + updated_host = user_input[CONF_HOST] + + self._async_abort_entries_match({CONF_HOST: updated_host}) + + errors: dict[str, str] = {} + + try: + await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # noqa: BLE001 + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + reconfigure_entry, data_updates={CONF_HOST: updated_host} + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=STEP_RECONFIGURE, + errors=errors, + ) + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/comelit/quality_scale.yaml b/homeassistant/components/comelit/quality_scale.yaml index a74fab22484..7465193ffa9 100644 --- a/homeassistant/components/comelit/quality_scale.yaml +++ b/homeassistant/components/comelit/quality_scale.yaml @@ -70,9 +70,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: - status: todo - comment: PR in progress + reconfiguration-flow: done repair-issues: status: exempt comment: no known use cases for repair issues or flows, yet diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 8f2ae1433e5..973fcad1999 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -23,11 +23,24 @@ "pin": "[%key:component::comelit::config::step::reauth_confirm::data_description::pin%]", "type": "The type of your Comelit device." } + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "pin": "[%key:common::config_flow::data::pin%]" + }, + "data_description": { + "host": "[%key:component::comelit::config::step::user::data_description::host%]", + "port": "[%key:component::comelit::config::step::user::data_description::port%]", + "pin": "[%key:component::comelit::config::step::reauth_confirm::data_description::pin%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "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%]" diff --git a/tests/components/comelit/test_config_flow.py b/tests/components/comelit/test_config_flow.py index dd1d1fb3836..1751a837026 100644 --- a/tests/components/comelit/test_config_flow.py +++ b/tests/components/comelit/test_config_flow.py @@ -219,3 +219,94 @@ async def test_reauth_not_successful( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_vedo_config_entry.data[CONF_PIN] == VEDO_PIN + + +async def test_reconfigure_successful( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test that the host can be reconfigured.""" + mock_serial_bridge_config_entry.add_to_hass(hass) + result = await mock_serial_bridge_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # original entry + assert mock_serial_bridge_config_entry.data[CONF_HOST] == "fake_bridge_host" + + new_host = "new_bridge_host" + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: new_host, + CONF_PORT: BRIDGE_PORT, + CONF_PIN: BRIDGE_PIN, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert mock_serial_bridge_config_entry.data[CONF_HOST] == new_host + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + (ConnectionResetError, "unknown"), + ], +) +async def test_reconfigure_fails( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: + """Test that the host can be reconfigured.""" + mock_serial_bridge_config_entry.add_to_hass(hass) + result = await mock_serial_bridge_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_serial_bridge.login.side_effect = side_effect + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.100.60", + CONF_PORT: BRIDGE_PORT, + CONF_PIN: BRIDGE_PIN, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + assert reconfigure_result["errors"] == {"base": error} + + mock_serial_bridge.login.side_effect = None + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.100.61", + CONF_PORT: BRIDGE_PORT, + CONF_PIN: BRIDGE_PIN, + }, + ) + + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + assert mock_serial_bridge_config_entry.data == { + CONF_HOST: "192.168.100.61", + CONF_PORT: BRIDGE_PORT, + CONF_PIN: BRIDGE_PIN, + CONF_TYPE: BRIDGE, + } From a8ecdb3bff2a15a7d527c110c7663e8120245043 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 17:34:41 +0300 Subject: [PATCH 0634/1175] Finish reconfigure test for Vodafone Station (#145230) --- .../vodafone_station/test_config_flow.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/components/vodafone_station/test_config_flow.py b/tests/components/vodafone_station/test_config_flow.py index 7ab56f2e967..4653230f7ca 100644 --- a/tests/components/vodafone_station/test_config_flow.py +++ b/tests/components/vodafone_station/test_config_flow.py @@ -302,3 +302,22 @@ async def test_reconfigure_fails( assert reconfigure_result["type"] is FlowResultType.FORM assert reconfigure_result["step_id"] == "reconfigure" assert reconfigure_result["errors"] == {"base": error} + + mock_vodafone_station_router.login.side_effect = None + + reconfigure_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.100.61", + CONF_PASSWORD: "fake_password", + CONF_USERNAME: "fake_username", + }, + ) + + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_HOST: "192.168.100.61", + CONF_PASSWORD: "fake_password", + CONF_USERNAME: "fake_username", + } From 752c73a2edfac130f6873ed741fdc27b4b27fc23 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 19 May 2025 11:26:42 -0400 Subject: [PATCH 0635/1175] Add trigger_variables to template trigger 'for' field (#136672) * Add trigger_variables to template trigger for * address comments --- homeassistant/components/template/trigger.py | 12 ++++--- tests/components/template/test_trigger.py | 33 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 44ac2d93051..c3e5a5d141f 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -48,6 +48,7 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] + variables = trigger_info["variables"] or {} value_template: Template = config[CONF_VALUE_TEMPLATE] time_delta = config.get(CONF_FOR) delay_cancel = None @@ -56,9 +57,7 @@ async def async_attach_trigger( # Arm at setup if the template is already false. try: - if not result_as_boolean( - value_template.async_render(trigger_info["variables"]) - ): + if not result_as_boolean(value_template.async_render(variables)): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( @@ -134,9 +133,12 @@ async def async_attach_trigger( call_action() return + data = {"trigger": template_variables} + period_variables = {**variables, **data} + try: period: timedelta = cv.positive_time_period( - template.render_complex(time_delta, {"trigger": template_variables}) + template.render_complex(time_delta, period_variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( @@ -150,7 +152,7 @@ async def async_attach_trigger( info = async_track_template_result( hass, - [TrackTemplate(value_template, trigger_info["variables"])], + [TrackTemplate(value_template, variables)], template_listener, ) unsub = info.async_remove diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 49b89b61d34..6de07612c36 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -788,6 +788,39 @@ async def test_if_fires_on_change_with_for_template_3( assert len(calls) == 1 +@pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)]) +@pytest.mark.parametrize( + "config", + [ + { + automation.DOMAIN: { + "trigger_variables": { + "seconds": 5, + "entity": "test.entity", + }, + "trigger": { + "platform": "template", + "value_template": "{{ is_state(entity, 'world') }}", + "for": "{{ seconds }}", + }, + "action": {"service": "test.automation"}, + } + }, + ], +) +@pytest.mark.usefixtures("start_ha") +async def test_if_fires_on_change_with_for_template_4( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test for firing on change with for template.""" + hass.states.async_set("test.entity", "world") + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + + @pytest.mark.parametrize(("count", "domain"), [(1, automation.DOMAIN)]) @pytest.mark.parametrize( "config", From f44cb9b03eec70a21f890a077136c67b12fd4b0b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 19 May 2025 18:30:34 +0300 Subject: [PATCH 0636/1175] Add action exceptions to Comelit integration (#143581) * Add action exceptions to Comelit integration * missing decorator * update quality scale --- homeassistant/components/comelit/climate.py | 3 + homeassistant/components/comelit/cover.py | 2 + .../components/comelit/humidifier.py | 5 + homeassistant/components/comelit/light.py | 2 + .../components/comelit/manifest.json | 2 +- .../components/comelit/quality_scale.yaml | 4 +- homeassistant/components/comelit/strings.json | 3 + homeassistant/components/comelit/switch.py | 2 + homeassistant/components/comelit/utils.py | 40 ++++++++ tests/components/comelit/test_utils.py | 93 +++++++++++++++++++ 10 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 tests/components/comelit/test_utils.py diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index e7890cddff8..69d95da01bf 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -23,6 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity +from .utils import bridge_api_call # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -155,6 +156,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): self._update_attributes() super()._handle_coordinator_update() + @bridge_api_call async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ( @@ -171,6 +173,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): self._attr_target_temperature = target_temp self.async_write_ha_state() + @bridge_api_call async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" diff --git a/homeassistant/components/comelit/cover.py b/homeassistant/components/comelit/cover.py index d4eaa9223df..691ebaec638 100644 --- a/homeassistant/components/comelit/cover.py +++ b/homeassistant/components/comelit/cover.py @@ -14,6 +14,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity +from .utils import bridge_api_call # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -83,6 +84,7 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity): """Return if the cover is opening.""" return self._current_action("opening") + @bridge_api_call async def _cover_set_state(self, action: int, state: int) -> None: """Set desired cover state.""" self._last_state = self.state diff --git a/homeassistant/components/comelit/humidifier.py b/homeassistant/components/comelit/humidifier.py index 816d5c6bb38..0c43744aadd 100644 --- a/homeassistant/components/comelit/humidifier.py +++ b/homeassistant/components/comelit/humidifier.py @@ -23,6 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity +from .utils import bridge_api_call # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -154,6 +155,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity): self._update_attributes() super()._handle_coordinator_update() + @bridge_api_call async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if not self._attr_is_on: @@ -171,6 +173,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity): self._attr_target_humidity = humidity self.async_write_ha_state() + @bridge_api_call async def async_set_mode(self, mode: str) -> None: """Set humidifier mode.""" await self.coordinator.api.set_humidity_status( @@ -179,6 +182,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity): self._attr_mode = mode self.async_write_ha_state() + @bridge_api_call async def async_turn_on(self, **kwargs: Any) -> None: """Turn on.""" await self.coordinator.api.set_humidity_status( @@ -187,6 +191,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity): self._attr_is_on = True self.async_write_ha_state() + @bridge_api_call async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" await self.coordinator.api.set_humidity_status( diff --git a/homeassistant/components/comelit/light.py b/homeassistant/components/comelit/light.py index 27d9a8d57dd..c04b88c7819 100644 --- a/homeassistant/components/comelit/light.py +++ b/homeassistant/components/comelit/light.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity +from .utils import bridge_api_call # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -39,6 +40,7 @@ class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity): _attr_name = None _attr_supported_color_modes = {ColorMode.ONOFF} + @bridge_api_call async def _light_set_state(self, state: int) -> None: """Set desired light state.""" await self.coordinator.api.set_device_status(LIGHT, self._device.index, state) diff --git a/homeassistant/components/comelit/manifest.json b/homeassistant/components/comelit/manifest.json index bea84c6b805..44101f0fd06 100644 --- a/homeassistant/components/comelit/manifest.json +++ b/homeassistant/components/comelit/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["aiocomelit"], - "quality_scale": "bronze", + "quality_scale": "silver", "requirements": ["aiocomelit==0.12.3"] } diff --git a/homeassistant/components/comelit/quality_scale.yaml b/homeassistant/components/comelit/quality_scale.yaml index 7465193ffa9..4fbbd79d60d 100644 --- a/homeassistant/components/comelit/quality_scale.yaml +++ b/homeassistant/components/comelit/quality_scale.yaml @@ -26,9 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: todo - comment: wrap api calls in try block + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 973fcad1999..7a04b5d2d04 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -89,6 +89,9 @@ "cannot_authenticate": { "message": "Error authenticating" }, + "cannot_retrieve_data": { + "message": "Error retrieving data: {error}" + }, "update_failed": { "message": "Failed to update data: {error}" } diff --git a/homeassistant/components/comelit/switch.py b/homeassistant/components/comelit/switch.py index 658f37f70af..1896071596f 100644 --- a/homeassistant/components/comelit/switch.py +++ b/homeassistant/components/comelit/switch.py @@ -13,6 +13,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity +from .utils import bridge_api_call # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -56,6 +57,7 @@ class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity): if device.type == OTHER: self._attr_device_class = SwitchDeviceClass.OUTLET + @bridge_api_call async def _switch_set_state(self, state: int) -> None: """Set desired switch state.""" await self.coordinator.api.set_device_status( diff --git a/homeassistant/components/comelit/utils.py b/homeassistant/components/comelit/utils.py index fe05e2412b0..5d16f6232df 100644 --- a/homeassistant/components/comelit/utils.py +++ b/homeassistant/components/comelit/utils.py @@ -1,13 +1,53 @@ """Utils for Comelit.""" +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate + +from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData from aiohttp import ClientSession, CookieJar from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client +from .const import DOMAIN +from .entity import ComelitBridgeBaseEntity + async def async_client_session(hass: HomeAssistant) -> ClientSession: """Return a new aiohttp session.""" return aiohttp_client.async_create_clientsession( hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) + + +def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P]( + func: Callable[Concatenate[_T, _P], Awaitable[None]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Catch Bridge API call exceptions.""" + + @wraps(func) + async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except CannotConnect as err: + self.coordinator.last_update_success = False + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + translation_placeholders={"error": repr(err)}, + ) from err + except CannotRetrieveData as err: + self.coordinator.last_update_success = False + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="cannot_retrieve_data", + translation_placeholders={"error": repr(err)}, + ) from err + except CannotAuthenticate: + self.coordinator.last_update_success = False + self.coordinator.config_entry.async_start_reauth(self.hass) + + return cmd_wrapper diff --git a/tests/components/comelit/test_utils.py b/tests/components/comelit/test_utils.py new file mode 100644 index 00000000000..413d0d0e561 --- /dev/null +++ b/tests/components/comelit/test_utils.py @@ -0,0 +1,93 @@ +"""Tests for Comelit SimpleHome switch platform.""" + +from unittest.mock import AsyncMock + +from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData +import pytest + +from homeassistant.components.comelit.const import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import setup_integration + +from tests.common import MockConfigEntry + +ENTITY_ID = "switch.switch0" + + +@pytest.mark.parametrize( + ("side_effect", "key", "error"), + [ + (CannotConnect, "cannot_connect", "CannotConnect()"), + (CannotRetrieveData, "cannot_retrieve_data", "CannotRetrieveData()"), + ], +) +async def test_bridge_api_call_exceptions( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, + side_effect: Exception, + key: str, + error: str, +) -> None: + """Test bridge_api_call decorator for exceptions.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_OFF + + mock_serial_bridge.set_device_status.side_effect = side_effect + + # Call API + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + assert exc_info.value.translation_domain == DOMAIN + assert exc_info.value.translation_key == key + assert exc_info.value.translation_placeholders == {"error": error} + + +async def test_bridge_api_call_reauth( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test bridge_api_call decorator for reauth.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_OFF + + mock_serial_bridge.set_device_status.side_effect = CannotAuthenticate + + # Call API + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + assert mock_serial_bridge_config_entry.state is ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_serial_bridge_config_entry.entry_id From e491629143e6817cf3d8e4f07fbf027b8cc61e7d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 17:40:12 +0200 Subject: [PATCH 0637/1175] Split update method in pioneer media player (#145212) Split method in pioneer media player --- homeassistant/components/pioneer/media_player.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 385acbe4818..8da2e171cef 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -59,7 +59,7 @@ def setup_platform( config[CONF_SOURCES], ) - if pioneer.update(): + if pioneer.update_device(): add_entities([pioneer]) @@ -122,7 +122,11 @@ class PioneerDevice(MediaPlayerEntity): except telnetlib.socket.timeout: _LOGGER.debug("Pioneer %s command %s timed out", self._name, command) - def update(self): + def update(self) -> None: + """Update the entity.""" + self.update_device() + + def update_device(self) -> bool: """Get the latest details from the device.""" try: telnet = telnetlib.Telnet(self._host, self._port, self._timeout) From 366f592a8ae0170a9c91280b38eb491fd349c174 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 17:52:23 +0200 Subject: [PATCH 0638/1175] Fix invalid type hints in netgear switch (#145226) * Fix invalid type hints in netgear switch * Adjust --- homeassistant/components/netgear/switch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index dd8468df099..712475b9b34 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -41,8 +41,8 @@ class NetgearSwitchEntityDescriptionRequired: class NetgearSwitchEntityDescription(SwitchEntityDescription): """Class describing Netgear Switch entities.""" - update: Callable[[NetgearRouter], bool] - action: Callable[[NetgearRouter], bool] + update: Callable[[NetgearRouter], Callable[[], bool | None]] + action: Callable[[NetgearRouter], Callable[[bool], bool]] ROUTER_SWITCH_TYPES = [ @@ -200,12 +200,12 @@ class NetgearRouterSwitchEntity(NetgearRouterEntity, SwitchEntity): self._attr_is_on = None self._attr_available = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Fetch state when entity is added.""" await self.async_update() await super().async_added_to_hass() - async def async_update(self): + async def async_update(self) -> None: """Poll the state of the switch.""" async with self._router.api_lock: response = await self.hass.async_add_executor_job( @@ -217,14 +217,14 @@ class NetgearRouterSwitchEntity(NetgearRouterEntity, SwitchEntity): self._attr_is_on = response self._attr_available = True - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" async with self._router.api_lock: await self.hass.async_add_executor_job( self.entity_description.action(self._router), True ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" async with self._router.api_lock: await self.hass.async_add_executor_job( From cadbe885d175818a2e5dfbd1467df9ee3f0c179b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 17:52:35 +0200 Subject: [PATCH 0639/1175] Add missing type hint in homematic (#145214) --- homeassistant/components/homematic/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 44e95e98f38..bf029b2806d 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -99,10 +99,10 @@ class HMDevice(Entity): return attr - def update(self): + def update(self) -> None: """Connect to HomeMatic init values.""" if self._connected: - return True + return # Initialize self._homematic = self.hass.data[DATA_HOMEMATIC] From e09dde2ea92c062dc86f142e15d388c9c0a94f98 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 May 2025 12:04:19 -0400 Subject: [PATCH 0640/1175] Allow TTS streams to generate temporary media source IDs (#145080) * Allow TTS streams to generate temporary media source IDs * Update tests/components/tts/test_media_source.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update assist snapshots --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../components/assist_pipeline/pipeline.py | 21 +------------ homeassistant/components/tts/__init__.py | 24 ++++++++++++-- homeassistant/components/tts/const.py | 2 ++ homeassistant/components/tts/media_source.py | 31 +++++++++++++++---- .../assist_pipeline/snapshots/test_init.ambr | 8 ++--- .../snapshots/test_pipeline.ambr | 2 +- .../snapshots/test_websocket.ambr | 8 ++--- tests/components/tts/test_media_source.py | 12 +++++++ 8 files changed, 71 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index a205db4e615..5f811ac955b 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -20,9 +20,6 @@ import hass_nabucasa import voluptuous as vol from homeassistant.components import conversation, stt, tts, wake_word, websocket_api -from homeassistant.components.tts import ( - generate_media_source_id as tts_generate_media_source_id, -) from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -1276,26 +1273,10 @@ class PipelineRun: ) ) - try: - # Synthesize audio and get URL - tts_media_id = tts_generate_media_source_id( - self.hass, - tts_input, - engine=self.tts_stream.engine, - language=self.tts_stream.language, - options=self.tts_stream.options, - ) - except Exception as src_error: - _LOGGER.exception("Unexpected error during text-to-speech") - raise TextToSpeechError( - code="tts-failed", - message="Unexpected error during text-to-speech", - ) from src_error - self.tts_stream.async_set_message(tts_input) tts_output = { - "media_id": tts_media_id, + "media_id": self.tts_stream.media_source_id, "token": self.tts_stream.token, "url": self.tts_stream.url, "mime_type": self.tts_stream.content_type, diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 526be21ad76..da8a0f2324e 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -25,6 +25,9 @@ import voluptuous as vol from homeassistant.components import ffmpeg, websocket_api from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_source import ( + generate_media_source_id as ms_generate_media_source_id, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, PLATFORM_FORMAT from homeassistant.core import ( @@ -58,6 +61,7 @@ from .const import ( DEFAULT_CACHE_DIR, DEFAULT_TIME_MEMORY, DOMAIN, + MEDIA_SOURCE_STREAM_PATH, TtsAudioType, ) from .entity import TextToSpeechEntity, TTSAudioRequest, TTSAudioResponse @@ -273,9 +277,17 @@ async def async_get_media_source_audio( media_source_id: str, ) -> tuple[str, bytes]: """Get TTS audio as extension, data.""" + manager = hass.data[DATA_TTS_MANAGER] parsed = parse_media_source_id(media_source_id) - stream = hass.data[DATA_TTS_MANAGER].async_create_result_stream(**parsed["options"]) - stream.async_set_message(parsed["message"]) + if "stream" in parsed: + stream = manager.async_get_result_stream( + parsed["stream"] # type: ignore[typeddict-item] + ) + if stream is None: + raise ValueError("Stream not found") + else: + stream = manager.async_create_result_stream(**parsed["options"]) + stream.async_set_message(parsed["message"]) data = b"".join([chunk async for chunk in stream.async_stream_result()]) return stream.extension, data @@ -478,6 +490,14 @@ class ResultStream: """Get the URL to stream the result.""" return f"/api/tts_proxy/{self.token}" + @cached_property + def media_source_id(self) -> str: + """Get the media source ID of this stream.""" + return ms_generate_media_source_id( + DOMAIN, + f"{MEDIA_SOURCE_STREAM_PATH}/{self.token}", + ) + @cached_property def _result_cache(self) -> asyncio.Future[TTSCache]: """Get the future that returns the cache.""" diff --git a/homeassistant/components/tts/const.py b/homeassistant/components/tts/const.py index 42c7d710ad4..830e0053cee 100644 --- a/homeassistant/components/tts/const.py +++ b/homeassistant/components/tts/const.py @@ -30,4 +30,6 @@ DATA_COMPONENT: HassKey[EntityComponent[TextToSpeechEntity]] = HassKey(DOMAIN) DATA_TTS_MANAGER: HassKey[SpeechManager] = HassKey("tts_manager") +MEDIA_SOURCE_STREAM_PATH = "-stream-" + type TtsAudioType = tuple[str | None, bytes | None] diff --git a/homeassistant/components/tts/media_source.py b/homeassistant/components/tts/media_source.py index f096e082364..91192fdca13 100644 --- a/homeassistant/components/tts/media_source.py +++ b/homeassistant/components/tts/media_source.py @@ -19,7 +19,7 @@ from homeassistant.components.media_source import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from .const import DATA_COMPONENT, DATA_TTS_MANAGER, DOMAIN +from .const import DATA_COMPONENT, DATA_TTS_MANAGER, DOMAIN, MEDIA_SOURCE_STREAM_PATH from .helper import get_engine_instance URL_QUERY_TTS_OPTIONS = "tts_options" @@ -81,10 +81,22 @@ class ParsedMediaSourceId(TypedDict): message: str +class ParsedMediaSourceStreamId(TypedDict): + """Parsed media source ID for a stream.""" + + stream: str + + @callback -def parse_media_source_id(media_source_id: str) -> ParsedMediaSourceId: +def parse_media_source_id( + media_source_id: str, +) -> ParsedMediaSourceId | ParsedMediaSourceStreamId: """Turn a media source ID into options.""" parsed = URL(media_source_id) + + if parsed.path.startswith(f"{MEDIA_SOURCE_STREAM_PATH}/"): + return {"stream": parsed.path[len(MEDIA_SOURCE_STREAM_PATH) + 1 :]} + if URL_QUERY_TTS_OPTIONS in parsed.query: try: options = json.loads(parsed.query[URL_QUERY_TTS_OPTIONS]) @@ -122,17 +134,24 @@ class TTSMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" + manager = self.hass.data[DATA_TTS_MANAGER] try: parsed = parse_media_source_id(item.identifier) - stream = self.hass.data[DATA_TTS_MANAGER].async_create_result_stream( - **parsed["options"] - ) - stream.async_set_message(parsed["message"]) + if "stream" in parsed: + stream = manager.async_get_result_stream( + parsed["stream"], # type: ignore[typeddict-item] + ) + else: + stream = manager.async_create_result_stream(**parsed["options"]) + stream.async_set_message(parsed["message"]) except Unresolvable: raise except HomeAssistantError as err: raise Unresolvable(str(err)) from err + if stream is None: + raise Unresolvable("Stream not found") + return PlayMedia(stream.url, stream.content_type) async def async_browse_media( diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 816430f58d0..5d2d25ddc5c 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -84,7 +84,7 @@ dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -183,7 +183,7 @@ dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22Arnold+Schwarzenegger%22%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -282,7 +282,7 @@ dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22Arnold+Schwarzenegger%22%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -405,7 +405,7 @@ dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index bbe08a2adbe..f5940edbc76 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -139,7 +139,7 @@ dict({ 'data': dict({ 'tts_output': dict({ - 'media_id': 'media-source://tts/tts.test?message=hello,+how+are+you?&language=en_US&tts_options=%7B%7D', + 'media_id': 'media-source://tts/-stream-/mocked-token.mp3', 'mime_type': 'audio/mpeg', 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 41bdba9f3cd..827b9c71ba8 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -80,7 +80,7 @@ # name: test_audio_pipeline.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -171,7 +171,7 @@ # name: test_audio_pipeline_debug.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -274,7 +274,7 @@ # name: test_audio_pipeline_with_enhancements.6 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', @@ -387,7 +387,7 @@ # name: test_audio_pipeline_with_wake_word_no_timeout.8 dict({ 'tts_output': dict({ - 'media_id': "media-source://tts/tts.test?message=Sorry,+I+couldn't+understand+that&language=en_US&tts_options=%7B%7D", + 'media_id': 'media-source://tts/-stream-/test_token.mp3', 'mime_type': 'audio/mpeg', 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index eb4b09cab5b..8ec0de8765d 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -17,6 +17,7 @@ from homeassistant.setup import async_setup_component from .common import ( DEFAULT_LANG, + MockResultStream, MockTTSEntity, MockTTSProvider, mock_config_entry_setup, @@ -198,6 +199,17 @@ async def test_resolving( assert language == "de_DE" assert mock_get_tts_audio.mock_calls[0][2]["options"] == {"voice": "Paulus"} + # Test with result stream + stream = MockResultStream(hass, "wav", b"") + media = await media_source.async_resolve_media(hass, stream.media_source_id, None) + assert media.url == stream.url + assert media.mime_type == stream.content_type + + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media( + hass, "media-source://tts/-stream-/not-a-valid-token", None + ) + @pytest.mark.parametrize( ("mock_provider", "mock_tts_entity"), From cff7aa229e271d8a78475a3ed10af09bbc865416 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 19:18:22 +0200 Subject: [PATCH 0641/1175] Add missing type hint in plex (#145217) --- homeassistant/components/plex/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4a1654959f6..ed96adeff8a 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -7,6 +7,7 @@ from functools import wraps import logging from typing import Any, Concatenate, cast +from plexapi.client import PlexClient import plexapi.exceptions import requests.exceptions @@ -189,7 +190,7 @@ class PlexMediaPlayer(MediaPlayerEntity): PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier), ) - def update(self): + def update(self) -> None: """Refresh key device data.""" if not self.session: self.force_idle() @@ -207,6 +208,7 @@ class PlexMediaPlayer(MediaPlayerEntity): self.device.proxyThroughServer() self._device_protocol_capabilities = self.device.protocolCapabilities + device: PlexClient for device in filter(None, [self.device, self.session_device]): self.device_make = self.device_make or device.device self.device_platform = self.device_platform or device.platform From 37fe25cfdc32ea6b29277b4db8785223caf59295 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 May 2025 13:43:06 -0400 Subject: [PATCH 0642/1175] Add support_streaming to ConversationEntity (#144998) * Add support_streaming to ConversationEntity * pipeline tests --- .../components/conversation/__init__.py | 6 +++- .../components/conversation/agent_manager.py | 1 + .../components/conversation/entity.py | 6 ++++ .../components/conversation/models.py | 1 + .../assist_pipeline/test_pipeline.py | 30 +++++++++++++++---- .../conversation/snapshots/test_init.ambr | 3 ++ tests/components/conversation/test_init.py | 7 +++++ 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 25aaf6df290..fff2c00641f 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -203,7 +203,11 @@ def async_get_agent_info( name = agent.name if not isinstance(name, str): name = agent.entity_id - return AgentInfo(id=agent.entity_id, name=name) + return AgentInfo( + id=agent.entity_id, + name=name, + supports_streaming=agent.supports_streaming, + ) manager = get_agent_manager(hass) diff --git a/homeassistant/components/conversation/agent_manager.py b/homeassistant/components/conversation/agent_manager.py index 5ff47977d88..38c0ca8db6b 100644 --- a/homeassistant/components/conversation/agent_manager.py +++ b/homeassistant/components/conversation/agent_manager.py @@ -166,6 +166,7 @@ class AgentManager: AgentInfo( id=agent_id, name=config_entry.title or config_entry.domain, + supports_streaming=False, ) ) return agents diff --git a/homeassistant/components/conversation/entity.py b/homeassistant/components/conversation/entity.py index ca4d18ab9f5..60cf24dbf96 100644 --- a/homeassistant/components/conversation/entity.py +++ b/homeassistant/components/conversation/entity.py @@ -18,8 +18,14 @@ class ConversationEntity(RestoreEntity): _attr_should_poll = False _attr_supported_features = ConversationEntityFeature(0) + _attr_supports_streaming = False __last_activity: str | None = None + @property + def supports_streaming(self) -> bool: + """Return if the entity supports streaming responses.""" + return self._attr_supports_streaming + @property @final def state(self) -> str | None: diff --git a/homeassistant/components/conversation/models.py b/homeassistant/components/conversation/models.py index 7bdd13afc01..00097f5b4d3 100644 --- a/homeassistant/components/conversation/models.py +++ b/homeassistant/components/conversation/models.py @@ -16,6 +16,7 @@ class AgentInfo: id: str name: str + supports_streaming: bool @dataclass(slots=True) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index abf6572afc9..f4e7c886d40 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1083,7 +1083,11 @@ async def test_sentence_trigger_overrides_conversation_agent( # Ensure prepare succeeds with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), ): await pipeline_input.validate() @@ -1161,7 +1165,11 @@ async def test_prefer_local_intents( # Ensure prepare succeeds with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), ): await pipeline_input.validate() @@ -1225,7 +1233,11 @@ async def test_intent_continue_conversation( # Ensure prepare succeeds with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), ): await pipeline_input.validate() @@ -1295,7 +1307,11 @@ async def test_intent_continue_conversation( # Ensure prepare succeeds with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), ) as mock_prepare: await pipeline_input.validate() @@ -1633,7 +1649,11 @@ async def test_chat_log_tts_streaming( with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", - return_value=conversation.AgentInfo(id="test-agent", name="Test Agent"), + return_value=conversation.AgentInfo( + id="test-agent", + name="Test Agent", + supports_streaming=False, + ), ): await pipeline_input.validate() diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 3d843d4e32a..a853faa7a3d 100644 --- a/tests/components/conversation/snapshots/test_init.ambr +++ b/tests/components/conversation/snapshots/test_init.ambr @@ -29,18 +29,21 @@ dict({ 'id': 'conversation.home_assistant', 'name': 'Home Assistant', + 'supports_streaming': False, }) # --- # name: test_get_agent_info.1 dict({ 'id': 'mock-entry', 'name': 'Mock Title', + 'supports_streaming': False, }) # --- # name: test_get_agent_info.2 dict({ 'id': 'conversation.home_assistant', 'name': 'Home Assistant', + 'supports_streaming': False, }) # --- # name: test_turn_on_intent[None-turn kitchen on-None] diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 9ac5c7d16a4..c3de5f1127c 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -220,6 +220,13 @@ async def test_get_agent_info( agent_info = conversation.async_get_agent_info(hass) assert agent_info == snapshot + default_agent = conversation.async_get_agent(hass) + default_agent._attr_supports_streaming = True + assert ( + conversation.async_get_agent_info(hass, "homeassistant").supports_streaming + is True + ) + @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) async def test_prepare_agent( From 5031ffe7676f9ceb72ae9183f9da323bc0eb2920 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 19 May 2025 20:02:37 +0200 Subject: [PATCH 0643/1175] Fix wording of "Estimated power production" sensors in `forecast_solar` (#145201) --- homeassistant/components/forecast_solar/strings.json | 6 +++--- tests/components/forecast_solar/test_sensor.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index 201a3cd415c..278e68db9a1 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -54,13 +54,13 @@ "name": "Estimated power production - now" }, "power_production_next_hour": { - "name": "Estimated power production - next hour" + "name": "Estimated power production - in 1 hour" }, "power_production_next_12hours": { - "name": "Estimated power production - next 12 hours" + "name": "Estimated power production - in 12 hours" }, "power_production_next_24hours": { - "name": "Estimated power production - next 24 hours" + "name": "Estimated power production - in 24 hours" }, "energy_current_hour": { "name": "Estimated energy production - this hour" diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index f78ca894acb..86bf4c6b392 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -194,17 +194,17 @@ async def test_disabled_by_default( [ ( "power_production_next_12hours", - "Estimated power production - next 12 hours", + "Estimated power production - in 12 hours", "600000", ), ( "power_production_next_24hours", - "Estimated power production - next 24 hours", + "Estimated power production - in 24 hours", "700000", ), ( "power_production_next_hour", - "Estimated power production - next hour", + "Estimated power production - in 1 hour", "400000", ), ], From 7e895f7d10c8761f61651b4c04bbbecc142e625a Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 19 May 2025 11:03:59 -0700 Subject: [PATCH 0644/1175] Fix history_stats with sliding window that ends before now (#145117) --- .../components/history_stats/data.py | 64 +++++++---- tests/components/history_stats/test_sensor.py | 101 +++++++++++++++++- 2 files changed, 139 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 756a6b3ce9d..fd950dbba23 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -60,6 +60,9 @@ class HistoryStats: self._start = start self._end = end + self._pending_events: list[Event[EventStateChangedData]] = [] + self._query_count = 0 + async def async_update( self, event: Event[EventStateChangedData] | None ) -> HistoryStatsState: @@ -85,6 +88,14 @@ class HistoryStats: utc_now = dt_util.utcnow() now_timestamp = floored_timestamp(utc_now) + # If we end up querying data from the recorder when we get triggered by a new state + # change event, it is possible this function could be reentered a second time before + # the first recorder query returns. In that case a second recorder query will be done + # and we need to hold the new event so that we can append it after the second query. + # Otherwise the event will be dropped. + if event: + self._pending_events.append(event) + if current_period_start_timestamp > now_timestamp: # History cannot tell the future self._history_current_period = [] @@ -113,15 +124,14 @@ class HistoryStats: start_changed = ( current_period_start_timestamp != previous_period_start_timestamp ) + end_changed = current_period_end_timestamp != previous_period_end_timestamp if start_changed: self._prune_history_cache(current_period_start_timestamp) new_data = False if event and (new_state := event.data["new_state"]) is not None: - if ( - current_period_start_timestamp - <= floored_timestamp(new_state.last_changed) - <= current_period_end_timestamp + if current_period_start_timestamp <= floored_timestamp( + new_state.last_changed ): self._history_current_period.append( HistoryState(new_state.state, new_state.last_changed_timestamp) @@ -131,26 +141,31 @@ class HistoryStats: not new_data and current_period_end_timestamp < now_timestamp and not start_changed + and not end_changed ): # If period has not changed and current time after the period end... # Don't compute anything as the value cannot have changed return self._state else: await self._async_history_from_db( - current_period_start_timestamp, current_period_end_timestamp + current_period_start_timestamp, now_timestamp ) - if event and (new_state := event.data["new_state"]) is not None: - if ( - current_period_start_timestamp - <= floored_timestamp(new_state.last_changed) - <= current_period_end_timestamp - ): - self._history_current_period.append( - HistoryState(new_state.state, new_state.last_changed_timestamp) - ) + for pending_event in self._pending_events: + if (new_state := pending_event.data["new_state"]) is not None: + if current_period_start_timestamp <= floored_timestamp( + new_state.last_changed + ): + self._history_current_period.append( + HistoryState( + new_state.state, new_state.last_changed_timestamp + ) + ) self._has_recorder_data = True + if self._query_count == 0: + self._pending_events.clear() + seconds_matched, match_count = self._async_compute_seconds_and_changes( now_timestamp, current_period_start_timestamp, @@ -165,12 +180,16 @@ class HistoryStats: current_period_end_timestamp: float, ) -> None: """Update history data for the current period from the database.""" - instance = get_instance(self.hass) - states = await instance.async_add_executor_job( - self._state_changes_during_period, - current_period_start_timestamp, - current_period_end_timestamp, - ) + self._query_count += 1 + try: + instance = get_instance(self.hass) + states = await instance.async_add_executor_job( + self._state_changes_during_period, + current_period_start_timestamp, + current_period_end_timestamp, + ) + finally: + self._query_count -= 1 self._history_current_period = [ HistoryState(state.state, state.last_changed.timestamp()) for state in states @@ -208,6 +227,9 @@ class HistoryStats: current_state_matches = history_state.state in self._entity_states state_change_timestamp = history_state.last_changed + if math.floor(state_change_timestamp) > end_timestamp: + break + if math.floor(state_change_timestamp) > now_timestamp: # Shouldn't count states that are in the future _LOGGER.debug( @@ -215,7 +237,7 @@ class HistoryStats: state_change_timestamp, now_timestamp, ) - continue + break if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index ee426cf3048..5b98000997e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1017,6 +1017,18 @@ async def test_start_from_history_then_watch_state_changes_sliding( } for i, sensor_type in enumerate(["time", "ratio", "count"]) ] + + [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": f"sensor_delayed{i}", + "state": "on", + "end": "{{ utcnow()-timedelta(minutes=5) }}", + "duration": {"minutes": 55}, + "type": sensor_type, + } + for i, sensor_type in enumerate(["time", "ratio", "count"]) + ] }, ) await hass.async_block_till_done() @@ -1028,6 +1040,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0" + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" with freeze_time(time): hass.states.async_set("binary_sensor.state", "on") @@ -1038,6 +1053,10 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "1" + # Delayed sensor will not have registered the turn on yet + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" # After sensor has been on for 15 minutes, check state time += timedelta(minutes=15) # 00:15 @@ -1048,6 +1067,10 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.25" assert hass.states.get("sensor.sensor1").state == "25.0" assert hass.states.get("sensor.sensor2").state == "1" + # Delayed sensor will only have data from 00:00 - 00:10 + assert hass.states.get("sensor.sensor_delayed0").state == "0.17" + assert hass.states.get("sensor.sensor_delayed1").state == "18.2" # 10 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" with freeze_time(time): hass.states.async_set("binary_sensor.state", "off") @@ -1064,6 +1087,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.25" assert hass.states.get("sensor.sensor1").state == "25.0" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.25" + assert hass.states.get("sensor.sensor_delayed1").state == "27.3" # 15 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=20) # 01:05 @@ -1075,6 +1101,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.17" assert hass.states.get("sensor.sensor1").state == "16.7" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.17" + assert hass.states.get("sensor.sensor_delayed1").state == "18.2" # 10 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=5) # 01:10 @@ -1086,6 +1115,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.08" assert hass.states.get("sensor.sensor1").state == "8.3" assert hass.states.get("sensor.sensor2").state == "1" + assert hass.states.get("sensor.sensor_delayed0").state == "0.08" + assert hass.states.get("sensor.sensor_delayed1").state == "9.1" # 5 / 55 + assert hass.states.get("sensor.sensor_delayed2").state == "1" time += timedelta(minutes=10) # 01:20 @@ -1096,6 +1128,9 @@ async def test_start_from_history_then_watch_state_changes_sliding( assert hass.states.get("sensor.sensor0").state == "0.0" assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0" + assert hass.states.get("sensor.sensor_delayed0").state == "0.0" + assert hass.states.get("sensor.sensor_delayed1").state == "0.0" + assert hass.states.get("sensor.sensor_delayed2").state == "0" async def test_does_not_work_into_the_future( @@ -1629,7 +1664,7 @@ async def test_state_change_during_window_rollover( "entity_id": "binary_sensor.state", "name": "sensor1", "state": "on", - "start": "{{ today_at() }}", + "start": "{{ today_at('12:00') if now().hour == 1 else today_at() }}", "end": "{{ now() }}", "type": "time", } @@ -1644,7 +1679,7 @@ async def test_state_change_during_window_rollover( assert hass.states.get("sensor.sensor1").state == "11.0" # Advance 59 minutes, to record the last minute update just before midnight, just like a real system would do. - t2 = start_time + timedelta(minutes=59, microseconds=300) + t2 = start_time + timedelta(minutes=59, microseconds=300) # 23:59 with freeze_time(t2): async_fire_time_changed(hass, t2) await hass.async_block_till_done() @@ -1653,7 +1688,7 @@ async def test_state_change_during_window_rollover( # One minute has passed and the time has now rolled over into a new day, resetting the recorder window. # The sensor will be ON since midnight. - t3 = t2 + timedelta(minutes=1) + t3 = t2 + timedelta(minutes=1) # 00:01 with freeze_time(t3): # The sensor turns off around this time, before the sensor does its normal polled update. hass.states.async_set("binary_sensor.state", "off") @@ -1662,13 +1697,69 @@ async def test_state_change_during_window_rollover( assert hass.states.get("sensor.sensor1").state == "0.0" # More time passes, and the history stats does a polled update again. It should be 0 since the sensor has been off since midnight. - t4 = t3 + timedelta(minutes=10) + # Turn the sensor back on. + t4 = t3 + timedelta(minutes=10) # 00:10 with freeze_time(t4): async_fire_time_changed(hass, t4) await hass.async_block_till_done() + hass.states.async_set("binary_sensor.state", "on") + await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.0" + # Due to time change, start time has now moved into the future. Turn off the sensor. + t5 = t4 + timedelta(hours=1) # 01:10 + with freeze_time(t5): + hass.states.async_set("binary_sensor.state", "off") + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("sensor.sensor1").state == STATE_UNKNOWN + + # Start time has moved back to start of today. Turn the sensor on at the same time it is recomputed + # Should query the recorder this time due to start time moving backwards in time. + t6 = t5 + timedelta(hours=1) # 02:10 + + def _fake_states_t6(*args, **kwargs): + return { + "binary_sensor.state": [ + ha.State( + "binary_sensor.state", + "off", + last_changed=t6.replace(hour=0, minute=0, second=0, microsecond=0), + ), + ha.State( + "binary_sensor.state", + "on", + last_changed=t6.replace(hour=0, minute=10, second=0, microsecond=0), + ), + ha.State( + "binary_sensor.state", + "off", + last_changed=t6.replace(hour=1, minute=10, second=0, microsecond=0), + ), + ] + } + + with ( + patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states_t6, + ), + freeze_time(t6), + ): + hass.states.async_set("binary_sensor.state", "on") + await hass.async_block_till_done(wait_background_tasks=True) + + assert hass.states.get("sensor.sensor1").state == "1.0" + + # Another hour passes since the re-query. Total 'On' time should be 2 hours (00:10-1:10, 2:10-now (3:10)) + t7 = t6 + timedelta(hours=1) # 03:10 + with freeze_time(t7): + async_fire_time_changed(hass, t7) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sensor1").state == "2.0" + @pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"]) async def test_end_time_with_microseconds_zeroed( @@ -1934,7 +2025,7 @@ async def test_history_stats_handles_floored_timestamps( await async_update_entity(hass, "sensor.sensor1") await hass.async_block_till_done() - assert last_times == (start_time, start_time + timedelta(hours=2)) + assert last_times == (start_time, start_time) async def test_unique_id( From 1f6faaacaba5d01ca588dcf5b70d18c37f60baf7 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 19 May 2025 21:41:00 +0300 Subject: [PATCH 0645/1175] Jewish Calendar: Implement diagnostics (#145180) * Implement diagnostics * Add testing * Remove implicitly tested code --- .../components/jewish_calendar/const.py | 1 + .../components/jewish_calendar/diagnostics.py | 28 +++++++ tests/components/jewish_calendar/conftest.py | 2 +- .../snapshots/test_diagnostics.ambr | 77 +++++++++++++++++++ .../jewish_calendar/test_diagnostics.py | 31 ++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/jewish_calendar/diagnostics.py create mode 100644 tests/components/jewish_calendar/snapshots/test_diagnostics.ambr create mode 100644 tests/components/jewish_calendar/test_diagnostics.py diff --git a/homeassistant/components/jewish_calendar/const.py b/homeassistant/components/jewish_calendar/const.py index 3c5b754fee4..b3a0dea5da0 100644 --- a/homeassistant/components/jewish_calendar/const.py +++ b/homeassistant/components/jewish_calendar/const.py @@ -6,6 +6,7 @@ ATTR_AFTER_SUNSET = "after_sunset" ATTR_DATE = "date" ATTR_NUSACH = "nusach" +CONF_ALTITUDE = "altitude" # The name used by the hdate library for elevation CONF_DIASPORA = "diaspora" CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" diff --git a/homeassistant/components/jewish_calendar/diagnostics.py b/homeassistant/components/jewish_calendar/diagnostics.py new file mode 100644 index 00000000000..27415282b6d --- /dev/null +++ b/homeassistant/components/jewish_calendar/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for Jewish Calendar integration.""" + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from .const import CONF_ALTITUDE +from .entity import JewishCalendarConfigEntry + +TO_REDACT = [ + CONF_ALTITUDE, + CONF_LATITUDE, + CONF_LONGITUDE, +] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: JewishCalendarConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + return { + "entry_data": async_redact_data(entry.data, TO_REDACT), + "data": async_redact_data(asdict(entry.runtime_data), TO_REDACT), + } diff --git a/tests/components/jewish_calendar/conftest.py b/tests/components/jewish_calendar/conftest.py index 5cd7ad34085..568affb9ab6 100644 --- a/tests/components/jewish_calendar/conftest.py +++ b/tests/components/jewish_calendar/conftest.py @@ -49,7 +49,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def location_data(request: pytest.FixtureRequest) -> _LocationData | None: """Return data based on location name.""" - if not hasattr(request, "param"): + if not hasattr(request, "param") or request.param is None: return None return LOCATIONS[request.param] diff --git a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..8dfd04afc08 --- /dev/null +++ b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr @@ -0,0 +1,77 @@ +# serializer version: 1 +# name: test_diagnostics[Jerusalem] + dict({ + 'data': dict({ + 'candle_lighting_offset': 40, + 'diaspora': False, + 'havdalah_offset': 0, + 'language': 'en', + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", + }), + }), + }), + 'entry_data': dict({ + 'diaspora': False, + 'language': 'en', + 'time_zone': 'Asia/Jerusalem', + }), + }) +# --- +# name: test_diagnostics[New York] + dict({ + 'data': dict({ + 'candle_lighting_offset': 18, + 'diaspora': True, + 'havdalah_offset': 0, + 'language': 'en', + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': True, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", + }), + }), + }), + 'entry_data': dict({ + 'diaspora': True, + 'language': 'en', + 'time_zone': 'America/New_York', + }), + }) +# --- +# name: test_diagnostics[None] + dict({ + 'data': dict({ + 'candle_lighting_offset': 18, + 'diaspora': False, + 'havdalah_offset': 0, + 'language': 'en', + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", + }), + }), + }), + 'entry_data': dict({ + 'language': 'en', + }), + }) +# --- diff --git a/tests/components/jewish_calendar/test_diagnostics.py b/tests/components/jewish_calendar/test_diagnostics.py new file mode 100644 index 00000000000..cd3ace24c8c --- /dev/null +++ b/tests/components/jewish_calendar/test_diagnostics.py @@ -0,0 +1,31 @@ +"""Tests for the diagnostics data provided by the Jewish Calendar integration.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.mark.parametrize( + ("location_data"), ["Jerusalem", "New York", None], indirect=True +) +async def test_diagnostics( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics with different locations.""" + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + diagnostics_data = await get_diagnostics_for_config_entry( + hass, hass_client, config_entry + ) + + assert diagnostics_data == snapshot From e78f4d2a29db09949ee5bca39bc537de048831d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 May 2025 14:54:21 -0400 Subject: [PATCH 0646/1175] TTS to only use stream entity method when streaming request comes in (#145167) Co-authored-by: Franck Nijhof --- homeassistant/components/tts/__init__.py | 18 ++++++------- homeassistant/components/tts/entity.py | 12 +++++++++ homeassistant/components/tts/legacy.py | 14 +++++++++- .../assist_pipeline/test_pipeline.py | 27 ++++++------------- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index da8a0f2324e..8292df07ef8 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -852,12 +852,9 @@ class SpeechManager: else: _LOGGER.debug("Generating audio for %s", message[0:32]) - async def message_stream() -> AsyncGenerator[str]: - yield message - extension = options.get(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) data_gen = self._async_generate_tts_audio( - engine_instance, message_stream(), language, options + engine_instance, message, language, options ) cache = TTSCache( @@ -931,7 +928,7 @@ class SpeechManager: async def _async_generate_tts_audio( self, engine_instance: TextToSpeechEntity | Provider, - message_stream: AsyncGenerator[str], + message_or_stream: str | AsyncGenerator[str], language: str, options: dict[str, Any], ) -> AsyncGenerator[bytes]: @@ -979,9 +976,12 @@ class SpeechManager: if engine_instance.name is None or engine_instance.name is UNDEFINED: raise HomeAssistantError("TTS engine name is not set.") - if isinstance(engine_instance, Provider): - message = "".join([chunk async for chunk in message_stream]) - extension, data = await engine_instance.async_get_tts_audio( + if isinstance(engine_instance, Provider) or isinstance(message_or_stream, str): + if isinstance(message_or_stream, str): + message = message_or_stream + else: + message = "".join([chunk async for chunk in message_or_stream]) + extension, data = await engine_instance.async_internal_get_tts_audio( message, language, options ) @@ -997,7 +997,7 @@ class SpeechManager: else: tts_result = await engine_instance.internal_async_stream_tts_audio( - TTSAudioRequest(language, options, message_stream) + TTSAudioRequest(language, options, message_or_stream) ) extension = tts_result.extension data_gen = tts_result.data_gen diff --git a/homeassistant/components/tts/entity.py b/homeassistant/components/tts/entity.py index 1f01a41c5ab..2c3fd446d2f 100644 --- a/homeassistant/components/tts/entity.py +++ b/homeassistant/components/tts/entity.py @@ -165,6 +165,18 @@ class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH self.async_write_ha_state() return await self.async_stream_tts_audio(request) + @final + async def async_internal_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> TtsAudioType: + """Load tts audio file from the engine and update state. + + Return a tuple of file extension and data as bytes. + """ + self.__last_tts_loaded = dt_util.utcnow().isoformat() + self.async_write_ha_state() + return await self.async_get_tts_audio(message, language, options=options) + async def async_stream_tts_audio( self, request: TTSAudioRequest ) -> TTSAudioResponse: diff --git a/homeassistant/components/tts/legacy.py b/homeassistant/components/tts/legacy.py index 877ecc034d6..c3d7eb6fdd6 100644 --- a/homeassistant/components/tts/legacy.py +++ b/homeassistant/components/tts/legacy.py @@ -7,7 +7,7 @@ from collections.abc import Coroutine, Mapping from functools import partial import logging from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, final import voluptuous as vol @@ -252,3 +252,15 @@ class Provider: return await self.hass.async_add_executor_job( partial(self.get_tts_audio, message, language, options=options) ) + + @final + async def async_internal_get_tts_audio( + self, message: str, language: str, options: dict[str, Any] + ) -> TtsAudioType: + """Load tts audio file from provider. + + Proxies request to mimic the entity interface. + + Return a tuple of file extension and data as bytes. + """ + return await self.async_get_tts_audio(message, language, options) diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index f4e7c886d40..1714c909a18 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1627,25 +1627,15 @@ async def test_chat_log_tts_streaming( ), ) - received_tts = [] - - async def async_stream_tts_audio( - request: tts.TTSAudioRequest, + async def async_get_tts_audio( + message: str, + language: str, + options: dict[str, Any] | None = None, ) -> tts.TTSAudioResponse: - """Mock stream TTS audio.""" + """Mock get TTS audio.""" + return ("mp3", b"".join([chunk.encode() for chunk in to_stream_tts])) - async def gen_data(): - async for msg in request.message_gen: - received_tts.append(msg) - yield msg.encode() - - return tts.TTSAudioResponse( - extension="mp3", - data_gen=gen_data(), - ) - - mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio - mock_tts_entity.async_supports_streaming_input = Mock(return_value=True) + mock_tts_entity.async_get_tts_audio = async_get_tts_audio with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", @@ -1717,7 +1707,6 @@ async def test_chat_log_tts_streaming( streamed_text = "".join(to_stream_tts) assert tts_result == streamed_text - assert len(received_tts) == expected_chunks - assert "".join(received_tts) == streamed_text + assert expected_chunks == 1 assert process_events(events) == snapshot From 1e9c585e8b446f1923e3ed13f5520162cc37a64c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 19 May 2025 21:12:51 +0200 Subject: [PATCH 0647/1175] Bump holidays to 0.73 (#145238) --- 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 9809862cd52..bd6fd51e726 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.72", "babel==2.15.0"] + "requirements": ["holidays==0.73", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 542b68169a3..7a03133dd86 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.72"] + "requirements": ["holidays==0.73"] } diff --git a/requirements_all.txt b/requirements_all.txt index b5c2036ba13..edb6716b10b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1158,7 +1158,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.72 +holidays==0.73 # homeassistant.components.frontend home-assistant-frontend==20250516.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4c96646188..7990cfd6e25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.72 +holidays==0.73 # homeassistant.components.frontend home-assistant-frontend==20250516.0 From 0ee0b2fcba712b0ab1cb06c252fd841a118ec5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 19 May 2025 21:34:36 +0200 Subject: [PATCH 0648/1175] Add missing Miele tumble dryer program codes (#145236) --- homeassistant/components/miele/const.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 338e8138352..a72cf916cf3 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -435,15 +435,27 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = { TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { -1: "no_program", # Extrapolated from other device types. 0: "no_program", # Extrapolated from other device types + 1: "automatic_plus", 2: "cottons", 3: "minimum_iron", 4: "woollens_handcare", 5: "delicates", 6: "warm_air", + 7: "cool_air", 8: "express", + 9: "cottons_eco", 10: "automatic_plus", + 12: "proofing", + 13: "denim", + 14: "shirts", + 15: "sportswear", + 16: "outerwear", + 17: "silks_handcare", + 19: "standard_pillows", 20: "cottons", + 22: "basket_program", 23: "cottons_hygiene", + 24: "steam_smoothing", 30: "minimum_iron", 31: "bed_linen", 40: "woollens_handcare", From e2f2c13e5e86fd16a159cf80fefb7f326e013595 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 19 May 2025 22:44:13 +0300 Subject: [PATCH 0649/1175] Jewish calendar - quality scale - fix missing translations (#144410) --- .../components/jewish_calendar/strings.json | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json index 33d58ea3487..b76127604c7 100644 --- a/homeassistant/components/jewish_calendar/strings.json +++ b/homeassistant/components/jewish_calendar/strings.json @@ -1,4 +1,13 @@ { + "common": { + "diaspora": "Outside of Israel?", + "time_zone": "Time zone", + "descr_diaspora": "Is the location outside of Israel?", + "descr_location": "Location to use for the Jewish calendar calculations. By default, the location is set to the Home Assistant location.", + "descr_time_zone": "If you specify a location, make sure to specify the time zone for correct calendar times calculations", + "descr_elevation": "Elevation in meters above sea level. This is used to calculate the times correctly.", + "descr_language": "Language to use when displaying values in the UI. This does not affect the Hebrew date." + }, "entity": { "binary_sensor": { "issur_melacha_in_effect": { @@ -90,17 +99,36 @@ }, "config": { "step": { - "user": { + "reconfigure": { "data": { - "name": "[%key:common::config_flow::data::name%]", - "diaspora": "Outside of Israel?", - "language": "Language for holidays and dates", "location": "[%key:common::config_flow::data::location%]", "elevation": "[%key:common::config_flow::data::elevation%]", - "time_zone": "Time zone" + "time_zone": "[%key:component::jewish_calendar::common::time_zone%]", + "diaspora": "[%key:component::jewish_calendar::common::diaspora%]", + "language": "[%key:common::config_flow::data::language%]" }, "data_description": { - "time_zone": "If you specify a location, make sure to specify the time zone for correct calendar times calculations" + "location": "[%key:component::jewish_calendar::common::descr_location%]", + "elevation": "[%key:component::jewish_calendar::common::descr_elevation%]", + "time_zone": "[%key:component::jewish_calendar::common::descr_time_zone%]", + "diaspora": "[%key:component::jewish_calendar::common::descr_diaspora%]", + "language": "[%key:component::jewish_calendar::common::descr_language%]" + } + }, + "user": { + "data": { + "location": "[%key:common::config_flow::data::location%]", + "elevation": "[%key:common::config_flow::data::elevation%]", + "time_zone": "[%key:component::jewish_calendar::common::time_zone%]", + "diaspora": "[%key:component::jewish_calendar::common::diaspora%]", + "language": "[%key:common::config_flow::data::language%]" + }, + "data_description": { + "location": "[%key:component::jewish_calendar::common::descr_location%]", + "elevation": "[%key:component::jewish_calendar::common::descr_elevation%]", + "time_zone": "[%key:component::jewish_calendar::common::descr_time_zone%]", + "diaspora": "[%key:component::jewish_calendar::common::descr_diaspora%]", + "language": "[%key:component::jewish_calendar::common::descr_language%]" } } }, From 7464e3944e85918786ff139d781f6913ab0c1a1f Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 19 May 2025 22:44:28 +0300 Subject: [PATCH 0650/1175] Jewish calendar: set parallel updates to 0 (#144986) * Set all Jewish calendar parallel updates to 0 * Update homeassistant/components/jewish_calendar/service.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/jewish_calendar/binary_sensor.py | 2 ++ homeassistant/components/jewish_calendar/sensor.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 8d06526c322..2e7edbefd3b 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -20,6 +20,8 @@ from homeassistant.util import dt as dt_util from .entity import JewishCalendarConfigEntry, JewishCalendarEntity +PARALLEL_UPDATES = 0 + @dataclass(frozen=True) class JewishCalendarBinarySensorMixIns(BinarySensorEntityDescription): diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 063818aedf3..973d354d368 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -24,6 +24,7 @@ from homeassistant.util import dt as dt_util from .entity import JewishCalendarConfigEntry, JewishCalendarEntity _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( From 761bb65ac62e5e3fdfba816f9743051955f3f49c Mon Sep 17 00:00:00 2001 From: disforw Date: Mon, 19 May 2025 15:47:01 -0400 Subject: [PATCH 0651/1175] Fix QNAP fail to load (#144675) * Update coordinator.py * Update coordinator.py @peternash * Update coordinator.py * Update coordinator.py * Update coordinator.py * Update coordinator.py --- homeassistant/components/qnap/coordinator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/qnap/coordinator.py b/homeassistant/components/qnap/coordinator.py index a6d654ddbbd..8b6cb930b4f 100644 --- a/homeassistant/components/qnap/coordinator.py +++ b/homeassistant/components/qnap/coordinator.py @@ -6,6 +6,7 @@ from contextlib import contextmanager, nullcontext from datetime import timedelta import logging from typing import Any +import warnings from qnapstats import QNAPStats import urllib3 @@ -37,7 +38,8 @@ def suppress_insecure_request_warning(): Was added in here to solve the following issue, not being solved upstream. https://github.com/colinodell/python-qnapstats/issues/96 """ - with urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", urllib3.exceptions.InsecureRequestWarning) yield From 6afb60d31b48695c92a7015e1e6d5b80157cb976 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Mon, 19 May 2025 22:52:06 +0300 Subject: [PATCH 0652/1175] Jewish Calendar - quality scale - use specific config flow (#144408) --- .../components/jewish_calendar/config_flow.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/jewish_calendar/config_flow.py b/homeassistant/components/jewish_calendar/config_flow.py index 4572f87a113..e896bc90c9e 100644 --- a/homeassistant/components/jewish_calendar/config_flow.py +++ b/homeassistant/components/jewish_calendar/config_flow.py @@ -9,12 +9,7 @@ import zoneinfo from hdate.translator import Language import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import ( CONF_ELEVATION, CONF_LANGUAGE, @@ -44,6 +39,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, ) +from .entity import JewishCalendarConfigEntry OPTIONS_SCHEMA = vol.Schema( { @@ -89,7 +85,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback def async_get_options_flow( - config_entry: ConfigEntry, + config_entry: JewishCalendarConfigEntry, ) -> JewishCalendarOptionsFlowHandler: """Get the options flow for this handler.""" return JewishCalendarOptionsFlowHandler() From 741cb23776f79cb8bd704ccbd01df38a28bc0f39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 May 2025 16:03:21 -0400 Subject: [PATCH 0653/1175] Only pass serializable data to media player intent (#145244) --- homeassistant/components/media_player/intent.py | 2 +- tests/components/media_player/test_intent.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/intent.py b/homeassistant/components/media_player/intent.py index 85f0598695b..c9caa2c4a91 100644 --- a/homeassistant/components/media_player/intent.py +++ b/homeassistant/components/media_player/intent.py @@ -336,6 +336,6 @@ class MediaSearchAndPlayHandler(intent.IntentHandler): # Success response = intent_obj.create_response() - response.async_set_speech_slots({"media": first_result}) + response.async_set_speech_slots({"media": first_result.as_dict()}) response.response_type = intent.IntentResponseType.ACTION_DONE return response diff --git a/tests/components/media_player/test_intent.py b/tests/components/media_player/test_intent.py index 6429d6889c0..4b08aa43158 100644 --- a/tests/components/media_player/test_intent.py +++ b/tests/components/media_player/test_intent.py @@ -688,8 +688,7 @@ async def test_search_and_play_media_player_intent(hass: HomeAssistant) -> None: # Response should contain a "media" slot with the matched item. assert not response.speech media = response.speech_slots.get("media") - assert isinstance(media, BrowseMedia) - assert media.title == "Test Track" + assert media["title"] == "Test Track" assert len(search_calls) == 1 search_call = search_calls[0] From ffb485aa87317eaa554b770f6d70340446a4ccb4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 20 May 2025 06:13:15 +1000 Subject: [PATCH 0654/1175] Fix streaming window cover entity in Teslemetry (#145012) --- homeassistant/components/teslemetry/cover.py | 24 ++++++++++++------- .../teslemetry/snapshots/test_cover.ambr | 6 ++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index be85a877c86..c58559ab308 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -201,14 +201,22 @@ class TeslemetryStreamingWindowEntity( def _handle_stream_update(self, data) -> None: """Update the entity attributes.""" - if value := data.get(Signal.FD_WINDOW): - self.fd = WindowState.get(value) == "closed" - if value := data.get(Signal.FP_WINDOW): - self.fp = WindowState.get(value) == "closed" - if value := data.get(Signal.RD_WINDOW): - self.rd = WindowState.get(value) == "closed" - if value := data.get(Signal.RP_WINDOW): - self.rp = WindowState.get(value) == "closed" + change = False + if value := data["data"].get(Signal.FD_WINDOW): + self.fd = WindowState.get(value) == "Closed" + change = True + if value := data["data"].get(Signal.FP_WINDOW): + self.fp = WindowState.get(value) == "Closed" + change = True + if value := data["data"].get(Signal.RD_WINDOW): + self.rd = WindowState.get(value) == "Closed" + change = True + if value := data["data"].get(Signal.RP_WINDOW): + self.rp = WindowState.get(value) == "Closed" + change = True + + if not change: + return if False in (self.fd, self.fp, self.rd, self.rp): self._attr_is_closed = False diff --git a/tests/components/teslemetry/snapshots/test_cover.ambr b/tests/components/teslemetry/snapshots/test_cover.ambr index 9548a911cf9..438738ff2b9 100644 --- a/tests/components/teslemetry/snapshots/test_cover.ambr +++ b/tests/components/teslemetry/snapshots/test_cover.ambr @@ -713,11 +713,11 @@ 'unknown' # --- # name: test_cover_streaming[cover.test_windows-closed] - 'unknown' + 'closed' # --- # name: test_cover_streaming[cover.test_windows-open] - 'unknown' + 'open' # --- # name: test_cover_streaming[cover.test_windows-unknown] - 'unknown' + 'open' # --- From d580f8a8a211328cdffd6a8d4ce68185b6946a81 Mon Sep 17 00:00:00 2001 From: Nick Kuiper <65495045+NickKoepr@users.noreply.github.com> Date: Mon, 19 May 2025 22:21:06 +0200 Subject: [PATCH 0655/1175] Updated code owners for the blue current integration. (#144962) Changed code owners for the blue current integration. --- CODEOWNERS | 4 ++-- homeassistant/components/blue_current/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bbbfb9394e2..72107041575 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,8 +202,8 @@ build.json @home-assistant/supervisor /tests/components/blebox/ @bbx-a @swistakm /homeassistant/components/blink/ @fronzbot @mkmer /tests/components/blink/ @fronzbot @mkmer -/homeassistant/components/blue_current/ @Floris272 @gleeuwen -/tests/components/blue_current/ @Floris272 @gleeuwen +/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23 +/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23 /homeassistant/components/bluemaestro/ @bdraco /tests/components/bluemaestro/ @bdraco /homeassistant/components/blueprint/ @home-assistant/core diff --git a/homeassistant/components/blue_current/manifest.json b/homeassistant/components/blue_current/manifest.json index 4f277e83656..e813b08131c 100644 --- a/homeassistant/components/blue_current/manifest.json +++ b/homeassistant/components/blue_current/manifest.json @@ -1,7 +1,7 @@ { "domain": "blue_current", "name": "Blue Current", - "codeowners": ["@Floris272", "@gleeuwen"], + "codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blue_current", "iot_class": "cloud_push", From e76bd1bbb9119b95e2716947364e32980433ec11 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 19 May 2025 22:39:04 +0200 Subject: [PATCH 0656/1175] Add media_source platform to Immich integration (#145159) * add media_source platform * fix error messages * use mime-type from asset info, instead of guessing it * add dependency for http * add tests * use direct imports and set can_play=False for images * fix tests --- homeassistant/components/immich/manifest.json | 1 + .../components/immich/media_source.py | 209 +++++++++++ tests/components/immich/conftest.py | 36 +- tests/components/immich/const.py | 21 ++ tests/components/immich/test_media_source.py | 336 ++++++++++++++++++ 5 files changed, 601 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/immich/media_source.py create mode 100644 tests/components/immich/test_media_source.py diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index bb8cbe720fd..fe7741821b6 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -3,6 +3,7 @@ "name": "Immich", "codeowners": ["@mib1185"], "config_flow": true, + "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/immich", "iot_class": "local_polling", "loggers": ["aioimmich"], diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py new file mode 100644 index 00000000000..f267433f233 --- /dev/null +++ b/homeassistant/components/immich/media_source.py @@ -0,0 +1,209 @@ +"""Immich as a media source.""" + +from __future__ import annotations + +from logging import getLogger +import mimetypes + +from aiohttp.web import HTTPNotFound, Request, Response +from aioimmich.exceptions import ImmichError + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player import MediaClass +from homeassistant.components.media_source import ( + BrowseError, + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, + Unresolvable, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import ImmichConfigEntry + +LOGGER = getLogger(__name__) + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up Immich media source.""" + entries = hass.config_entries.async_entries( + DOMAIN, include_disabled=False, include_ignore=False + ) + hass.http.register_view(ImmichMediaView(hass)) + return ImmichMediaSource(hass, entries) + + +class ImmichMediaSourceIdentifier: + """Immich media item identifier.""" + + def __init__(self, identifier: str) -> None: + """Split identifier into parts.""" + parts = identifier.split("/") + # coonfig_entry.unique_id/album_id/asset_it/filename + self.unique_id = parts[0] + self.album_id = parts[1] if len(parts) > 1 else None + self.asset_id = parts[2] if len(parts) > 2 else None + self.file_name = parts[3] if len(parts) > 2 else None + + +class ImmichMediaSource(MediaSource): + """Provide Immich as media sources.""" + + name = "Immich" + + def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None: + """Initialize Immich media source.""" + super().__init__(DOMAIN) + self.hass = hass + self.entries = entries + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if not self.hass.config_entries.async_loaded_entries(DOMAIN): + raise BrowseError("Immich is not configured") + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title="Immich", + can_play=False, + can_expand=True, + children_media_class=MediaClass.DIRECTORY, + children=[ + *await self._async_build_immich(item), + ], + ) + + async def _async_build_immich( + self, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing different immich instances.""" + if not item.identifier: + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.unique_id, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title=entry.title, + can_play=False, + can_expand=True, + ) + for entry in self.entries + ] + identifier = ImmichMediaSourceIdentifier(item.identifier) + entry: ImmichConfigEntry | None = ( + self.hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, identifier.unique_id + ) + ) + assert entry + immich_api = entry.runtime_data.api + + if identifier.album_id is None: + # Get Albums + try: + albums = await immich_api.albums.async_get_all_albums() + except ImmichError: + return [] + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{item.identifier}/{album.album_id}", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title=album.name, + can_play=False, + can_expand=True, + thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail", + ) + for album in albums + ] + + # Request items of album + try: + album_info = await immich_api.albums.async_get_album_info( + identifier.album_id + ) + except ImmichError: + return [] + + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=( + f"{identifier.unique_id}/" + f"{identifier.album_id}/" + f"{asset.asset_id}/" + f"{asset.file_name}" + ), + media_class=MediaClass.IMAGE, + media_content_type=asset.mime_type, + title=asset.file_name, + can_play=False, + can_expand=False, + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail", + ) + for asset in album_info.assets + if asset.mime_type.startswith("image/") + ] + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + identifier = ImmichMediaSourceIdentifier(item.identifier) + if identifier.file_name is None: + raise Unresolvable("No file name") + mime_type, _ = mimetypes.guess_type(identifier.file_name) + if not isinstance(mime_type, str): + raise Unresolvable("No file extension") + return PlayMedia( + ( + f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize" + ), + mime_type, + ) + + +class ImmichMediaView(HomeAssistantView): + """Immich Media Finder View.""" + + url = "/immich/{source_dir_id}/{location:.*}" + name = "immich" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the media view.""" + self.hass = hass + + async def get( + self, request: Request, source_dir_id: str, location: str + ) -> Response: + """Start a GET request.""" + if not self.hass.config_entries.async_loaded_entries(DOMAIN): + raise HTTPNotFound + asset_id, file_name, size = location.split("/") + + mime_type, _ = mimetypes.guess_type(file_name) + if not isinstance(mime_type, str): + raise HTTPNotFound + + entry: ImmichConfigEntry | None = ( + self.hass.config_entries.async_entry_for_domain_unique_id( + DOMAIN, source_dir_id + ) + ) + assert entry + immich_api = entry.runtime_data.api + + try: + image = await immich_api.assets.async_view_asset(asset_id, size) + except ImmichError as exc: + raise HTTPNotFound from exc + return Response(body=image, content_type=mime_type) diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py index 2c9483c3955..d26eddfd55e 100644 --- a/tests/components/immich/conftest.py +++ b/tests/components/immich/conftest.py @@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator, Generator from datetime import datetime from unittest.mock import AsyncMock, patch -from aioimmich import ImmichServer, ImmichUsers +from aioimmich import ImmichAlbums, ImmichAssests, ImmichServer, ImmichUsers from aioimmich.server.models import ( ImmichServerAbout, ImmichServerStatistics, @@ -21,6 +21,10 @@ from homeassistant.const import ( CONF_SSL, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_ALBUM_WITH_ASSETS, MOCK_ALBUM_WITHOUT_ASSETS from tests.common import MockConfigEntry @@ -51,6 +55,23 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +def mock_immich_albums() -> AsyncMock: + """Mock the Immich server.""" + mock = AsyncMock(spec=ImmichAlbums) + mock.async_get_all_albums.return_value = [MOCK_ALBUM_WITHOUT_ASSETS] + mock.async_get_album_info.return_value = MOCK_ALBUM_WITH_ASSETS + return mock + + +@pytest.fixture +def mock_immich_assets() -> AsyncMock: + """Mock the Immich server.""" + mock = AsyncMock(spec=ImmichAssests) + mock.async_view_asset.return_value = b"xxxx" + return mock + + @pytest.fixture def mock_immich_server() -> AsyncMock: """Mock the Immich server.""" @@ -116,7 +137,10 @@ def mock_immich_user() -> AsyncMock: @pytest.fixture async def mock_immich( - mock_immich_server: AsyncMock, mock_immich_user: AsyncMock + mock_immich_albums: AsyncMock, + mock_immich_assets: AsyncMock, + mock_immich_server: AsyncMock, + mock_immich_user: AsyncMock, ) -> AsyncGenerator[AsyncMock]: """Mock the Immich API.""" with ( @@ -124,6 +148,8 @@ async def mock_immich( patch("homeassistant.components.immich.config_flow.Immich", new=mock_immich), ): client = mock_immich.return_value + client.albums = mock_immich_albums + client.assets = mock_immich_assets client.server = mock_immich_server client.users = mock_immich_user yield client @@ -134,3 +160,9 @@ async def mock_non_admin_immich(mock_immich: AsyncMock) -> AsyncMock: """Mock the Immich API.""" mock_immich.users.async_get_my_user.return_value.is_admin = False return mock_immich + + +@pytest.fixture +async def setup_media_source(hass: HomeAssistant) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py index 2779a02be55..aeec4764732 100644 --- a/tests/components/immich/const.py +++ b/tests/components/immich/const.py @@ -1,5 +1,8 @@ """Constants for the Immich integration tests.""" +from aioimmich.albums.models import ImmichAlbum +from aioimmich.assets.models import ImmichAsset + from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -22,3 +25,21 @@ MOCK_CONFIG_ENTRY_DATA = { CONF_SSL: False, CONF_VERIFY_SSL: False, } + +MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum( + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "My Album", + "This is my first great album", + "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", + 1, + [], +) + +MOCK_ALBUM_WITH_ASSETS = ImmichAlbum( + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "My Album", + "This is my first great album", + "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", + 1, + [ImmichAsset("2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg")], +) diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py new file mode 100644 index 00000000000..772f0535f02 --- /dev/null +++ b/tests/components/immich/test_media_source.py @@ -0,0 +1,336 @@ +"""Tests for Immich media source.""" + +from pathlib import Path +import tempfile +from unittest.mock import Mock, patch + +from aiohttp import web +from aioimmich.exceptions import ImmichError +import pytest + +from homeassistant.components.immich.const import DOMAIN +from homeassistant.components.immich.media_source import ( + ImmichMediaSource, + ImmichMediaView, + async_get_media_source, +) +from homeassistant.components.media_player import MediaClass +from homeassistant.components.media_source import ( + BrowseError, + BrowseMedia, + MediaSourceItem, + Unresolvable, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.aiohttp import MockRequest + +from . import setup_integration +from .const import MOCK_ALBUM_WITHOUT_ASSETS + +from tests.common import MockConfigEntry + + +async def test_get_media_source(hass: HomeAssistant) -> None: + """Test the async_get_media_source.""" + assert await async_setup_component(hass, "media_source", {}) + + source = await async_get_media_source(hass) + assert isinstance(source, ImmichMediaSource) + assert source.domain == DOMAIN + + +@pytest.mark.parametrize( + ("identifier", "exception_msg"), + [ + ("unique_id", "No file name"), + ("unique_id/album_id", "No file name"), + ("unique_id/album_id/asset_id/filename", "No file extension"), + ], +) +async def test_resolve_media_bad_identifier( + hass: HomeAssistant, identifier: str, exception_msg: str +) -> None: + """Test resolve_media with bad identifiers.""" + assert await async_setup_component(hass, "media_source", {}) + + source = await async_get_media_source(hass) + item = MediaSourceItem(hass, DOMAIN, identifier, None) + with pytest.raises(Unresolvable, match=exception_msg): + await source.async_resolve_media(item) + + +@pytest.mark.parametrize( + ("identifier", "url", "mime_type"), + [ + ( + "unique_id/album_id/asset_id/filename.jpg", + "/immich/unique_id/asset_id/filename.jpg/fullsize", + "image/jpeg", + ), + ( + "unique_id/album_id/asset_id/filename.png", + "/immich/unique_id/asset_id/filename.png/fullsize", + "image/png", + ), + ], +) +async def test_resolve_media_success( + hass: HomeAssistant, identifier: str, url: str, mime_type: str +) -> None: + """Test successful resolving an item.""" + assert await async_setup_component(hass, "media_source", {}) + + source = await async_get_media_source(hass) + item = MediaSourceItem(hass, DOMAIN, identifier, None) + result = await source.async_resolve_media(item) + + assert result.url == url + assert result.mime_type == mime_type + + +async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: + """Test browse_media without any devices being configured.""" + assert await async_setup_component(hass, "media_source", {}) + + source = await async_get_media_source(hass) + item = MediaSourceItem( + hass, DOMAIN, "unique_id/album_id/asset_id/filename.png", None + ) + with pytest.raises(BrowseError, match="Immich is not configured"): + await source.async_browse_media(item) + + +async def test_browse_media_album_error( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media with unknown album.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + # exception in get_albums() + mock_immich.albums.async_get_all_albums.side_effect = ImmichError( + { + "message": "Not found or no album.read access", + "error": "Bad Request", + "statusCode": 400, + "correlationId": "e0hlizyl", + } + ) + + source = await async_get_media_source(hass) + + item = MediaSourceItem(hass, DOMAIN, mock_config_entry.unique_id, None) + result = await source.async_browse_media(item) + + assert result + assert result.identifier is None + assert len(result.children) == 0 + + +async def test_browse_media_get_root( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning root media sources.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + item = MediaSourceItem(hass, DOMAIN, "", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "Someone" + assert media_file.media_content_id == ( + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" + ) + + +async def test_browse_media_get_albums( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning albums.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "My Album" + assert media_file.media_content_id == ( + "media-source://immich/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" + ) + + +async def test_browse_media_get_items_error( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning albums.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + + # unknown album + mock_immich.albums.async_get_album_info.return_value = MOCK_ALBUM_WITHOUT_ASSETS + item = MediaSourceItem( + hass, + DOMAIN, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + None, + ) + result = await source.async_browse_media(item) + + assert result + assert result.identifier is None + assert len(result.children) == 0 + + # exception in async_get_album_info() + mock_immich.albums.async_get_album_info.side_effect = ImmichError( + { + "message": "Not found or no album.read access", + "error": "Bad Request", + "statusCode": 400, + "correlationId": "e0hlizyl", + } + ) + item = MediaSourceItem( + hass, + DOMAIN, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + None, + ) + result = await source.async_browse_media(item) + + assert result + assert result.identifier is None + assert len(result.children) == 0 + + +async def test_browse_media_get_items( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning albums.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + + item = MediaSourceItem( + hass, + DOMAIN, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + None, + ) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.identifier == ( + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" + ) + assert media_file.title == "filename.jpg" + assert media_file.media_class == MediaClass.IMAGE + assert media_file.media_content_type == "image/jpeg" + assert media_file.can_play is False + assert not media_file.can_expand + assert media_file.thumbnail == ( + "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail" + ) + + +async def test_media_view( + hass: HomeAssistant, + tmp_path: Path, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test SynologyDsmMediaView returning albums.""" + view = ImmichMediaView(hass) + request = MockRequest(b"", DOMAIN) + + # immich noch configured + with pytest.raises(web.HTTPNotFound): + await view.get(request, "", "") + + # setup immich + assert await async_setup_component(hass, "media_source", {}) + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + # wrong url (without file extension) + with pytest.raises(web.HTTPNotFound): + await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename/thumbnail", + ) + + # exception in async_view_asset() + mock_immich.assets.async_view_asset.side_effect = ImmichError( + { + "message": "Not found or no asset.read access", + "error": "Bad Request", + "statusCode": 400, + "correlationId": "e0hlizyl", + } + ) + with pytest.raises(web.HTTPNotFound): + await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + ) + + # success + mock_immich.assets.async_view_asset.side_effect = None + mock_immich.assets.async_view_asset.return_value = b"xxxx" + with patch.object(tempfile, "tempdir", tmp_path): + result = await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + ) + assert isinstance(result, web.Response) + with patch.object(tempfile, "tempdir", tmp_path): + result = await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize", + ) + assert isinstance(result, web.Response) From df3688ef081e6df9a02cc43f5a4b9d13474094e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 19 May 2025 22:47:24 +0200 Subject: [PATCH 0657/1175] Mark entity methods and properties as mandatory in pylint plugin (#145210) * Mark entity methods and properties as mandatory in pylint plugin * Fixes --- homeassistant/components/hdmi_cec/entity.py | 2 +- homeassistant/components/smart_meter_texas/sensor.py | 2 +- homeassistant/components/utility_meter/sensor.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 9 +++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hdmi_cec/entity.py b/homeassistant/components/hdmi_cec/entity.py index bdb796e6a36..60ea4e1a0d0 100644 --- a/homeassistant/components/hdmi_cec/entity.py +++ b/homeassistant/components/hdmi_cec/entity.py @@ -57,7 +57,7 @@ class CecEntity(Entity): self._attr_available = False self.schedule_update_ha_state(False) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register HDMI callbacks after initialization.""" self._device.set_update_callback(self._update) self.hass.bus.async_listen( diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index c6e18bf43c1..480188ab2a6 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -74,7 +74,7 @@ class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self._attr_native_value = self.meter.reading self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" await super().async_added_to_hass() self.async_on_remove(self.coordinator.async_add_listener(self._state_update)) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index cda538386c1..d424692ac95 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -605,7 +605,7 @@ class UtilityMeterSensor(RestoreSensor): self._attr_native_value = Decimal(str(value)) self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 27ea23b0df3..ea4bd75d667 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -667,6 +667,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="should_poll", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="unique_id", @@ -725,6 +726,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="force_update", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="supported_features", @@ -733,10 +735,12 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="entity_registry_enabled_default", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="entity_registry_visible_default", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="attribution", @@ -749,23 +753,28 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="async_removed_from_registry", return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_added_to_hass", return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_will_remove_from_hass", return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_registry_entry_updated", return_type=None, + mandatory=True, ), TypeHintMatch( function_name="update", return_type=None, has_async_counterpart=True, + mandatory=True, ), ] _RESTORE_ENTITY_MATCH: list[TypeHintMatch] = [ From 20ce879471ba75c52d7185c58687b925d4b49d88 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Mon, 19 May 2025 21:50:09 +0100 Subject: [PATCH 0658/1175] Add new Probe Plus integration (#143424) * Add probe_plus integration * Changes for quality scale * sentence-casing * Update homeassistant/components/probe_plus/config_flow.py Co-authored-by: Erwin Douna * Update homeassistant/components/probe_plus/config_flow.py Co-authored-by: Erwin Douna * Update tests/components/probe_plus/test_config_flow.py Co-authored-by: Erwin Douna * Update tests/components/probe_plus/test_config_flow.py Co-authored-by: Erwin Douna * remove version from configflow * remove address var from async_step_bluetooth_confirm * move timedelta to SCAN_INTERVAL in coordinator * update tests * updates from review * add voltage device class * remove unused logger * remove names * update tests * Update config flow tests * Update unit tests * Reorder successful tests * Update config entry typing * Remove icons * ruff * Update async_add_entities logic Co-authored-by: Joost Lekkerkerker * sensor platform formatting --------- Co-authored-by: Erwin Douna Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + .../components/probe_plus/__init__.py | 24 ++++ .../components/probe_plus/config_flow.py | 125 ++++++++++++++++ homeassistant/components/probe_plus/const.py | 3 + .../components/probe_plus/coordinator.py | 68 +++++++++ homeassistant/components/probe_plus/entity.py | 54 +++++++ .../components/probe_plus/icons.json | 9 ++ .../components/probe_plus/manifest.json | 19 +++ .../components/probe_plus/quality_scale.yaml | 100 +++++++++++++ homeassistant/components/probe_plus/sensor.py | 106 ++++++++++++++ .../components/probe_plus/strings.json | 49 +++++++ homeassistant/generated/bluetooth.py | 6 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/probe_plus/__init__.py | 14 ++ tests/components/probe_plus/conftest.py | 60 ++++++++ .../components/probe_plus/test_config_flow.py | 133 ++++++++++++++++++ 19 files changed, 785 insertions(+) create mode 100644 homeassistant/components/probe_plus/__init__.py create mode 100644 homeassistant/components/probe_plus/config_flow.py create mode 100644 homeassistant/components/probe_plus/const.py create mode 100644 homeassistant/components/probe_plus/coordinator.py create mode 100644 homeassistant/components/probe_plus/entity.py create mode 100644 homeassistant/components/probe_plus/icons.json create mode 100644 homeassistant/components/probe_plus/manifest.json create mode 100644 homeassistant/components/probe_plus/quality_scale.yaml create mode 100644 homeassistant/components/probe_plus/sensor.py create mode 100644 homeassistant/components/probe_plus/strings.json create mode 100644 tests/components/probe_plus/__init__.py create mode 100644 tests/components/probe_plus/conftest.py create mode 100644 tests/components/probe_plus/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 72107041575..be7c1e5ee84 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1178,6 +1178,8 @@ build.json @home-assistant/supervisor /tests/components/powerwall/ @bdraco @jrester @daniel-simpson /homeassistant/components/private_ble_device/ @Jc2k /tests/components/private_ble_device/ @Jc2k +/homeassistant/components/probe_plus/ @pantherale0 +/tests/components/probe_plus/ @pantherale0 /homeassistant/components/profiler/ @bdraco /tests/components/profiler/ @bdraco /homeassistant/components/progettihwsw/ @ardaseremet diff --git a/homeassistant/components/probe_plus/__init__.py b/homeassistant/components/probe_plus/__init__.py new file mode 100644 index 00000000000..be1faf4a297 --- /dev/null +++ b/homeassistant/components/probe_plus/__init__.py @@ -0,0 +1,24 @@ +"""The Probe Plus integration.""" + +from __future__ import annotations + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import ProbePlusConfigEntry, ProbePlusDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ProbePlusConfigEntry) -> bool: + """Set up Probe Plus from a config entry.""" + coordinator = ProbePlusDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ProbePlusConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/probe_plus/config_flow.py b/homeassistant/components/probe_plus/config_flow.py new file mode 100644 index 00000000000..1e9a858e9fc --- /dev/null +++ b/homeassistant/components/probe_plus/config_flow.py @@ -0,0 +1,125 @@ +"""Config flow for probe_plus integration.""" + +from __future__ import annotations + +import dataclasses +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ADDRESS + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@dataclasses.dataclass(frozen=True) +class Discovery: + """Represents a discovered Bluetooth device. + + Attributes: + title: The name or title of the discovered device. + discovery_info: Information about the discovered device. + + """ + + title: str + discovery_info: BluetoothServiceInfo + + +def title(discovery_info: BluetoothServiceInfo) -> str: + """Return a title for the discovered device.""" + return f"{discovery_info.name} {discovery_info.address}" + + +class ProbeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for BT Probe.""" + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, Discovery] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> ConfigFlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered BT device: %s", discovery_info) + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + self.context["title_placeholders"] = {"name": title(discovery_info)} + self._discovered_devices[discovery_info.address] = Discovery( + title(discovery_info), discovery_info + ) + + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the bluetooth confirmation step.""" + if user_input is not None: + assert self.unique_id + self._abort_if_unique_id_configured() + discovery = self._discovered_devices[self.unique_id] + return self.async_create_entry( + title=discovery.title, + data={ + CONF_ADDRESS: discovery.discovery_info.address, + }, + ) + self._set_confirm_only() + assert self.unique_id + return self.async_show_form( + step_id="bluetooth_confirm", + description_placeholders={ + "name": title(self._discovered_devices[self.unique_id].discovery_info) + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + discovery = self._discovered_devices[address] + return self.async_create_entry( + title=discovery.title, + data=user_input, + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + + self._discovered_devices[address] = Discovery( + title(discovery_info), discovery_info + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + titles = { + address: discovery.title + for (address, discovery) in self._discovered_devices.items() + } + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In(titles), + } + ), + ) diff --git a/homeassistant/components/probe_plus/const.py b/homeassistant/components/probe_plus/const.py new file mode 100644 index 00000000000..d0e2a7d6992 --- /dev/null +++ b/homeassistant/components/probe_plus/const.py @@ -0,0 +1,3 @@ +"""Constants for the Probe Plus integration.""" + +DOMAIN = "probe_plus" diff --git a/homeassistant/components/probe_plus/coordinator.py b/homeassistant/components/probe_plus/coordinator.py new file mode 100644 index 00000000000..b712e3fc84b --- /dev/null +++ b/homeassistant/components/probe_plus/coordinator.py @@ -0,0 +1,68 @@ +"""Coordinator for the probe_plus integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging + +from pyprobeplus import ProbePlusDevice +from pyprobeplus.exceptions import ProbePlusDeviceNotFound, ProbePlusError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +type ProbePlusConfigEntry = ConfigEntry[ProbePlusDataUpdateCoordinator] + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=15) + + +class ProbePlusDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Coordinator to manage data updates for a probe device. + + This class handles the communication with Probe Plus devices. + + Data is updated by the device itself. + """ + + config_entry: ProbePlusConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ProbePlusConfigEntry) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name="ProbePlusDataUpdateCoordinator", + update_interval=SCAN_INTERVAL, + config_entry=entry, + ) + + self.device: ProbePlusDevice = ProbePlusDevice( + address_or_ble_device=entry.data[CONF_ADDRESS], + name=entry.title, + notify_callback=self.async_update_listeners, + ) + + async def _async_update_data(self) -> None: + """Connect to the Probe Plus device on a set interval. + + This method is called periodically to reconnect to the device + Data updates are handled by the device itself. + """ + # Already connected, no need to update any data as the device streams this. + if self.device.connected: + return + + # Probe is not connected, try to connect + try: + await self.device.connect() + except (ProbePlusError, ProbePlusDeviceNotFound, TimeoutError) as e: + _LOGGER.debug( + "Could not connect to scale: %s, Error: %s", + self.config_entry.data[CONF_ADDRESS], + e, + ) + self.device.device_disconnected_handler(notify=False) + return diff --git a/homeassistant/components/probe_plus/entity.py b/homeassistant/components/probe_plus/entity.py new file mode 100644 index 00000000000..c2c53f5bca4 --- /dev/null +++ b/homeassistant/components/probe_plus/entity.py @@ -0,0 +1,54 @@ +"""Probe Plus base entity type.""" + +from dataclasses import dataclass + +from pyprobeplus import ProbePlusDevice + +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + format_mac, +) +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ProbePlusDataUpdateCoordinator + + +@dataclass +class ProbePlusEntity(CoordinatorEntity[ProbePlusDataUpdateCoordinator]): + """Base class for Probe Plus entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: ProbePlusDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + + # Set the unique ID for the entity + self._attr_unique_id = ( + f"{format_mac(coordinator.device.mac)}_{entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, format_mac(coordinator.device.mac))}, + name=coordinator.device.name, + manufacturer="Probe Plus", + suggested_area="Kitchen", + connections={(CONNECTION_BLUETOOTH, coordinator.device.mac)}, + ) + + @property + def available(self) -> bool: + """Return True if the entity is available.""" + return super().available and self.coordinator.device.connected + + @property + def device(self) -> ProbePlusDevice: + """Return the device associated with this entity.""" + return self.coordinator.device diff --git a/homeassistant/components/probe_plus/icons.json b/homeassistant/components/probe_plus/icons.json new file mode 100644 index 00000000000..d76bbd39873 --- /dev/null +++ b/homeassistant/components/probe_plus/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "sensor": { + "probe_temperature": { + "default": "mdi:thermometer-bluetooth" + } + } + } +} diff --git a/homeassistant/components/probe_plus/manifest.json b/homeassistant/components/probe_plus/manifest.json new file mode 100644 index 00000000000..cf61e394a83 --- /dev/null +++ b/homeassistant/components/probe_plus/manifest.json @@ -0,0 +1,19 @@ +{ + "domain": "probe_plus", + "name": "Probe Plus", + "bluetooth": [ + { + "connectable": true, + "manufacturer_id": 36606, + "local_name": "FM2*" + } + ], + "codeowners": ["@pantherale0"], + "config_flow": true, + "dependencies": ["bluetooth_adapters"], + "documentation": "https://www.home-assistant.io/integrations/probe_plus", + "integration_type": "device", + "iot_class": "local_push", + "quality_scale": "bronze", + "requirements": ["pyprobeplus==1.0.0"] +} diff --git a/homeassistant/components/probe_plus/quality_scale.yaml b/homeassistant/components/probe_plus/quality_scale.yaml new file mode 100644 index 00000000000..d06d36d41de --- /dev/null +++ b/homeassistant/components/probe_plus/quality_scale.yaml @@ -0,0 +1,100 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + No custom actions are defined. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: | + No custom actions are defined. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + No explicit event subscriptions. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: + status: exempt + comment: | + Device is expected to be offline most of the time, but needs to connect quickly once available. + unique-config-entry: done + # Silver + action-exceptions: + status: exempt + comment: | + No custom actions are defined. + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: + status: done + comment: | + Handled by coordinator. + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + No authentication required. + test-coverage: todo + # Gold + devices: done + diagnostics: todo + discovery-update-info: + status: exempt + comment: | + No IP discovery. + discovery: + status: done + comment: | + The integration uses Bluetooth discovery to find devices. + docs-data-update: done + docs-examples: todo + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: done + docs-use-cases: done + dynamic-devices: done + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: + status: exempt + comment: | + No custom exceptions are defined. + icon-translations: done + reconfiguration-flow: + status: exempt + comment: | + No reconfiguration flow is needed as the only thing that could be changed is the MAC, which is already hardcoded on the device itself. + repair-issues: + status: exempt + comment: | + No repair issues. + stale-devices: + status: exempt + comment: | + The device itself is the integration. + + # Platinum + async-dependency: done + inject-websession: + status: exempt + comment: | + No web session is used. + strict-typing: todo diff --git a/homeassistant/components/probe_plus/sensor.py b/homeassistant/components/probe_plus/sensor.py new file mode 100644 index 00000000000..9834a1433a4 --- /dev/null +++ b/homeassistant/components/probe_plus/sensor.py @@ -0,0 +1,106 @@ +"""Support for Probe Plus BLE sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, + UnitOfElectricPotential, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import ProbePlusConfigEntry, ProbePlusDevice +from .entity import ProbePlusEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(kw_only=True, frozen=True) +class ProbePlusSensorEntityDescription(SensorEntityDescription): + """Description for Probe Plus sensor entities.""" + + value_fn: Callable[[ProbePlusDevice], int | float | None] + + +SENSOR_DESCRIPTIONS: tuple[ProbePlusSensorEntityDescription, ...] = ( + ProbePlusSensorEntityDescription( + key="probe_temperature", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda device: device.device_state.probe_temperature, + device_class=SensorDeviceClass.TEMPERATURE, + ), + ProbePlusSensorEntityDescription( + key="probe_battery", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.device_state.probe_battery, + device_class=SensorDeviceClass.BATTERY, + ), + ProbePlusSensorEntityDescription( + key="relay_battery", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.device_state.relay_battery, + device_class=SensorDeviceClass.BATTERY, + ), + ProbePlusSensorEntityDescription( + key="probe_rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.device_state.probe_rssi, + entity_registry_enabled_default=False, + ), + ProbePlusSensorEntityDescription( + key="relay_voltage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.VOLTAGE, + value_fn=lambda device: device.device_state.relay_voltage, + entity_registry_enabled_default=False, + ), + ProbePlusSensorEntityDescription( + key="probe_voltage", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.VOLTAGE, + value_fn=lambda device: device.device_state.probe_voltage, + entity_registry_enabled_default=False, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ProbePlusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Probe Plus sensors.""" + coordinator = entry.runtime_data + async_add_entities(ProbeSensor(coordinator, desc) for desc in SENSOR_DESCRIPTIONS) + + +class ProbeSensor(ProbePlusEntity, RestoreSensor): + """Representation of a Probe Plus sensor.""" + + entity_description: ProbePlusSensorEntityDescription + + @property + def native_value(self) -> int | float | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.device) diff --git a/homeassistant/components/probe_plus/strings.json b/homeassistant/components/probe_plus/strings.json new file mode 100644 index 00000000000..45fd4be39ce --- /dev/null +++ b/homeassistant/components/probe_plus/strings.json @@ -0,0 +1,49 @@ +{ + "config": { + "flow_title": "{name}", + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + }, + "error": { + "device_not_found": "Device could not be found.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:common::config_flow::data::device%]" + }, + "data_description": { + "address": "Select BLE probe you want to set up" + } + } + } + }, + "entity": { + "sensor": { + "probe_battery": { + "name": "Probe battery" + }, + "probe_temperature": { + "name": "Probe temperature" + }, + "probe_rssi": { + "name": "Probe RSSI" + }, + "probe_voltage": { + "name": "Probe voltage" + }, + "relay_battery": { + "name": "Relay battery" + }, + "relay_voltage": { + "name": "Relay voltage" + } + } + } +} diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index e796625f81c..f5303f09302 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -593,6 +593,12 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [ "domain": "oralb", "manufacturer_id": 220, }, + { + "connectable": True, + "domain": "probe_plus", + "local_name": "FM2*", + "manufacturer_id": 36606, + }, { "connectable": False, "domain": "qingping", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1b7536ed4b9..e1211ac20d0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -487,6 +487,7 @@ FLOWS = { "powerfox", "powerwall", "private_ble_device", + "probe_plus", "profiler", "progettihwsw", "prosegur", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 66addc2f5b5..7f335f4091d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5048,6 +5048,12 @@ "config_flow": true, "iot_class": "local_push" }, + "probe_plus": { + "name": "Probe Plus", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "profiler": { "name": "Profiler", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index edb6716b10b..7b8ee92a996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2244,6 +2244,9 @@ pyplaato==0.0.19 # homeassistant.components.point pypoint==3.0.0 +# homeassistant.components.probe_plus +pyprobeplus==1.0.0 + # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7990cfd6e25..f298814e015 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1838,6 +1838,9 @@ pyplaato==0.0.19 # homeassistant.components.point pypoint==3.0.0 +# homeassistant.components.probe_plus +pyprobeplus==1.0.0 + # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/tests/components/probe_plus/__init__.py b/tests/components/probe_plus/__init__.py new file mode 100644 index 00000000000..22f0d7dd1c3 --- /dev/null +++ b/tests/components/probe_plus/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the Probe Plus integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the Probe Plus 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/probe_plus/conftest.py b/tests/components/probe_plus/conftest.py new file mode 100644 index 00000000000..ddbad5c46b1 --- /dev/null +++ b/tests/components/probe_plus/conftest.py @@ -0,0 +1,60 @@ +"""Common fixtures for the Probe Plus tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from pyprobeplus.parser import ParserBase, ProbePlusData +import pytest + +from homeassistant.components.probe_plus.const import DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.probe_plus.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="FM210 aa:bb:cc:dd:ee:ff", + domain=DOMAIN, + version=1, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + unique_id="aa:bb:cc:dd:ee:ff", + ) + + +@pytest.fixture +def mock_probe_plus() -> MagicMock: + """Mock the Probe Plus device.""" + with patch( + "homeassistant.components.probe_plus.coordinator.ProbePlusDevice", + autospec=True, + ) as mock_device: + device = mock_device.return_value + device.connected = True + device.name = "FM210 aa:bb:cc:dd:ee:ff" + mock_state = ParserBase() + mock_state.state = ProbePlusData( + relay_battery=50, + probe_battery=50, + probe_temperature=25.0, + probe_rssi=200, + probe_voltage=3.7, + relay_status=1, + relay_voltage=9.0, + ) + device._device_state = mock_state + yield device diff --git a/tests/components/probe_plus/test_config_flow.py b/tests/components/probe_plus/test_config_flow.py new file mode 100644 index 00000000000..1d248144311 --- /dev/null +++ b/tests/components/probe_plus/test_config_flow.py @@ -0,0 +1,133 @@ +"""Test the config flow for the Probe Plus.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.probe_plus.const import DOMAIN +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from tests.common import MockConfigEntry + +service_info = BluetoothServiceInfo( + name="FM210", + address="aa:bb:cc:dd:ee:ff", + rssi=-63, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", +) + + +@pytest.fixture +def mock_discovered_service_info() -> Generator[AsyncMock]: + """Override getting Bluetooth service info.""" + with patch( + "homeassistant.components.probe_plus.config_flow.async_discovered_service_info", + return_value=[service_info], + ) as mock_discovered_service_info: + yield mock_discovered_service_info + + +async def test_user_config_flow_creates_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test the user configuration flow successfully creates a config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + }, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff" + assert result["title"] == "FM210 aa:bb:cc:dd:ee:ff" + assert result["data"] == {CONF_ADDRESS: "aa:bb:cc:dd:ee:ff"} + + +async def test_user_flow_already_configured( + hass: HomeAssistant, + mock_discovered_service_info: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test that the user flow aborts when the entry is already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + # this aborts with no devices found as the config flow + # already checks for existing config entries when validating the discovered devices + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_bluetooth_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test we can discover a device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "FM210 aa:bb:cc:dd:ee:ff" + assert result["result"].unique_id == "aa:bb:cc:dd:ee:ff" + assert result["data"] == { + CONF_ADDRESS: service_info.address, + } + + +async def test_already_configured_bluetooth_discovery( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Ensure configure device is not discovered again.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=service_info + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_no_bluetooth_devices( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_discovered_service_info: AsyncMock, +) -> None: + """Test flow aborts on unsupported device.""" + mock_discovered_service_info.return_value = [] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From eb90f5a5812af345a7d1d6df57872d3591c92ada Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 00:11:34 +0200 Subject: [PATCH 0659/1175] Improve type hints in xiaomi_aqara light turn_on (#145257) --- homeassistant/components/xiaomi_aqara/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index ef1f06695f9..b19719dc5dc 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -97,7 +97,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): """Return the hs color value.""" return self._hs - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_HS_COLOR in kwargs: self._hs = kwargs[ATTR_HS_COLOR] @@ -107,8 +107,8 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): rgb = color_util.color_hs_to_RGB(*self._hs) rgba = (self._brightness, *rgb) - rgbhex = binascii.hexlify(struct.pack("BBBB", *rgba)).decode("ASCII") - rgbhex = int(rgbhex, 16) + rgbhex_str = binascii.hexlify(struct.pack("BBBB", *rgba)).decode("ASCII") + rgbhex = int(rgbhex_str, 16) if self._write_to_hub(self._sid, **{self._data_key: rgbhex}): self._state = True From f700a1faa3515795272f39ebfaead300777889a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 00:11:52 +0200 Subject: [PATCH 0660/1175] Use shorthand attributes in raspyrfm (#145250) --- homeassistant/components/raspyrfm/switch.py | 30 +++++++-------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index a609ddb27d3..19a1b724c48 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from raspyrfm_client import RaspyRFMClient from raspyrfm_client.device_implementations.controlunit.actions import Action from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( @@ -100,41 +102,27 @@ def setup_platform( class RaspyRFMSwitch(SwitchEntity): """Representation of a RaspyRFM switch.""" + _attr_assumed_state = True _attr_should_poll = False def __init__(self, raspyrfm_client, name: str, gateway, controlunit) -> None: """Initialize the switch.""" self._raspyrfm_client = raspyrfm_client - self._name = name + self._attr_name = name self._gateway = gateway self._controlunit = controlunit - self._state = None + self._attr_is_on = None - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def assumed_state(self) -> bool: - """Return True when the current state cannot be queried.""" - return True - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._raspyrfm_client.send(self._gateway, self._controlunit, Action.ON) - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if Action.OFF in self._controlunit.get_supported_actions(): @@ -142,5 +130,5 @@ class RaspyRFMSwitch(SwitchEntity): else: self._raspyrfm_client.send(self._gateway, self._controlunit, Action.ON) - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() From f4b0baecd33c86754237af36abd426934da57592 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 00:21:44 +0200 Subject: [PATCH 0661/1175] Improve type hints in omnilogic (#145259) --- homeassistant/components/omnilogic/switch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index a9f8bc77d8a..9583194f41b 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -92,12 +92,12 @@ class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): ) self._state_key = state_key - self._state = None - self._last_action = 0 + self._state: bool | None = None + self._last_action = 0.0 self._state_delay = 30 @property - def is_on(self): + def is_on(self) -> bool: """Return the on/off state of the switch.""" state_int = 0 @@ -119,7 +119,7 @@ class OmniLogicSwitch(OmniLogicEntity, SwitchEntity): class OmniLogicRelayControl(OmniLogicSwitch): """Define the OmniLogic Relay entity.""" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the relay.""" self._state = True self._last_action = time.time() @@ -132,7 +132,7 @@ class OmniLogicRelayControl(OmniLogicSwitch): 1, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the relay.""" self._state = False self._last_action = time.time() @@ -178,7 +178,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): self._last_speed = None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the pump.""" self._state = True self._last_action = time.time() @@ -196,7 +196,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): on_value, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the pump.""" self._state = False self._last_action = time.time() From b84e93f462ae0b69deaeaaba69e6ad3ce67a4caf Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 20 May 2025 08:25:44 +0300 Subject: [PATCH 0662/1175] Sort usb ports in Z-Wave flow so unknown devices are last (#145211) * Sort usb ports in Z-Wave flow so unknown devices are last * tweak * Apply suggestions from code review Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- .../components/zwave_js/config_flow.py | 9 +++++- tests/components/zwave_js/test_config_flow.py | 30 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e442fb59cfc..324011a3009 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -149,7 +149,14 @@ def get_usb_ports() -> dict[str, str]: pid, ) port_descriptions[dev_path] = human_name - return port_descriptions + + # Sort the dictionary by description, putting "n/a" last + return dict( + sorted( + port_descriptions.items(), + key=lambda x: x[1].lower().startswith("n/a"), + ) + ) async def async_get_usb_ports(hass: HomeAssistant) -> dict[str, str]: diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 7a2788a7b75..68489b304d2 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -17,7 +17,7 @@ from zwave_js_server.exceptions import FailedCommand from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow -from homeassistant.components.zwave_js.config_flow import TITLE +from homeassistant.components.zwave_js.config_flow import TITLE, get_usb_ports from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant @@ -4661,3 +4661,31 @@ async def test_configure_addon_usb_ports_failure( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "usb_ports_failed" + + +async def test_get_usb_ports_sorting(hass: HomeAssistant) -> None: + """Test that get_usb_ports sorts ports with 'n/a' descriptions last.""" + mock_ports = [ + ListPortInfo("/dev/ttyUSB0"), + ListPortInfo("/dev/ttyUSB1"), + ListPortInfo("/dev/ttyUSB2"), + ListPortInfo("/dev/ttyUSB3"), + ] + mock_ports[0].description = "n/a" + mock_ports[1].description = "Device A" + mock_ports[2].description = "N/A" + mock_ports[3].description = "Device B" + + with patch("serial.tools.list_ports.comports", return_value=mock_ports): + result = get_usb_ports() + + descriptions = list(result.values()) + + # Verify that descriptions containing "n/a" are at the end + + assert descriptions == [ + "Device A - /dev/ttyUSB1, s/n: n/a", + "Device B - /dev/ttyUSB3, s/n: n/a", + "n/a - /dev/ttyUSB0, s/n: n/a", + "N/A - /dev/ttyUSB2, s/n: n/a", + ] From a12bc70543e7f904da356a766634ae840a3f21e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 09:15:26 +0200 Subject: [PATCH 0663/1175] Use runtime_data in smarttub (#145279) --- homeassistant/components/smarttub/__init__.py | 19 ++++------- .../components/smarttub/binary_sensor.py | 32 ++++++++++++------ homeassistant/components/smarttub/climate.py | 13 +++++--- homeassistant/components/smarttub/const.py | 2 -- .../components/smarttub/controller.py | 33 ++++++++++--------- homeassistant/components/smarttub/entity.py | 19 ++++++++--- homeassistant/components/smarttub/light.py | 19 +++++------ homeassistant/components/smarttub/sensor.py | 21 +++++++----- homeassistant/components/smarttub/switch.py | 13 +++++--- tests/components/smarttub/test_init.py | 5 ++- 10 files changed, 99 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index 8406fdc4c2f..178fd9a70e2 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -1,11 +1,9 @@ """SmartTub integration.""" -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, SMARTTUB_CONTROLLER -from .controller import SmartTubController +from .controller import SmartTubConfigEntry, SmartTubController PLATFORMS = [ Platform.BINARY_SENSOR, @@ -16,26 +14,21 @@ PLATFORMS = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SmartTubConfigEntry) -> bool: """Set up a smarttub config entry.""" controller = SmartTubController(hass) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - SMARTTUB_CONTROLLER: controller, - } if not await controller.async_setup_entry(entry): return False + entry.runtime_data = controller + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: SmartTubConfigEntry) -> bool: """Remove a smarttub config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 2e8792140b0..a120650e84b 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -2,20 +2,23 @@ from __future__ import annotations -from smarttub import SpaError, SpaReminder +from typing import Any + +from smarttub import Spa, SpaError, SpaReminder import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import VolDictType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER +from .const import ATTR_ERRORS, ATTR_REMINDERS +from .controller import SmartTubConfigEntry from .entity import SmartTubEntity, SmartTubSensorBase # whether the reminder has been snoozed (bool) @@ -44,12 +47,12 @@ SNOOZE_REMINDER_SCHEMA: VolDictType = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SmartTubConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up binary sensor entities for the binary sensors in the tub.""" - controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + controller = entry.runtime_data entities: list[BinarySensorEntity] = [] for spa in controller.spas: @@ -83,7 +86,9 @@ class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity): # This seems to be very noisy and not generally useful, so disable by default. _attr_entity_registry_enabled_default = False - def __init__(self, coordinator, spa): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], spa: Spa + ) -> None: """Initialize the entity.""" super().__init__(coordinator, spa, "Online", "online") @@ -98,7 +103,12 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PROBLEM - def __init__(self, coordinator, spa, reminder): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, Any]], + spa: Spa, + reminder: SpaReminder, + ) -> None: """Initialize the entity.""" super().__init__( coordinator, @@ -119,7 +129,7 @@ class SmartTubReminder(SmartTubEntity, BinarySensorEntity): return self.reminder.remaining_days == 0 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_REMINDER_SNOOZED: self.reminder.snoozed, @@ -145,7 +155,9 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PROBLEM - def __init__(self, coordinator, spa): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], spa: Spa + ) -> None: """Initialize the entity.""" super().__init__( coordinator, @@ -167,7 +179,7 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): return self.error is not None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if (error := self.error) is None: return {} diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index f5759f32fa3..7e79ce0eb12 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -14,13 +14,14 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.unit_conversion import TemperatureConverter -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SMARTTUB_CONTROLLER +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP +from .controller import SmartTubConfigEntry from .entity import SmartTubEntity PRESET_DAY = "day" @@ -43,12 +44,12 @@ HVAC_ACTIONS = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SmartTubConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up climate entity for the thermostat in the tub.""" - controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + controller = entry.runtime_data entities = [ SmartTubThermostat(controller.coordinator, spa) for spa in controller.spas @@ -71,7 +72,9 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_preset_modes = list(PRESET_MODES.values()) - def __init__(self, coordinator, spa): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], spa: Spa + ) -> None: """Initialize the entity.""" super().__init__(coordinator, spa, "Thermostat") diff --git a/homeassistant/components/smarttub/const.py b/homeassistant/components/smarttub/const.py index f97ef65a54c..dadc66da942 100644 --- a/homeassistant/components/smarttub/const.py +++ b/homeassistant/components/smarttub/const.py @@ -4,8 +4,6 @@ DOMAIN = "smarttub" EVENT_SMARTTUB = "smarttub" -SMARTTUB_CONTROLLER = "smarttub_controller" - SCAN_INTERVAL = 60 POLLING_TIMEOUT = 10 diff --git a/homeassistant/components/smarttub/controller.py b/homeassistant/components/smarttub/controller.py index 353e2093997..d8299bbd786 100644 --- a/homeassistant/components/smarttub/controller.py +++ b/homeassistant/components/smarttub/controller.py @@ -3,13 +3,15 @@ import asyncio from datetime import timedelta import logging +from typing import Any from aiohttp import client_exceptions -from smarttub import APIError, LoginFailed, SmartTub +from smarttub import APIError, LoginFailed, SmartTub, Spa from smarttub.api import Account +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -29,19 +31,21 @@ from .helpers import get_spa_name _LOGGER = logging.getLogger(__name__) +type SmartTubConfigEntry = ConfigEntry[SmartTubController] + class SmartTubController: """Interface between Home Assistant and the SmartTub API.""" - def __init__(self, hass): + coordinator: DataUpdateCoordinator[dict[str, Any]] + spas: list[Spa] + _account: Account + + def __init__(self, hass: HomeAssistant) -> None: """Initialize an interface to SmartTub.""" self._hass = hass - self._account = None - self.spas = set() - self.coordinator = None - - async def async_setup_entry(self, entry): + async def async_setup_entry(self, entry: SmartTubConfigEntry) -> bool: """Perform initial setup. Authenticate, query static state, set up polling, and otherwise make @@ -79,7 +83,7 @@ class SmartTubController: return True - async def async_update_data(self): + async def async_update_data(self) -> dict[str, Any]: """Query the API and return the new state.""" data = {} @@ -92,7 +96,7 @@ class SmartTubController: return data - async def _get_spa_data(self, spa): + async def _get_spa_data(self, spa: Spa) -> dict[str, Any]: full_status, reminders, errors = await asyncio.gather( spa.get_status_full(), spa.get_reminders(), @@ -107,7 +111,7 @@ class SmartTubController: } @callback - def async_register_devices(self, entry): + def async_register_devices(self, entry: SmartTubConfigEntry) -> None: """Register devices with the device registry for all spas.""" device_registry = dr.async_get(self._hass) for spa in self.spas: @@ -119,11 +123,8 @@ class SmartTubController: model=spa.model, ) - async def login(self, email, password) -> Account: - """Retrieve the account corresponding to the specified email and password. - - Returns None if the credentials are invalid. - """ + async def login(self, email: str, password: str) -> Account: + """Retrieve the account corresponding to the specified email and password.""" api = SmartTub(async_get_clientsession(self._hass)) diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index f9ab1d10bfe..069fd50c5f2 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,6 +1,8 @@ """Base classes for SmartTub entities.""" -import smarttub +from typing import Any + +from smarttub import Spa, SpaState from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -16,7 +18,10 @@ class SmartTubEntity(CoordinatorEntity): """Base class for SmartTub entities.""" def __init__( - self, coordinator: DataUpdateCoordinator, spa: smarttub.Spa, entity_name + self, + coordinator: DataUpdateCoordinator[dict[str, Any]], + spa: Spa, + entity_name: str, ) -> None: """Initialize the entity. @@ -36,7 +41,7 @@ class SmartTubEntity(CoordinatorEntity): self._attr_name = f"{spa_name} {entity_name}" @property - def spa_status(self) -> smarttub.SpaState: + def spa_status(self) -> SpaState: """Retrieve the result of Spa.get_status().""" return self.coordinator.data[self.spa.id].get("status") @@ -45,7 +50,13 @@ class SmartTubEntity(CoordinatorEntity): class SmartTubSensorBase(SmartTubEntity): """Base class for SmartTub sensors.""" - def __init__(self, coordinator, spa, sensor_name, state_key): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, Any]], + spa: Spa, + sensor_name: str, + state_key: str, + ) -> None: """Initialize the entity.""" super().__init__(coordinator, spa, sensor_name) self._state_key = state_key diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index dda936aa56a..b6e056d37e0 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -12,29 +12,24 @@ from homeassistant.components.light import ( LightEntity, LightEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import ( - ATTR_LIGHTS, - DEFAULT_LIGHT_BRIGHTNESS, - DEFAULT_LIGHT_EFFECT, - DOMAIN, - SMARTTUB_CONTROLLER, -) +from .const import ATTR_LIGHTS, DEFAULT_LIGHT_BRIGHTNESS, DEFAULT_LIGHT_EFFECT +from .controller import SmartTubConfigEntry from .entity import SmartTubEntity from .helpers import get_spa_name async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SmartTubConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up entities for any lights in the tub.""" - controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + controller = entry.runtime_data entities = [ SmartTubLight(controller.coordinator, light) @@ -52,7 +47,9 @@ class SmartTubLight(SmartTubEntity, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_features = LightEntityFeature.EFFECT - def __init__(self, coordinator, light): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], light: SpaLight + ) -> None: """Initialize the entity.""" super().__init__(coordinator, light.spa, "light") self.light_zone = light.zone diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index b2bb1170d09..5116bfb3aee 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -1,18 +1,19 @@ """Platform for sensor integration.""" from enum import Enum +from typing import Any import smarttub import voluptuous as vol from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import VolDictType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, SMARTTUB_CONTROLLER +from .controller import SmartTubConfigEntry from .entity import SmartTubSensorBase # the desired duration, in hours, of the cycle @@ -44,12 +45,12 @@ SET_SECONDARY_FILTRATION_SCHEMA: VolDictType = { async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SmartTubConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up sensor entities for the sensors in the tub.""" - controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + controller = entry.runtime_data entities = [] for spa in controller.spas: @@ -107,7 +108,9 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity): class SmartTubPrimaryFiltrationCycle(SmartTubSensor): """The primary filtration cycle.""" - def __init__(self, coordinator, spa): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], spa: smarttub.Spa + ) -> None: """Initialize the entity.""" super().__init__( coordinator, spa, "Primary Filtration Cycle", "primary_filtration" @@ -124,7 +127,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): return self.cycle.status.name.lower() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_DURATION: self.cycle.duration, @@ -145,7 +148,9 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor): class SmartTubSecondaryFiltrationCycle(SmartTubSensor): """The secondary filtration cycle.""" - def __init__(self, coordinator, spa): + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], spa: smarttub.Spa + ) -> None: """Initialize the entity.""" super().__init__( coordinator, spa, "Secondary Filtration Cycle", "secondary_filtration" @@ -162,7 +167,7 @@ class SmartTubSecondaryFiltrationCycle(SmartTubSensor): return self.cycle.status.name.lower() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_CYCLE_LAST_UPDATED: self.cycle.last_updated.isoformat(), diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index 2dedad8e18a..12d15d63f9b 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -6,23 +6,24 @@ from typing import Any from smarttub import SpaPump from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import API_TIMEOUT, ATTR_PUMPS, DOMAIN, SMARTTUB_CONTROLLER +from .const import API_TIMEOUT, ATTR_PUMPS +from .controller import SmartTubConfigEntry from .entity import SmartTubEntity from .helpers import get_spa_name async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: SmartTubConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up switch entities for the pumps on the tub.""" - controller = hass.data[DOMAIN][entry.entry_id][SMARTTUB_CONTROLLER] + controller = entry.runtime_data entities = [ SmartTubPump(controller.coordinator, pump) @@ -36,7 +37,9 @@ async def async_setup_entry( class SmartTubPump(SmartTubEntity, SwitchEntity): """A pump on a spa.""" - def __init__(self, coordinator, pump: SpaPump) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[dict[str, Any]], pump: SpaPump + ) -> None: """Initialize the entity.""" super().__init__(coordinator, pump.spa, "pump") self.pump_id = pump.id diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index b1eac3fd98b..ff27820fca1 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -4,7 +4,6 @@ from unittest.mock import patch from smarttub import LoginFailed -from homeassistant.components import smarttub from homeassistant.components.smarttub.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant @@ -61,13 +60,13 @@ async def test_config_passed_to_config_entry( ) -> None: """Test that configured options are loaded via config entry.""" config_entry.add_to_hass(hass) - assert await async_setup_component(hass, smarttub.DOMAIN, config_data) + assert await async_setup_component(hass, DOMAIN, config_data) async def test_unload_entry(hass: HomeAssistant, config_entry) -> None: """Test being able to unload an entry.""" config_entry.add_to_hass(hass) - assert await async_setup_component(hass, smarttub.DOMAIN, {}) is True + assert await async_setup_component(hass, DOMAIN, {}) is True assert await hass.config_entries.async_unload(config_entry.entry_id) From fd1ddbd93df4486c8f272420501ea2d9bbe67908 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 09:31:42 +0200 Subject: [PATCH 0664/1175] Improve type hints in blebox climate (#145282) --- homeassistant/components/blebox/climate.py | 17 +++++++++-------- tests/components/blebox/test_climate.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index dbf4a326990..2d1f6c5ae9e 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -21,7 +21,6 @@ from .entity import BleBoxEntity SCAN_INTERVAL = timedelta(seconds=5) BLEBOX_TO_HVACMODE = { - None: None, 0: HVACMode.OFF, 1: HVACMode.HEAT, 2: HVACMode.COOL, @@ -59,12 +58,14 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn _attr_temperature_unit = UnitOfTemperature.CELSIUS @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return list of supported HVAC modes.""" + if self._feature.mode is None: + return [HVACMode.OFF] return [HVACMode.OFF, BLEBOX_TO_HVACMODE[self._feature.mode]] @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode | None: """Return the desired HVAC mode.""" if self._feature.is_on is None: return None @@ -75,7 +76,7 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn return HVACMode.HEAT if self._feature.is_on else HVACMode.OFF @property - def hvac_action(self): + def hvac_action(self) -> HVACAction | None: """Return the actual current HVAC action.""" if self._feature.hvac_action is not None: if not self._feature.is_on: @@ -88,22 +89,22 @@ class BleBoxClimateEntity(BleBoxEntity[blebox_uniapi.climate.Climate], ClimateEn return HVACAction.HEATING if self._feature.is_heating else HVACAction.IDLE @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature supported.""" return self._feature.max_temp @property - def min_temp(self): + def min_temp(self) -> float: """Return the maximum temperature supported.""" return self._feature.min_temp @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._feature.current @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the desired thermostat temperature.""" return self._feature.desired diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index e402a3d5fbd..9da2d9a8a68 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -93,7 +93,7 @@ async def test_init( supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & ClimateEntityFeature.TARGET_TEMPERATURE - assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF, None] + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF] assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_HVAC_MODE not in state.attributes From 502574e86fe9313f039bb0d03e983991704a32bb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 09:32:50 +0200 Subject: [PATCH 0665/1175] Use shorthand attributes in yi camera (#145276) --- homeassistant/components/yi/camera.py | 30 +++++++-------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index b2fac03954d..10b84f933ef 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -66,36 +66,22 @@ async def async_setup_platform( class YiCamera(Camera): """Define an implementation of a Yi Camera.""" - def __init__(self, hass, config): + _attr_brand = DEFAULT_BRAND + + def __init__(self, hass: HomeAssistant, config: ConfigType) -> None: """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._last_image = None + self._last_image: bytes | None = None self._last_url = None self._manager = get_ffmpeg_manager(hass) - self._name = config[CONF_NAME] - self._is_on = True + self._attr_name = config[CONF_NAME] self.host = config[CONF_HOST] self.port = config[CONF_PORT] self.path = config[CONF_PATH] self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - @property - def brand(self): - """Camera brand.""" - return DEFAULT_BRAND - - @property - def is_on(self): - """Determine whether the camera is on.""" - return self._is_on - - @property - def name(self): - """Return the name of this camera.""" - return self._name - async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" ftp = Client() @@ -122,14 +108,14 @@ class YiCamera(Camera): return None await ftp.quit() - self._is_on = True + self._attr_is_on = True return ( f"ftp://{self.user}:{self.passwd}@{self.host}:" f"{self.port}{self.path}/{latest_dir}/{videos[-1]}" ) except (ConnectionRefusedError, StatusCodeError) as err: _LOGGER.error("Error while fetching video: %s", err) - self._is_on = False + self._attr_is_on = False return None async def async_camera_image( @@ -151,7 +137,7 @@ class YiCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - if not self._is_on: + if not self._attr_is_on: return None stream = CameraMjpeg(self._manager.binary) From ef6d3a5236ae575e1e641cd814daf1a8a0c26226 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Tue, 20 May 2025 09:49:27 +0200 Subject: [PATCH 0666/1175] Bump aiontfy to 0.5.3 (#145263) --- homeassistant/components/ntfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ntfy/manifest.json b/homeassistant/components/ntfy/manifest.json index fde1569d622..d9d864d10a3 100644 --- a/homeassistant/components/ntfy/manifest.json +++ b/homeassistant/components/ntfy/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_push", "loggers": ["aionfty"], "quality_scale": "bronze", - "requirements": ["aiontfy==0.5.2"] + "requirements": ["aiontfy==0.5.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7b8ee92a996..d233bfbd826 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -319,7 +319,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.2 +aiontfy==0.5.3 # homeassistant.components.nut aionut==4.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f298814e015..efccb311141 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -301,7 +301,7 @@ aionanoleaf==0.2.1 aionotion==2024.03.0 # homeassistant.components.ntfy -aiontfy==0.5.2 +aiontfy==0.5.3 # homeassistant.components.nut aionut==4.3.4 From 77ea654a1fb8030d21918ee9cee20c0d87659616 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Tue, 20 May 2025 02:51:29 -0500 Subject: [PATCH 0667/1175] Bump pyaprilaire to 0.9.0 (#145260) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index b40460dd61b..6fe3beae3bc 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.8.1"] + "requirements": ["pyaprilaire==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d233bfbd826..2cd73d5cafe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1829,7 +1829,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.8.1 +pyaprilaire==0.9.0 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index efccb311141..7aa4752ba5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1510,7 +1510,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.8.1 +pyaprilaire==0.9.0 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From cd91aca3b515c96067e7ae378c20c3e31bd0538e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:09:25 +0200 Subject: [PATCH 0668/1175] Use shorthand attributes in tfiac climate (#145289) --- homeassistant/components/tfiac/climate.py | 57 +++++------------------ 1 file changed, 11 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 7fc6e2594c4..bab05bfc25e 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -36,9 +36,6 @@ PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.st _LOGGER = logging.getLogger(__name__) -MIN_TEMP = 61 -MAX_TEMP = 88 - HVAC_MAP = { HVACMode.HEAT: "heat", HVACMode.AUTO: "selfFeel", @@ -50,9 +47,6 @@ HVAC_MAP = { HVAC_MAP_REV = {v: k for k, v in HVAC_MAP.items()} -SUPPORT_FAN = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] -SUPPORT_SWING = [SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL, SWING_BOTH] - CURR_TEMP = "current_temp" TARGET_TEMP = "target_temp" OPERATION_MODE = "operation" @@ -74,7 +68,7 @@ async def async_setup_platform( except futures.TimeoutError: _LOGGER.error("Unable to connect to %s", config[CONF_HOST]) return - async_add_entities([TfiacClimate(hass, tfiac_client)]) + async_add_entities([TfiacClimate(tfiac_client)]) class TfiacClimate(ClimateEntity): @@ -88,34 +82,23 @@ class TfiacClimate(ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.FAHRENHEIT + _attr_min_temp = 61 + _attr_max_temp = 88 + _attr_fan_modes = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] + _attr_hvac_modes = list(HVAC_MAP) + _attr_swing_modes = [SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL, SWING_BOTH] - def __init__(self, hass, client): + def __init__(self, client: Tfiac) -> None: """Init class.""" self._client = client - self._available = True - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._available async def async_update(self) -> None: """Update status via socket polling.""" try: await self._client.update() - self._available = True + self._attr_available = True except futures.TimeoutError: - self._available = False - - @property - def min_temp(self): - """Return the minimum temperature.""" - return MIN_TEMP - - @property - def max_temp(self): - """Return the maximum temperature.""" - return MAX_TEMP + self._attr_available = False @property def name(self): @@ -145,33 +128,15 @@ class TfiacClimate(ClimateEntity): return HVAC_MAP_REV.get(state) @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - return list(HVAC_MAP) - - @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the fan setting.""" return self._client.status["fan_mode"].lower() @property - def fan_modes(self): - """Return the list of available fan modes.""" - return SUPPORT_FAN - - @property - def swing_mode(self): + def swing_mode(self) -> str: """Return the swing setting.""" return self._client.status["swing_mode"].lower() - @property - def swing_modes(self): - """List of available swing modes.""" - return SUPPORT_SWING - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: From ed2024e67a876d03dab7a82f01250e56aa4544ea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:09:47 +0200 Subject: [PATCH 0669/1175] Drop useless unit conversion in smarttub (#145287) --- homeassistant/components/smarttub/climate.py | 21 +++----------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index 7e79ce0eb12..62a81857764 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -18,7 +18,6 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.unit_conversion import TemperatureConverter from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from .controller import SmartTubConfigEntry @@ -70,6 +69,8 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_min_temp = DEFAULT_MIN_TEMP + _attr_max_temp = DEFAULT_MAX_TEMP _attr_preset_modes = list(PRESET_MODES.values()) def __init__( @@ -93,23 +94,7 @@ class SmartTubThermostat(SmartTubEntity, ClimateEntity): raise NotImplementedError(hvac_mode) @property - def min_temp(self): - """Return the minimum temperature.""" - min_temp = DEFAULT_MIN_TEMP - return TemperatureConverter.convert( - min_temp, UnitOfTemperature.CELSIUS, self.temperature_unit - ) - - @property - def max_temp(self): - """Return the maximum temperature.""" - max_temp = DEFAULT_MAX_TEMP - return TemperatureConverter.convert( - max_temp, UnitOfTemperature.CELSIUS, self.temperature_unit - ) - - @property - def preset_mode(self): + def preset_mode(self) -> str: """Return the current preset mode.""" return PRESET_MODES[self.spa_status.heat_mode] From 99f91003d8fe1380f34286148e081645df5a8676 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:10:12 +0200 Subject: [PATCH 0670/1175] Use shorthand attributes in melissa climate (#145286) --- homeassistant/components/melissa/climate.py | 39 +++++---------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index ff68820d70f..bee457bada9 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -57,6 +57,7 @@ async def async_setup_platform( class MelissaClimate(ClimateEntity): """Representation of a Melissa Climate device.""" + _attr_fan_modes = FAN_MODES _attr_hvac_modes = OP_MODES _attr_supported_features = ( ClimateEntityFeature.FAN_MODE @@ -64,11 +65,14 @@ class MelissaClimate(ClimateEntity): | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON ) + _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_min_temp = 16 + _attr_max_temp = 30 def __init__(self, api, serial_number, init_data): """Initialize the climate device.""" - self._name = init_data["name"] + self._attr_name = init_data["name"] self._api = api self._serial_number = serial_number self._data = init_data["controller_log"] @@ -76,36 +80,26 @@ class MelissaClimate(ClimateEntity): self._cur_settings = None @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._name - - @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the current fan mode.""" if self._cur_settings is not None: return self.melissa_fan_to_hass(self._cur_settings[self._api.FAN]) return None @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if self._data: return self._data[self._api.TEMP] return None @property - def current_humidity(self): + def current_humidity(self) -> float | None: """Return the current humidity value.""" if self._data: return self._data[self._api.HUMIDITY] return None - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return PRECISION_WHOLE - @property def hvac_mode(self) -> HVACMode | None: """Return the current operation mode.""" @@ -123,27 +117,12 @@ class MelissaClimate(ClimateEntity): return self.melissa_op_to_hass(self._cur_settings[self._api.MODE]) @property - def fan_modes(self): - """List of available fan modes.""" - return FAN_MODES - - @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self._cur_settings is None: return None return self._cur_settings[self._api.TEMP] - @property - def min_temp(self): - """Return the minimum supported temperature for the thermostat.""" - return 16 - - @property - def max_temp(self): - """Return the maximum supported temperature for the thermostat.""" - return 30 - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) From c8183bd35a476c1a90662270b9bb9869d88ef9a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:10:24 +0200 Subject: [PATCH 0671/1175] Use shorthand attributes in intesishome climate (#145285) --- .../components/intesishome/climate.py | 119 +++++------------- 1 file changed, 32 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index a04a6ee6377..3465a7e5c07 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -145,7 +145,9 @@ async def async_setup_platform( class IntesisAC(ClimateEntity): """Represents an Intesishome air conditioning device.""" + _attr_preset_modes = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST] _attr_should_poll = False + _attr_target_temperature_step = 1 _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__(self, ih_device_id, ih_device, controller): @@ -153,26 +155,18 @@ class IntesisAC(ClimateEntity): self._controller = controller self._device_id = ih_device_id self._ih_device = ih_device - self._device_name = ih_device.get("name") + self._attr_name = ih_device.get("name") self._device_type = controller.device_type self._connected = None - self._setpoint_step = 1 - self._current_temp = None - self._max_temp = None self._attr_hvac_modes = [] - self._min_temp = None - self._target_temp = None self._outdoor_temp = None self._hvac_mode = None - self._preset = None - self._preset_list = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST] self._run_hours = None self._rssi = None - self._swing_list = [SWING_OFF] + self._attr_swing_modes = [SWING_OFF] self._vvane = None self._hvane = None self._power = False - self._fan_speed = None self._power_consumption_heat = None self._power_consumption_cool = None @@ -182,17 +176,20 @@ class IntesisAC(ClimateEntity): # Setup swing list if controller.has_vertical_swing(ih_device_id): - self._swing_list.append(SWING_VERTICAL) + self._attr_swing_modes.append(SWING_VERTICAL) if controller.has_horizontal_swing(ih_device_id): - self._swing_list.append(SWING_HORIZONTAL) - if SWING_HORIZONTAL in self._swing_list and SWING_VERTICAL in self._swing_list: - self._swing_list.append(SWING_BOTH) - if len(self._swing_list) > 1: + self._attr_swing_modes.append(SWING_HORIZONTAL) + if ( + SWING_HORIZONTAL in self._attr_swing_modes + and SWING_VERTICAL in self._attr_swing_modes + ): + self._attr_swing_modes.append(SWING_BOTH) + if len(self._attr_swing_modes) > 1: self._attr_supported_features |= ClimateEntityFeature.SWING_MODE # Setup fan speeds - self._fan_modes = controller.get_fan_speed_list(ih_device_id) - if self._fan_modes: + self._attr_fan_modes = controller.get_fan_speed_list(ih_device_id) + if self._attr_fan_modes: self._attr_supported_features |= ClimateEntityFeature.FAN_MODE # Preset support @@ -220,11 +217,6 @@ class IntesisAC(ClimateEntity): _LOGGER.error("Exception connecting to IntesisHome: %s", ex) raise PlatformNotReady from ex - @property - def name(self): - """Return the name of the AC device.""" - return self._device_name - @property def extra_state_attributes(self): """Return the device specific state attributes.""" @@ -247,21 +239,6 @@ class IntesisAC(ClimateEntity): """Return unique ID for this device.""" return self._device_id - @property - def target_temperature_step(self) -> float: - """Return whether setpoint should be whole or half degree precision.""" - return self._setpoint_step - - @property - def preset_modes(self): - """Return a list of HVAC preset modes.""" - return self._preset_list - - @property - def preset_mode(self): - """Return the current preset mode.""" - return self._preset - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if hvac_mode := kwargs.get(ATTR_HVAC_MODE): @@ -270,7 +247,7 @@ class IntesisAC(ClimateEntity): if temperature := kwargs.get(ATTR_TEMPERATURE): _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature) await self._controller.set_temperature(self._device_id, temperature) - self._target_temp = temperature + self._attr_target_temperature = temperature # Write updated temperature to HA state to avoid flapping (API confirmation is slow) self.async_write_ha_state() @@ -294,8 +271,10 @@ class IntesisAC(ClimateEntity): await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode]) # Send the temperature again in case changing modes has changed it - if self._target_temp: - await self._controller.set_temperature(self._device_id, self._target_temp) + if self._attr_target_temperature: + await self._controller.set_temperature( + self._device_id, self._attr_target_temperature + ) # Updates can take longer than 2 seconds, so update locally self._hvac_mode = hvac_mode @@ -306,7 +285,7 @@ class IntesisAC(ClimateEntity): await self._controller.set_fan_speed(self._device_id, fan_mode) # Updates can take longer than 2 seconds, so update locally - self._fan_speed = fan_mode + self._attr_fan_mode = fan_mode self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -328,14 +307,16 @@ class IntesisAC(ClimateEntity): """Copy values from controller dictionary to climate device.""" # Update values from controller's device dictionary self._connected = self._controller.is_connected - self._current_temp = self._controller.get_temperature(self._device_id) - self._fan_speed = self._controller.get_fan_speed(self._device_id) + self._attr_current_temperature = self._controller.get_temperature( + self._device_id + ) + self._attr_fan_mode = self._controller.get_fan_speed(self._device_id) self._power = self._controller.is_on(self._device_id) - self._min_temp = self._controller.get_min_setpoint(self._device_id) - self._max_temp = self._controller.get_max_setpoint(self._device_id) + self._attr_min_temp = self._controller.get_min_setpoint(self._device_id) + self._attr_max_temp = self._controller.get_max_setpoint(self._device_id) self._rssi = self._controller.get_rssi(self._device_id) self._run_hours = self._controller.get_run_hours(self._device_id) - self._target_temp = self._controller.get_setpoint(self._device_id) + self._attr_target_temperature = self._controller.get_setpoint(self._device_id) self._outdoor_temp = self._controller.get_outdoor_temperature(self._device_id) # Operation mode @@ -344,7 +325,7 @@ class IntesisAC(ClimateEntity): # Preset mode preset = self._controller.get_preset_mode(self._device_id) - self._preset = MAP_IH_TO_PRESET_MODE.get(preset) + self._attr_preset_mode = MAP_IH_TO_PRESET_MODE.get(preset) # Swing mode # Climate module only supports one swing setting. @@ -364,12 +345,11 @@ class IntesisAC(ClimateEntity): await self._controller.stop() @property - def icon(self): + def icon(self) -> str | None: """Return the icon for the current state.""" - icon = None if self._power: - icon = MAP_STATE_ICONS.get(self._hvac_mode) - return icon + return MAP_STATE_ICONS.get(self._hvac_mode) + return None async def async_update_callback(self, device_id=None): """Let HA know there has been an update from the controller.""" @@ -405,22 +385,7 @@ class IntesisAC(ClimateEntity): self.async_schedule_update_ha_state(True) @property - def min_temp(self): - """Return the minimum temperature for the current mode of operation.""" - return self._min_temp - - @property - def max_temp(self): - """Return the maximum temperature for the current mode of operation.""" - return self._max_temp - - @property - def fan_mode(self): - """Return whether the fan is on.""" - return self._fan_speed - - @property - def swing_mode(self): + def swing_mode(self) -> str: """Return current swing mode.""" if self._vvane == IH_SWING_SWING and self._hvane == IH_SWING_SWING: swing = SWING_BOTH @@ -432,34 +397,14 @@ class IntesisAC(ClimateEntity): swing = SWING_OFF return swing - @property - def fan_modes(self): - """List of available fan modes.""" - return self._fan_modes - - @property - def swing_modes(self): - """List of available swing positions.""" - return self._swing_list - @property def available(self) -> bool: """If the device hasn't been able to connect, mark as unavailable.""" return self._connected or self._connected is None - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temp - @property def hvac_mode(self) -> HVACMode: """Return the current mode of operation if unit is on.""" if self._power: return self._hvac_mode return HVACMode.OFF - - @property - def target_temperature(self): - """Return the current setpoint temperature if unit is on.""" - return self._target_temp From f9000ae08c69e1253d5544e62966fc78df4b30fe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:10:42 +0200 Subject: [PATCH 0672/1175] Use shorthand attributes in push camera (#145273) * Use shorthand attributes in push camera * Improve --- homeassistant/components/push/camera.py | 31 +++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 603fe89d542..7c1d37712bb 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -61,7 +61,7 @@ async def async_setup_platform( if PUSH_CAMERA_DATA not in hass.data: hass.data[PUSH_CAMERA_DATA] = {} - webhook_id = config.get(CONF_WEBHOOK_ID) + webhook_id = config[CONF_WEBHOOK_ID] cameras = [ PushCamera( @@ -101,16 +101,27 @@ async def handle_webhook( class PushCamera(Camera): """The representation of a Push camera.""" - def __init__(self, hass, name, buffer_size, timeout, image_field, webhook_id): + _attr_motion_detection_enabled = False + name: str + + def __init__( + self, + hass: HomeAssistant, + name: str, + buffer_size: int, + timeout: timedelta, + image_field: str, + webhook_id: str, + ) -> None: """Initialize push camera component.""" super().__init__() - self._name = name + self._attr_name = name self._last_trip = None self._filename = None self._expired_listener = None self._timeout = timeout - self.queue = deque([], buffer_size) - self._current_image = None + self.queue: deque[bytes] = deque([], buffer_size) + self._current_image: bytes | None = None self._image_field = image_field self.webhook_id = webhook_id self.webhook_url = webhook.async_generate_url(hass, webhook_id) @@ -171,16 +182,6 @@ class PushCamera(Camera): return self._current_image - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def motion_detection_enabled(self): - """Camera Motion Detection Status.""" - return False - @property def extra_state_attributes(self): """Return the state attributes.""" From 072bf75d7110569312046a62cebb71cfd1546ac0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:11:23 +0200 Subject: [PATCH 0673/1175] Improve type hints in homematic climate (#145283) --- homeassistant/components/homematic/climate.py | 30 ++++-------- homeassistant/components/homematic/const.py | 46 +++++++++---------- homeassistant/components/homematic/entity.py | 3 +- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 6e16e16ba99..28943774b6c 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -63,6 +63,11 @@ class HMThermostat(HMDevice, ClimateEntity): | ClimateEntityFeature.TURN_ON ) _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_min_temp = 4.5 + _attr_max_temp = 30.5 + _attr_target_temperature_step = 0.5 + + _state: str @property def hvac_mode(self) -> HVACMode: @@ -93,7 +98,7 @@ class HMThermostat(HMDevice, ClimateEntity): return [HVACMode.HEAT, HVACMode.OFF] @property - def preset_mode(self): + def preset_mode(self) -> str: """Return the current preset mode, e.g., home, away, temp.""" if self._data.get("BOOST_MODE", False): return "boost" @@ -110,7 +115,7 @@ class HMThermostat(HMDevice, ClimateEntity): return mode @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return a list of available preset modes.""" return [ HM_PRESET_MAP[mode] @@ -119,7 +124,7 @@ class HMThermostat(HMDevice, ClimateEntity): ] @property - def current_humidity(self): + def current_humidity(self) -> float | None: """Return the current humidity.""" for node in HM_HUMI_MAP: if node in self._data: @@ -127,7 +132,7 @@ class HMThermostat(HMDevice, ClimateEntity): return None @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" for node in HM_TEMP_MAP: if node in self._data: @@ -135,7 +140,7 @@ class HMThermostat(HMDevice, ClimateEntity): return None @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the target temperature.""" return self._data.get(self._state) @@ -164,21 +169,6 @@ class HMThermostat(HMDevice, ClimateEntity): elif preset_mode == PRESET_ECO: self._hmdevice.MODE = self._hmdevice.LOWERING_MODE - @property - def min_temp(self): - """Return the minimum temperature.""" - return 4.5 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 30.5 - - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - return 0.5 - @property def _hm_control_mode(self): """Return Control mode.""" diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 91ef2e90242..484ab5ada2a 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -215,31 +215,31 @@ HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { ] } -HM_ATTRIBUTE_SUPPORT = { - "LOWBAT": ["battery", {0: "High", 1: "Low"}], - "LOW_BAT": ["battery", {0: "High", 1: "Low"}], - "ERROR": ["error", {0: "No"}], - "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "RSSI_PEER": ["rssi_peer", {}], - "RSSI_DEVICE": ["rssi_device", {}], - "VALVE_STATE": ["valve", {}], - "LEVEL": ["level", {}], - "BATTERY_STATE": ["battery", {}], - "CONTROL_MODE": [ +HM_ATTRIBUTE_SUPPORT: dict[str, tuple[str, dict[int, str]]] = { + "LOWBAT": ("battery", {0: "High", 1: "Low"}), + "LOW_BAT": ("battery", {0: "High", 1: "Low"}), + "ERROR": ("error", {0: "No"}), + "ERROR_SABOTAGE": ("sabotage", {0: "No", 1: "Yes"}), + "SABOTAGE": ("sabotage", {0: "No", 1: "Yes"}), + "RSSI_PEER": ("rssi_peer", {}), + "RSSI_DEVICE": ("rssi_device", {}), + "VALVE_STATE": ("valve", {}), + "LEVEL": ("level", {}), + "BATTERY_STATE": ("battery", {}), + "CONTROL_MODE": ( "mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, - ], - "POWER": ["power", {}], - "CURRENT": ["current", {}], - "VOLTAGE": ["voltage", {}], - "OPERATING_VOLTAGE": ["voltage", {}], - "WORKING": ["working", {0: "No", 1: "Yes"}], - "STATE_UNCERTAIN": ["state_uncertain", {}], - "SENDERID": ["last_senderid", {}], - "SENDERADDRESS": ["last_senderaddress", {}], - "ERROR_ALARM_TEST": ["error_alarm_test", {0: "No", 1: "Yes"}], - "ERROR_SMOKE_CHAMBER": ["error_smoke_chamber", {0: "No", 1: "Yes"}], + ), + "POWER": ("power", {}), + "CURRENT": ("current", {}), + "VOLTAGE": ("voltage", {}), + "OPERATING_VOLTAGE": ("voltage", {}), + "WORKING": ("working", {0: "No", 1: "Yes"}), + "STATE_UNCERTAIN": ("state_uncertain", {}), + "SENDERID": ("last_senderid", {}), + "SENDERADDRESS": ("last_senderaddress", {}), + "ERROR_ALARM_TEST": ("error_alarm_test", {0: "No", 1: "Yes"}), + "ERROR_SMOKE_CHAMBER": ("error_smoke_chamber", {0: "No", 1: "Yes"}), } HM_PRESS_EVENTS = [ diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index bf029b2806d..3b5d2ebb509 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -5,6 +5,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import timedelta import logging +from typing import Any from pyhomematic import HMConnection from pyhomematic.devicetypes.generic import HMGeneric @@ -50,7 +51,7 @@ class HMDevice(Entity): self._channel = config.get(ATTR_CHANNEL) self._state = config.get(ATTR_PARAM) self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data: dict[str, str] = {} + self._data: dict[str, Any] = {} self._connected = False self._available = False self._channel_map: dict[str, str] = {} From 611d5be40a3958f1ee23ee14d5481d2fdcf17a59 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:12:20 +0200 Subject: [PATCH 0674/1175] Use shorthand attributes in touchline climate (#145292) --- homeassistant/components/touchline/climate.py | 52 +++++-------------- 1 file changed, 13 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 86526f4718b..971c83c2b39 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -67,6 +67,7 @@ class Touchline(ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] + _attr_preset_modes = list(PRESET_MODES) _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) @@ -75,52 +76,25 @@ class Touchline(ClimateEntity): def __init__(self, touchline_thermostat): """Initialize the Touchline device.""" self.unit = touchline_thermostat - self._name = None - self._current_temperature = None - self._target_temperature = None + self._attr_name = None self._current_operation_mode = None - self._preset_mode = None + self._attr_preset_mode = None def update(self) -> None: """Update thermostat attributes.""" self.unit.update() - self._name = self.unit.get_name() - self._current_temperature = self.unit.get_current_temperature() - self._target_temperature = self.unit.get_target_temperature() - self._preset_mode = TOUCHLINE_HA_PRESETS.get( + self._attr_name = self.unit.get_name() + self._attr_current_temperature = self.unit.get_current_temperature() + self._attr_target_temperature = self.unit.get_target_temperature() + self._attr_preset_mode = TOUCHLINE_HA_PRESETS.get( (self.unit.get_operation_mode(), self.unit.get_week_program()) ) - @property - def name(self): - """Return the name of the climate device.""" - return self._name - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def preset_mode(self): - """Return the current preset mode.""" - return self._preset_mode - - @property - def preset_modes(self): - """Return available preset modes.""" - return list(PRESET_MODES) - - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" - preset_mode = PRESET_MODES[preset_mode] - self.unit.set_operation_mode(preset_mode.mode) - self.unit.set_week_program(preset_mode.program) + preset = PRESET_MODES[preset_mode] + self.unit.set_operation_mode(preset.mode) + self.unit.set_week_program(preset.program) def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" @@ -129,5 +103,5 @@ class Touchline(ClimateEntity): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if kwargs.get(ATTR_TEMPERATURE) is not None: - self._target_temperature = kwargs.get(ATTR_TEMPERATURE) - self.unit.set_target_temperature(self._target_temperature) + self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) + self.unit.set_target_temperature(self._attr_target_temperature) From 0cd93e7e6500b864cb4535a591438d9d437aafa6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:15:04 +0200 Subject: [PATCH 0675/1175] Use shorthand attributes in vivotek camera (#145275) --- homeassistant/components/vivotek/camera.py | 73 +++++++--------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index a8bf652e963..c044e99a82e 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -62,85 +62,58 @@ def setup_platform( ) -> None: """Set up a Vivotek IP Camera.""" creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}" - args = { - "config": config, - "cam": VivotekCamera( - host=config[CONF_IP_ADDRESS], - port=(443 if config[CONF_SSL] else 80), - verify_ssl=config[CONF_VERIFY_SSL], - usr=config[CONF_USERNAME], - pwd=config[CONF_PASSWORD], - digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION, - sec_lvl=config[CONF_SECURITY_LEVEL], - ), - "stream_source": ( - f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}" - ), - } - add_entities([VivotekCam(**args)], True) + cam = VivotekCamera( + host=config[CONF_IP_ADDRESS], + port=(443 if config[CONF_SSL] else 80), + verify_ssl=config[CONF_VERIFY_SSL], + usr=config[CONF_USERNAME], + pwd=config[CONF_PASSWORD], + digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION, + sec_lvl=config[CONF_SECURITY_LEVEL], + ) + stream_source = ( + f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}" + ) + add_entities([VivotekCam(config, cam, stream_source)], True) class VivotekCam(Camera): """A Vivotek IP camera.""" + _attr_brand = DEFAULT_CAMERA_BRAND _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, config, cam, stream_source): + def __init__( + self, config: ConfigType, cam: VivotekCamera, stream_source: str + ) -> None: """Initialize a Vivotek camera.""" super().__init__() self._cam = cam - self._frame_interval = 1 / config[CONF_FRAMERATE] - self._motion_detection_enabled = False - self._model_name = None - self._name = config[CONF_NAME] + self._attr_frame_interval = 1 / config[CONF_FRAMERATE] + self._attr_name = config[CONF_NAME] self._stream_source = stream_source - @property - def frame_interval(self): - """Return the interval between frames of the mjpeg stream.""" - return self._frame_interval - def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" return self._cam.snapshot() - @property - def name(self): - """Return the name of this device.""" - return self._name - - async def stream_source(self): + async def stream_source(self) -> str: """Return the source of the stream.""" return self._stream_source - @property - def motion_detection_enabled(self): - """Return the camera motion detection status.""" - return self._motion_detection_enabled - def disable_motion_detection(self) -> None: """Disable motion detection in camera.""" response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0) - self._motion_detection_enabled = int(response) == 1 + self._attr_motion_detection_enabled = int(response) == 1 def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1) - self._motion_detection_enabled = int(response) == 1 - - @property - def brand(self): - """Return the camera brand.""" - return DEFAULT_CAMERA_BRAND - - @property - def model(self): - """Return the camera model.""" - return self._model_name + self._attr_motion_detection_enabled = int(response) == 1 def update(self) -> None: """Update entity status.""" - self._model_name = self._cam.model_name + self._attr_model = self._cam.model_name From 2e4226d7d3142c55bf62c1bb23cd1acccb6df6ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:25:22 +0200 Subject: [PATCH 0676/1175] Use shorthand attributes in venstar climate (#145294) --- homeassistant/components/venstar/climate.py | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index ade86e8dd71..a471dc9cfcd 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.climate import ( @@ -111,8 +113,11 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_AUTO] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF, HVACMode.AUTO] + _attr_preset_modes = [PRESET_NONE, PRESET_AWAY, HOLD_MODE_TEMPERATURE] _attr_precision = PRECISION_HALVES _attr_name = None + _attr_min_humidity = 0 # Hardcoded to 0 in API. + _attr_max_humidity = 60 # Hardcoded to 60 in API. def __init__( self, @@ -155,12 +160,12 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return UnitOfTemperature.CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._client.get_indoor_temp() @property - def current_humidity(self): + def current_humidity(self) -> float | None: """Return the current humidity.""" return self._client.get_indoor_humidity() @@ -187,14 +192,14 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return HVACAction.OFF @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the current fan mode.""" if self._client.fan == self._client.FAN_ON: return FAN_ON return FAN_AUTO @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" return { ATTR_FAN_STATE: self._client.fanstate, @@ -202,7 +207,7 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): } @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the target temperature we try to reach.""" if self._client.mode == self._client.MODE_HEAT: return self._client.heattemp @@ -211,36 +216,26 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return None @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound temp if auto mode is on.""" if self._client.mode == self._client.MODE_AUTO: return self._client.heattemp return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound temp if auto mode is on.""" if self._client.mode == self._client.MODE_AUTO: return self._client.cooltemp return None @property - def target_humidity(self): + def target_humidity(self) -> float | None: """Return the humidity we try to reach.""" return self._client.hum_setpoint @property - def min_humidity(self): - """Return the minimum humidity. Hardcoded to 0 in API.""" - return 0 - - @property - def max_humidity(self): - """Return the maximum humidity. Hardcoded to 60 in API.""" - return 60 - - @property - def preset_mode(self): + def preset_mode(self) -> str: """Return current preset.""" if self._client.away: return PRESET_AWAY @@ -248,11 +243,6 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): return HOLD_MODE_TEMPERATURE return PRESET_NONE - @property - def preset_modes(self): - """Return valid preset modes.""" - return [PRESET_NONE, PRESET_AWAY, HOLD_MODE_TEMPERATURE] - def _set_operation_mode(self, operation_mode: HVACMode): """Change the operation mode (internal).""" if operation_mode == HVACMode.HEAT: From 15915680b581177b33a1645c5c12c4c1c2e8d871 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:42:41 +0200 Subject: [PATCH 0677/1175] Use shorthand attributes in xs1 climate (#145298) * Use shorthand attributes in xs1 climate * Improve --- homeassistant/components/xs1/climate.py | 29 +++++++++---------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 3f44cb1504d..0747b2130bd 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -5,6 +5,8 @@ from __future__ import annotations from typing import Any from xs1_api_client.api_constants import ActuatorType +from xs1_api_client.device.actuator import XS1Actuator +from xs1_api_client.device.sensor import XS1Sensor from homeassistant.components.climate import ( ClimateEntity, @@ -19,9 +21,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ACTUATORS, DOMAIN, SENSORS from .entity import XS1DeviceEntity -MIN_TEMP = 8 -MAX_TEMP = 25 - def setup_platform( hass: HomeAssistant, @@ -30,8 +29,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 thermostat platform.""" - actuators = hass.data[DOMAIN][ACTUATORS] - sensors = hass.data[DOMAIN][SENSORS] + actuators: list[XS1Actuator] = hass.data[DOMAIN][ACTUATORS] + sensors: list[XS1Sensor] = hass.data[DOMAIN][SENSORS] thermostat_entities = [] for actuator in actuators: @@ -56,19 +55,21 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_min_temp = 8 + _attr_max_temp = 25 - def __init__(self, device, sensor): + def __init__(self, device: XS1Actuator, sensor: XS1Sensor) -> None: """Initialize the actuator.""" super().__init__(device) self.sensor = sensor @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self.device.name() @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if self.sensor is None: return None @@ -81,20 +82,10 @@ class XS1ThermostatEntity(XS1DeviceEntity, ClimateEntity): return self.device.unit() @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the current target temperature.""" return self.device.new_value() - @property - def min_temp(self): - """Return the minimum temperature.""" - return MIN_TEMP - - @property - def max_temp(self): - """Return the maximum temperature.""" - return MAX_TEMP - def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) From 7f9b454922bd7fddc3dacf1754ca04e05ae61166 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 10:44:34 +0200 Subject: [PATCH 0678/1175] Improve type hints in xs1 entities (#145299) --- homeassistant/components/xs1/entity.py | 4 +++- homeassistant/components/xs1/sensor.py | 14 ++++++++------ homeassistant/components/xs1/switch.py | 7 ++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/xs1/entity.py b/homeassistant/components/xs1/entity.py index c1ec43ec33c..61601066636 100644 --- a/homeassistant/components/xs1/entity.py +++ b/homeassistant/components/xs1/entity.py @@ -2,6 +2,8 @@ import asyncio +from xs1_api_client.device import XS1Device + from homeassistant.helpers.entity import Entity # Lock used to limit the amount of concurrent update requests @@ -13,7 +15,7 @@ UPDATE_LOCK = asyncio.Lock() class XS1DeviceEntity(Entity): """Representation of a base XS1 device.""" - def __init__(self, device): + def __init__(self, device: XS1Device) -> None: """Initialize the XS1 device.""" self.device = device diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py index 26c009b15ee..d1411fe540b 100644 --- a/homeassistant/components/xs1/sensor.py +++ b/homeassistant/components/xs1/sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations from xs1_api_client.api_constants import ActuatorType +from xs1_api_client.device.actuator import XS1Actuator +from xs1_api_client.device.sensor import XS1Sensor from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant @@ -20,8 +22,8 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 sensor platform.""" - sensors = hass.data[DOMAIN][SENSORS] - actuators = hass.data[DOMAIN][ACTUATORS] + sensors: list[XS1Sensor] = hass.data[DOMAIN][SENSORS] + actuators: list[XS1Actuator] = hass.data[DOMAIN][ACTUATORS] sensor_entities = [] for sensor in sensors: @@ -35,16 +37,16 @@ def setup_platform( break if not belongs_to_climate_actuator: - sensor_entities.append(XS1Sensor(sensor)) + sensor_entities.append(XS1SensorEntity(sensor)) add_entities(sensor_entities) -class XS1Sensor(XS1DeviceEntity, SensorEntity): +class XS1SensorEntity(XS1DeviceEntity, SensorEntity): """Representation of a Sensor.""" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self.device.name() @@ -54,6 +56,6 @@ class XS1Sensor(XS1DeviceEntity, SensorEntity): return self.device.value() @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self.device.unit() diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py index 5e107099515..232bd590c61 100644 --- a/homeassistant/components/xs1/switch.py +++ b/homeassistant/components/xs1/switch.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from xs1_api_client.api_constants import ActuatorType +from xs1_api_client.device.actuator import XS1Actuator from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant @@ -22,7 +23,7 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the XS1 switch platform.""" - actuators = hass.data[DOMAIN][ACTUATORS] + actuators: list[XS1Actuator] = hass.data[DOMAIN][ACTUATORS] add_entities( XS1SwitchEntity(actuator) @@ -36,12 +37,12 @@ class XS1SwitchEntity(XS1DeviceEntity, SwitchEntity): """Representation of a XS1 switch actuator.""" @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self.device.name() @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self.device.value() == 100 From c3fe5f012e4ed60122f43d4f0551bdf66e7b438e Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Tue, 20 May 2025 21:09:46 +1200 Subject: [PATCH 0679/1175] add date and time service to bosch_alarm (#142243) * add date and time service * update quality scale * add changes from review * fix issues after merge * fix icons * apply changes from review * remove list from service schema * update quality scale * update strings * Update homeassistant/components/bosch_alarm/services.py Co-authored-by: Joost Lekkerkerker * apply changes from review * apply changes from review * Update tests/components/bosch_alarm/test_services.py Co-authored-by: Joost Lekkerkerker * validate exception messages * use schema to validate service call * update docstring * update error message --------- Co-authored-by: Joost Lekkerkerker --- .../components/bosch_alarm/__init__.py | 14 +- .../bosch_alarm/alarm_control_panel.py | 2 +- homeassistant/components/bosch_alarm/const.py | 5 +- .../components/bosch_alarm/diagnostics.py | 2 +- .../components/bosch_alarm/icons.json | 5 + .../components/bosch_alarm/quality_scale.yaml | 10 +- .../components/bosch_alarm/services.py | 76 +++++++ .../components/bosch_alarm/services.yaml | 12 ++ .../components/bosch_alarm/strings.json | 28 +++ homeassistant/components/bosch_alarm/types.py | 7 + tests/components/bosch_alarm/test_services.py | 192 ++++++++++++++++++ 11 files changed, 339 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/bosch_alarm/services.py create mode 100644 homeassistant/components/bosch_alarm/services.yaml create mode 100644 homeassistant/components/bosch_alarm/types.py create mode 100644 tests/components/bosch_alarm/test_services.py diff --git a/homeassistant/components/bosch_alarm/__init__.py b/homeassistant/components/bosch_alarm/__init__.py index 410adbd8d51..7f37476f1bb 100644 --- a/homeassistant/components/bosch_alarm/__init__.py +++ b/homeassistant/components/bosch_alarm/__init__.py @@ -6,14 +6,18 @@ from ssl import SSLError from bosch_alarm_mode2 import Panel -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.typing import ConfigType from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN +from .services import setup_services +from .types import BoschAlarmConfigEntry + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) PLATFORMS: list[Platform] = [ Platform.ALARM_CONTROL_PANEL, @@ -22,7 +26,11 @@ PLATFORMS: list[Platform] = [ Platform.SWITCH, ] -type BoschAlarmConfigEntry = ConfigEntry[Panel] + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up bosch alarm services.""" + setup_services(hass) + return True async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool: diff --git a/homeassistant/components/bosch_alarm/alarm_control_panel.py b/homeassistant/components/bosch_alarm/alarm_control_panel.py index 7115bae415a..60365070587 100644 --- a/homeassistant/components/bosch_alarm/alarm_control_panel.py +++ b/homeassistant/components/bosch_alarm/alarm_control_panel.py @@ -12,8 +12,8 @@ from homeassistant.components.alarm_control_panel import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import BoschAlarmConfigEntry from .entity import BoschAlarmAreaEntity +from .types import BoschAlarmConfigEntry async def async_setup_entry( diff --git a/homeassistant/components/bosch_alarm/const.py b/homeassistant/components/bosch_alarm/const.py index 7205831391c..33ec0ae526a 100644 --- a/homeassistant/components/bosch_alarm/const.py +++ b/homeassistant/components/bosch_alarm/const.py @@ -1,6 +1,9 @@ """Constants for the Bosch Alarm integration.""" DOMAIN = "bosch_alarm" -HISTORY_ATTR = "history" +ATTR_HISTORY = "history" CONF_INSTALLER_CODE = "installer_code" CONF_USER_CODE = "user_code" +ATTR_DATETIME = "datetime" +SERVICE_SET_DATE_TIME = "set_date_time" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" diff --git a/homeassistant/components/bosch_alarm/diagnostics.py b/homeassistant/components/bosch_alarm/diagnostics.py index 2e93052ea95..ea9988960b5 100644 --- a/homeassistant/components/bosch_alarm/diagnostics.py +++ b/homeassistant/components/bosch_alarm/diagnostics.py @@ -6,8 +6,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from . import BoschAlarmConfigEntry from .const import CONF_INSTALLER_CODE, CONF_USER_CODE +from .types import BoschAlarmConfigEntry TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD] diff --git a/homeassistant/components/bosch_alarm/icons.json b/homeassistant/components/bosch_alarm/icons.json index b13822fa711..c396350e37e 100644 --- a/homeassistant/components/bosch_alarm/icons.json +++ b/homeassistant/components/bosch_alarm/icons.json @@ -1,4 +1,9 @@ { + "services": { + "set_date_time": { + "service": "mdi:clock-edit" + } + }, "entity": { "sensor": { "alarms_gas": { diff --git a/homeassistant/components/bosch_alarm/quality_scale.yaml b/homeassistant/components/bosch_alarm/quality_scale.yaml index 5bbd1df0ebb..474dc348fd8 100644 --- a/homeassistant/components/bosch_alarm/quality_scale.yaml +++ b/homeassistant/components/bosch_alarm/quality_scale.yaml @@ -13,10 +13,7 @@ rules: config-flow-test-coverage: done config-flow: done dependency-transparency: done - docs-actions: - status: exempt - comment: | - No custom actions are defined. + docs-actions: done docs-high-level-description: done docs-installation-instructions: done docs-removal-instructions: done @@ -29,10 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: | - No custom actions are defined. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: todo docs-installation-parameters: todo diff --git a/homeassistant/components/bosch_alarm/services.py b/homeassistant/components/bosch_alarm/services.py new file mode 100644 index 00000000000..d9d6a1339a2 --- /dev/null +++ b/homeassistant/components/bosch_alarm/services.py @@ -0,0 +1,76 @@ +"""Services for the bosch_alarm integration.""" + +from __future__ import annotations + +import asyncio +import datetime as dt +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import config_validation as cv +from homeassistant.util import dt as dt_util + +from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME +from .types import BoschAlarmConfigEntry + + +def validate_datetime(value: Any) -> dt.datetime: + """Validate that a provided datetime is supported on a bosch alarm panel.""" + date_val = cv.datetime(value) + if date_val.year < 2010: + raise vol.RangeInvalid("datetime must be after 2009") + + if date_val.year > 2037: + raise vol.RangeInvalid("datetime must be before 2038") + + return date_val + + +SET_DATE_TIME_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, + vol.Optional(ATTR_DATETIME): validate_datetime, + } +) + + +def setup_services(hass: HomeAssistant) -> None: + """Set up the services for the bosch alarm integration.""" + + async def async_set_panel_date(call: ServiceCall) -> None: + """Set the date and time on a bosch alarm panel.""" + config_entry: BoschAlarmConfigEntry | None + value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now()) + entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + if not (config_entry := hass.config_entries.async_get_entry(entry_id)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="integration_not_found", + translation_placeholders={"target": entry_id}, + ) + if config_entry.state is not ConfigEntryState.LOADED: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="not_loaded", + translation_placeholders={"target": config_entry.title}, + ) + panel = config_entry.runtime_data + try: + await panel.set_panel_date(value) + except asyncio.InvalidStateError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + translation_placeholders={"target": config_entry.title}, + ) from err + + hass.services.async_register( + DOMAIN, + SERVICE_SET_DATE_TIME, + async_set_panel_date, + schema=SET_DATE_TIME_SCHEMA, + ) diff --git a/homeassistant/components/bosch_alarm/services.yaml b/homeassistant/components/bosch_alarm/services.yaml new file mode 100644 index 00000000000..a3e8d800005 --- /dev/null +++ b/homeassistant/components/bosch_alarm/services.yaml @@ -0,0 +1,12 @@ +set_date_time: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: bosch_alarm + datetime: + required: false + example: "2025-05-10 00:00:00" + selector: + datetime: diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 7a9d291a67f..40e0c315437 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -51,6 +51,18 @@ } }, "exceptions": { + "integration_not_found": { + "message": "Integration \"{target}\" not found in registry." + }, + "not_loaded": { + "message": "{target} is not loaded." + }, + "connection_error": { + "message": "Could not connect to \"{target}\"." + }, + "unknown_error": { + "message": "An unknown error occurred while setting the date and time on \"{target}\"." + }, "cannot_connect": { "message": "Could not connect to panel." }, @@ -61,6 +73,22 @@ "message": "Door cannot be manipulated while it is momentarily unlocked." } }, + "services": { + "set_date_time": { + "name": "Set date & time", + "description": "Sets the date and time on the alarm panel.", + "fields": { + "datetime": { + "name": "Date & time", + "description": "The date and time to set. The time zone of the Home Assistant instance is assumed. If omitted, the current date and time is used." + }, + "config_entry_id": { + "name": "Config entry", + "description": "The Bosch Alarm integration ID." + } + } + } + }, "entity": { "binary_sensor": { "panel_fault_battery_mising": { diff --git a/homeassistant/components/bosch_alarm/types.py b/homeassistant/components/bosch_alarm/types.py new file mode 100644 index 00000000000..7d45094b208 --- /dev/null +++ b/homeassistant/components/bosch_alarm/types.py @@ -0,0 +1,7 @@ +"""Types for the Bosch Alarm integration.""" + +from bosch_alarm_mode2 import Panel + +from homeassistant.config_entries import ConfigEntry + +type BoschAlarmConfigEntry = ConfigEntry[Panel] diff --git a/tests/components/bosch_alarm/test_services.py b/tests/components/bosch_alarm/test_services.py new file mode 100644 index 00000000000..7b5088f32c3 --- /dev/null +++ b/tests/components/bosch_alarm/test_services.py @@ -0,0 +1,192 @@ +"""Tests for Bosch Alarm component.""" + +import asyncio +from collections.abc import AsyncGenerator +import datetime as dt +from unittest.mock import AsyncMock, patch + +import pytest +import voluptuous as vol + +from homeassistant.components.bosch_alarm.const import ( + ATTR_CONFIG_ENTRY_ID, + ATTR_DATETIME, + DOMAIN, + SERVICE_SET_DATE_TIME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +async def platforms() -> AsyncGenerator[None]: + """Return the platforms to be loaded for this test.""" + with patch("homeassistant.components.bosch_alarm.PLATFORMS", []): + yield + + +async def test_set_date_time_service( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls succeed if the service call is valid.""" + await setup_integration(hass, mock_config_entry) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: dt_util.now(), + }, + blocking=True, + ) + mock_panel.set_panel_date.assert_called_once() + + +async def test_set_date_time_service_fails_bad_entity( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the service call is done for an incorrect entity.""" + await setup_integration(hass, mock_config_entry) + with pytest.raises( + ServiceValidationError, + match='Integration "bad-config_id" not found in registry', + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: "bad-config_id", + ATTR_DATETIME: dt_util.now(), + }, + blocking=True, + ) + + +async def test_set_date_time_service_fails_bad_params( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the service call is done with incorrect params.""" + await setup_integration(hass, mock_config_entry) + with pytest.raises( + vol.MultipleInvalid, + match=r"Invalid datetime specified: for dictionary value @ data\['datetime'\]", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: "", + }, + blocking=True, + ) + + +async def test_set_date_time_service_fails_bad_year_before( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the panel fails the service call.""" + await setup_integration(hass, mock_config_entry) + with pytest.raises( + vol.MultipleInvalid, + match=r"datetime must be before 2038 for dictionary value @ data\['datetime'\]", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: dt.datetime(2038, 1, 1), + }, + blocking=True, + ) + + +async def test_set_date_time_service_fails_bad_year_after( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the panel fails the service call.""" + await setup_integration(hass, mock_config_entry) + mock_panel.set_panel_date.side_effect = ValueError() + with pytest.raises( + vol.MultipleInvalid, + match=r"datetime must be after 2009 for dictionary value @ data\['datetime'\]", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: dt.datetime(2009, 1, 1), + }, + blocking=True, + ) + + +async def test_set_date_time_service_fails_connection_error( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the panel fails the service call.""" + await setup_integration(hass, mock_config_entry) + mock_panel.set_panel_date.side_effect = asyncio.InvalidStateError() + with pytest.raises( + HomeAssistantError, + match=f'Could not connect to "{mock_config_entry.title}"', + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: dt_util.now(), + }, + blocking=True, + ) + + +async def test_set_date_time_service_fails_unloaded( + hass: HomeAssistant, + mock_panel: AsyncMock, + area: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the service calls fail if the config entry is unloaded.""" + await async_setup_component(hass, DOMAIN, {}) + mock_config_entry.add_to_hass(hass) + with pytest.raises( + HomeAssistantError, + match=f"{mock_config_entry.title} is not loaded", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATE_TIME, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATETIME: dt_util.now(), + }, + blocking=True, + ) From f2233b3034c0d9323cff8e63ff008a5ade3d1c71 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:46:53 +0200 Subject: [PATCH 0680/1175] Refactor set_temperature in venstar climate (#145297) Clarify logic in venstar climate set_temperature --- homeassistant/components/venstar/climate.py | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index a471dc9cfcd..67fa08fcc12 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -258,32 +258,28 @@ class VenstarThermostat(VenstarEntity, ClimateEntity): _LOGGER.error("Failed to change the operation mode") return success - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" set_temp = True - operation_mode = kwargs.get(ATTR_HVAC_MODE) - temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - temperature = kwargs.get(ATTR_TEMPERATURE) + operation_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE) + temp_low: float | None = kwargs.get(ATTR_TARGET_TEMP_LOW) + temp_high: float | None = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temperature: float | None = kwargs.get(ATTR_TEMPERATURE) - if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: + client_mode = self._client.mode + if ( + operation_mode + and (new_mode := self._mode_map.get(operation_mode)) != client_mode + ): set_temp = self._set_operation_mode(operation_mode) + client_mode = new_mode if set_temp: - if ( - self._mode_map.get(operation_mode, self._client.mode) - == self._client.MODE_HEAT - ): + if client_mode == self._client.MODE_HEAT: success = self._client.set_setpoints(temperature, self._client.cooltemp) - elif ( - self._mode_map.get(operation_mode, self._client.mode) - == self._client.MODE_COOL - ): + elif client_mode == self._client.MODE_COOL: success = self._client.set_setpoints(self._client.heattemp, temperature) - elif ( - self._mode_map.get(operation_mode, self._client.mode) - == self._client.MODE_AUTO - ): + elif client_mode == self._client.MODE_AUTO: success = self._client.set_setpoints(temp_low, temp_high) else: success = False From 43ae0f2541996bf1784f65ad9add48a020402b87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:47:26 +0200 Subject: [PATCH 0681/1175] Use shorthand attributes in xiaomi_aqara (#145253) * Use shorthand attributes for is_on/is_locked in xiaomi_aqara * Use _attr_changed_by * Use _attr_device_class * Remove unused class variable * More --- .../components/xiaomi_aqara/binary_sensor.py | 73 ++++++++----------- .../components/xiaomi_aqara/entity.py | 12 +-- .../components/xiaomi_aqara/light.py | 13 +--- homeassistant/components/xiaomi_aqara/lock.py | 22 ++---- .../components/xiaomi_aqara/sensor.py | 2 +- .../components/xiaomi_aqara/switch.py | 13 +--- 6 files changed, 48 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 47cc823ad7f..c81d29729c9 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -140,20 +140,9 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): def __init__(self, device, name, xiaomi_hub, data_key, device_class, config_entry): """Initialize the XiaomiSmokeSensor.""" self._data_key = data_key - self._device_class = device_class - self._density = 0 + self._attr_device_class = device_class super().__init__(device, name, xiaomi_hub, config_entry) - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of binary sensor.""" - return self._device_class - def update(self) -> None: """Update the sensor state.""" _LOGGER.debug("Updating xiaomi sensor (%s) by polling", self._sid) @@ -180,7 +169,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -192,13 +181,13 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): return False if value in ("1", "2"): - if self._state: + if self._attr_is_on: return False - self._state = True + self._attr_is_on = True return True if value == "0": - if self._state: - self._state = False + if self._attr_is_on: + self._attr_is_on = False return True return False @@ -232,13 +221,13 @@ class XiaomiMotionSensor(XiaomiBinarySensor): def _async_set_no_motion(self, now): """Set state to False.""" self._unsub_set_no_motion = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway. @@ -274,7 +263,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor): if NO_MOTION in data: self._no_motion_since = data[NO_MOTION] - self._state = False + self._attr_is_on = False return True value = data.get(self._data_key) @@ -295,9 +284,9 @@ class XiaomiMotionSensor(XiaomiBinarySensor): ) self._no_motion_since = 0 - if self._state: + if self._attr_is_on: return False - self._state = True + self._attr_is_on = True return True return False @@ -335,7 +324,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): if (state := await self.async_get_last_state()) is None: return - self._state = state.state == "on" + self._attr_is_on = state.state == "on" def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -350,14 +339,14 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): if value == "open": self._attr_should_poll = True - if self._state: + if self._attr_is_on: return False - self._state = True + self._attr_is_on = True return True if value == "close": self._open_since = 0 - if self._state: - self._state = False + if self._attr_is_on: + self._attr_is_on = False return True return False @@ -385,7 +374,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -397,13 +386,13 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): if value == "leak": self._attr_should_poll = True - if self._state: + if self._attr_is_on: return False - self._state = True + self._attr_is_on = True return True if value == "no_leak": - if self._state: - self._state = False + if self._attr_is_on: + self._attr_is_on = False return True return False @@ -430,7 +419,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -441,13 +430,13 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): return False if value in ("1", "2"): - if self._state: + if self._attr_is_on: return False - self._state = True + self._attr_is_on = True return True if value == "0": - if self._state: - self._state = False + if self._attr_is_on: + self._attr_is_on = False return True return False @@ -472,7 +461,7 @@ class XiaomiVibration(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -512,7 +501,7 @@ class XiaomiButton(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" @@ -521,10 +510,10 @@ class XiaomiButton(XiaomiBinarySensor): return False if value == "long_click_press": - self._state = True + self._attr_is_on = True click_type = "long_click_press" elif value == "long_click_release": - self._state = False + self._attr_is_on = False click_type = "hold" elif value == "click": click_type = "single" @@ -576,7 +565,7 @@ class XiaomiCube(XiaomiBinarySensor): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() - self._state = False + self._attr_is_on = False def parse_data(self, data, raw_data): """Parse data sent by gateway.""" diff --git a/homeassistant/components/xiaomi_aqara/entity.py b/homeassistant/components/xiaomi_aqara/entity.py index 59107984ddf..11c20db8b0b 100644 --- a/homeassistant/components/xiaomi_aqara/entity.py +++ b/homeassistant/components/xiaomi_aqara/entity.py @@ -26,7 +26,6 @@ class XiaomiDevice(Entity): def __init__(self, device, device_type, xiaomi_hub, config_entry): """Initialize the Xiaomi device.""" - self._state = None self._is_available = True self._sid = device["sid"] self._model = device["model"] @@ -36,7 +35,7 @@ class XiaomiDevice(Entity): self._type = device_type self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub - self._extra_state_attributes = {} + self._attr_extra_state_attributes = {} self._remove_unavailability_tracker = None self._xiaomi_hub = xiaomi_hub self.parse_data(device["data"], device["raw_data"]) @@ -104,11 +103,6 @@ class XiaomiDevice(Entity): """Return True if entity is available.""" return self._is_available - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._extra_state_attributes - @callback def _async_set_unavailable(self, now): """Set state to UNAVAILABLE.""" @@ -154,11 +148,11 @@ class XiaomiDevice(Entity): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] - self._extra_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) + self._attr_extra_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 - self._extra_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1) + self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = round(percent, 1) return True def parse_data(self, data, raw_data): diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index b19719dc5dc..88b138eebfa 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -53,11 +53,6 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): super().__init__(device, name, xiaomi_hub, config_entry) - @property - def is_on(self): - """Return true if it is on.""" - return self._state - def parse_data(self, data, raw_data): """Parse data sent by gateway.""" value = data.get(self._data_key) @@ -65,7 +60,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): return False if value == 0: - self._state = False + self._attr_is_on = False return True rgbhexstr = f"{value:x}" @@ -84,7 +79,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): self._brightness = brightness self._hs = color_util.color_RGB_to_hs(*rgb) - self._state = True + self._attr_is_on = True return True @property @@ -111,11 +106,11 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): rgbhex = int(rgbhex_str, 16) if self._write_to_hub(self._sid, **{self._data_key: rgbhex}): - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if self._write_to_hub(self._sid, **{self._data_key: 0}): - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index b3f4e9f4caf..16686983230 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.lock import LockEntity, LockState +from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -40,23 +40,11 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): def __init__(self, device, name, xiaomi_hub, config_entry): """Initialize the XiaomiAqaraLock.""" - self._changed_by = 0 + self._attr_changed_by = "0" self._verified_wrong_times = 0 super().__init__(device, name, xiaomi_hub, config_entry) - @property - def is_locked(self) -> bool | None: - """Return true if lock is locked.""" - if self._state is not None: - return self._state == LockState.LOCKED - return None - - @property - def changed_by(self) -> str: - """Last change triggered by.""" - return self._changed_by - @property def extra_state_attributes(self) -> dict[str, int]: """Return the state attributes.""" @@ -65,7 +53,7 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): @callback def clear_unlock_state(self, _): """Clear unlock state automatically.""" - self._state = LockState.LOCKED + self._attr_is_locked = True self.async_write_ha_state() def parse_data(self, data, raw_data): @@ -76,9 +64,9 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): if (value := data.get(key)) is not None: - self._changed_by = int(value) + self._attr_changed_by = str(int(value)) self._verified_wrong_times = 0 - self._state = LockState.UNLOCKED + self._attr_is_locked = False async_call_later( self.hass, UNLOCK_MAINTAIN_TIME, self.clear_unlock_state ) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 59ccee5a1a8..1d686147d0c 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -206,7 +206,7 @@ class XiaomiBatterySensor(XiaomiDevice, SensorEntity): succeed = super().parse_voltage(data) if not succeed: return False - battery_level = int(self._extra_state_attributes.pop(ATTR_BATTERY_LEVEL)) + battery_level = int(self._attr_extra_state_attributes.pop(ATTR_BATTERY_LEVEL)) if battery_level <= 0 or battery_level > 100: return False self._attr_native_value = battery_level diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 7d3abf47bd1..1ac15fe148c 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -162,11 +162,6 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): return "mdi:power-plug" return "mdi:power-socket" - @property - def is_on(self): - """Return true if it is on.""" - return self._state - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -184,13 +179,13 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._write_to_hub(self._sid, **{self._data_key: "on"}): - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self._write_to_hub(self._sid, **{self._data_key: "off"}): - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() def parse_data(self, data, raw_data): @@ -213,9 +208,9 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): return False state = value == "on" - if self._state == state: + if self._attr_is_on == state: return False - self._state = state + self._attr_is_on = state return True def update(self) -> None: From 642dc5b49c6e900941c3afc413716e4add0f6456 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:47:49 +0200 Subject: [PATCH 0682/1175] Use shorthand attributes in rpi_camera camera (#145274) * Use shorthand attributes in rpi_camera camera * Improve --- homeassistant/components/rpi_camera/camera.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index a8ebaaaca6f..0e4bb40919c 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -7,6 +7,7 @@ import os import shutil import subprocess from tempfile import NamedTemporaryFile +from typing import Any from homeassistant.components.camera import Camera from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP @@ -87,11 +88,11 @@ def setup_platform( class RaspberryCamera(Camera): """Representation of a Raspberry Pi camera.""" - def __init__(self, device_info): + def __init__(self, device_info: dict[str, Any]) -> None: """Initialize Raspberry Pi camera component.""" super().__init__() - self._name = device_info[CONF_NAME] + self._attr_name = device_info[CONF_NAME] self._config = device_info # Kill if there's raspistill instance @@ -150,11 +151,6 @@ class RaspberryCamera(Camera): return file.read() @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def frame_interval(self): + def frame_interval(self) -> float: """Return the interval between frames of the stream.""" return self._config[CONF_TIMELAPSE] / 1000 From a8264ae8ae0c997bd9f91b06d23eb667400a55da Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:48:33 +0200 Subject: [PATCH 0683/1175] Mark button methods and properties as mandatory in pylint plugin (#145269) --- homeassistant/components/starline/button.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/starline/button.py b/homeassistant/components/starline/button.py index 1d238e232b9..fd449607f52 100644 --- a/homeassistant/components/starline/button.py +++ b/homeassistant/components/starline/button.py @@ -68,6 +68,6 @@ class StarlineButton(StarlineEntity, ButtonEntity): """Return True if entity is available.""" return super().available and self._device.online - def press(self): + def press(self) -> None: """Press the button.""" self._account.api.set_car_state(self._device.device_id, self._key, True) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index ea4bd75d667..fe0e664d546 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -941,12 +941,13 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { matches=[ TypeHintMatch( function_name="device_class", - return_type=["ButtonDeviceClass", "str", None], + return_type=["ButtonDeviceClass", None], ), TypeHintMatch( function_name="press", return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From cf6cb0bd39b95c2bcd09dbc9bf09875104062e1d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 20 May 2025 11:49:50 +0200 Subject: [PATCH 0684/1175] Fix typos in user-facing strings of `zha` (#145305) --- homeassistant/components/zha/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index d6a812569f5..05ee1f2ac7e 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -905,7 +905,7 @@ "name": "Fade time" }, "regulator_set_point": { - "name": "Regulator set point" + "name": "Regulator setpoint" }, "detection_delay": { "name": "Detection delay" @@ -1207,7 +1207,7 @@ "name": "Decoupled mode" }, "detection_sensitivity": { - "name": "Detection Sensitivity" + "name": "Detection sensitivity" }, "keypad_lockout": { "name": "Keypad lockout" @@ -1638,7 +1638,7 @@ "name": "Total power factor" }, "self_test_result": { - "name": "Self test result" + "name": "Self-test result" }, "lower_explosive_limit": { "name": "% Lower explosive limit" From 1f1fd8de878fa91097d6129af23c918eec93243e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:50:22 +0200 Subject: [PATCH 0685/1175] Mark alarm_control_panel methods and properties as mandatory in pylint plugin (#145270) --- pylint/plugins/hass_enforce_type_hints.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index fe0e664d546..44ec135c3a4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -840,10 +840,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="code_arm_required", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="supported_features", return_type="AlarmControlPanelEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="alarm_disarm", @@ -852,6 +854,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_arm_home", @@ -860,6 +863,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_arm_away", @@ -868,6 +872,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_arm_night", @@ -876,6 +881,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_arm_vacation", @@ -884,6 +890,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_trigger", @@ -892,6 +899,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="alarm_arm_custom_bypass", @@ -900,6 +908,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From c1da554eb156227c17f2ad61fe235e6e46978e70 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:50:31 +0200 Subject: [PATCH 0686/1175] Mark calendar methods and properties as mandatory in pylint plugin (#145271) --- pylint/plugins/hass_enforce_type_hints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 44ec135c3a4..60da232f938 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -985,6 +985,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { 3: "datetime", }, return_type="list[CalendarEvent]", + mandatory=True, ), ], ), From e39c8e350cc051dfb9ddf59c83e4ac242660c66d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 11:56:17 +0200 Subject: [PATCH 0687/1175] Add class init type hint to xiaomi_aqara (#145255) --- .../components/xiaomi_aqara/binary_sensor.py | 97 ++++++++++++++++--- .../components/xiaomi_aqara/cover.py | 11 ++- .../components/xiaomi_aqara/entity.py | 17 +++- .../components/xiaomi_aqara/light.py | 10 +- homeassistant/components/xiaomi_aqara/lock.py | 12 ++- .../components/xiaomi_aqara/sensor.py | 12 ++- .../components/xiaomi_aqara/switch.py | 16 +-- 7 files changed, 150 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index c81d29729c9..b7a6d7ba935 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -1,6 +1,9 @@ """Support for Xiaomi aqara binary sensors.""" import logging +from typing import Any + +from xiaomi_gateway import XiaomiGateway from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -137,7 +140,15 @@ async def async_setup_entry( class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): """Representation of a base XiaomiBinarySensor.""" - def __init__(self, device, name, xiaomi_hub, data_key, device_class, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + xiaomi_hub: XiaomiGateway, + data_key: str, + device_class: BinarySensorDeviceClass | None, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiSmokeSensor.""" self._data_key = data_key self._attr_device_class = device_class @@ -152,11 +163,21 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorEntity): class XiaomiNatgasSensor(XiaomiBinarySensor): """Representation of a XiaomiNatgasSensor.""" - def __init__(self, device, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiSmokeSensor.""" self._density = None super().__init__( - device, "Natgas Sensor", xiaomi_hub, "alarm", "gas", config_entry + device, + "Natgas Sensor", + xiaomi_hub, + "alarm", + BinarySensorDeviceClass.GAS, + config_entry, ) @property @@ -197,7 +218,13 @@ class XiaomiNatgasSensor(XiaomiBinarySensor): class XiaomiMotionSensor(XiaomiBinarySensor): """Representation of a XiaomiMotionSensor.""" - def __init__(self, device, hass, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + hass: HomeAssistant, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiMotionSensor.""" self._hass = hass self._no_motion_since = 0 @@ -207,7 +234,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor): else: data_key = "motion_status" super().__init__( - device, "Motion Sensor", xiaomi_hub, data_key, "motion", config_entry + device, + "Motion Sensor", + xiaomi_hub, + data_key, + BinarySensorDeviceClass.MOTION, + config_entry, ) @property @@ -295,7 +327,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor): class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): """Representation of a XiaomiDoorSensor.""" - def __init__(self, device, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiDoorSensor.""" self._open_since = 0 if "proto" not in device or int(device["proto"][0:1]) == 1: @@ -356,7 +393,12 @@ class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity): class XiaomiWaterLeakSensor(XiaomiBinarySensor): """Representation of a XiaomiWaterLeakSensor.""" - def __init__(self, device, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiWaterLeakSensor.""" if "proto" not in device or int(device["proto"][0:1]) == 1: data_key = "status" @@ -402,11 +444,21 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): class XiaomiSmokeSensor(XiaomiBinarySensor): """Representation of a XiaomiSmokeSensor.""" - def __init__(self, device, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiSmokeSensor.""" self._density = 0 super().__init__( - device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke", config_entry + device, + "Smoke Sensor", + xiaomi_hub, + "alarm", + BinarySensorDeviceClass.SMOKE, + config_entry, ) @property @@ -446,7 +498,14 @@ class XiaomiSmokeSensor(XiaomiBinarySensor): class XiaomiVibration(XiaomiBinarySensor): """Representation of a Xiaomi Vibration Sensor.""" - def __init__(self, device, name, data_key, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + data_key: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiVibration.""" self._last_action = None super().__init__(device, name, xiaomi_hub, data_key, None, config_entry) @@ -485,7 +544,15 @@ class XiaomiVibration(XiaomiBinarySensor): class XiaomiButton(XiaomiBinarySensor): """Representation of a Xiaomi Button.""" - def __init__(self, device, name, data_key, hass, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + data_key: str, + hass: HomeAssistant, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiButton.""" self._hass = hass self._last_action = None @@ -545,7 +612,13 @@ class XiaomiButton(XiaomiBinarySensor): class XiaomiCube(XiaomiBinarySensor): """Representation of a Xiaomi Cube.""" - def __init__(self, device, hass, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + hass: HomeAssistant, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the Xiaomi Cube.""" self._hass = hass self._last_action = None diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index 82d5129ac5e..ebab3344250 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -2,6 +2,8 @@ from typing import Any +from xiaomi_gateway import XiaomiGateway + from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -40,7 +42,14 @@ async def async_setup_entry( class XiaomiGenericCover(XiaomiDevice, CoverEntity): """Representation of a XiaomiGenericCover.""" - def __init__(self, device, name, data_key, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + data_key: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiGenericCover.""" self._data_key = data_key self._pos = 0 diff --git a/homeassistant/components/xiaomi_aqara/entity.py b/homeassistant/components/xiaomi_aqara/entity.py index 11c20db8b0b..3f640b67516 100644 --- a/homeassistant/components/xiaomi_aqara/entity.py +++ b/homeassistant/components/xiaomi_aqara/entity.py @@ -2,8 +2,11 @@ from datetime import timedelta import logging -from typing import Any +from typing import TYPE_CHECKING, Any +from xiaomi_gateway import XiaomiGateway + +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_MAC from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -24,7 +27,13 @@ class XiaomiDevice(Entity): _attr_should_poll = False - def __init__(self, device, device_type, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + device_type: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the Xiaomi device.""" self._is_available = True self._sid = device["sid"] @@ -50,6 +59,8 @@ class XiaomiDevice(Entity): if config_entry.data[CONF_MAC] == format_mac(self._sid): # this entity belongs to the gateway itself self._is_gateway = True + if TYPE_CHECKING: + assert config_entry.unique_id self._device_id = config_entry.unique_id else: # this entity is connected through zigbee @@ -86,6 +97,8 @@ class XiaomiDevice(Entity): model=self._model, ) else: + if TYPE_CHECKING: + assert self._gateway_id is not None device_info = DeviceInfo( connections={(dr.CONNECTION_ZIGBEE, self._device_id)}, identifiers={(DOMAIN, self._device_id)}, diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 88b138eebfa..47b9e5a6730 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -5,6 +5,8 @@ import logging import struct from typing import Any +from xiaomi_gateway import XiaomiGateway + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -45,7 +47,13 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): _attr_color_mode = ColorMode.HS _attr_supported_color_modes = {ColorMode.HS} - def __init__(self, device, name, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiGatewayLight.""" self._data_key = "rgb" self._hs = (0, 0) diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 16686983230..86d20a7024f 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -2,6 +2,10 @@ from __future__ import annotations +from typing import Any + +from xiaomi_gateway import XiaomiGateway + from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -38,7 +42,13 @@ async def async_setup_entry( class XiaomiAqaraLock(LockEntity, XiaomiDevice): """Representation of a XiaomiAqaraLock.""" - def __init__(self, device, name, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiAqaraLock.""" self._attr_changed_by = "0" self._verified_wrong_times = 0 diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 1d686147d0c..2855bf14a3f 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -3,6 +3,9 @@ from __future__ import annotations import logging +from typing import Any + +from xiaomi_gateway import XiaomiGateway from homeassistant.components.sensor import ( SensorDeviceClass, @@ -164,7 +167,14 @@ async def async_setup_entry( class XiaomiSensor(XiaomiDevice, SensorEntity): """Representation of a XiaomiSensor.""" - def __init__(self, device, name, data_key, xiaomi_hub, config_entry): + def __init__( + self, + device: dict[str, Any], + name: str, + data_key: str, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiSensor.""" self._data_key = data_key self.entity_description = SENSOR_TYPES[data_key] diff --git a/homeassistant/components/xiaomi_aqara/switch.py b/homeassistant/components/xiaomi_aqara/switch.py index 1ac15fe148c..e9e2c92314e 100644 --- a/homeassistant/components/xiaomi_aqara/switch.py +++ b/homeassistant/components/xiaomi_aqara/switch.py @@ -3,6 +3,8 @@ import logging from typing import Any +from xiaomi_gateway import XiaomiGateway + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -138,13 +140,13 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchEntity): def __init__( self, - device, - name, - data_key, - supports_power_consumption, - xiaomi_hub, - config_entry, - ): + device: dict[str, Any], + name: str, + data_key: str, + supports_power_consumption: bool, + xiaomi_hub: XiaomiGateway, + config_entry: ConfigEntry, + ) -> None: """Initialize the XiaomiPlug.""" self._data_key = data_key self._in_use = None From d15a1a671197b05316f386727519d90be4f0834f Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Tue, 20 May 2025 21:56:53 +1200 Subject: [PATCH 0688/1175] Tidy up service call for bosch_alarm (#145306) tidy up service call for bosch_alarm --- .../components/bosch_alarm/services.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/bosch_alarm/services.py b/homeassistant/components/bosch_alarm/services.py index d9d6a1339a2..5d9a5f5645f 100644 --- a/homeassistant/components/bosch_alarm/services.py +++ b/homeassistant/components/bosch_alarm/services.py @@ -38,36 +38,37 @@ SET_DATE_TIME_SCHEMA = vol.Schema( ) +async def async_set_panel_date(call: ServiceCall) -> None: + """Set the date and time on a bosch alarm panel.""" + config_entry: BoschAlarmConfigEntry | None + value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now()) + entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + if not (config_entry := call.hass.config_entries.async_get_entry(entry_id)): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="integration_not_found", + translation_placeholders={"target": entry_id}, + ) + if config_entry.state is not ConfigEntryState.LOADED: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="not_loaded", + translation_placeholders={"target": config_entry.title}, + ) + panel = config_entry.runtime_data + try: + await panel.set_panel_date(value) + except asyncio.InvalidStateError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + translation_placeholders={"target": config_entry.title}, + ) from err + + def setup_services(hass: HomeAssistant) -> None: """Set up the services for the bosch alarm integration.""" - async def async_set_panel_date(call: ServiceCall) -> None: - """Set the date and time on a bosch alarm panel.""" - config_entry: BoschAlarmConfigEntry | None - value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now()) - entry_id = call.data[ATTR_CONFIG_ENTRY_ID] - if not (config_entry := hass.config_entries.async_get_entry(entry_id)): - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="integration_not_found", - translation_placeholders={"target": entry_id}, - ) - if config_entry.state is not ConfigEntryState.LOADED: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="not_loaded", - translation_placeholders={"target": config_entry.title}, - ) - panel = config_entry.runtime_data - try: - await panel.set_panel_date(value) - except asyncio.InvalidStateError as err: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key="connection_error", - translation_placeholders={"target": config_entry.title}, - ) from err - hass.services.async_register( DOMAIN, SERVICE_SET_DATE_TIME, From f3f5fca0b90ea394f885ff5d523c91f173b7cbb8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 12:00:10 +0200 Subject: [PATCH 0689/1175] Mark turn_on/turn_off/toggle as mandatory in pylint plugin (#145249) * Mark turn_on/turn_off/toggle as mandatory in pylint plugin * Fixes --- homeassistant/components/rainbird/switch.py | 5 +++-- homeassistant/components/triggercmd/switch.py | 5 +++-- homeassistant/components/tuya/humidifier.py | 5 +++-- pylint/plugins/hass_enforce_type_hints.py | 3 +++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index f188350138e..5ba30d5803b 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from pyrainbird.exceptions import RainbirdApiException, RainbirdDeviceBusyException import voluptuous as vol @@ -91,7 +92,7 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) """Return state attributes.""" return {"zone": self._zone} - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" try: await self.coordinator.controller.irrigate_zone( @@ -111,7 +112,7 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) self.async_write_ha_state() await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" try: await self.coordinator.controller.stop_irrigation() diff --git a/homeassistant/components/triggercmd/switch.py b/homeassistant/components/triggercmd/switch.py index e04cf5ee7e8..e03ff333751 100644 --- a/homeassistant/components/triggercmd/switch.py +++ b/homeassistant/components/triggercmd/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import Any from triggercmd import client, ha @@ -59,13 +60,13 @@ class TRIGGERcmdSwitch(SwitchEntity): """Return True if hub is available.""" return self._switch.hub.online - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.trigger("on") self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self.trigger("off") self._attr_is_on = False diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 6c47148eeda..36fcf8f52aa 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from tuya_sharing import CustomerDevice, Manager @@ -165,11 +166,11 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): return round(self._current_humidity.scale_value(current_humidity)) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._send_command([{"code": self._switch_dpcode, "value": True}]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._send_command([{"code": self._switch_dpcode, "value": False}]) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 60da232f938..bc1af17f97a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -801,18 +801,21 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_off", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="toggle", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { From 1ff5dd8ef5b52e5606d6606272e9ff56fc4239ab Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Tue, 20 May 2025 22:26:41 +1200 Subject: [PATCH 0690/1175] Fix issues with bosch alarm dhcp discovery (#145034) * fix issues with checking mac address for panels added manually * add test * don't allow discovery to pick up a host twice * make sure we validate tests without a mac address * check entry is loaded * Update config_flow.py * apply changes from review * assert unique id * assert unique id --- .../components/bosch_alarm/config_flow.py | 18 ++++- tests/components/bosch_alarm/conftest.py | 15 ++-- .../snapshots/test_diagnostics.ambr | 3 - .../bosch_alarm/test_config_flow.py | 78 +++++++++++++++++++ 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bosch_alarm/config_flow.py b/homeassistant/components/bosch_alarm/config_flow.py index 71e15f5959a..e492e2e7c14 100644 --- a/homeassistant/components/bosch_alarm/config_flow.py +++ b/homeassistant/components/bosch_alarm/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ( SOURCE_DHCP, SOURCE_RECONFIGURE, SOURCE_USER, + ConfigEntryState, ConfigFlow, ConfigFlowResult, ) @@ -152,7 +153,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_MAC] == self.mac: + if entry.data.get(CONF_MAC) == self.mac: result = self.hass.config_entries.async_update_entry( entry, data={ @@ -163,6 +164,21 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN): if result: self.hass.config_entries.async_schedule_reload(entry.entry_id) return self.async_abort(reason="already_configured") + if entry.data[CONF_HOST] == discovery_info.ip: + if ( + not entry.data.get(CONF_MAC) + and entry.state is ConfigEntryState.LOADED + ): + result = self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_MAC: self.mac, + }, + ) + if result: + self.hass.config_entries.async_schedule_reload(entry.entry_id) + return self.async_abort(reason="already_configured") try: # Use load_selector = 0 to fetch the panel model without authentication. (model, _) = await try_connect( diff --git a/tests/components/bosch_alarm/conftest.py b/tests/components/bosch_alarm/conftest.py index 283eb158d5c..01b6252229a 100644 --- a/tests/components/bosch_alarm/conftest.py +++ b/tests/components/bosch_alarm/conftest.py @@ -201,15 +201,16 @@ def mock_config_entry( mac_address: str | None, ) -> MockConfigEntry: """Mock config entry for bosch alarm.""" + data = { + CONF_HOST: "0.0.0.0", + CONF_PORT: 7700, + CONF_MODEL: "bosch_alarm_test_data.model", + } + if mac_address: + data[CONF_MAC] = format_mac(mac_address) return MockConfigEntry( domain=DOMAIN, unique_id=serial_number, entry_id="01JQ917ACKQ33HHM7YCFXYZX51", - data={ - CONF_HOST: "0.0.0.0", - CONF_PORT: 7700, - CONF_MODEL: "bosch_alarm_test_data.model", - CONF_MAC: mac_address and format_mac(mac_address), - } - | extra_config_entry_data, + data=data | extra_config_entry_data, ) diff --git a/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr b/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr index 670db709a1a..ad8b7cfbc38 100644 --- a/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr +++ b/tests/components/bosch_alarm/snapshots/test_diagnostics.ambr @@ -89,7 +89,6 @@ 'entry_data': dict({ 'host': '0.0.0.0', 'installer_code': '**REDACTED**', - 'mac': None, 'model': 'AMAX 3000', 'password': '**REDACTED**', 'port': 7700, @@ -185,7 +184,6 @@ }), 'entry_data': dict({ 'host': '0.0.0.0', - 'mac': None, 'model': 'B5512 (US1B)', 'password': '**REDACTED**', 'port': 7700, @@ -281,7 +279,6 @@ }), 'entry_data': dict({ 'host': '0.0.0.0', - 'mac': None, 'model': 'Solution 3000', 'port': 7700, 'user_code': '**REDACTED**', diff --git a/tests/components/bosch_alarm/test_config_flow.py b/tests/components/bosch_alarm/test_config_flow.py index afdd98bb1c0..d39bff935d5 100644 --- a/tests/components/bosch_alarm/test_config_flow.py +++ b/tests/components/bosch_alarm/test_config_flow.py @@ -309,6 +309,55 @@ async def test_dhcp_updates_host( assert mock_config_entry.data[CONF_HOST] == "4.5.6.7" +@pytest.mark.parametrize("serial_number", ["12345678"]) +async def test_dhcp_discovery_if_panel_setup_config_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_panel: AsyncMock, + serial_number: str, + model_name: str, + config_flow_data: dict[str, Any], +) -> None: + """Test DHCP discovery doesn't fail if a different panel was set up via config flow.""" + await setup_integration(hass, mock_config_entry) + + # change out the serial number so we can test discovery for a different panel + mock_panel.serial_number = "789101112" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="4.5.6.7", + macaddress="34ea34b43b5a", + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "auth" + assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + config_flow_data, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"Bosch {model_name}" + assert result["data"] == { + CONF_HOST: "4.5.6.7", + CONF_MAC: "34:ea:34:b4:3b:5a", + CONF_PORT: 7700, + CONF_MODEL: model_name, + **config_flow_data, + } + assert mock_config_entry.unique_id == serial_number + assert result["result"].unique_id == "789101112" + + @pytest.mark.parametrize("model", ["solution_3000", "amax_3000"]) async def test_dhcp_abort_ongoing_flow( hass: HomeAssistant, @@ -341,6 +390,35 @@ async def test_dhcp_abort_ongoing_flow( assert result["reason"] == "already_in_progress" +async def test_dhcp_updates_mac( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_panel: AsyncMock, + model_name: str, + serial_number: str, + config_flow_data: dict[str, Any], +) -> None: + """Test DHCP discovery flow updates mac if the previous entry did not have a mac address.""" + await setup_integration(hass, mock_config_entry) + assert CONF_MAC not in mock_config_entry.data + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="test", + ip="0.0.0.0", + macaddress="34ea34b43b5a", + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_MAC] == "34:ea:34:b4:3b:5a" + + async def test_reauth_flow_success( hass: HomeAssistant, mock_setup_entry: AsyncMock, From a3c0b83deeb2b446cc567bcf69b46de7fea89005 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 20 May 2025 20:33:49 +1000 Subject: [PATCH 0691/1175] Bump teslemetry_stream to 0.7.9 in Teslemetry (#145303) Bump stream to 0.7.9 --- homeassistant/components/teslemetry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 5b7454b87b6..855cdc9f364 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.7"] + "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2cd73d5cafe..421951e3957 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2900,7 +2900,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.7 +teslemetry-stream==0.7.9 # homeassistant.components.tessie tessie-api==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7aa4752ba5a..cafa586be5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2344,7 +2344,7 @@ tesla-powerwall==0.5.2 tesla-wall-connector==1.0.2 # homeassistant.components.teslemetry -teslemetry-stream==0.7.7 +teslemetry-stream==0.7.9 # homeassistant.components.tessie tessie-api==0.1.1 From fb0cb7cad66bc83550c2e1c54d584acc40d2e9c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 20 May 2025 13:16:27 +0200 Subject: [PATCH 0692/1175] Add Wh/km unit for energy distance (#145243) --- homeassistant/components/number/const.py | 2 +- homeassistant/components/sensor/const.py | 2 +- homeassistant/const.py | 1 + homeassistant/util/unit_conversion.py | 1 + tests/util/test_unit_conversion.py | 12 ++++++++++++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 6a5809610ee..2a9c4057168 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -176,7 +176,7 @@ class NumberDeviceClass(StrEnum): Use this device class for sensors measuring energy by distance, for example the amount of electric energy consumed by an electric car. - Unit of measurement: `kWh/100km`, `mi/kWh`, `km/kWh` + Unit of measurement: `kWh/100km`, `Wh/km`, `mi/kWh`, `km/kWh` """ ENERGY_STORAGE = "energy_storage" diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 31b33303dd4..c466bc52703 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -205,7 +205,7 @@ class SensorDeviceClass(StrEnum): Use this device class for sensors measuring energy by distance, for example the amount of electric energy consumed by an electric car. - Unit of measurement: `kWh/100km`, `mi/kWh`, `km/kWh` + Unit of measurement: `kWh/100km`, `Wh/km`, `mi/kWh`, `km/kWh` """ ENERGY_STORAGE = "energy_storage" diff --git a/homeassistant/const.py b/homeassistant/const.py index a3674d6e5d6..5b299fd0187 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -647,6 +647,7 @@ class UnitOfEnergyDistance(StrEnum): """Energy Distance units.""" KILO_WATT_HOUR_PER_100_KM = "kWh/100km" + WATT_HOUR_PER_KM = "Wh/km" MILES_PER_KILO_WATT_HOUR = "mi/kWh" KM_PER_KILO_WATT_HOUR = "km/kWh" diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 0355aa96aca..2ee7b5cd384 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -313,6 +313,7 @@ class EnergyDistanceConverter(BaseUnitConverter): UNIT_CLASS = "energy_distance" _UNIT_CONVERSION: dict[str | None, float] = { UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM: 1, + UnitOfEnergyDistance.WATT_HOUR_PER_KM: 10, UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR: 100 * _KM_TO_M / _MILE_TO_M, UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR: 100, } diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 885757b7eb4..0e9da5dbf3d 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -509,6 +509,18 @@ _CONVERTED_VALUE: dict[ 6.213712, UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR, ), + ( + 10, + UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, + 100, + UnitOfEnergyDistance.WATT_HOUR_PER_KM, + ), + ( + 15, + UnitOfEnergyDistance.WATT_HOUR_PER_KM, + 1.5, + UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, + ), ( 25, UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM, From 64d6101fb75137fb0bb668791e03d0b5370fbd08 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 13:30:22 +0200 Subject: [PATCH 0693/1175] Mark camera methods and properties as mandatory in pylint plugin (#145272) --- pylint/plugins/hass_enforce_type_hints.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index bc1af17f97a..bbc0c4b7972 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1008,18 +1008,22 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="entity_picture", return_type="str", + mandatory=True, ), TypeHintMatch( function_name="supported_features", return_type="CameraEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="is_recording", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="is_streaming", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="brand", @@ -1028,6 +1032,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="motion_detection_enabled", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="model", @@ -1036,6 +1041,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="frame_interval", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="frontend_stream_type", @@ -1044,6 +1050,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="available", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="async_create_stream", @@ -1076,6 +1083,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { 2: "float", }, return_type="StreamResponse", + mandatory=True, ), TypeHintMatch( function_name="handle_async_mjpeg_stream", @@ -1087,26 +1095,31 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="is_on", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="turn_off", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_on", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="enable_motion_detection", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="disable_motion_detection", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="async_handle_async_webrtc_offer", @@ -1116,6 +1129,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { 3: "WebRTCSendMessage", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="async_on_webrtc_candidate", @@ -1124,6 +1138,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { 2: "RTCIceCandidateInit", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="close_webrtc_session", @@ -1131,10 +1146,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { 1: "str", }, return_type=None, + mandatory=True, ), TypeHintMatch( function_name="_async_get_webrtc_client_configuration", return_type="WebRTCClientConfiguration", + mandatory=True, ), ], ), From 258c91d483dcda6a2babb908642df0c459a9bc31 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 20 May 2025 13:40:17 +0200 Subject: [PATCH 0694/1175] Mark climate methods and properties as mandatory in pylint plugin (#145280) * Mark climate methods and properties as mandatory in pylint plugin * One more --- homeassistant/components/airtouch4/climate.py | 6 +++--- homeassistant/components/econet/climate.py | 4 ++-- homeassistant/components/ephember/climate.py | 4 ++-- homeassistant/components/maxcube/climate.py | 4 ++-- homeassistant/components/nuheat/climate.py | 4 ++-- homeassistant/components/schluter/climate.py | 4 ++-- homeassistant/components/tuya/climate.py | 2 +- homeassistant/components/zhong_hong/climate.py | 4 ++-- pylint/plugins/hass_enforce_type_hints.py | 18 ++++++++++++++++++ 9 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 6d393ed0c99..3cb6a78128b 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -142,7 +142,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): return AT_TO_HA_STATE[self._airtouch.acs[self._ac_number].AcMode] @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" airtouch_modes = self._airtouch.GetSupportedCoolingModesForAc(self._ac_number) modes = [AT_TO_HA_STATE[mode] for mode in airtouch_modes] @@ -226,12 +226,12 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): return super()._handle_coordinator_update() @property - def min_temp(self): + def min_temp(self) -> float: """Return Minimum Temperature for AC of this group.""" return self._airtouch.acs[self._unit.BelongsToAc].MinSetpoint @property - def max_temp(self): + def max_temp(self) -> float: """Return Max Temperature for AC of this group.""" return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 69ca3a827ec..c5d45d75dcf 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -206,12 +206,12 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity): self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode]) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._econet.set_point_limits[0] @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._econet.set_point_limits[1] diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index efdd106b34b..8e72457f4a7 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -159,7 +159,7 @@ class EphEmberThermostat(ClimateEntity): self._ember.set_zone_target_temperature(self._zone["zoneid"], temperature) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" # Hot water temp doesn't support being changed if self._hot_water: @@ -168,7 +168,7 @@ class EphEmberThermostat(ClimateEntity): return 5.0 @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" if self._hot_water: return zone_target_temperature(self._zone) diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index 296da4f0ab4..69a0eb8a553 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -93,7 +93,7 @@ class MaxCubeClimate(ClimateEntity): ] @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" temp = self._device.min_temperature or MIN_TEMPERATURE # OFF_TEMPERATURE (always off) a is valid temperature to maxcube but not to Home Assistant. @@ -101,7 +101,7 @@ class MaxCubeClimate(ClimateEntity): return max(temp, MIN_TEMPERATURE) @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._device.max_temperature or MAX_TEMPERATURE diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 376a07ddb7b..85e24c116f9 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -130,7 +130,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return HVACAction.HEATING if self._thermostat.heating else HVACAction.IDLE @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum supported temperature for the thermostat.""" if self._temperature_unit == "C": return self._thermostat.min_celsius @@ -138,7 +138,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return self._thermostat.min_fahrenheit @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum supported temperature for the thermostat.""" if self._temperature_unit == "C": return self._thermostat.max_celsius diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 7db15d3923c..581140d9406 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -118,12 +118,12 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): return self.coordinator.data[self._serial_number].set_point_temp @property - def min_temp(self): + def min_temp(self) -> float: """Identify min_temp in Schluter API.""" return self.coordinator.data[self._serial_number].min_temp @property - def max_temp(self): + def max_temp(self) -> float: """Identify max_temp in Schluter API.""" return self.coordinator.data[self._serial_number].max_temp diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index deccb08c5aa..547f3a14c93 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -293,7 +293,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) self._send_command(commands) - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" commands = [{"code": DPCode.MODE, "value": preset_mode}] self._send_command(commands) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index af3287d3068..217636edbd5 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -216,12 +216,12 @@ class ZhongHongClimate(ClimateEntity): return self._device.fan_list @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._device.min_temp @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._device.max_temp diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index bbc0c4b7972..e92429d1620 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1171,10 +1171,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="precision", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="temperature_unit", return_type="str", + mandatory=True, ), TypeHintMatch( function_name="current_humidity", @@ -1191,6 +1193,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="hvac_modes", return_type="list[HVACMode]", + mandatory=True, ), TypeHintMatch( function_name="hvac_action", @@ -1249,6 +1252,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { kwargs_type="Any", return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_humidity", @@ -1257,6 +1261,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_fan_mode", @@ -1265,6 +1270,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_hvac_mode", @@ -1273,6 +1279,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_swing_mode", @@ -1281,6 +1288,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_preset_mode", @@ -1289,46 +1297,56 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_aux_heat_on", return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_aux_heat_off", return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_on", return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_off", return_type="None", has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="supported_features", return_type="ClimateEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="min_temp", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="max_temp", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="min_humidity", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="max_humidity", return_type="float", + mandatory=True, ), ], ), From e68cf80531861d67a00dee8154aa942b7d985ce3 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 20 May 2025 14:07:57 +0200 Subject: [PATCH 0695/1175] Make spelling of "setpoint" consistent in `opentherm_gw` (#145318) --- homeassistant/components/opentherm_gw/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index ae1a1eb9276..5d35311b69a 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -379,7 +379,7 @@ }, "set_central_heating_ovrd": { "name": "Set central heating override", - "description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a 'Set control set point' action with a value other than 0), the gateway automatically enables the central heating override to start heating. This action can then be used to control the central heating override status. To return control of the central heating to the thermostat, use the 'Set control set point' action with temperature value 0. You will only need this if you are writing your own software thermostat.", + "description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a 'Set control setpoint' action with a value other than 0), the gateway automatically enables the central heating override to start heating. This action can then be used to control the central heating override status. To return control of the central heating to the thermostat, use the 'Set control setpoint' action with temperature value 0. You will only need this if you are writing your own software thermostat.", "fields": { "gateway_id": { "name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]", @@ -410,7 +410,7 @@ } }, "set_control_setpoint": { - "name": "Set control set point", + "name": "Set control setpoint", "description": "Sets the central heating control setpoint override on the gateway. You will only need this if you are writing your own software thermostat.", "fields": { "gateway_id": { @@ -438,7 +438,7 @@ } }, "set_hot_water_setpoint": { - "name": "Set hot water set point", + "name": "Set hot water setpoint", "description": "Sets the domestic hot water setpoint on the gateway.", "fields": { "gateway_id": { From 0813adc3277baa0126a359a220f9c7970ee63976 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Wed, 21 May 2025 00:19:51 +1200 Subject: [PATCH 0696/1175] Update binary sensor translations for bosch_alarm (#145315) update binary sensor translations --- .../components/bosch_alarm/strings.json | 8 +- .../snapshots/test_binary_sensor.ambr | 144 +++++++++--------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/bosch_alarm/strings.json b/homeassistant/components/bosch_alarm/strings.json index 40e0c315437..76c15a0a5c7 100644 --- a/homeassistant/components/bosch_alarm/strings.json +++ b/homeassistant/components/bosch_alarm/strings.json @@ -104,16 +104,16 @@ "name": "Phone line failure" }, "panel_fault_sdi_fail_since_rps_hang_up": { - "name": "SDI failure since RPS hang up" + "name": "SDI failure since last RPS connection" }, "panel_fault_user_code_tamper_since_rps_hang_up": { - "name": "User code tamper since RPS hang up" + "name": "User code tamper since last RPS connection" }, "panel_fault_fail_to_call_rps_since_rps_hang_up": { - "name": "Failure to call RPS since RPS hang up" + "name": "Failure to call RPS since last RPS connection" }, "panel_fault_point_bus_fail_since_rps_hang_up": { - "name": "Point bus failure since RPS hang up" + "name": "Point bus failure since last RPS connection" }, "panel_fault_log_overflow": { "name": "Log overflow" diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr index 377a9e23426..da11b9d4692 100644 --- a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -332,7 +332,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -345,7 +345,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -357,7 +357,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Failure to call RPS since RPS hang up', + 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -366,13 +366,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bosch AMAX 3000 Failure to call RPS since RPS hang up', + 'friendly_name': 'Bosch AMAX 3000 Failure to call RPS since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_failure_to_call_rps_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -523,7 +523,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -536,7 +536,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -548,7 +548,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Point bus failure since RPS hang up', + 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -557,14 +557,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch AMAX 3000 Point bus failure since RPS hang up', + 'friendly_name': 'Bosch AMAX 3000 Point bus failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_point_bus_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -619,7 +619,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -632,7 +632,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -644,7 +644,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'SDI failure since RPS hang up', + 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -653,21 +653,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch AMAX 3000 SDI failure since RPS hang up', + 'friendly_name': 'Bosch AMAX 3000 SDI failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_sdi_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-entry] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -680,7 +680,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -692,7 +692,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'User code tamper since RPS hang up', + 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -701,14 +701,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up-state] +# name: test_binary_sensor[None-amax_3000][binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch AMAX 3000 User code tamper since RPS hang up', + 'friendly_name': 'Bosch AMAX 3000 User code tamper since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_amax_3000_user_code_tamper_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1330,7 +1330,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1343,7 +1343,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1355,7 +1355,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Failure to call RPS since RPS hang up', + 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -1364,13 +1364,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) Failure to call RPS since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_failure_to_call_rps_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1521,7 +1521,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1534,7 +1534,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1546,7 +1546,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Point bus failure since RPS hang up', + 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -1555,14 +1555,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) Point bus failure since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) Point bus failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_point_bus_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1617,7 +1617,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1630,7 +1630,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1642,7 +1642,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'SDI failure since RPS hang up', + 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -1651,21 +1651,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) SDI failure since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) SDI failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_sdi_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-entry] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1678,7 +1678,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1690,7 +1690,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'User code tamper since RPS hang up', + 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -1699,14 +1699,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up-state] +# name: test_binary_sensor[None-b5512][binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch B5512 (US1B) User code tamper since RPS hang up', + 'friendly_name': 'Bosch B5512 (US1B) User code tamper since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_b5512_us1b_user_code_tamper_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2328,7 +2328,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-entry] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2341,7 +2341,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2353,7 +2353,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Failure to call RPS since RPS hang up', + 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -2362,13 +2362,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up-state] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since RPS hang up', + 'friendly_name': 'Bosch Solution 3000 Failure to call RPS since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_failure_to_call_rps_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2519,7 +2519,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2532,7 +2532,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2544,7 +2544,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Point bus failure since RPS hang up', + 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -2553,14 +2553,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 Point bus failure since RPS hang up', + 'friendly_name': 'Bosch Solution 3000 Point bus failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_point_bus_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2615,7 +2615,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-entry] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2628,7 +2628,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2640,7 +2640,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'SDI failure since RPS hang up', + 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -2649,21 +2649,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up-state] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 SDI failure since RPS hang up', + 'friendly_name': 'Bosch Solution 3000 SDI failure since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_sdi_failure_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-entry] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2676,7 +2676,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2688,7 +2688,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'User code tamper since RPS hang up', + 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, 'supported_features': 0, @@ -2697,14 +2697,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up-state] +# name: test_binary_sensor[None-solution_3000][binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'Bosch Solution 3000 User code tamper since RPS hang up', + 'friendly_name': 'Bosch Solution 3000 User code tamper since last RPS connection', }), 'context': , - 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_rps_hang_up', + 'entity_id': 'binary_sensor.bosch_solution_3000_user_code_tamper_since_last_rps_connection', 'last_changed': , 'last_reported': , 'last_updated': , From b16d4dd94b64b2dcf20c19e896e04ed95f3df16d Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Tue, 20 May 2025 14:31:51 +0200 Subject: [PATCH 0697/1175] Use preferred spelling of "setpoint" in `smartthings` (#145319) * Use preferred spelling of "setpoint" in `smartthings` Change three occurrences of "set point" to "setpoint" to match the preferred spelling in Home Assistant. * Update test_sensor.ambr * Update test_sensor.ambr (2) --- .../components/smartthings/strings.json | 6 +-- .../smartthings/snapshots/test_sensor.ambr | 48 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 2c77f7b9fe0..607583c8941 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -405,7 +405,7 @@ } }, "oven_setpoint": { - "name": "Set point" + "name": "Setpoint" }, "energy_difference": { "name": "Energy difference" @@ -472,13 +472,13 @@ } }, "thermostat_cooling_setpoint": { - "name": "Cooling set point" + "name": "Cooling setpoint" }, "thermostat_fan_mode": { "name": "Fan mode" }, "thermostat_heating_setpoint": { - "name": "Heating set point" + "name": "Heating setpoint" }, "thermostat_mode": { "name": "Mode" diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 2884ded50af..f5fe09cc4d5 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -3635,7 +3635,7 @@ 'state': 'others', }) # --- -# name: test_all_entities[da_ks_microwave_0101x][sensor.microwave_set_point-entry] +# name: test_all_entities[da_ks_microwave_0101x][sensor.microwave_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3648,7 +3648,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.microwave_set_point', + 'entity_id': 'sensor.microwave_setpoint', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3660,7 +3660,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Set point', + 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, @@ -3669,15 +3669,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[da_ks_microwave_0101x][sensor.microwave_set_point-state] +# name: test_all_entities[da_ks_microwave_0101x][sensor.microwave_setpoint-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Microwave Set point', + 'friendly_name': 'Microwave Setpoint', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.microwave_set_point', + 'entity_id': 'sensor.microwave_setpoint', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4033,7 +4033,7 @@ 'state': 'bake', }) # --- -# name: test_all_entities[da_ks_oven_01061][sensor.oven_set_point-entry] +# name: test_all_entities[da_ks_oven_01061][sensor.oven_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4046,7 +4046,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.oven_set_point', + 'entity_id': 'sensor.oven_setpoint', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4058,7 +4058,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Set point', + 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, @@ -4067,15 +4067,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[da_ks_oven_01061][sensor.oven_set_point-state] +# name: test_all_entities[da_ks_oven_01061][sensor.oven_setpoint-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Oven Set point', + 'friendly_name': 'Oven Setpoint', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.oven_set_point', + 'entity_id': 'sensor.oven_setpoint', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4488,7 +4488,7 @@ 'state': 'bake', }) # --- -# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_set_point-entry] +# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4501,7 +4501,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.vulcan_set_point', + 'entity_id': 'sensor.vulcan_setpoint', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4513,7 +4513,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Set point', + 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, @@ -4522,15 +4522,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_set_point-state] +# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_setpoint-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Vulcan Set point', + 'friendly_name': 'Vulcan Setpoint', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.vulcan_set_point', + 'entity_id': 'sensor.vulcan_setpoint', 'last_changed': , 'last_reported': , 'last_updated': , @@ -11374,7 +11374,7 @@ 'state': 'cool', }) # --- -# name: test_all_entities[sensibo_airconditioner_1][sensor.office_cooling_set_point-entry] +# name: test_all_entities[sensibo_airconditioner_1][sensor.office_cooling_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -11387,7 +11387,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.office_cooling_set_point', + 'entity_id': 'sensor.office_cooling_setpoint', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -11399,7 +11399,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cooling set point', + 'original_name': 'Cooling setpoint', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, @@ -11408,15 +11408,15 @@ 'unit_of_measurement': , }) # --- -# name: test_all_entities[sensibo_airconditioner_1][sensor.office_cooling_set_point-state] +# name: test_all_entities[sensibo_airconditioner_1][sensor.office_cooling_setpoint-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Office Cooling set point', + 'friendly_name': 'Office Cooling setpoint', 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.office_cooling_set_point', + 'entity_id': 'sensor.office_cooling_setpoint', 'last_changed': , 'last_reported': , 'last_updated': , From 010b4f6b15957e0b2402c2296eb47247e1572986 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 20 May 2025 14:48:33 +0200 Subject: [PATCH 0698/1175] Remove deprecated aux heat from Climate Entity component (#145151) --- homeassistant/components/climate/__init__.py | 104 +------ homeassistant/components/climate/const.py | 3 - .../components/climate/services.yaml | 12 - .../components/climate/significant_change.py | 3 - homeassistant/components/climate/strings.json | 23 -- homeassistant/components/econet/climate.py | 1 - tests/components/climate/common.py | 27 -- tests/components/climate/test_init.py | 256 ------------------ .../climate/test_significant_change.py | 3 - 9 files changed, 2 insertions(+), 430 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 287a2397121..03acaa08294 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -18,23 +18,20 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue +from homeassistant.loader import async_suggest_report_issue from homeassistant.util.hass_dict import HassKey from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( # noqa: F401 - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -77,7 +74,6 @@ from .const import ( # noqa: F401 PRESET_HOME, PRESET_NONE, PRESET_SLEEP, - SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, @@ -168,12 +164,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_handle_set_preset_mode_service", [ClimateEntityFeature.PRESET_MODE], ) - component.async_register_entity_service( - SERVICE_SET_AUX_HEAT, - {vol.Required(ATTR_AUX_HEAT): cv.boolean}, - async_service_aux_heat, - [ClimateEntityFeature.AUX_HEAT], - ) component.async_register_entity_service( SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, @@ -239,7 +229,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = { "target_temperature_low", "preset_mode", "preset_modes", - "is_aux_heat", "fan_mode", "fan_modes", "swing_mode", @@ -279,7 +268,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_hvac_action: HVACAction | None = None _attr_hvac_mode: HVACMode | None _attr_hvac_modes: list[HVACMode] - _attr_is_aux_heat: bool | None _attr_max_humidity: float = DEFAULT_MAX_HUMIDITY _attr_max_temp: float _attr_min_humidity: float = DEFAULT_MIN_HUMIDITY @@ -299,52 +287,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): _attr_target_temperature: float | None = None _attr_temperature_unit: str - __climate_reported_legacy_aux = False - - def _report_legacy_aux(self) -> None: - """Log warning and create an issue if the entity implements legacy auxiliary heater.""" - - report_issue = async_suggest_report_issue( - self.hass, - integration_domain=self.platform.platform_name, - module=type(self).__module__, - ) - _LOGGER.warning( - ( - "%s::%s implements the `is_aux_heat` property or uses the auxiliary " - "heater methods in a subclass of ClimateEntity which is " - "deprecated and will be unsupported from Home Assistant 2025.4." - " Please %s" - ), - self.platform.platform_name, - self.__class__.__name__, - report_issue, - ) - - translation_placeholders = {"platform": self.platform.platform_name} - translation_key = "deprecated_climate_aux_no_url" - issue_tracker = async_get_issue_tracker( - self.hass, - integration_domain=self.platform.platform_name, - module=type(self).__module__, - ) - if issue_tracker: - translation_placeholders["issue_tracker"] = issue_tracker - translation_key = "deprecated_climate_aux_url_custom" - ir.async_create_issue( - self.hass, - DOMAIN, - f"deprecated_climate_aux_{self.platform.platform_name}", - breaks_in_ha_version="2025.4.0", - is_fixable=False, - is_persistent=False, - issue_domain=self.platform.platform_name, - severity=ir.IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders=translation_placeholders, - ) - self.__climate_reported_legacy_aux = True - @final @property def state(self) -> str | None: @@ -453,14 +395,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features: data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_mode - if ClimateEntityFeature.AUX_HEAT in supported_features: - data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF - if ( - self.__climate_reported_legacy_aux is False - and "custom_components" in type(self).__module__ - ): - self._report_legacy_aux() - return data @cached_property @@ -540,14 +474,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """ return self._attr_preset_modes - @cached_property - def is_aux_heat(self) -> bool | None: - """Return true if aux heater. - - Requires ClimateEntityFeature.AUX_HEAT. - """ - return self._attr_is_aux_heat - @cached_property def fan_mode(self) -> str | None: """Return the fan setting. @@ -732,22 +658,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Set new preset mode.""" await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) - def turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - raise NotImplementedError - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_on) - - def turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - raise NotImplementedError - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_off) - def turn_on(self) -> None: """Turn the entity on.""" raise NotImplementedError @@ -845,16 +755,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self._attr_max_humidity -async def async_service_aux_heat( - entity: ClimateEntity, service_call: ServiceCall -) -> None: - """Handle aux heat service.""" - if service_call.data[ATTR_AUX_HEAT]: - await entity.async_turn_aux_heat_on() - else: - await entity.async_turn_aux_heat_off() - - async def async_service_humidity_set( entity: ClimateEntity, service_call: ServiceCall ) -> None: diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index ecc0066cd93..7db80281635 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -96,7 +96,6 @@ class HVACAction(StrEnum): CURRENT_HVAC_ACTIONS = [cls.value for cls in HVACAction] -ATTR_AUX_HEAT = "aux_heat" ATTR_CURRENT_HUMIDITY = "current_humidity" ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_FAN_MODES = "fan_modes" @@ -128,7 +127,6 @@ DOMAIN = "climate" INTENT_SET_TEMPERATURE = "HassClimateSetTemperature" -SERVICE_SET_AUX_HEAT = "set_aux_heat" SERVICE_SET_FAN_MODE = "set_fan_mode" SERVICE_SET_PRESET_MODE = "set_preset_mode" SERVICE_SET_HUMIDITY = "set_humidity" @@ -147,7 +145,6 @@ class ClimateEntityFeature(IntFlag): FAN_MODE = 8 PRESET_MODE = 16 SWING_MODE = 32 - AUX_HEAT = 64 TURN_OFF = 128 TURN_ON = 256 SWING_HORIZONTAL_MODE = 512 diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 68421bf2386..fb5ba4f1796 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -1,17 +1,5 @@ # Describes the format for available climate services -set_aux_heat: - target: - entity: - domain: climate - supported_features: - - climate.ClimateEntityFeature.AUX_HEAT - fields: - aux_heat: - required: true - selector: - boolean: - set_preset_mode: target: entity: diff --git a/homeassistant/components/climate/significant_change.py b/homeassistant/components/climate/significant_change.py index 2b7e2c5d8b1..7bc42d5dbd5 100644 --- a/homeassistant/components/climate/significant_change.py +++ b/homeassistant/components/climate/significant_change.py @@ -12,7 +12,6 @@ from homeassistant.helpers.significant_change import ( ) from . import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -27,7 +26,6 @@ from . import ( ) SIGNIFICANT_ATTRIBUTES: set[str] = { - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -67,7 +65,6 @@ def async_check_significant_change( for attr_name in changed_attrs: if attr_name in [ - ATTR_AUX_HEAT, ATTR_FAN_MODE, ATTR_HVAC_ACTION, ATTR_PRESET_MODE, diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 250b2a67efe..bd6ed083650 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -36,9 +36,6 @@ "fan_only": "Fan only" }, "state_attributes": { - "aux_heat": { - "name": "Aux heat" - }, "current_humidity": { "name": "Current humidity" }, @@ -149,16 +146,6 @@ } }, "services": { - "set_aux_heat": { - "name": "Turn on/off auxiliary heater", - "description": "Turns auxiliary heater on/off.", - "fields": { - "aux_heat": { - "name": "Auxiliary heating", - "description": "New value of auxiliary heater." - } - } - }, "set_preset_mode": { "name": "Set preset mode", "description": "Sets preset mode.", @@ -267,16 +254,6 @@ } } }, - "issues": { - "deprecated_climate_aux_url_custom": { - "title": "The {platform} custom integration is using deprecated climate auxiliary heater", - "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease create a bug report at {issue_tracker}.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." - }, - "deprecated_climate_aux_no_url": { - "title": "[%key:component::climate::issues::deprecated_climate_aux_url_custom::title%]", - "description": "The custom integration `{platform}` implements the `is_aux_heat` property or uses the auxiliary heater methods in a subclass of ClimateEntity.\n\nPlease report it to the author of the {platform} integration.\n\nOnce an updated version of `{platform}` is available, install it and restart Home Assistant to fix this issue." - } - }, "exceptions": { "not_valid_preset_mode": { "message": "Preset mode {mode} is not valid. Valid preset modes are: {modes}." diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index c5d45d75dcf..81fc7ceb298 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -53,7 +53,6 @@ SUPPORT_FLAGS_THERMOSTAT = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.AUX_HEAT ) diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 8f5834d9180..ca214ec2d70 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -6,7 +6,6 @@ components. Instead call the service directly. from homeassistant.components.climate import ( _LOGGER, - ATTR_AUX_HEAT, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, @@ -16,7 +15,6 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, - SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, @@ -62,31 +60,6 @@ def set_preset_mode( hass.services.call(DOMAIN, SERVICE_SET_PRESET_MODE, data) -async def async_set_aux_heat( - hass: HomeAssistant, aux_heat: bool, entity_id: str = ENTITY_MATCH_ALL -) -> None: - """Turn all or specified climate devices auxiliary heater on.""" - data = {ATTR_AUX_HEAT: aux_heat} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - await hass.services.async_call(DOMAIN, SERVICE_SET_AUX_HEAT, data, blocking=True) - - -@bind_hass -def set_aux_heat( - hass: HomeAssistant, aux_heat: bool, entity_id: str = ENTITY_MATCH_ALL -) -> None: - """Turn all or specified climate devices auxiliary heater on.""" - data = {ATTR_AUX_HEAT: aux_heat} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) - - async def async_set_temperature( hass: HomeAssistant, temperature: float | None = None, diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 8900a9faefa..a81efa1640c 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -37,21 +37,14 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL_ON, ClimateEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from tests.common import ( MockConfigEntry, MockEntity, - MockModule, - MockPlatform, async_mock_service, - mock_integration, - mock_platform, setup_test_component_platform, ) @@ -500,255 +493,6 @@ async def test_sync_toggle(hass: HomeAssistant) -> None: assert climate.toggle.called -ISSUE_TRACKER = "https://blablabla.com" - - -@pytest.mark.parametrize( - ( - "manifest_extra", - "translation_key", - "translation_placeholders_extra", - "report", - "module", - ), - [ - ( - {}, - "deprecated_climate_aux_no_url", - {}, - "report it to the author of the 'test' custom integration", - "custom_components.test.climate", - ), - ( - {"issue_tracker": ISSUE_TRACKER}, - "deprecated_climate_aux_url_custom", - {"issue_tracker": ISSUE_TRACKER}, - "create a bug report at https://blablabla.com", - "custom_components.test.climate", - ), - ], -) -async def test_issue_aux_property_deprecated( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - config_flow_fixture: None, - manifest_extra: dict[str, str], - translation_key: str, - translation_placeholders_extra: dict[str, str], - report: str, - module: str, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the issue is raised on deprecated auxiliary heater attributes.""" - - class MockClimateEntityWithAux(MockClimateEntity): - """Mock climate class with mocked aux heater.""" - - _attr_supported_features = ( - ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE - ) - - @property - def is_aux_heat(self) -> bool | None: - """Return true if aux heater. - - Requires ClimateEntityFeature.AUX_HEAT. - """ - return True - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_on) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_off) - - # Fake the module is custom component or built in - MockClimateEntityWithAux.__module__ = module - - climate_entity = MockClimateEntityWithAux( - name="Testing", - entity_id="climate.testing", - ) - - async def async_setup_entry_init( - hass: HomeAssistant, config_entry: ConfigEntry - ) -> bool: - """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN]) - return True - - async def async_setup_entry_climate_platform( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, - ) -> None: - """Set up test weather platform via config entry.""" - async_add_entities([climate_entity]) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=async_setup_entry_init, - partial_manifest=manifest_extra, - ), - built_in=False, - ) - mock_platform( - hass, - "test.climate", - MockPlatform(async_setup_entry=async_setup_entry_climate_platform), - ) - - 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() - - assert climate_entity.state == HVACMode.HEAT - - issue = issue_registry.async_get_issue("climate", "deprecated_climate_aux_test") - assert issue - assert issue.issue_domain == "test" - assert issue.issue_id == "deprecated_climate_aux_test" - assert issue.translation_key == translation_key - assert ( - issue.translation_placeholders - == {"platform": "test"} | translation_placeholders_extra - ) - - assert ( - "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " - "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " - f"and will be unsupported from Home Assistant 2025.4. Please {report}" - ) in caplog.text - - # Assert we only log warning once - caplog.clear() - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - { - "entity_id": "climate.test", - "temperature": "25", - }, - blocking=True, - ) - await hass.async_block_till_done() - - assert ("implements the `is_aux_heat` property") not in caplog.text - - -@pytest.mark.parametrize( - ( - "manifest_extra", - "translation_key", - "translation_placeholders_extra", - "report", - "module", - ), - [ - ( - {"issue_tracker": ISSUE_TRACKER}, - "deprecated_climate_aux_url", - {"issue_tracker": ISSUE_TRACKER}, - "create a bug report at https://blablabla.com", - "homeassistant.components.test.climate", - ), - ], -) -async def test_no_issue_aux_property_deprecated_for_core( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, - manifest_extra: dict[str, str], - translation_key: str, - translation_placeholders_extra: dict[str, str], - report: str, - module: str, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the no issue on deprecated auxiliary heater attributes for core integrations.""" - - class MockClimateEntityWithAux(MockClimateEntity): - """Mock climate class with mocked aux heater.""" - - _attr_supported_features = ClimateEntityFeature.AUX_HEAT - - @property - def is_aux_heat(self) -> bool | None: - """Return true if aux heater. - - Requires ClimateEntityFeature.AUX_HEAT. - """ - return True - - async def async_turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_on) - - async def async_turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - await self.hass.async_add_executor_job(self.turn_aux_heat_off) - - # Fake the module is custom component or built in - MockClimateEntityWithAux.__module__ = module - - climate_entity = MockClimateEntityWithAux( - name="Testing", - entity_id="climate.testing", - ) - - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - assert climate_entity.state == HVACMode.HEAT - - issue = issue_registry.async_get_issue("climate", "deprecated_climate_aux_test") - assert not issue - - assert ( - "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " - "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " - f"and will be unsupported from Home Assistant 2024.10. Please {report}" - ) not in caplog.text - - -async def test_no_issue_no_aux_property( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - register_test_integration: MockConfigEntry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test the issue is raised on deprecated auxiliary heater attributes.""" - - climate_entity = MockClimateEntity( - name="Testing", - entity_id="climate.testing", - ) - - setup_test_component_platform( - hass, DOMAIN, entities=[climate_entity], from_config_entry=True - ) - assert await hass.config_entries.async_setup(register_test_integration.entry_id) - await hass.async_block_till_done() - - assert climate_entity.state == HVACMode.HEAT - - assert len(issue_registry.issues) == 0 - - assert ( - "test::MockClimateEntityWithAux implements the `is_aux_heat` property or uses " - "the auxiliary heater methods in a subclass of ClimateEntity which is deprecated " - "and will be unsupported from Home Assistant 2024.10." - ) not in caplog.text - - async def test_humidity_validation( hass: HomeAssistant, register_test_integration: MockConfigEntry, diff --git a/tests/components/climate/test_significant_change.py b/tests/components/climate/test_significant_change.py index 7d709090357..6fa53c306db 100644 --- a/tests/components/climate/test_significant_change.py +++ b/tests/components/climate/test_significant_change.py @@ -3,7 +3,6 @@ import pytest from homeassistant.components.climate import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -37,8 +36,6 @@ async def test_significant_state_change(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("unit_system", "old_attrs", "new_attrs", "expected_result"), [ - (METRIC, {ATTR_AUX_HEAT: "old_value"}, {ATTR_AUX_HEAT: "old_value"}, False), - (METRIC, {ATTR_AUX_HEAT: "old_value"}, {ATTR_AUX_HEAT: "new_value"}, True), (METRIC, {ATTR_FAN_MODE: "old_value"}, {ATTR_FAN_MODE: "old_value"}, False), (METRIC, {ATTR_FAN_MODE: "old_value"}, {ATTR_FAN_MODE: "new_value"}, True), ( From fc62bc5fc16e260f2415b66e6f834338f31d740a Mon Sep 17 00:00:00 2001 From: Joris Drenth Date: Tue, 20 May 2025 15:19:48 +0200 Subject: [PATCH 0699/1175] Add solar charging options to Wallbox integration (#139286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- homeassistant/components/wallbox/__init__.py | 8 +- homeassistant/components/wallbox/const.py | 11 ++ .../components/wallbox/coordinator.py | 36 ++++++ homeassistant/components/wallbox/icons.json | 5 + homeassistant/components/wallbox/select.py | 105 +++++++++++++++ homeassistant/components/wallbox/strings.json | 15 +++ tests/components/wallbox/__init__.py | 113 ++++++++++++++++ tests/components/wallbox/const.py | 1 + tests/components/wallbox/test_select.py | 122 ++++++++++++++++++ 9 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/wallbox/select.py create mode 100644 tests/components/wallbox/test_select.py diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index fc8c6e00e84..9336ab0e36b 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -12,7 +12,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DOMAIN, UPDATE_INTERVAL from .coordinator import InvalidAuth, WallboxCoordinator, async_validate_input -PLATFORMS = [Platform.LOCK, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index c38b8967776..dfa7fd5a4c1 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -38,6 +38,9 @@ CHARGER_STATE_OF_CHARGE_KEY = "state_of_charge" CHARGER_STATUS_ID_KEY = "status_id" CHARGER_STATUS_DESCRIPTION_KEY = "status_description" CHARGER_CONNECTIONS = "connections" +CHARGER_ECO_SMART_KEY = "ecosmart" +CHARGER_ECO_SMART_STATUS_KEY = "enabled" +CHARGER_ECO_SMART_MODE_KEY = "mode" class ChargerStatus(StrEnum): @@ -61,3 +64,11 @@ class ChargerStatus(StrEnum): WAITING_MID_SAFETY = "Waiting MID safety margin exceeded" WAITING_IN_QUEUE_ECO_SMART = "Waiting in queue by Eco-Smart" UNKNOWN = "Unknown" + + +class EcoSmartMode(StrEnum): + """Charger Eco mode select options.""" + + OFF = "off" + ECO_MODE = "eco_mode" + FULL_SOLAR = "full_solar" diff --git a/homeassistant/components/wallbox/coordinator.py b/homeassistant/components/wallbox/coordinator.py index 4f20f5c406d..60f062e57cc 100644 --- a/homeassistant/components/wallbox/coordinator.py +++ b/homeassistant/components/wallbox/coordinator.py @@ -19,6 +19,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CHARGER_CURRENCY_KEY, CHARGER_DATA_KEY, + CHARGER_ECO_SMART_KEY, + CHARGER_ECO_SMART_MODE_KEY, + CHARGER_ECO_SMART_STATUS_KEY, CHARGER_ENERGY_PRICE_KEY, CHARGER_FEATURES_KEY, CHARGER_LOCKED_UNLOCKED_KEY, @@ -33,6 +36,7 @@ from .const import ( DOMAIN, UPDATE_INTERVAL, ChargerStatus, + EcoSmartMode, ) _LOGGER = logging.getLogger(__name__) @@ -160,6 +164,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN ) + + # Set current solar charging mode + eco_smart_enabled = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ + CHARGER_ECO_SMART_STATUS_KEY + ] + eco_smart_mode = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][ + CHARGER_ECO_SMART_MODE_KEY + ] + if eco_smart_enabled is False: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF + elif eco_smart_mode == 0: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE + elif eco_smart_mode == 1: + data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR + return data async def _async_update_data(self) -> dict[str, Any]: @@ -241,6 +260,23 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.hass.async_add_executor_job(self._pause_charger, pause) await self.async_request_refresh() + @_require_authentication + def _set_eco_smart(self, option: str) -> None: + """Set wallbox solar charging mode.""" + + if option == EcoSmartMode.ECO_MODE: + self._wallbox.enableEcoSmart(self._station, 0) + elif option == EcoSmartMode.FULL_SOLAR: + self._wallbox.enableEcoSmart(self._station, 1) + else: + self._wallbox.disableEcoSmart(self._station) + + async def async_set_eco_smart(self, option: str) -> None: + """Set wallbox solar charging mode.""" + + await self.hass.async_add_executor_job(self._set_eco_smart, option) + await self.async_request_refresh() + class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/wallbox/icons.json b/homeassistant/components/wallbox/icons.json index 359e05cb441..d4495939d6d 100644 --- a/homeassistant/components/wallbox/icons.json +++ b/homeassistant/components/wallbox/icons.json @@ -1,5 +1,10 @@ { "entity": { + "select": { + "ecosmart": { + "default": "mdi:solar-power" + } + }, "sensor": { "charging_speed": { "default": "mdi:speedometer" diff --git a/homeassistant/components/wallbox/select.py b/homeassistant/components/wallbox/select.py new file mode 100644 index 00000000000..7ad7a135bc8 --- /dev/null +++ b/homeassistant/components/wallbox/select.py @@ -0,0 +1,105 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The switch component creates a switch entity.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from requests import HTTPError + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import ( + CHARGER_DATA_KEY, + CHARGER_ECO_SMART_KEY, + CHARGER_FEATURES_KEY, + CHARGER_PLAN_KEY, + CHARGER_POWER_BOOST_KEY, + CHARGER_SERIAL_NUMBER_KEY, + DOMAIN, + EcoSmartMode, +) +from .coordinator import WallboxCoordinator +from .entity import WallboxEntity + + +@dataclass(frozen=True, kw_only=True) +class WallboxSelectEntityDescription(SelectEntityDescription): + """Describes Wallbox select entity.""" + + current_option_fn: Callable[[WallboxCoordinator], str | None] + select_option_fn: Callable[[WallboxCoordinator, str], Awaitable[None]] + supported_fn: Callable[[WallboxCoordinator], bool] + + +SELECT_TYPES: dict[str, WallboxSelectEntityDescription] = { + CHARGER_ECO_SMART_KEY: WallboxSelectEntityDescription( + key=CHARGER_ECO_SMART_KEY, + translation_key=CHARGER_ECO_SMART_KEY, + options=[ + EcoSmartMode.OFF, + EcoSmartMode.ECO_MODE, + EcoSmartMode.FULL_SOLAR, + ], + select_option_fn=lambda coordinator, mode: coordinator.async_set_eco_smart( + mode + ), + current_option_fn=lambda coordinator: coordinator.data[CHARGER_ECO_SMART_KEY], + supported_fn=lambda coordinator: coordinator.data[CHARGER_DATA_KEY][ + CHARGER_PLAN_KEY + ][CHARGER_FEATURES_KEY].count(CHARGER_POWER_BOOST_KEY), + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Create wallbox select entities in HASS.""" + coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + WallboxSelect(coordinator, description) + for ent in coordinator.data + if ( + (description := SELECT_TYPES.get(ent)) + and description.supported_fn(coordinator) + ) + ) + + +class WallboxSelect(WallboxEntity, SelectEntity): + """Representation of the Wallbox portal.""" + + entity_description: WallboxSelectEntityDescription + + def __init__( + self, + coordinator: WallboxCoordinator, + description: WallboxSelectEntityDescription, + ) -> None: + """Initialize a Wallbox select entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" + + @property + def current_option(self) -> str | None: + """Return an option.""" + return self.entity_description.current_option_fn(self.coordinator) + + async def async_select_option(self, option: str) -> None: + """Handle the selection of an option.""" + try: + await self.entity_description.select_option_fn(self.coordinator, option) + except (ConnectionError, HTTPError) as e: + raise HomeAssistantError( + translation_key="api_failed", translation_domain=DOMAIN + ) from e + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index f4378b328d8..7f401981286 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -91,6 +91,21 @@ "pause_resume": { "name": "Pause/resume" } + }, + "select": { + "ecosmart": { + "name": "Solar charging", + "state": { + "off": "[%key:common::state::off%]", + "eco_mode": "Eco mode", + "full_solar": "Full solar" + } + } + } + }, + "exceptions": { + "api_failed": { + "message": "Error communicating with Wallbox API" } } } diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 9ec10dc72aa..d347777f7e8 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -2,6 +2,7 @@ from http import HTTPStatus +import requests import requests_mock from homeassistant.components.wallbox.const import ( @@ -12,6 +13,9 @@ from homeassistant.components.wallbox.const import ( CHARGER_CURRENCY_KEY, CHARGER_CURRENT_VERSION_KEY, CHARGER_DATA_KEY, + CHARGER_ECO_SMART_KEY, + CHARGER_ECO_SMART_MODE_KEY, + CHARGER_ECO_SMART_STATUS_KEY, CHARGER_ENERGY_PRICE_KEY, CHARGER_FEATURES_KEY, CHARGER_LOCKED_UNLOCKED_KEY, @@ -50,6 +54,10 @@ test_response = { CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, CHARGER_MAX_ICP_CURRENT_KEY: 20, CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, + CHARGER_ECO_SMART_KEY: { + CHARGER_ECO_SMART_STATUS_KEY: False, + CHARGER_ECO_SMART_MODE_KEY: 0, + }, }, } @@ -71,9 +79,89 @@ test_response_bidir = { CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, CHARGER_MAX_ICP_CURRENT_KEY: 20, CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, + CHARGER_ECO_SMART_KEY: { + CHARGER_ECO_SMART_STATUS_KEY: False, + CHARGER_ECO_SMART_MODE_KEY: 0, + }, }, } +test_response_eco_mode = { + CHARGER_CHARGING_POWER_KEY: 0, + CHARGER_STATUS_ID_KEY: 193, + CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0, + CHARGER_CHARGING_SPEED_KEY: 0, + CHARGER_ADDED_RANGE_KEY: 150, + CHARGER_ADDED_ENERGY_KEY: 44.697, + CHARGER_NAME_KEY: "WallboxName", + CHARGER_DATA_KEY: { + CHARGER_MAX_CHARGING_CURRENT_KEY: 24, + CHARGER_ENERGY_PRICE_KEY: 0.4, + CHARGER_LOCKED_UNLOCKED_KEY: False, + CHARGER_SERIAL_NUMBER_KEY: "20000", + CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", + CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, + CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, + CHARGER_MAX_ICP_CURRENT_KEY: 20, + CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, + CHARGER_ECO_SMART_KEY: { + CHARGER_ECO_SMART_STATUS_KEY: True, + CHARGER_ECO_SMART_MODE_KEY: 0, + }, + }, +} + + +test_response_full_solar = { + CHARGER_CHARGING_POWER_KEY: 0, + CHARGER_STATUS_ID_KEY: 193, + CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0, + CHARGER_CHARGING_SPEED_KEY: 0, + CHARGER_ADDED_RANGE_KEY: 150, + CHARGER_ADDED_ENERGY_KEY: 44.697, + CHARGER_NAME_KEY: "WallboxName", + CHARGER_DATA_KEY: { + CHARGER_MAX_CHARGING_CURRENT_KEY: 24, + CHARGER_ENERGY_PRICE_KEY: 0.4, + CHARGER_LOCKED_UNLOCKED_KEY: False, + CHARGER_SERIAL_NUMBER_KEY: "20000", + CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", + CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, + CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, + CHARGER_MAX_ICP_CURRENT_KEY: 20, + CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]}, + CHARGER_ECO_SMART_KEY: { + CHARGER_ECO_SMART_STATUS_KEY: True, + CHARGER_ECO_SMART_MODE_KEY: 1, + }, + }, +} + +test_response_no_power_boost = { + CHARGER_CHARGING_POWER_KEY: 0, + CHARGER_STATUS_ID_KEY: 193, + CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0, + CHARGER_CHARGING_SPEED_KEY: 0, + CHARGER_ADDED_RANGE_KEY: 150, + CHARGER_ADDED_ENERGY_KEY: 44.697, + CHARGER_NAME_KEY: "WallboxName", + CHARGER_DATA_KEY: { + CHARGER_MAX_CHARGING_CURRENT_KEY: 24, + CHARGER_ENERGY_PRICE_KEY: 0.4, + CHARGER_LOCKED_UNLOCKED_KEY: False, + CHARGER_SERIAL_NUMBER_KEY: "20000", + CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", + CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"}, + CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"}, + CHARGER_MAX_ICP_CURRENT_KEY: 20, + CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: []}, + }, +} + + +http_404_error = requests.exceptions.HTTPError() +http_404_error.response = requests.Response() +http_404_error.response.status_code = HTTPStatus.NOT_FOUND authorisation_response = { "data": { @@ -128,6 +216,31 @@ async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None await hass.async_block_till_done() +async def setup_integration_select( + hass: HomeAssistant, entry: MockConfigEntry, response +) -> None: + """Test wallbox sensor class setup.""" + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, + status_code=HTTPStatus.OK, + ) + mock_request.get( + "https://api.wall-box.com/chargers/status/12345", + json=response, + status_code=HTTPStatus.OK, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, + status_code=HTTPStatus.OK, + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + async def setup_integration_bidir(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Test wallbox sensor class setup.""" with requests_mock.Mocker() as mock_request: diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index a86ae9fc3b9..82c9e5169d5 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -15,3 +15,4 @@ MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.wallbox_wallboxname_charging_speed" MOCK_SENSOR_CHARGING_POWER_ID = "sensor.wallbox_wallboxname_charging_power" MOCK_SENSOR_MAX_AVAILABLE_POWER = "sensor.wallbox_wallboxname_max_available_power" MOCK_SWITCH_ENTITY_ID = "switch.wallbox_wallboxname_pause_resume" +MOCK_SELECT_ENTITY_ID = "select.wallbox_wallboxname_solar_charging" diff --git a/tests/components/wallbox/test_select.py b/tests/components/wallbox/test_select.py new file mode 100644 index 00000000000..516b1e87c27 --- /dev/null +++ b/tests/components/wallbox/test_select.py @@ -0,0 +1,122 @@ +"""Test Wallbox Select component.""" + +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY, EcoSmartMode +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, HomeAssistantError + +from . import ( + authorisation_response, + http_404_error, + setup_integration_select, + test_response, + test_response_eco_mode, + test_response_full_solar, + test_response_no_power_boost, +) +from .const import MOCK_SELECT_ENTITY_ID + +from tests.common import MockConfigEntry + +TEST_OPTIONS = [ + (EcoSmartMode.OFF, test_response), + (EcoSmartMode.ECO_MODE, test_response_eco_mode), + (EcoSmartMode.FULL_SOLAR, test_response_full_solar), +] + + +@pytest.fixture +def mock_authenticate(): + """Fixture to patch Wallbox methods.""" + with patch( + "homeassistant.components.wallbox.Wallbox.authenticate", + new=Mock(return_value=authorisation_response), + ): + yield + + +@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS) +async def test_wallbox_select_solar_charging_class( + hass: HomeAssistant, entry: MockConfigEntry, mode, response, mock_authenticate +) -> None: + """Test wallbox select class.""" + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.enableEcoSmart", + new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}), + ), + patch( + "homeassistant.components.wallbox.Wallbox.disableEcoSmart", + new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}), + ), + ): + await setup_integration_select(hass, entry, response) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID, + ATTR_OPTION: mode, + }, + blocking=True, + ) + + state = hass.states.get(MOCK_SELECT_ENTITY_ID) + assert state.state == mode + + +async def test_wallbox_select_no_power_boost_class( + hass: HomeAssistant, entry: MockConfigEntry +) -> None: + """Test wallbox select class.""" + + await setup_integration_select(hass, entry, test_response_no_power_boost) + + state = hass.states.get(MOCK_SELECT_ENTITY_ID) + assert state is None + + +@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS) +@pytest.mark.parametrize("error", [http_404_error, ConnectionError]) +async def test_wallbox_select_class_error( + hass: HomeAssistant, + entry: MockConfigEntry, + mode, + response, + error, + mock_authenticate, +) -> None: + """Test wallbox select class connection error.""" + + await setup_integration_select(hass, entry, response) + + with ( + patch( + "homeassistant.components.wallbox.Wallbox.disableEcoSmart", + new=Mock(side_effect=error), + ), + patch( + "homeassistant.components.wallbox.Wallbox.enableEcoSmart", + new=Mock(side_effect=error), + ), + pytest.raises(HomeAssistantError, match="Error communicating with Wallbox API"), + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID, + ATTR_OPTION: mode, + }, + blocking=True, + ) From 8e74f63d47ab5b30393d9c96c69a6c486951a4c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 20 May 2025 15:23:52 +0200 Subject: [PATCH 0700/1175] Create repair issue if not all add-ons or folders were backed up (#144999) * Create repair issue if not all add-ons or folders were backed up * Fix spelling * Fix _collect_errors * Make time patching by freezegun work with mashumaro * Addd test to hassio * Add fixture * Fix generating list of folders * Add issue creation tests * Include name of failing add-on in message * Improve code formatting * Rename AddonError to AddonErrorData --- homeassistant/components/backup/__init__.py | 2 + homeassistant/components/backup/manager.py | 107 +++++++--- homeassistant/components/backup/strings.json | 12 ++ homeassistant/components/hassio/backup.py | 46 +++++ tests/components/backup/conftest.py | 2 + tests/components/backup/test_manager.py | 186 +++++++++++++++++- .../backup_done_with_addon_folder_errors.json | 162 +++++++++++++++ tests/components/hassio/test_backup.py | 126 +++++++++++- tests/conftest.py | 47 +---- tests/patch_time.py | 43 ++++ 10 files changed, 660 insertions(+), 73 deletions(-) create mode 100644 tests/components/hassio/fixtures/backup_done_with_addon_folder_errors.json diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 124ce8b872c..9e013d72d60 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -23,6 +23,7 @@ from .const import DATA_MANAGER, DOMAIN from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator from .http import async_register_http_views from .manager import ( + AddonErrorData, BackupManager, BackupManagerError, BackupPlatformEvent, @@ -48,6 +49,7 @@ from .util import suggested_filename, suggested_filename_from_name_date from .websocket import async_register_websocket_handlers __all__ = [ + "AddonErrorData", "AddonInfo", "AgentBackup", "BackupAgent", diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 43a7be6db8d..39a7c60c3f1 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -106,11 +106,21 @@ class ManagerBackup(BaseBackup): with_automatic_settings: bool | None +@dataclass(frozen=True, kw_only=True, slots=True) +class AddonErrorData: + """Addon error class.""" + + name: str + errors: list[tuple[str, str]] + + @dataclass(frozen=True, kw_only=True, slots=True) class WrittenBackup: """Written backup class.""" + addon_errors: dict[str, AddonErrorData] backup: AgentBackup + folder_errors: dict[Folder, list[tuple[str, str]]] open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]] release_stream: Callable[[], Coroutine[Any, Any, None]] @@ -1208,7 +1218,9 @@ class BackupManager: backup_success = True if with_automatic_settings: - self._update_issue_after_agent_upload(agent_errors, unavailable_agents) + self._update_issue_after_agent_upload( + written_backup, agent_errors, unavailable_agents + ) # delete old backups more numerous than copies # try this regardless of agent errors above await delete_backups_exceeding_configured_count(self) @@ -1354,8 +1366,10 @@ class BackupManager: for subscription in self._backup_event_subscriptions: subscription(event) - def _update_issue_backup_failed(self) -> None: - """Update issue registry when a backup fails.""" + def _create_automatic_backup_failed_issue( + self, translation_key: str, translation_placeholders: dict[str, str] | None + ) -> None: + """Create an issue in the issue registry for automatic backup failures.""" ir.async_create_issue( self.hass, DOMAIN, @@ -1364,37 +1378,64 @@ class BackupManager: is_persistent=True, learn_more_url="homeassistant://config/backup", severity=ir.IssueSeverity.WARNING, - translation_key="automatic_backup_failed_create", + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + def _update_issue_backup_failed(self) -> None: + """Update issue registry when a backup fails.""" + self._create_automatic_backup_failed_issue( + "automatic_backup_failed_create", None ) def _update_issue_after_agent_upload( - self, agent_errors: dict[str, Exception], unavailable_agents: list[str] + self, + written_backup: WrittenBackup, + agent_errors: dict[str, Exception], + unavailable_agents: list[str], ) -> None: """Update issue registry after a backup is uploaded to agents.""" - if not agent_errors and not unavailable_agents: + + addon_errors = written_backup.addon_errors + failed_agents = unavailable_agents + [ + self.backup_agents[agent_id].name for agent_id in agent_errors + ] + folder_errors = written_backup.folder_errors + + if not failed_agents and not addon_errors and not folder_errors: + # No issues to report, clear previous error ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed") return - ir.async_create_issue( - self.hass, - DOMAIN, - "automatic_backup_failed", - is_fixable=False, - is_persistent=True, - learn_more_url="homeassistant://config/backup", - severity=ir.IssueSeverity.WARNING, - translation_key="automatic_backup_failed_upload_agents", - translation_placeholders={ - "failed_agents": ", ".join( - chain( - ( - self.backup_agents[agent_id].name - for agent_id in agent_errors - ), - unavailable_agents, - ) - ) - }, - ) + if (agent_errors or unavailable_agents) and not (addon_errors or folder_errors): + # No issues with add-ons or folders, but issues with agents + self._create_automatic_backup_failed_issue( + "automatic_backup_failed_upload_agents", + {"failed_agents": ", ".join(failed_agents)}, + ) + elif addon_errors and not (agent_errors or unavailable_agents or folder_errors): + # No issues with agents or folders, but issues with add-ons + self._create_automatic_backup_failed_issue( + "automatic_backup_failed_addons", + {"failed_addons": ", ".join(val.name for val in addon_errors.values())}, + ) + elif folder_errors and not (agent_errors or unavailable_agents or addon_errors): + # No issues with agents or add-ons, but issues with folders + self._create_automatic_backup_failed_issue( + "automatic_backup_failed_folders", + {"failed_folders": ", ".join(folder for folder in folder_errors)}, + ) + else: + # Issues with agents, add-ons, and/or folders + self._create_automatic_backup_failed_issue( + "automatic_backup_failed_agents_addons_folders", + { + "failed_agents": ", ".join(failed_agents) or "-", + "failed_addons": ( + ", ".join(val.name for val in addon_errors.values()) or "-" + ), + "failed_folders": ", ".join(f for f in folder_errors) or "-", + }, + ) async def async_can_decrypt_on_download( self, @@ -1677,7 +1718,11 @@ class CoreBackupReaderWriter(BackupReaderWriter): raise BackupReaderWriterError(str(err)) from err return WrittenBackup( - backup=backup, open_stream=open_backup, release_stream=remove_backup + addon_errors={}, + backup=backup, + folder_errors={}, + open_stream=open_backup, + release_stream=remove_backup, ) finally: # Inform integrations the backup is done @@ -1816,7 +1861,11 @@ class CoreBackupReaderWriter(BackupReaderWriter): await async_add_executor_job(temp_file.unlink, True) return WrittenBackup( - backup=backup, open_stream=open_backup, release_stream=remove_backup + addon_errors={}, + backup=backup, + folder_errors={}, + open_stream=open_backup, + release_stream=remove_backup, ) async def async_restore_backup( diff --git a/homeassistant/components/backup/strings.json b/homeassistant/components/backup/strings.json index 37adf9e9faf..bdd338835aa 100644 --- a/homeassistant/components/backup/strings.json +++ b/homeassistant/components/backup/strings.json @@ -11,6 +11,18 @@ "automatic_backup_failed_upload_agents": { "title": "Automatic backup could not be uploaded to the configured locations", "description": "The automatic backup could not be uploaded to the configured locations {failed_agents}. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + }, + "automatic_backup_failed_addons": { + "title": "Not all add-ons could be included in automatic backup", + "description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + }, + "automatic_backup_failed_agents_addons_folders": { + "title": "Automatic backup was created with errors", + "description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + }, + "automatic_backup_failed_folders": { + "title": "Not all folders could be included in automatic backup", + "description": "Folders {failed_folders} could not be included in automatic backup. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." } }, "services": { diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index 38bf3c82561..950ea910d0c 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -19,12 +19,14 @@ from aiohasupervisor.exceptions import ( ) from aiohasupervisor.models import ( backups as supervisor_backups, + jobs as supervisor_jobs, mounts as supervisor_mounts, ) from aiohasupervisor.models.backups import LOCATION_CLOUD_BACKUP, LOCATION_LOCAL_STORAGE from homeassistant.components.backup import ( DATA_MANAGER, + AddonErrorData, AddonInfo, AgentBackup, BackupAgent, @@ -401,6 +403,25 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): f"Backup failed: {create_errors or 'no backup_id'}" ) + # The backup was created successfully, check for non critical errors + full_status = await self._client.jobs.get_job(backup.job_id) + _addon_errors = _collect_errors( + full_status, "backup_store_addons", "backup_addon_save" + ) + addon_errors: dict[str, AddonErrorData] = {} + for slug, errors in _addon_errors.items(): + try: + addon_info = await self._client.addons.addon_info(slug) + addon_errors[slug] = AddonErrorData(name=addon_info.name, errors=errors) + except SupervisorError as err: + _LOGGER.debug("Error getting addon %s: %s", slug, err) + addon_errors[slug] = AddonErrorData(name=slug, errors=errors) + + _folder_errors = _collect_errors( + full_status, "backup_store_folders", "backup_folder_save" + ) + folder_errors = {Folder(key): val for key, val in _folder_errors.items()} + async def open_backup() -> AsyncIterator[bytes]: try: return await self._client.backups.download_backup(backup_id) @@ -430,7 +451,9 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): ) from err return WrittenBackup( + addon_errors=addon_errors, backup=_backup_details_to_agent_backup(details, locations[0]), + folder_errors=folder_errors, open_stream=open_backup, release_stream=remove_backup, ) @@ -474,7 +497,9 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): details = await self._client.backups.backup_info(backup_id) return WrittenBackup( + addon_errors={}, backup=_backup_details_to_agent_backup(details, locations[0]), + folder_errors={}, open_stream=open_backup, release_stream=remove_backup, ) @@ -696,6 +721,27 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): on_event(job.to_dict()) +def _collect_errors( + job: supervisor_jobs.Job, child_job_name: str, grandchild_job_name: str +) -> dict[str, list[tuple[str, str]]]: + """Collect errors from a job's grandchildren.""" + errors: dict[str, list[tuple[str, str]]] = {} + for child_job in job.child_jobs: + if child_job.name != child_job_name: + continue + for grandchild in child_job.child_jobs: + if ( + grandchild.name != grandchild_job_name + or not grandchild.errors + or not grandchild.reference + ): + continue + errors[grandchild.reference] = [ + (error.type, error.message) for error in grandchild.errors + ] + return errors + + async def _default_agent(client: SupervisorClient) -> str: """Return the default agent for creating a backup.""" mounts = await client.mounts.info() diff --git a/tests/components/backup/conftest.py b/tests/components/backup/conftest.py index d391df44475..8fffdba7cc2 100644 --- a/tests/components/backup/conftest.py +++ b/tests/components/backup/conftest.py @@ -110,8 +110,10 @@ CONFIG_DIR_DIRS = { def mock_create_backup() -> Generator[AsyncMock]: """Mock manager create backup.""" mock_written_backup = MagicMock(spec_set=WrittenBackup) + mock_written_backup.addon_errors = {} mock_written_backup.backup.backup_id = "abc123" mock_written_backup.backup.protected = False + mock_written_backup.folder_errors = {} mock_written_backup.open_stream = AsyncMock() mock_written_backup.release_stream = AsyncMock() fut: Future[MagicMock] = Future() diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 04072dae864..24eead134cf 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -35,6 +35,7 @@ from homeassistant.components.backup import ( from homeassistant.components.backup.agent import BackupAgentError from homeassistant.components.backup.const import DATA_MANAGER from homeassistant.components.backup.manager import ( + AddonErrorData, BackupManagerError, BackupManagerExceptionGroup, BackupManagerState, @@ -123,7 +124,9 @@ async def test_create_backup_service( new_backup = NewBackup(backup_job_id="time-123") backup_task = AsyncMock( return_value=WrittenBackup( + addon_errors={}, backup=TEST_BACKUP_ABC123, + folder_errors={}, open_stream=AsyncMock(), release_stream=AsyncMock(), ), @@ -320,7 +323,9 @@ async def test_async_create_backup( new_backup = NewBackup(backup_job_id="time-123") backup_task = AsyncMock( return_value=WrittenBackup( + addon_errors={}, backup=TEST_BACKUP_ABC123, + folder_errors={}, open_stream=AsyncMock(), release_stream=AsyncMock(), ), @@ -962,6 +967,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( "automatic_agents", "create_backup_command", + "create_backup_addon_errors", + "create_backup_folder_errors", "create_backup_side_effect", "upload_side_effect", "create_backup_result", @@ -972,6 +979,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate", "agent_ids": ["test.remote"]}, + {}, + {}, None, None, True, @@ -980,6 +989,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate_with_automatic_settings"}, + {}, + {}, None, None, True, @@ -989,6 +1000,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote", "test.unknown"], {"type": "backup/generate", "agent_ids": ["test.remote", "test.unknown"]}, + {}, + {}, None, None, True, @@ -1005,6 +1018,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote", "test.unknown"], {"type": "backup/generate_with_automatic_settings"}, + {}, + {}, None, None, True, @@ -1026,6 +1041,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate", "agent_ids": ["test.remote"]}, + {}, + {}, Exception("Boom!"), None, False, @@ -1034,6 +1051,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate_with_automatic_settings"}, + {}, + {}, Exception("Boom!"), None, False, @@ -1048,6 +1067,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate", "agent_ids": ["test.remote"]}, + {}, + {}, delayed_boom, None, True, @@ -1056,6 +1077,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate_with_automatic_settings"}, + {}, + {}, delayed_boom, None, True, @@ -1070,6 +1093,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate", "agent_ids": ["test.remote"]}, + {}, + {}, None, Exception("Boom!"), True, @@ -1078,6 +1103,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: ( ["test.remote"], {"type": "backup/generate_with_automatic_settings"}, + {}, + {}, None, Exception("Boom!"), True, @@ -1088,6 +1115,157 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: } }, ), + # Add-ons can't be backed up + ( + ["test.remote"], + {"type": "backup/generate", "agent_ids": ["test.remote"]}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {}, + None, + None, + True, + {}, + ), + ( + ["test.remote"], + {"type": "backup/generate_with_automatic_settings"}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {}, + None, + None, + True, + { + (DOMAIN, "automatic_backup_failed"): { + "translation_key": "automatic_backup_failed_addons", + "translation_placeholders": {"failed_addons": "Test Add-on"}, + } + }, + ), + # Folders can't be backed up + ( + ["test.remote"], + {"type": "backup/generate", "agent_ids": ["test.remote"]}, + {}, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + {}, + ), + ( + ["test.remote"], + {"type": "backup/generate_with_automatic_settings"}, + {}, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + { + (DOMAIN, "automatic_backup_failed"): { + "translation_key": "automatic_backup_failed_folders", + "translation_placeholders": {"failed_folders": "media"}, + } + }, + ), + # Add-ons and folders can't be backed up + ( + ["test.remote"], + {"type": "backup/generate", "agent_ids": ["test.remote"]}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + {}, + ), + ( + ["test.remote"], + {"type": "backup/generate_with_automatic_settings"}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + { + (DOMAIN, "automatic_backup_failed"): { + "translation_key": "automatic_backup_failed_agents_addons_folders", + "translation_placeholders": { + "failed_addons": "Test Add-on", + "failed_agents": "-", + "failed_folders": "media", + }, + }, + }, + ), + # Add-ons and folders can't be backed up, one agent unavailable + ( + ["test.remote", "test.unknown"], + {"type": "backup/generate", "agent_ids": ["test.remote"]}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + { + (DOMAIN, "automatic_backup_agents_unavailable_test.unknown"): { + "translation_key": "automatic_backup_agents_unavailable", + "translation_placeholders": { + "agent_id": "test.unknown", + "backup_settings": "/config/backup/settings", + }, + }, + }, + ), + ( + ["test.remote", "test.unknown"], + {"type": "backup/generate_with_automatic_settings"}, + { + "test_addon": AddonErrorData( + name="Test Add-on", errors=[("test_error", "Boom!")] + ) + }, + {Folder.MEDIA: [("test_error", "Boom!")]}, + None, + None, + True, + { + (DOMAIN, "automatic_backup_failed"): { + "translation_key": "automatic_backup_failed_agents_addons_folders", + "translation_placeholders": { + "failed_addons": "Test Add-on", + "failed_agents": "test.unknown", + "failed_folders": "media", + }, + }, + (DOMAIN, "automatic_backup_agents_unavailable_test.unknown"): { + "translation_key": "automatic_backup_agents_unavailable", + "translation_placeholders": { + "agent_id": "test.unknown", + "backup_settings": "/config/backup/settings", + }, + }, + }, + ), ], ) async def test_create_backup_failure_raises_issue( @@ -1096,16 +1274,20 @@ async def test_create_backup_failure_raises_issue( create_backup: AsyncMock, automatic_agents: list[str], create_backup_command: dict[str, Any], + create_backup_addon_errors: dict[str, str], + create_backup_folder_errors: dict[Folder, str], create_backup_side_effect: Exception | None, upload_side_effect: Exception | None, create_backup_result: bool, issues_after_create_backup: dict[tuple[str, str], dict[str, Any]], ) -> None: - """Test backup issue is cleared after backup is created.""" + """Test issue is created when create backup has error.""" mock_agents = await setup_backup_integration(hass, remote_agents=["test.remote"]) ws_client = await hass_ws_client(hass) + create_backup.return_value[1].result().addon_errors = create_backup_addon_errors + create_backup.return_value[1].result().folder_errors = create_backup_folder_errors create_backup.side_effect = create_backup_side_effect await ws_client.send_json_auto_id( @@ -1857,7 +2039,9 @@ async def test_receive_backup_busy_manager( # finish the backup backup_task.set_result( WrittenBackup( + addon_errors={}, backup=TEST_BACKUP_ABC123, + folder_errors={}, open_stream=AsyncMock(), release_stream=AsyncMock(), ) diff --git a/tests/components/hassio/fixtures/backup_done_with_addon_folder_errors.json b/tests/components/hassio/fixtures/backup_done_with_addon_folder_errors.json new file mode 100644 index 00000000000..183a38a60db --- /dev/null +++ b/tests/components/hassio/fixtures/backup_done_with_addon_folder_errors.json @@ -0,0 +1,162 @@ +{ + "result": "ok", + "data": { + "name": "backup_manager_partial_backup", + "reference": "14a1ea4b", + "uuid": "400a90112553472a90d84a7e60d5265e", + "progress": 0, + "stage": "finishing_file", + "done": true, + "errors": [], + "created": "2025-05-14T08:56:22.801143+00:00", + "child_jobs": [ + { + "name": "backup_store_homeassistant", + "reference": "14a1ea4b", + "uuid": "176318a1a8184b02b7e9ad3ec54ee5ec", + "progress": 0, + "stage": null, + "done": true, + "errors": [], + "created": "2025-05-14T08:56:22.807078+00:00", + "child_jobs": [] + }, + { + "name": "backup_store_addons", + "reference": "14a1ea4b", + "uuid": "42664cb8fd4e474f8919bd737877125b", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't backup add-on core_ssh: Can't write tarfile: FAKE OS error during add-on backup", + "stage": null + }, + { + "type": "BackupError", + "message": "Can't backup add-on core_whisper: Can't write tarfile: FAKE OS error during add-on backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.843960+00:00", + "child_jobs": [ + { + "name": "backup_addon_save", + "reference": "core_ssh", + "uuid": "7cc7feb782e54345bdb5ca653928233f", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't write tarfile: FAKE OS error during add-on backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.844160+00:00", + "child_jobs": [] + }, + { + "name": "backup_addon_save", + "reference": "core_whisper", + "uuid": "0cfb1163751740929e63a68df59dc13b", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't write tarfile: FAKE OS error during add-on backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.850376+00:00", + "child_jobs": [] + } + ] + }, + { + "name": "backup_store_folders", + "reference": "14a1ea4b", + "uuid": "dd4685b4aac9460ab0e1150fe5c968e1", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't backup folder share: Can't write tarfile: FAKE OS error during folder backup", + "stage": null + }, + { + "type": "BackupError", + "message": "Can't backup folder ssl: Can't write tarfile: FAKE OS error during folder backup", + "stage": null + }, + { + "type": "BackupError", + "message": "Can't backup folder media: Can't write tarfile: FAKE OS error during folder backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.858227+00:00", + "child_jobs": [ + { + "name": "backup_folder_save", + "reference": "share", + "uuid": "8a4dccd988f641a383abb469a478cbdb", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't write tarfile: FAKE OS error during folder backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.858385+00:00", + "child_jobs": [] + }, + { + "name": "backup_folder_save", + "reference": "ssl", + "uuid": "f9b437376cc9428090606779eff35b41", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't write tarfile: FAKE OS error during folder backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.859973+00:00", + "child_jobs": [] + }, + { + "name": "backup_folder_save", + "reference": "media", + "uuid": "b920835ef079403784fba4ff54437197", + "progress": 0, + "stage": null, + "done": true, + "errors": [ + { + "type": "BackupError", + "message": "Can't write tarfile: FAKE OS error during folder backup", + "stage": null + } + ], + "created": "2025-05-14T08:56:22.860792+00:00", + "child_jobs": [] + } + ] + } + ] + } +} diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index 544b9bd5958..9065fb55bd2 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -10,6 +10,7 @@ from collections.abc import ( Iterable, ) from dataclasses import replace +import datetime as dt from datetime import datetime from io import StringIO import os @@ -47,12 +48,13 @@ from homeassistant.components.backup import ( from homeassistant.components.hassio import DOMAIN from homeassistant.components.hassio.backup import RESTORE_JOB_ID_ENV from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.backup import async_initialize_backup from homeassistant.setup import async_setup_component from .test_init import MOCK_ENVIRON -from tests.common import mock_platform +from tests.common import load_json_object_fixture, mock_platform from tests.typing import ClientSessionGenerator, WebSocketGenerator TEST_BACKUP = supervisor_backups.Backup( @@ -986,6 +988,128 @@ async def test_reader_writer_create( assert response["event"] == {"manager_state": "idle"} +@pytest.mark.usefixtures("addon_info", "hassio_client", "setup_backup_integration") +@pytest.mark.parametrize( + "addon_info_side_effect", + # Getting info fails for one of the addons, should fall back to slug + [[Mock(), SupervisorError("Boom")]], +) +async def test_reader_writer_create_addon_folder_error( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + freezer: FrozenDateTimeFactory, + supervisor_client: AsyncMock, + addon_info_side_effect: list[Any], +) -> None: + """Test generating a backup.""" + addon_info_side_effect[0].name = "Advanced SSH & Web Terminal" + assert dt.datetime.__name__ == "HAFakeDatetime" + assert dt.HAFakeDatetime.__name__ == "HAFakeDatetime" + client = await hass_ws_client(hass) + freezer.move_to("2025-01-30 13:42:12.345678") + supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) + supervisor_client.backups.backup_info.return_value = TEST_BACKUP_DETAILS + supervisor_client.jobs.get_job.side_effect = [ + TEST_JOB_NOT_DONE, + supervisor_jobs.Job.from_dict( + load_json_object_fixture( + "backup_done_with_addon_folder_errors.json", DOMAIN + )["data"] + ), + ] + + issue_registry = ir.async_get(hass) + assert not issue_registry.issues + + await client.send_json_auto_id({"type": "backup/subscribe_events"}) + response = await client.receive_json() + assert response["event"] == {"manager_state": "idle"} + response = await client.receive_json() + assert response["success"] + + await client.send_json_auto_id( + { + "type": "backup/config/update", + "create_backup": { + "agent_ids": ["hassio.local"], + "include_addons": ["core_ssh", "core_whisper"], + "include_all_addons": False, + "include_database": True, + "include_folders": ["media", "share"], + "name": "Test", + }, + } + ) + response = await client.receive_json() + assert response["success"] + + await client.send_json_auto_id({"type": "backup/generate_with_automatic_settings"}) + response = await client.receive_json() + assert response["event"] == { + "manager_state": "create_backup", + "reason": None, + "stage": None, + "state": "in_progress", + } + + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"backup_job_id": TEST_JOB_ID} + + supervisor_client.backups.partial_backup.assert_called_once_with( + replace( + DEFAULT_BACKUP_OPTIONS, + addons={"core_ssh", "core_whisper"}, + extra=DEFAULT_BACKUP_OPTIONS.extra | {"with_automatic_settings": True}, + folders={Folder.MEDIA, Folder.SHARE, Folder.SSL}, + ) + ) + + await client.send_json_auto_id( + { + "type": "supervisor/event", + "data": { + "event": "job", + "data": {"done": True, "uuid": TEST_JOB_ID, "reference": "test_slug"}, + }, + } + ) + response = await client.receive_json() + assert response["success"] + + response = await client.receive_json() + assert response["event"] == { + "manager_state": "create_backup", + "reason": None, + "stage": "upload_to_agents", + "state": "in_progress", + } + + response = await client.receive_json() + assert response["event"] == { + "manager_state": "create_backup", + "reason": None, + "stage": None, + "state": "completed", + } + + supervisor_client.backups.download_backup.assert_not_called() + supervisor_client.backups.remove_backup.assert_not_called() + + response = await client.receive_json() + assert response["event"] == {"manager_state": "idle"} + + # Check that the expected issue was created + assert list(issue_registry.issues) == [("backup", "automatic_backup_failed")] + issue = issue_registry.issues[("backup", "automatic_backup_failed")] + assert issue.translation_key == "automatic_backup_failed_agents_addons_folders" + assert issue.translation_placeholders == { + "failed_addons": "Advanced SSH & Web Terminal, core_whisper", + "failed_agents": "-", + "failed_folders": "share, ssl, media", + } + + @pytest.mark.usefixtures("hassio_client", "setup_backup_integration") async def test_reader_writer_create_report_progress( hass: HomeAssistant, diff --git a/tests/conftest.py b/tests/conftest.py index 2c23270daee..d13384055b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,7 +52,7 @@ from homeassistant.exceptions import ServiceNotFound from . import patch_recorder # isort:skip # Setup patching of dt_util time functions before any other Home Assistant imports -from . import patch_time # noqa: F401, isort:skip +from . import patch_time # isort:skip from homeassistant import components, core as ha, loader, runner from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY @@ -190,14 +190,14 @@ def pytest_runtest_setup() -> None: pytest_socket.socket_allow_hosts(["127.0.0.1"]) pytest_socket.disable_socket(allow_unix_socket=True) - freezegun.api.datetime_to_fakedatetime = ha_datetime_to_fakedatetime # type: ignore[attr-defined] - freezegun.api.FakeDatetime = HAFakeDatetime # type: ignore[attr-defined] + freezegun.api.datetime_to_fakedatetime = patch_time.ha_datetime_to_fakedatetime # type: ignore[attr-defined] + freezegun.api.FakeDatetime = patch_time.HAFakeDatetime # type: ignore[attr-defined] def adapt_datetime(val): return val.isoformat(" ") # Setup HAFakeDatetime converter for sqlite3 - sqlite3.register_adapter(HAFakeDatetime, adapt_datetime) + sqlite3.register_adapter(patch_time.HAFakeDatetime, adapt_datetime) # Setup HAFakeDatetime converter for pymysql try: @@ -206,48 +206,11 @@ def pytest_runtest_setup() -> None: except ImportError: pass else: - MySQLdb_converters.conversions[HAFakeDatetime] = ( + MySQLdb_converters.conversions[patch_time.HAFakeDatetime] = ( MySQLdb_converters.DateTime2literal ) -def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined] - """Convert datetime to FakeDatetime. - - Modified to include https://github.com/spulec/freezegun/pull/424. - """ - return freezegun.api.FakeDatetime( # type: ignore[attr-defined] - datetime.year, - datetime.month, - datetime.day, - datetime.hour, - datetime.minute, - datetime.second, - datetime.microsecond, - datetime.tzinfo, - fold=datetime.fold, - ) - - -class HAFakeDatetime(freezegun.api.FakeDatetime): # type: ignore[name-defined] - """Modified to include https://github.com/spulec/freezegun/pull/424.""" - - @classmethod - def now(cls, tz=None): - """Return frozen now.""" - now = cls._time_to_freeze() or freezegun.api.real_datetime.now() - if tz: - result = tz.fromutc(now.replace(tzinfo=tz)) - else: - result = now - - # Add the _tz_offset only if it's non-zero to preserve fold - if cls._tz_offset(): - result += cls._tz_offset() - - return ha_datetime_to_fakedatetime(result) - - def check_real[**_P, _R](func: Callable[_P, Coroutine[Any, Any, _R]]): """Force a function to require a keyword _test_real to be passed in.""" diff --git a/tests/patch_time.py b/tests/patch_time.py index 362296ab8b2..76d31d6a75a 100644 --- a/tests/patch_time.py +++ b/tests/patch_time.py @@ -5,6 +5,49 @@ from __future__ import annotations import datetime import time +import freezegun + + +def ha_datetime_to_fakedatetime(datetime) -> freezegun.api.FakeDatetime: # type: ignore[name-defined] + """Convert datetime to FakeDatetime. + + Modified to include https://github.com/spulec/freezegun/pull/424. + """ + return freezegun.api.FakeDatetime( # type: ignore[attr-defined] + datetime.year, + datetime.month, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.microsecond, + datetime.tzinfo, + fold=datetime.fold, + ) + + +class HAFakeDatetime(freezegun.api.FakeDatetime): # type: ignore[name-defined] + """Modified to include https://github.com/spulec/freezegun/pull/424.""" + + @classmethod + def now(cls, tz=None): + """Return frozen now.""" + now = cls._time_to_freeze() or freezegun.api.real_datetime.now() + if tz: + result = tz.fromutc(now.replace(tzinfo=tz)) + else: + result = now + + # Add the _tz_offset only if it's non-zero to preserve fold + if cls._tz_offset(): + result += cls._tz_offset() + + return ha_datetime_to_fakedatetime(result) + + +# Needed by Mashumaro +datetime.HAFakeDatetime = HAFakeDatetime + # Do not add any Home Assistant import here From 4160ed190ce92350a6a7629ff33f90c533a21c25 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 20 May 2025 16:20:06 +0200 Subject: [PATCH 0701/1175] Add Albanian (Shqip) language (#145324) --- homeassistant/generated/languages.py | 2 ++ script/languages.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/generated/languages.py b/homeassistant/generated/languages.py index 7e56952f7a5..86d8c93d1ff 100644 --- a/homeassistant/generated/languages.py +++ b/homeassistant/generated/languages.py @@ -57,6 +57,7 @@ LANGUAGES = { "ru", "sk", "sl", + "sq", "sr", "sr-Latn", "sv", @@ -109,6 +110,7 @@ NATIVE_ENTITY_IDS = { "ro", "sk", "sl", + "sq", "sr-Latn", "sv", "tr", diff --git a/script/languages.py b/script/languages.py index bfc811a0905..d13f8ba06c8 100644 --- a/script/languages.py +++ b/script/languages.py @@ -51,8 +51,8 @@ NATIVE_ENTITY_IDS = { "lb", # Lëtzebuergesch "lt", # Lietuvių "lv", # Latviešu - "nb", # Nederlands - "nl", # Norsk Bokmål + "nb", # Norsk Bokmål + "nl", # Nederlands "nn", # Norsk Nynorsk" "pl", # Polski "pt", # Português @@ -60,6 +60,7 @@ NATIVE_ENTITY_IDS = { "ro", # Română "sk", # Slovenčina "sl", # Slovenščina + "sq", # Shqip "sr-Latn", # Srpski "sv", # Svenska "tr", # Türkçe From 473709172204f1fec2fe3287ddad3cda8d7fbc56 Mon Sep 17 00:00:00 2001 From: jb101010-2 <168106462+jb101010-2@users.noreply.github.com> Date: Tue, 20 May 2025 16:22:35 +0200 Subject: [PATCH 0702/1175] Suez water: fetch historical data in statistics (#131166) * Suez water: fetch historical data in statistics * test review * wip: fix few things * Python is smarter than me * use snapshots for statistics and add hard limit for historical stats * refactor refresh + handle missing price * No more auth error raised * fix after rebase * Review - much cleaner <3 * fix changes * test without snapshots * fix imports --- .../components/suez_water/coordinator.py | 206 +++++++++++++-- .../components/suez_water/manifest.json | 1 + homeassistant/components/suez_water/sensor.py | 8 + tests/components/suez_water/conftest.py | 15 +- .../suez_water/snapshots/test_init.ambr | 231 +++++++++++++++++ .../suez_water/snapshots/test_sensor.ambr | 4 +- .../components/suez_water/test_config_flow.py | 3 +- tests/components/suez_water/test_init.py | 238 ++++++++++++++++-- tests/components/suez_water/test_sensor.py | 27 +- 9 files changed, 682 insertions(+), 51 deletions(-) create mode 100644 tests/components/suez_water/snapshots/test_init.ambr diff --git a/homeassistant/components/suez_water/coordinator.py b/homeassistant/components/suez_water/coordinator.py index 10d4d3cdbcb..83283ae8ec5 100644 --- a/homeassistant/components/suez_water/coordinator.py +++ b/homeassistant/components/suez_water/coordinator.py @@ -1,18 +1,35 @@ """Suez water update coordinator.""" from dataclasses import dataclass -from datetime import date +from datetime import date, datetime +import logging -from pysuez import PySuezError, SuezClient +from pysuez import PySuezError, SuezClient, TelemetryMeasure +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import ( + StatisticMeanType, + StatisticsRow, + async_add_external_statistics, + get_last_statistics, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import _LOGGER, HomeAssistant +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CURRENCY_EURO, + UnitOfVolume, +) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util from .const import CONF_COUNTER_ID, DATA_REFRESH_INTERVAL, DOMAIN +_LOGGER = logging.getLogger(__name__) + @dataclass class SuezWaterAggregatedAttributes: @@ -32,7 +49,7 @@ class SuezWaterData: aggregated_value: float aggregated_attr: SuezWaterAggregatedAttributes - price: float + price: float | None type SuezWaterConfigEntry = ConfigEntry[SuezWaterCoordinator] @@ -54,6 +71,11 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]): always_update=True, config_entry=config_entry, ) + self._counter_id = self.config_entry.data[CONF_COUNTER_ID] + self._cost_statistic_id = f"{DOMAIN}:{self._counter_id}_water_cost_statistics" + self._water_statistic_id = ( + f"{DOMAIN}:{self._counter_id}_water_consumption_statistics" + ) async def _async_setup(self) -> None: self._suez_client = SuezClient( @@ -72,19 +94,165 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]): try: aggregated = await self._suez_client.fetch_aggregated_data() - data = SuezWaterData( - aggregated_value=aggregated.value, - aggregated_attr=SuezWaterAggregatedAttributes( - this_month_consumption=map_dict(aggregated.current_month), - previous_month_consumption=map_dict(aggregated.previous_month), - highest_monthly_consumption=aggregated.highest_monthly_consumption, - last_year_overall=aggregated.previous_year, - this_year_overall=aggregated.current_year, - history=map_dict(aggregated.history), - ), - price=(await self._suez_client.get_price()).price, - ) except PySuezError as err: - raise UpdateFailed(f"Suez data update failed: {err}") from err + raise UpdateFailed("Suez coordinator error communicating with API") from err + + price = None + try: + price = (await self._suez_client.get_price()).price + except PySuezError: + _LOGGER.debug("Failed to fetch water price", stack_info=True) + + try: + await self._update_statistics(price) + except PySuezError as err: + raise UpdateFailed("Failed to update suez water statistics") from err + _LOGGER.debug("Successfully fetched suez data") - return data + return SuezWaterData( + aggregated_value=aggregated.value, + aggregated_attr=SuezWaterAggregatedAttributes( + this_month_consumption=map_dict(aggregated.current_month), + previous_month_consumption=map_dict(aggregated.previous_month), + highest_monthly_consumption=aggregated.highest_monthly_consumption, + last_year_overall=aggregated.previous_year, + this_year_overall=aggregated.current_year, + history=map_dict(aggregated.history), + ), + price=price, + ) + + async def _update_statistics(self, current_price: float | None) -> None: + """Update daily statistics.""" + _LOGGER.debug("Updating statistics for %s", self._water_statistic_id) + + water_last_stat = await self._get_last_stat(self._water_statistic_id) + cost_last_stat = await self._get_last_stat(self._cost_statistic_id) + consumption_sum = ( + water_last_stat["sum"] + if water_last_stat and water_last_stat["sum"] + else 0.0 + ) + cost_sum = ( + cost_last_stat["sum"] if cost_last_stat and cost_last_stat["sum"] else 0.0 + ) + last_stats = ( + datetime.fromtimestamp(water_last_stat["start"]).date() + if water_last_stat + else None + ) + + _LOGGER.debug( + "Updating suez stat since %s for %s", + str(last_stats), + water_last_stat, + ) + if not ( + usage := await self._suez_client.fetch_all_daily_data( + since=last_stats, + ) + ): + _LOGGER.debug("No recent usage data. Skipping update") + return + _LOGGER.debug("fetched data: %s", len(usage)) + + consumption_statistics, cost_statistics = self._build_statistics( + current_price, consumption_sum, cost_sum, last_stats, usage + ) + + self._persist_statistics(consumption_statistics, cost_statistics) + + def _build_statistics( + self, + current_price: float | None, + consumption_sum: float, + cost_sum: float, + last_stats: date | None, + usage: list[TelemetryMeasure], + ) -> tuple[list[StatisticData], list[StatisticData]]: + """Build statistics data from fetched data.""" + consumption_statistics = [] + cost_statistics = [] + + for data in usage: + if ( + (last_stats is not None and data.date <= last_stats) + or not data.index + or data.volume is None + ): + continue + consumption_date = dt_util.start_of_local_day(data.date) + + consumption_sum += data.volume + consumption_statistics.append( + StatisticData( + start=consumption_date, + state=data.volume, + sum=consumption_sum, + ) + ) + if current_price is not None: + day_cost = (data.volume / 1000) * current_price + cost_sum += day_cost + cost_statistics.append( + StatisticData( + start=consumption_date, + state=day_cost, + sum=cost_sum, + ) + ) + + return consumption_statistics, cost_statistics + + def _persist_statistics( + self, + consumption_statistics: list[StatisticData], + cost_statistics: list[StatisticData], + ) -> None: + """Persist given statistics in recorder.""" + consumption_metadata = self._get_statistics_metadata( + id=self._water_statistic_id, name="Consumption", unit=UnitOfVolume.LITERS + ) + + _LOGGER.debug( + "Adding %s statistics for %s", + len(consumption_statistics), + self._water_statistic_id, + ) + async_add_external_statistics( + self.hass, consumption_metadata, consumption_statistics + ) + + if len(cost_statistics) > 0: + _LOGGER.debug( + "Adding %s statistics for %s", + len(cost_statistics), + self._cost_statistic_id, + ) + cost_metadata = self._get_statistics_metadata( + id=self._cost_statistic_id, name="Cost", unit=CURRENCY_EURO + ) + async_add_external_statistics(self.hass, cost_metadata, cost_statistics) + + _LOGGER.debug("Updated statistics for %s", self._water_statistic_id) + + def _get_statistics_metadata( + self, id: str, name: str, unit: str + ) -> StatisticMetaData: + """Build statistics metadata for requested configuration.""" + return StatisticMetaData( + has_mean=False, + mean_type=StatisticMeanType.NONE, + has_sum=True, + name=f"Suez water {name} {self._counter_id}", + source=DOMAIN, + statistic_id=id, + unit_of_measurement=unit, + ) + + async def _get_last_stat(self, id: str) -> StatisticsRow | None: + """Find last registered statistics of given id.""" + last_stat = await get_instance(self.hass).async_add_executor_job( + get_last_statistics, self.hass, 1, id, True, {"sum"} + ) + return last_stat[id][0] if last_stat else None diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 128f7aa4d8d..9149f216563 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,6 +1,7 @@ { "domain": "suez_water", "name": "Suez Water", + "after_dependencies": ["recorder"], "codeowners": ["@ooii", "@jb101010-2"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/suez_water", diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index a162cc6168d..9bbe24abb59 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -87,6 +87,14 @@ class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity): ) self.entity_description = entity_description + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + self.coordinator.last_update_success + and self.entity_description.value_fn(self.coordinator.data) is not None + ) + @property def native_value(self) -> float | str | None: """Return the state of the sensor.""" diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 73557fd3bde..9d29191289e 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -8,17 +8,26 @@ from pysuez import AggregatedData, PriceResult from pysuez.const import ATTRIBUTION import pytest +from homeassistant.components.recorder import Recorder from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN from tests.common import MockConfigEntry +from tests.conftest import RecorderInstanceContextManager MOCK_DATA = { "username": "test-username", "password": "test-password", - CONF_COUNTER_ID: "test-counter", + CONF_COUNTER_ID: "123456", } +@pytest.fixture +async def mock_recorder_before_hass( + async_test_recorder: RecorderInstanceContextManager, +) -> None: + """Set up recorder.""" + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Create mock config_entry needed by suez_water integration.""" @@ -32,7 +41,7 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[AsyncMock]: +def mock_setup_entry(recorder_mock: Recorder) -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( "homeassistant.components.suez_water.async_setup_entry", return_value=True @@ -41,7 +50,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="suez_client") -def mock_suez_client() -> Generator[AsyncMock]: +def mock_suez_client(recorder_mock: Recorder) -> Generator[AsyncMock]: """Create mock for suez_water external api.""" with ( patch( diff --git a/tests/components/suez_water/snapshots/test_init.ambr b/tests/components/suez_water/snapshots/test_init.ambr new file mode 100644 index 00000000000..24e11654cd0 --- /dev/null +++ b/tests/components/suez_water/snapshots/test_init.ambr @@ -0,0 +1,231 @@ +# serializer version: 1 +# name: test_statistics[water_consumption_statistics][test_statistics_call1] + defaultdict({ + 'suez_water:123456_water_consumption_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 500.0, + 'sum': 500.0, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 500.0, + 'sum': 1000.0, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 500.0, + 'sum': 1500.0, + }), + ]), + }) +# --- +# name: test_statistics[water_consumption_statistics][test_statistics_call2] + defaultdict({ + 'suez_water:123456_water_consumption_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 500.0, + 'sum': 500.0, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 500.0, + 'sum': 1000.0, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 500.0, + 'sum': 1500.0, + }), + ]), + }) +# --- +# name: test_statistics[water_consumption_statistics][test_statistics_call3] + defaultdict({ + 'suez_water:123456_water_consumption_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 500.0, + 'sum': 500.0, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 500.0, + 'sum': 1000.0, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 500.0, + 'sum': 1500.0, + }), + ]), + }) +# --- +# name: test_statistics[water_consumption_statistics][test_statistics_call4] + defaultdict({ + 'suez_water:123456_water_consumption_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 500.0, + 'sum': 500.0, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 500.0, + 'sum': 1000.0, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 500.0, + 'sum': 1500.0, + }), + dict({ + 'end': 1733389200.0, + 'last_reset': None, + 'start': 1733385600.0, + 'state': 500.0, + 'sum': 2000.0, + }), + ]), + }) +# --- +# name: test_statistics[water_cost_statistics][test_statistics_call1] + defaultdict({ + 'suez_water:123456_water_cost_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 2.37, + 'sum': 2.37, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 2.37, + 'sum': 4.74, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 2.37, + 'sum': 7.11, + }), + ]), + }) +# --- +# name: test_statistics[water_cost_statistics][test_statistics_call2] + defaultdict({ + 'suez_water:123456_water_cost_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 2.37, + 'sum': 2.37, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 2.37, + 'sum': 4.74, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 2.37, + 'sum': 7.11, + }), + ]), + }) +# --- +# name: test_statistics[water_cost_statistics][test_statistics_call3] + defaultdict({ + 'suez_water:123456_water_cost_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 2.37, + 'sum': 2.37, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 2.37, + 'sum': 4.74, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 2.37, + 'sum': 7.11, + }), + ]), + }) +# --- +# name: test_statistics[water_cost_statistics][test_statistics_call4] + defaultdict({ + 'suez_water:123456_water_cost_statistics': list([ + dict({ + 'end': 1733043600.0, + 'last_reset': None, + 'start': 1733040000.0, + 'state': 2.37, + 'sum': 2.37, + }), + dict({ + 'end': 1733130000.0, + 'last_reset': None, + 'start': 1733126400.0, + 'state': 2.37, + 'sum': 4.74, + }), + dict({ + 'end': 1733216400.0, + 'last_reset': None, + 'start': 1733212800.0, + 'state': 2.37, + 'sum': 7.11, + }), + dict({ + 'end': 1733389200.0, + 'last_reset': None, + 'start': 1733385600.0, + 'state': 2.37, + 'sum': 9.48, + }), + ]), + }) +# --- diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index 536e79df606..0ce631bf1b3 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -29,7 +29,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'water_price', - 'unique_id': 'test-counter_water_price', + 'unique_id': '123456_water_price', 'unit_of_measurement': '€', }) # --- @@ -79,7 +79,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'water_usage_yesterday', - 'unique_id': 'test-counter_water_usage_yesterday', + 'unique_id': '123456_water_usage_yesterday', 'unit_of_measurement': , }) # --- diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index bebb4fd72ac..656c804e4d9 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -6,6 +6,7 @@ from pysuez.exception import PySuezError import pytest from homeassistant import config_entries +from homeassistant.components.recorder import Recorder from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -70,7 +71,7 @@ async def test_form_invalid_auth( async def test_form_already_configured( - hass: HomeAssistant, suez_client: AsyncMock + hass: HomeAssistant, recorder_mock: Recorder, suez_client: AsyncMock ) -> None: """Test we abort when entry is already configured.""" diff --git a/tests/components/suez_water/test_init.py b/tests/components/suez_water/test_init.py index 16d32b61dee..ce010f50153 100644 --- a/tests/components/suez_water/test_init.py +++ b/tests/components/suez_water/test_init.py @@ -1,30 +1,32 @@ """Test Suez_water integration initialization.""" +from datetime import datetime, timedelta from unittest.mock import AsyncMock -from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN -from homeassistant.components.suez_water.coordinator import PySuezError +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.components.suez_water.const import ( + CONF_COUNTER_ID, + DATA_REFRESH_INTERVAL, + DOMAIN, +) +from homeassistant.components.suez_water.coordinator import ( + PySuezError, + TelemetryMeasure, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from . import setup_integration from .conftest import MOCK_DATA -from tests.common import MockConfigEntry - - -async def test_initialization_invalid_credentials( - hass: HomeAssistant, - suez_client: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test that suez_water can't be loaded with invalid credentials.""" - - suez_client.check_credentials.return_value = False - await setup_integration(hass, mock_config_entry) - - assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done async def test_initialization_setup_api_error( @@ -40,6 +42,210 @@ async def test_initialization_setup_api_error( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_init_auth_failed( + hass: HomeAssistant, + suez_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that suez_water reflect authentication failure.""" + suez_client.check_credentials.return_value = False + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_init_refresh_failed( + hass: HomeAssistant, + suez_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that suez_water reflect authentication failure.""" + suez_client.fetch_aggregated_data.side_effect = PySuezError("Update failed") + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_init_statistics_failed( + hass: HomeAssistant, + suez_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that suez_water reflect authentication failure.""" + suez_client.fetch_all_daily_data.side_effect = PySuezError("Update failed") + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.usefixtures("recorder_mock") +async def test_statistics_no_price( + hass: HomeAssistant, + suez_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that suez_water statistics does not register when no price.""" + # New data retrieved but no price + suez_client.get_price.side_effect = PySuezError("will fail") + suez_client.fetch_all_daily_data.return_value = [ + TelemetryMeasure( + (datetime.now().date()).strftime("%Y-%m-%d %H:%M:%S"), 0.5, 0.5 + ) + ] + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + statistic_id = ( + f"{DOMAIN}:{mock_config_entry.data[CONF_COUNTER_ID]}_water_cost_statistics" + ) + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + datetime.now() - timedelta(days=1), + None, + [statistic_id], + "hour", + None, + {"start", "state", "mean", "min", "max", "last_reset", "sum"}, + ) + + assert stats.get(statistic_id) is None + + +@pytest.mark.usefixtures("recorder_mock") +@pytest.mark.parametrize( + "statistic", + [ + "water_cost_statistics", + "water_consumption_statistics", + ], +) +async def test_statistics( + hass: HomeAssistant, + suez_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, + statistic: str, +) -> None: + """Test that suez_water statistics are working.""" + nb_samples = 3 + + start = datetime.fromisoformat("2024-12-04T02:00:00.0") + freezer.move_to(start) + + origin = dt_util.start_of_local_day(start.date()) - timedelta(days=nb_samples) + result = [ + TelemetryMeasure( + date=((origin + timedelta(days=d)).date()).strftime("%Y-%m-%d %H:%M:%S"), + volume=0.5, + index=0.5 * (d + 1), + ) + for d in range(nb_samples) + ] + suez_client.fetch_all_daily_data.return_value = result + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Init data retrieved + await _test_for_data( + hass, + suez_client, + snapshot, + statistic, + origin, + mock_config_entry.data[CONF_COUNTER_ID], + 1, + ) + + # No new data retrieved + suez_client.fetch_all_daily_data.return_value = [] + freezer.tick(DATA_REFRESH_INTERVAL) + async_fire_time_changed(hass) + + await _test_for_data( + hass, + suez_client, + snapshot, + statistic, + origin, + mock_config_entry.data[CONF_COUNTER_ID], + 2, + ) + # Old data retrieved + suez_client.fetch_all_daily_data.return_value = [ + TelemetryMeasure( + date=(origin.date() - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"), + volume=0.5, + index=0.5 * (121 + 1), + ) + ] + freezer.tick(DATA_REFRESH_INTERVAL) + async_fire_time_changed(hass) + + await _test_for_data( + hass, + suez_client, + snapshot, + statistic, + origin, + mock_config_entry.data[CONF_COUNTER_ID], + 3, + ) + + # New daily data retrieved + suez_client.fetch_all_daily_data.return_value = [ + TelemetryMeasure( + date=(datetime.now().date()).strftime("%Y-%m-%d %H:%M:%S"), + volume=0.5, + index=0.5 * (121 + 1), + ) + ] + freezer.tick(DATA_REFRESH_INTERVAL) + async_fire_time_changed(hass) + + await _test_for_data( + hass, + suez_client, + snapshot, + statistic, + origin, + mock_config_entry.data[CONF_COUNTER_ID], + 4, + ) + + +async def _test_for_data( + hass: HomeAssistant, + suez_client: AsyncMock, + snapshot: SnapshotAssertion, + statistic: str, + origin: datetime, + counter_id: str, + nb_calls: int, +) -> None: + await hass.async_block_till_done(True) + await async_wait_recording_done(hass) + + assert suez_client.fetch_all_daily_data.call_count == nb_calls + statistic_id = f"{DOMAIN}:{counter_id}_{statistic}" + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + origin - timedelta(days=1), + None, + [statistic_id], + "hour", + None, + {"start", "state", "mean", "min", "max", "last_reset", "sum"}, + ) + assert stats == snapshot(name=f"test_statistics_call{nb_calls}") + + async def test_migration_version_rollback( hass: HomeAssistant, suez_client: AsyncMock, diff --git a/tests/components/suez_water/test_sensor.py b/tests/components/suez_water/test_sensor.py index f9e7ff1f9e6..3ed0d8f0bed 100644 --- a/tests/components/suez_water/test_sensor.py +++ b/tests/components/suez_water/test_sensor.py @@ -41,16 +41,23 @@ async def test_sensors_valid_state( assert previous.get(str(date.fromisoformat("2024-12-01"))) == 154 -@pytest.mark.parametrize("method", [("fetch_aggregated_data"), ("get_price")]) +@pytest.mark.parametrize( + ("method", "price_on_error", "consumption_on_error"), + [ + ("fetch_aggregated_data", STATE_UNAVAILABLE, STATE_UNAVAILABLE), + ("get_price", STATE_UNAVAILABLE, "160"), + ], +) async def test_sensors_failed_update( hass: HomeAssistant, suez_client: AsyncMock, mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, method: str, + price_on_error: str, + consumption_on_error: str, ) -> None: """Test that suez_water sensor reflect failure when api fails.""" - await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is ConfigEntryState.LOADED @@ -58,10 +65,10 @@ async def test_sensors_failed_update( entity_ids = await hass.async_add_executor_job(hass.states.entity_ids) assert len(entity_ids) == 2 - for entity in entity_ids: - state = hass.states.get(entity) - assert entity - assert state.state != STATE_UNAVAILABLE + state = hass.states.get("sensor.suez_mock_device_water_price") + assert state.state == "4.74" + state = hass.states.get("sensor.suez_mock_device_water_usage_yesterday") + assert state.state == "160" getattr(suez_client, method).side_effect = PySuezError("Should fail to update") @@ -69,7 +76,7 @@ async def test_sensors_failed_update( async_fire_time_changed(hass) await hass.async_block_till_done(True) - for entity in entity_ids: - state = hass.states.get(entity) - assert entity - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.suez_mock_device_water_price") + assert state.state == price_on_error + state = hass.states.get("sensor.suez_mock_device_water_usage_yesterday") + assert state.state == consumption_on_error From 40faa156e26fb9c480760c2204c9eb495d99ba96 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 20 May 2025 18:35:24 +0300 Subject: [PATCH 0703/1175] Jewish calendar : icon translations (#145329) * Move icons to icons.json * Fix tests --- .../jewish_calendar/binary_sensor.py | 1 - .../components/jewish_calendar/icons.json | 32 +++++++++++++++++++ .../components/jewish_calendar/sensor.py | 23 ------------- .../components/jewish_calendar/test_sensor.py | 4 --- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 2e7edbefd3b..c336bce5ed3 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -41,7 +41,6 @@ BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( JewishCalendarBinarySensorEntityDescription( key="issur_melacha_in_effect", translation_key="issur_melacha_in_effect", - icon="mdi:power-plug-off", is_on=lambda state, now: bool(state.issur_melacha_in_effect(now)), ), JewishCalendarBinarySensorEntityDescription( diff --git a/homeassistant/components/jewish_calendar/icons.json b/homeassistant/components/jewish_calendar/icons.json index 24b922df7a2..ae2f752f0f6 100644 --- a/homeassistant/components/jewish_calendar/icons.json +++ b/homeassistant/components/jewish_calendar/icons.json @@ -3,5 +3,37 @@ "count_omer": { "service": "mdi:counter" } + }, + "entity": { + "binary_sensor": { + "issur_melacha_in_effect": { "default": "mdi:power-plug-off" }, + "erev_shabbat_hag": { "default": "mdi:candle-light" }, + "motzei_shabbat_hag": { "default": "mdi:fire" } + }, + "sensor": { + "hebrew_date": { "default": "mdi:star-david" }, + "weekly_portion": { "default": "mdi:book-open-variant" }, + "holiday": { "default": "mdi:calendar-star" }, + "omer_count": { "default": "mdi:counter" }, + "daf_yomi": { "default": "mdi:book-open-variant" }, + "alot_hashachar": { "default": "mdi:weather-sunset-up" }, + "talit_and_tefillin": { "default": "mdi:calendar-clock" }, + "netz_hachama": { "default": "mdi:calendar-clock" }, + "sof_zman_shema_gra": { "default": "mdi:calendar-clock" }, + "sof_zman_shema_mga": { "default": "mdi:calendar-clock" }, + "sof_zman_tfilla_gra": { "default": "mdi:calendar-clock" }, + "sof_zman_tfilla_mga": { "default": "mdi:calendar-clock" }, + "chatzot_hayom": { "default": "mdi:calendar-clock" }, + "mincha_gedola": { "default": "mdi:calendar-clock" }, + "mincha_ketana": { "default": "mdi:calendar-clock" }, + "plag_hamincha": { "default": "mdi:weather-sunset-down" }, + "shkia": { "default": "mdi:weather-sunset" }, + "tset_hakohavim_tsom": { "default": "mdi:weather-night" }, + "tset_hakohavim_shabbat": { "default": "mdi:weather-night" }, + "upcoming_shabbat_candle_lighting": { "default": "mdi:candle" }, + "upcoming_shabbat_havdalah": { "default": "mdi:weather-night" }, + "upcoming_candle_lighting": { "default": "mdi:candle" }, + "upcoming_havdalah": { "default": "mdi:weather-night" } + } } } diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 973d354d368..9a54f162056 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -30,30 +30,25 @@ INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="date", translation_key="hebrew_date", - icon="mdi:star-david", ), SensorEntityDescription( key="weekly_portion", translation_key="weekly_portion", - icon="mdi:book-open-variant", device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="holiday", translation_key="holiday", - icon="mdi:calendar-star", device_class=SensorDeviceClass.ENUM, ), SensorEntityDescription( key="omer_count", translation_key="omer_count", - icon="mdi:counter", entity_registry_enabled_default=False, ), SensorEntityDescription( key="daf_yomi", translation_key="daf_yomi", - icon="mdi:book-open-variant", entity_registry_enabled_default=False, ), ) @@ -62,106 +57,88 @@ TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="alot_hashachar", translation_key="alot_hashachar", - icon="mdi:weather-sunset-up", entity_registry_enabled_default=False, ), SensorEntityDescription( key="talit_and_tefillin", translation_key="talit_and_tefillin", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="netz_hachama", translation_key="netz_hachama", - icon="mdi:calendar-clock", ), SensorEntityDescription( key="sof_zman_shema_gra", translation_key="sof_zman_shema_gra", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_shema_mga", translation_key="sof_zman_shema_mga", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_tfilla_gra", translation_key="sof_zman_tfilla_gra", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="sof_zman_tfilla_mga", translation_key="sof_zman_tfilla_mga", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="chatzot_hayom", translation_key="chatzot_hayom", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="mincha_gedola", translation_key="mincha_gedola", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="mincha_ketana", translation_key="mincha_ketana", - icon="mdi:calendar-clock", entity_registry_enabled_default=False, ), SensorEntityDescription( key="plag_hamincha", translation_key="plag_hamincha", - icon="mdi:weather-sunset-down", entity_registry_enabled_default=False, ), SensorEntityDescription( key="shkia", translation_key="shkia", - icon="mdi:weather-sunset", ), SensorEntityDescription( key="tset_hakohavim_tsom", translation_key="tset_hakohavim_tsom", - icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="tset_hakohavim_shabbat", translation_key="tset_hakohavim_shabbat", - icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_candle_lighting", translation_key="upcoming_shabbat_candle_lighting", - icon="mdi:candle", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_shabbat_havdalah", translation_key="upcoming_shabbat_havdalah", - icon="mdi:weather-night", entity_registry_enabled_default=False, ), SensorEntityDescription( key="upcoming_candle_lighting", translation_key="upcoming_candle_lighting", - icon="mdi:candle", ), SensorEntityDescription( key="upcoming_havdalah", translation_key="upcoming_havdalah", - icon="mdi:weather-night", ), ) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 9364fcda40c..0cc1e60efc8 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -59,7 +59,6 @@ TEST_PARAMS = [ "attr": { "device_class": "enum", "friendly_name": "Jewish Calendar Holiday", - "icon": "mdi:calendar-star", "id": "rosh_hashana_i", "type": "YOM_TOV", "options": lambda: HolidayDatabase(False).get_all_names(), @@ -77,7 +76,6 @@ TEST_PARAMS = [ "attr": { "device_class": "enum", "friendly_name": "Jewish Calendar Holiday", - "icon": "mdi:calendar-star", "id": "chanukah, rosh_chodesh", "type": "MELACHA_PERMITTED_HOLIDAY, ROSH_CHODESH", "options": lambda: HolidayDatabase(False).get_all_names(), @@ -95,7 +93,6 @@ TEST_PARAMS = [ "attr": { "device_class": "enum", "friendly_name": "Jewish Calendar Weekly Torah portion", - "icon": "mdi:book-open-variant", "options": [str(p) for p in Parasha], }, }, @@ -144,7 +141,6 @@ TEST_PARAMS = [ "hebrew_year": "5779", "hebrew_month_name": "מרחשוון", "hebrew_day": "6", - "icon": "mdi:star-david", "friendly_name": "Jewish Calendar Date", }, }, From 734d6cd247de554fb573d3dc774e14b92beee8d6 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 20 May 2025 18:52:52 +0200 Subject: [PATCH 0704/1175] bump aioimmich to 0.6.0 (#145334) --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index fe7741821b6..454adae5501 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.5.0"] + "requirements": ["aioimmich==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 421951e3957..a719c0253b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -277,7 +277,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.5.0 +aioimmich==0.6.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cafa586be5f..8b68589cd1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.5.0 +aioimmich==0.6.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From b71870aba38db77f0e64833efcc63d4552b93afc Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 20 May 2025 20:01:24 +0300 Subject: [PATCH 0705/1175] Jewish calendar: move value calculation to entity description (1/3) (#144272) * Move make_zmanim() method to entity * Move enum values to setup * Create a Jewish Calendar Sensor Description * Hold a single variable for the runtime data in the entity * Move value calculation to sensor description * Use a base class to keep timestamp sensor inheritance * Move attr to entity description as well * Move options to entity description as well * Fix tests after merge * Put multiline in parentheses * Fix diagnostics tests --- .../jewish_calendar/binary_sensor.py | 13 +- .../components/jewish_calendar/entity.py | 31 +- .../components/jewish_calendar/sensor.py | 269 ++++++++++-------- .../snapshots/test_diagnostics.ambr | 150 +++++++++- .../jewish_calendar/test_diagnostics.py | 8 +- 5 files changed, 322 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index c336bce5ed3..79b49050cc2 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -82,18 +82,9 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - zmanim = self._get_zmanim() + zmanim = self.make_zmanim(dt.date.today()) return self.entity_description.is_on(zmanim, dt_util.now()) - def _get_zmanim(self) -> Zmanim: - """Return the Zmanim object for now().""" - return Zmanim( - date=dt.date.today(), - location=self._location, - candle_lighting_offset=self._candle_lighting_offset, - havdalah_offset=self._havdalah_offset, - ) - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() @@ -116,7 +107,7 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity): def _schedule_update(self) -> None: """Schedule the next update of the sensor.""" now = dt_util.now() - zmanim = self._get_zmanim() + zmanim = self.make_zmanim(dt.date.today()) update = zmanim.netz_hachama.local + dt.timedelta(days=1) candle_lighting = zmanim.candle_lighting if candle_lighting is not None and now < candle_lighting < update: diff --git a/homeassistant/components/jewish_calendar/entity.py b/homeassistant/components/jewish_calendar/entity.py index b048b0d4bb7..b92d30048f0 100644 --- a/homeassistant/components/jewish_calendar/entity.py +++ b/homeassistant/components/jewish_calendar/entity.py @@ -1,8 +1,9 @@ """Entity representing a Jewish Calendar sensor.""" from dataclasses import dataclass +import datetime as dt -from hdate import Location +from hdate import HDateInfo, Location, Zmanim from hdate.translator import Language, set_language from homeassistant.config_entries import ConfigEntry @@ -14,6 +15,16 @@ from .const import DOMAIN type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData] +@dataclass +class JewishCalendarDataResults: + """Jewish Calendar results dataclass.""" + + daytime_date: HDateInfo + after_shkia_date: HDateInfo + after_tzais_date: HDateInfo + zmanim: Zmanim + + @dataclass class JewishCalendarData: """Jewish Calendar runtime dataclass.""" @@ -23,6 +34,7 @@ class JewishCalendarData: location: Location candle_lighting_offset: int havdalah_offset: int + results: JewishCalendarDataResults | None = None class JewishCalendarEntity(Entity): @@ -42,9 +54,14 @@ class JewishCalendarEntity(Entity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, config_entry.entry_id)}, ) - data = config_entry.runtime_data - self._location = data.location - self._candle_lighting_offset = data.candle_lighting_offset - self._havdalah_offset = data.havdalah_offset - self._diaspora = data.diaspora - set_language(data.language) + self.data = config_entry.runtime_data + set_language(self.data.language) + + def make_zmanim(self, date: dt.date) -> Zmanim: + """Create a Zmanim object.""" + return Zmanim( + date=date, + location=self.data.location, + candle_lighting_offset=self.data.candle_lighting_offset, + havdalah_offset=self.data.havdalah_offset, + ) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 9a54f162056..230adef9894 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import datetime as dt import logging -from typing import Any from hdate import HDateInfo, Zmanim from hdate.holidays import HolidayDatabase @@ -21,124 +22,192 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import dt as dt_util -from .entity import JewishCalendarConfigEntry, JewishCalendarEntity +from .entity import ( + JewishCalendarConfigEntry, + JewishCalendarDataResults, + JewishCalendarEntity, +) _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -INFO_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass(frozen=True, kw_only=True) +class JewishCalendarBaseSensorDescription(SensorEntityDescription): + """Base class describing Jewish Calendar sensor entities.""" + + value_fn: Callable | None + + +@dataclass(frozen=True, kw_only=True) +class JewishCalendarSensorDescription(JewishCalendarBaseSensorDescription): + """Class describing Jewish Calendar sensor entities.""" + + value_fn: Callable[[JewishCalendarDataResults], str | int] + attr_fn: Callable[[JewishCalendarDataResults], dict[str, str]] | None = None + options_fn: Callable[[bool], list[str]] | None = None + + +@dataclass(frozen=True, kw_only=True) +class JewishCalendarTimestampSensorDescription(JewishCalendarBaseSensorDescription): + """Class describing Jewish Calendar sensor timestamp entities.""" + + value_fn: ( + Callable[[HDateInfo, Callable[[dt.date], Zmanim]], dt.datetime | None] | None + ) = None + + +INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = ( + JewishCalendarSensorDescription( key="date", translation_key="hebrew_date", + value_fn=lambda results: str(results.after_shkia_date.hdate), + attr_fn=lambda results: { + "hebrew_year": str(results.after_shkia_date.hdate.year), + "hebrew_month_name": str(results.after_shkia_date.hdate.month), + "hebrew_day": str(results.after_shkia_date.hdate.day), + }, ), - SensorEntityDescription( + JewishCalendarSensorDescription( key="weekly_portion", translation_key="weekly_portion", device_class=SensorDeviceClass.ENUM, + options_fn=lambda _: [str(p) for p in Parasha], + value_fn=lambda results: str(results.after_tzais_date.upcoming_shabbat.parasha), ), - SensorEntityDescription( + JewishCalendarSensorDescription( key="holiday", translation_key="holiday", device_class=SensorDeviceClass.ENUM, + options_fn=lambda diaspora: HolidayDatabase(diaspora).get_all_names(), + value_fn=lambda results: ", ".join( + str(holiday) for holiday in results.after_shkia_date.holidays + ), + attr_fn=lambda results: { + "id": ", ".join( + holiday.name for holiday in results.after_shkia_date.holidays + ), + "type": ", ".join( + dict.fromkeys( + _holiday.type.name for _holiday in results.after_shkia_date.holidays + ) + ), + }, ), - SensorEntityDescription( + JewishCalendarSensorDescription( key="omer_count", translation_key="omer_count", entity_registry_enabled_default=False, + value_fn=lambda results: ( + results.after_shkia_date.omer.total_days + if results.after_shkia_date.omer + else 0 + ), ), - SensorEntityDescription( + JewishCalendarSensorDescription( key="daf_yomi", translation_key="daf_yomi", entity_registry_enabled_default=False, + value_fn=lambda results: str(results.daytime_date.daf_yomi), ), ) -TIME_SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = ( + JewishCalendarTimestampSensorDescription( key="alot_hashachar", translation_key="alot_hashachar", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="talit_and_tefillin", translation_key="talit_and_tefillin", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="netz_hachama", translation_key="netz_hachama", ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="sof_zman_shema_gra", translation_key="sof_zman_shema_gra", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="sof_zman_shema_mga", translation_key="sof_zman_shema_mga", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="sof_zman_tfilla_gra", translation_key="sof_zman_tfilla_gra", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="sof_zman_tfilla_mga", translation_key="sof_zman_tfilla_mga", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="chatzot_hayom", translation_key="chatzot_hayom", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="mincha_gedola", translation_key="mincha_gedola", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="mincha_ketana", translation_key="mincha_ketana", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="plag_hamincha", translation_key="plag_hamincha", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="shkia", translation_key="shkia", ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="tset_hakohavim_tsom", translation_key="tset_hakohavim_tsom", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="tset_hakohavim_shabbat", translation_key="tset_hakohavim_shabbat", entity_registry_enabled_default=False, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="upcoming_shabbat_candle_lighting", translation_key="upcoming_shabbat_candle_lighting", entity_registry_enabled_default=False, + value_fn=lambda at_date, mz: mz( + at_date.upcoming_shabbat.previous_day.gdate + ).candle_lighting, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="upcoming_shabbat_havdalah", translation_key="upcoming_shabbat_havdalah", entity_registry_enabled_default=False, + value_fn=lambda at_date, mz: mz(at_date.upcoming_shabbat.gdate).havdalah, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="upcoming_candle_lighting", translation_key="upcoming_candle_lighting", + value_fn=lambda at_date, mz: mz( + at_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate + ).candle_lighting, ), - SensorEntityDescription( + JewishCalendarTimestampSensorDescription( key="upcoming_havdalah", translation_key="upcoming_havdalah", + value_fn=lambda at_date, mz: mz( + at_date.upcoming_shabbat_or_yom_tov.last_day.gdate + ).havdalah, ), ) @@ -149,40 +218,30 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Jewish calendar sensors .""" - sensors = [ + sensors: list[JewishCalendarBaseSensor] = [ JewishCalendarSensor(config_entry, description) for description in INFO_SENSORS ] sensors.extend( JewishCalendarTimeSensor(config_entry, description) for description in TIME_SENSORS ) - async_add_entities(sensors) -class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): - """Representation of an Jewish calendar sensor.""" +class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): + """Base class for Jewish calendar sensors.""" _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__( - self, - config_entry: JewishCalendarConfigEntry, - description: SensorEntityDescription, - ) -> None: - """Initialize the Jewish calendar sensor.""" - super().__init__(config_entry, description) - self._attrs: dict[str, str] = {} - async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" await super().async_added_to_hass() - await self.async_update() + await self.async_update_data() - async def async_update(self) -> None: + async def async_update_data(self) -> None: """Update the state of the sensor.""" now = dt_util.now() - _LOGGER.debug("Now: %s Location: %r", now, self._location) + _LOGGER.debug("Now: %s Location: %r", now, self.data.location) today = now.date() event_date = get_astral_event_date(self.hass, SUN_EVENT_SUNSET, today) @@ -195,7 +254,7 @@ class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - daytime_date = HDateInfo(today, diaspora=self._diaspora) + daytime_date = HDateInfo(today, diaspora=self.data.diaspora) # The Jewish day starts after darkness (called "tzais") and finishes at # sunset ("shkia"). The time in between is a gray area @@ -214,95 +273,57 @@ class JewishCalendarSensor(JewishCalendarEntity, SensorEntity): if today_times.havdalah and now > today_times.havdalah: after_tzais_date = daytime_date.next_day - self._attr_native_value = self.get_state( - daytime_date, after_shkia_date, after_tzais_date - ) - _LOGGER.debug( - "New value for %s: %s", self.entity_description.key, self._attr_native_value + self.data.results = JewishCalendarDataResults( + daytime_date, after_shkia_date, after_tzais_date, today_times ) - def make_zmanim(self, date: dt.date) -> Zmanim: - """Create a Zmanim object.""" - return Zmanim( - date=date, - location=self._location, - candle_lighting_offset=self._candle_lighting_offset, - havdalah_offset=self._havdalah_offset, - ) + +class JewishCalendarSensor(JewishCalendarBaseSensor): + """Representation of an Jewish calendar sensor.""" + + entity_description: JewishCalendarSensorDescription + + def __init__( + self, + config_entry: JewishCalendarConfigEntry, + description: SensorEntityDescription, + ) -> None: + """Initialize the Jewish calendar sensor.""" + super().__init__(config_entry, description) + # Set the options for enumeration sensors + if self.entity_description.options_fn is not None: + self._attr_options = self.entity_description.options_fn(self.data.diaspora) + + @property + def native_value(self) -> str | int | dt.datetime | None: + """Return the state of the sensor.""" + if self.data.results is None: + return None + return self.entity_description.value_fn(self.data.results) @property def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" - return self._attrs - - def get_state( - self, - daytime_date: HDateInfo, - after_shkia_date: HDateInfo, - after_tzais_date: HDateInfo, - ) -> Any | None: - """For a given type of sensor, return the state.""" - # Terminology note: by convention in py-libhdate library, "upcoming" - # refers to "current" or "upcoming" dates. - if self.entity_description.key == "date": - hdate = after_shkia_date.hdate - self._attrs = { - "hebrew_year": str(hdate.year), - "hebrew_month_name": str(hdate.month), - "hebrew_day": str(hdate.day), - } - return after_shkia_date.hdate - if self.entity_description.key == "weekly_portion": - self._attr_options = [str(p) for p in Parasha] - # Compute the weekly portion based on the upcoming shabbat. - return str(after_tzais_date.upcoming_shabbat.parasha) - if self.entity_description.key == "holiday": - _holidays = after_shkia_date.holidays - _id = ", ".join(holiday.name for holiday in _holidays) - _type = ", ".join( - dict.fromkeys(_holiday.type.name for _holiday in _holidays) - ) - self._attrs = {"id": _id, "type": _type} - self._attr_options = HolidayDatabase(self._diaspora).get_all_names() - return ", ".join(str(holiday) for holiday in _holidays) if _holidays else "" - if self.entity_description.key == "omer_count": - return after_shkia_date.omer.total_days if after_shkia_date.omer else 0 - if self.entity_description.key == "daf_yomi": - return daytime_date.daf_yomi - - return None + if self.data.results is None: + return {} + if self.entity_description.attr_fn is not None: + return self.entity_description.attr_fn(self.data.results) + return {} -class JewishCalendarTimeSensor(JewishCalendarSensor): +class JewishCalendarTimeSensor(JewishCalendarBaseSensor): """Implement attributes for sensors returning times.""" _attr_device_class = SensorDeviceClass.TIMESTAMP + entity_description: JewishCalendarTimestampSensorDescription - def get_state( - self, - daytime_date: HDateInfo, - after_shkia_date: HDateInfo, - after_tzais_date: HDateInfo, - ) -> Any | None: - """For a given type of sensor, return the state.""" - if self.entity_description.key == "upcoming_shabbat_candle_lighting": - times = self.make_zmanim( - after_tzais_date.upcoming_shabbat.previous_day.gdate - ) - return times.candle_lighting - if self.entity_description.key == "upcoming_candle_lighting": - times = self.make_zmanim( - after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate - ) - return times.candle_lighting - if self.entity_description.key == "upcoming_shabbat_havdalah": - times = self.make_zmanim(after_tzais_date.upcoming_shabbat.gdate) - return times.havdalah - if self.entity_description.key == "upcoming_havdalah": - times = self.make_zmanim( - after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate - ) - return times.havdalah - - times = self.make_zmanim(dt_util.now().date()) - return times.zmanim[self.entity_description.key].local + @property + def native_value(self) -> dt.datetime | None: + """Return the state of the sensor.""" + if self.data.results is None: + return None + if self.entity_description.value_fn is None: + return self.data.results.zmanim.zmanim[self.entity_description.key].local + return self.entity_description.value_fn( + self.data.results.after_tzais_date, self.make_zmanim + ) diff --git a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr index 8dfd04afc08..3c8acde6e72 100644 --- a/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr +++ b/tests/components/jewish_calendar/snapshots/test_diagnostics.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_diagnostics[Jerusalem] +# name: test_diagnostics[test_time0-Jerusalem] dict({ 'data': dict({ 'candle_lighting_offset': 40, @@ -17,6 +17,54 @@ 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", }), }), + 'results': dict({ + 'after_shkia_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'after_tzais_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'daytime_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 40, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')", + }), + }), + }), + }), }), 'entry_data': dict({ 'diaspora': False, @@ -25,7 +73,7 @@ }), }) # --- -# name: test_diagnostics[New York] +# name: test_diagnostics[test_time0-New York] dict({ 'data': dict({ 'candle_lighting_offset': 18, @@ -43,6 +91,54 @@ 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", }), }), + 'results': dict({ + 'after_shkia_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': True, + 'nusach': 'sephardi', + }), + 'after_tzais_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': True, + 'nusach': 'sephardi', + }), + 'daytime_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': True, + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': True, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='America/New_York')", + }), + }), + }), + }), }), 'entry_data': dict({ 'diaspora': True, @@ -51,7 +147,7 @@ }), }) # --- -# name: test_diagnostics[None] +# name: test_diagnostics[test_time0-None] dict({ 'data': dict({ 'candle_lighting_offset': 18, @@ -69,6 +165,54 @@ 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", }), }), + 'results': dict({ + 'after_shkia_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'after_tzais_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'daytime_date': dict({ + 'date': dict({ + 'day': 21, + 'month': 10, + 'year': 5785, + }), + 'diaspora': False, + 'nusach': 'sephardi', + }), + 'zmanim': dict({ + 'candle_lighting_offset': 18, + 'date': dict({ + '__type': "", + 'isoformat': '2025-05-19', + }), + 'havdalah_offset': 0, + 'location': dict({ + 'altitude': '**REDACTED**', + 'diaspora': False, + 'latitude': '**REDACTED**', + 'longitude': '**REDACTED**', + 'name': 'test home', + 'timezone': dict({ + '__type': "", + 'repr': "zoneinfo.ZoneInfo(key='US/Pacific')", + }), + }), + }), + }), }), 'entry_data': dict({ 'language': 'en', diff --git a/tests/components/jewish_calendar/test_diagnostics.py b/tests/components/jewish_calendar/test_diagnostics.py index cd3ace24c8c..31d224a756d 100644 --- a/tests/components/jewish_calendar/test_diagnostics.py +++ b/tests/components/jewish_calendar/test_diagnostics.py @@ -1,5 +1,7 @@ """Tests for the diagnostics data provided by the Jewish Calendar integration.""" +import datetime as dt + import pytest from syrupy.assertion import SnapshotAssertion @@ -13,6 +15,8 @@ from tests.typing import ClientSessionGenerator @pytest.mark.parametrize( ("location_data"), ["Jerusalem", "New York", None], indirect=True ) +@pytest.mark.parametrize("test_time", [dt.datetime(2025, 5, 19)], indirect=True) +@pytest.mark.usefixtures("setup_at_time") async def test_diagnostics( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -20,10 +24,6 @@ async def test_diagnostics( snapshot: SnapshotAssertion, ) -> None: """Test diagnostics with different locations.""" - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - diagnostics_data = await get_diagnostics_for_config_entry( hass, hass_client, config_entry ) From 5d76d92bcf90588785d1d97324dc66f1044edf2f Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 20 May 2025 13:13:40 -0400 Subject: [PATCH 0706/1175] bump aiokem to 0.5.11 (#145332) fix: bump aiokem --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 6b2f6190883..798fd4b61d2 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.10"] + "requirements": ["aiokem==0.5.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index a719c0253b6..cade178bdd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.10 +aiokem==0.5.11 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b68589cd1b..ac538873a10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimmich==0.6.0 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.10 +aiokem==0.5.11 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 37e13505cf7f4ef4f040768706044bb6e027acc5 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Tue, 20 May 2025 19:42:10 +0200 Subject: [PATCH 0707/1175] Handle more exceptions in azure_storage (#145320) --- .../components/azure_storage/__init__.py | 4 +-- .../components/azure_storage/backup.py | 16 +++++++++- tests/components/azure_storage/test_backup.py | 30 ++++++++++++++----- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/azure_storage/__init__.py b/homeassistant/components/azure_storage/__init__.py index 00e419fd3c9..78d85dd6a59 100644 --- a/homeassistant/components/azure_storage/__init__.py +++ b/homeassistant/components/azure_storage/__init__.py @@ -2,8 +2,8 @@ from aiohttp import ClientTimeout from azure.core.exceptions import ( + AzureError, ClientAuthenticationError, - HttpResponseError, ResourceNotFoundError, ) from azure.core.pipeline.transport._aiohttp import ( @@ -70,7 +70,7 @@ async def async_setup_entry( translation_key="invalid_auth", translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]}, ) from err - except HttpResponseError as err: + except AzureError as err: raise ConfigEntryNotReady( translation_domain=DOMAIN, translation_key="cannot_connect", diff --git a/homeassistant/components/azure_storage/backup.py b/homeassistant/components/azure_storage/backup.py index 4a9254213dc..54fd069a11f 100644 --- a/homeassistant/components/azure_storage/backup.py +++ b/homeassistant/components/azure_storage/backup.py @@ -8,7 +8,7 @@ import json import logging from typing import Any, Concatenate -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError from azure.storage.blob import BlobProperties from homeassistant.components.backup import ( @@ -80,6 +80,20 @@ def handle_backup_errors[_R, **P]( f"Error during backup operation in {func.__name__}:" f" Status {err.status_code}, message: {err.message}" ) from err + except ServiceRequestError as err: + raise BackupAgentError( + f"Timeout during backup operation in {func.__name__}" + ) from err + except AzureError as err: + _LOGGER.debug( + "Error during backup in %s: %s", + func.__name__, + err, + exc_info=True, + ) + raise BackupAgentError( + f"Error during backup operation in {func.__name__}: {err}" + ) from err return wrapper diff --git a/tests/components/azure_storage/test_backup.py b/tests/components/azure_storage/test_backup.py index 7c5912a4981..ebb491c2b7c 100644 --- a/tests/components/azure_storage/test_backup.py +++ b/tests/components/azure_storage/test_backup.py @@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from io import StringIO from unittest.mock import ANY, Mock, patch -from azure.core.exceptions import HttpResponseError +from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError from azure.storage.blob import BlobProperties import pytest @@ -276,14 +276,33 @@ async def test_agents_error_on_download_not_found( assert mock_client.download_blob.call_count == 0 +@pytest.mark.parametrize( + ("error", "message"), + [ + ( + HttpResponseError("http error"), + "Error during backup operation in async_delete_backup: Status None, message: http error", + ), + ( + ServiceRequestError("timeout"), + "Timeout during backup operation in async_delete_backup", + ), + ( + AzureError("generic error"), + "Error during backup operation in async_delete_backup: generic error", + ), + ], +) async def test_error_during_delete( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, mock_client: MagicMock, mock_config_entry: MockConfigEntry, + error: Exception, + message: str, ) -> None: """Test the error wrapper.""" - mock_client.delete_blob.side_effect = HttpResponseError("Failed to delete backup") + mock_client.delete_blob.side_effect = error client = await hass_ws_client(hass) @@ -297,12 +316,7 @@ async def test_error_during_delete( assert response["success"] assert response["result"] == { - "agent_errors": { - f"{DOMAIN}.{mock_config_entry.entry_id}": ( - "Error during backup operation in async_delete_backup: " - "Status None, message: Failed to delete backup" - ) - } + "agent_errors": {f"{DOMAIN}.{mock_config_entry.entry_id}": message} } From abcf925b7987db3929dc38e151f12a636476e34b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 May 2025 14:00:27 -0400 Subject: [PATCH 0708/1175] Assist Pipeline stream TTS when supported and long response (#145264) * Assist Pipeline stream TTS when supported and long response * Indicate in run-start if streaming supported * Simplify a little bit * Trigger streaming based on characters * 60 --- .../components/assist_pipeline/pipeline.py | 76 +++++- .../assist_pipeline/snapshots/test_init.ambr | 5 + .../snapshots/test_pipeline.ambr | 223 +++++++++++++++++- .../snapshots/test_websocket.ambr | 17 ++ .../assist_pipeline/test_pipeline.py | 58 ++++- 5 files changed, 363 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 5f811ac955b..7d5f98e87f6 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -89,6 +89,8 @@ KEY_ASSIST_PIPELINE: HassKey[PipelineData] = HassKey(DOMAIN) KEY_PIPELINE_CONVERSATION_DATA: HassKey[dict[str, PipelineConversationData]] = HassKey( "pipeline_conversation_data" ) +# Number of response parts to handle before streaming the response +STREAM_RESPONSE_CHARS = 60 def validate_language(data: dict[str, Any]) -> Any: @@ -552,7 +554,7 @@ class PipelineRun: event_callback: PipelineEventCallback language: str = None # type: ignore[assignment] runner_data: Any | None = None - intent_agent: str | None = None + intent_agent: conversation.AgentInfo | None = None tts_audio_output: str | dict[str, Any] | None = None wake_word_settings: WakeWordSettings | None = None audio_settings: AudioSettings = field(default_factory=AudioSettings) @@ -588,6 +590,9 @@ class PipelineRun: _intent_agent_only = False """If request should only be handled by agent, ignoring sentence triggers and local processing.""" + _streamed_response_text = False + """If the conversation agent streamed response text to TTS result.""" + def __post_init__(self) -> None: """Set language for pipeline.""" self.language = self.pipeline.language or self.hass.config.language @@ -649,6 +654,11 @@ class PipelineRun: "token": self.tts_stream.token, "url": self.tts_stream.url, "mime_type": self.tts_stream.content_type, + "stream_response": ( + self.tts_stream.supports_streaming_input + and self.intent_agent + and self.intent_agent.supports_streaming + ), } self.process_event(PipelineEvent(PipelineEventType.RUN_START, data)) @@ -896,12 +906,12 @@ class PipelineRun: ) -> str: """Run speech-to-text portion of pipeline. Returns the spoken text.""" # Create a background task to prepare the conversation agent - if self.end_stage >= PipelineStage.INTENT: + if self.end_stage >= PipelineStage.INTENT and self.intent_agent: self.hass.async_create_background_task( conversation.async_prepare_agent( - self.hass, self.intent_agent, self.language + self.hass, self.intent_agent.id, self.language ), - f"prepare conversation agent {self.intent_agent}", + f"prepare conversation agent {self.intent_agent.id}", ) if isinstance(self.stt_provider, stt.Provider): @@ -1042,7 +1052,7 @@ class PipelineRun: message=f"Intent recognition engine {engine} is not found", ) - self.intent_agent = agent_info.id + self.intent_agent = agent_info async def recognize_intent( self, @@ -1075,7 +1085,7 @@ class PipelineRun: PipelineEvent( PipelineEventType.INTENT_START, { - "engine": self.intent_agent, + "engine": self.intent_agent.id, "language": input_language, "intent_input": intent_input, "conversation_id": conversation_id, @@ -1092,11 +1102,11 @@ class PipelineRun: conversation_id=conversation_id, device_id=device_id, language=input_language, - agent_id=self.intent_agent, + agent_id=self.intent_agent.id, extra_system_prompt=conversation_extra_system_prompt, ) - agent_id = self.intent_agent + agent_id = self.intent_agent.id processed_locally = agent_id == conversation.HOME_ASSISTANT_AGENT intent_response: intent.IntentResponse | None = None if not processed_locally and not self._intent_agent_only: @@ -1118,7 +1128,7 @@ class PipelineRun: # If the LLM has API access, we filter out some sentences that are # interfering with LLM operation. if ( - intent_agent_state := self.hass.states.get(self.intent_agent) + intent_agent_state := self.hass.states.get(self.intent_agent.id) ) and intent_agent_state.attributes.get( ATTR_SUPPORTED_FEATURES, 0 ) & conversation.ConversationEntityFeature.CONTROL: @@ -1140,6 +1150,13 @@ class PipelineRun: agent_id = conversation.HOME_ASSISTANT_AGENT processed_locally = True + if self.tts_stream and self.tts_stream.supports_streaming_input: + tts_input_stream: asyncio.Queue[str | None] | None = asyncio.Queue() + else: + tts_input_stream = None + chat_log_role = None + delta_character_count = 0 + @callback def chat_log_delta_listener( chat_log: conversation.ChatLog, delta: dict @@ -1153,6 +1170,42 @@ class PipelineRun: }, ) ) + if tts_input_stream is None: + return + + nonlocal chat_log_role + + if role := delta.get("role"): + chat_log_role = role + + # We are only interested in assistant deltas with content + if chat_log_role != "assistant" or not ( + content := delta.get("content") + ): + return + + tts_input_stream.put_nowait(content) + + if self._streamed_response_text: + return + + nonlocal delta_character_count + + delta_character_count += len(content) + if delta_character_count < STREAM_RESPONSE_CHARS: + return + + # Streamed responses are not cached. We only start streaming text after + # we have received a couple of words that indicates it will be a long response. + self._streamed_response_text = True + + async def tts_input_stream_generator() -> AsyncGenerator[str]: + """Yield TTS input stream.""" + while (tts_input := await tts_input_stream.get()) is not None: + yield tts_input + + assert self.tts_stream is not None + self.tts_stream.async_set_message_stream(tts_input_stream_generator()) with ( chat_session.async_get_chat_session( @@ -1196,6 +1249,8 @@ class PipelineRun: speech = conversation_result.response.speech.get("plain", {}).get( "speech", "" ) + if tts_input_stream and self._streamed_response_text: + tts_input_stream.put_nowait(None) except Exception as src_error: _LOGGER.exception("Unexpected error during intent recognition") @@ -1273,7 +1328,8 @@ class PipelineRun: ) ) - self.tts_stream.async_set_message(tts_input) + if not self._streamed_response_text: + self.tts_stream.async_set_message(tts_input) tts_output = { "media_id": self.tts_stream.media_source_id, diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 5d2d25ddc5c..4ae4b5dce4c 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -8,6 +8,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -107,6 +108,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -206,6 +208,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -305,6 +308,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -428,6 +432,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index f5940edbc76..2e005fb4c13 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_chat_log_tts_streaming[to_stream_tts0-1] +# name: test_chat_log_tts_streaming[to_stream_tts0-0-] list([ dict({ 'data': dict({ @@ -8,6 +8,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': True, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -153,6 +154,225 @@ }), ]) # --- +# name: test_chat_log_tts_streaming[to_stream_tts1-16-hello, how are you? I'm doing well, thank you. What about you?] + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + 'tts_output': dict({ + 'mime_type': 'audio/mpeg', + 'stream_response': True, + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'test-agent', + 'intent_input': 'Set a timer', + 'language': 'en', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'role': 'assistant', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'hello, ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'how ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'are ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '? ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': "I'm ", + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'doing ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'well', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': ', ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'thank ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '. ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'What ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'about ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '?', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': True, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': "hello, how are you? I'm doing well, thank you. What about you?", + }), + }), + }), + }), + 'processed_locally': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'tts.test', + 'language': 'en_US', + 'tts_input': "hello, how are you? I'm doing well, thank you. What about you?", + 'voice': None, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'tts_output': dict({ + 'media_id': 'media-source://tts/-stream-/mocked-token.mp3', + 'mime_type': 'audio/mpeg', + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- # name: test_pipeline_language_used_instead_of_conversation_language list([ dict({ @@ -321,6 +541,7 @@ 'pipeline': , 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 827b9c71ba8..4f29fd79568 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -10,6 +10,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -101,6 +102,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -204,6 +206,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -295,6 +298,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -408,6 +412,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'test_token.mp3', 'url': '/api/tts_proxy/test_token.mp3', }), @@ -616,6 +621,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -670,6 +676,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -686,6 +693,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -702,6 +710,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -718,6 +727,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -734,6 +744,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -868,6 +879,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -884,6 +896,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -941,6 +954,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -957,6 +971,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -1017,6 +1032,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), @@ -1033,6 +1049,7 @@ }), 'tts_output': dict({ 'mime_type': 'audio/mpeg', + 'stream_response': False, 'token': 'mocked-token.mp3', 'url': '/api/tts_proxy/mocked-token.mp3', }), diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index 1714c909a18..d8550f34deb 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1575,8 +1575,9 @@ async def test_pipeline_language_used_instead_of_conversation_language( @pytest.mark.parametrize( - ("to_stream_tts", "expected_chunks"), + ("to_stream_tts", "expected_chunks", "chunk_text"), [ + # Size below STREAM_RESPONSE_CHUNKS ( [ "hello,", @@ -1588,7 +1589,33 @@ async def test_pipeline_language_used_instead_of_conversation_language( "you", "?", ], - 1, + # We are not streaming, so 0 chunks via streaming method + 0, + "", + ), + # Size above STREAM_RESPONSE_CHUNKS + ( + [ + "hello, ", + "how ", + "are ", + "you", + "? ", + "I'm ", + "doing ", + "well", + ", ", + "thank ", + "you", + ". ", + "What ", + "about ", + "you", + "?", + ], + # We are streamed, so equal to count above list items + 16, + "hello, how are you? I'm doing well, thank you. What about you?", ), ], ) @@ -1602,6 +1629,7 @@ async def test_chat_log_tts_streaming( pipeline_data: assist_pipeline.pipeline.PipelineData, to_stream_tts: list[str], expected_chunks: int, + chunk_text: str, ) -> None: """Test that chat log events are streamed to the TTS entity.""" events: list[assist_pipeline.PipelineEvent] = [] @@ -1627,22 +1655,41 @@ async def test_chat_log_tts_streaming( ), ) + received_tts = [] + + async def async_stream_tts_audio( + request: tts.TTSAudioRequest, + ) -> tts.TTSAudioResponse: + """Mock stream TTS audio.""" + + async def gen_data(): + async for msg in request.message_gen: + received_tts.append(msg) + yield msg.encode() + + return tts.TTSAudioResponse( + extension="mp3", + data_gen=gen_data(), + ) + async def async_get_tts_audio( message: str, language: str, options: dict[str, Any] | None = None, - ) -> tts.TTSAudioResponse: + ) -> tts.TtsAudioType: """Mock get TTS audio.""" return ("mp3", b"".join([chunk.encode() for chunk in to_stream_tts])) mock_tts_entity.async_get_tts_audio = async_get_tts_audio + mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio + mock_tts_entity.async_supports_streaming_input = Mock(return_value=True) with patch( "homeassistant.components.assist_pipeline.pipeline.conversation.async_get_agent_info", return_value=conversation.AgentInfo( id="test-agent", name="Test Agent", - supports_streaming=False, + supports_streaming=True, ), ): await pipeline_input.validate() @@ -1707,6 +1754,7 @@ async def test_chat_log_tts_streaming( streamed_text = "".join(to_stream_tts) assert tts_result == streamed_text - assert expected_chunks == 1 + assert len(received_tts) == expected_chunks + assert "".join(received_tts) == chunk_text assert process_events(events) == snapshot From b0415588d733623abfcb6084db2cddecb72ae298 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 20 May 2025 20:40:22 +0200 Subject: [PATCH 0709/1175] Add support for videos in Immich media source (#145254) add support for videos --- .../components/immich/media_source.py | 45 +++++++++++++++-- homeassistant/util/aiohttp.py | 3 ++ tests/components/immich/__init__.py | 9 ++++ tests/components/immich/conftest.py | 2 + tests/components/immich/const.py | 9 +++- tests/components/immich/test_media_source.py | 49 ++++++++++++++++++- 6 files changed, 110 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index f267433f233..201076f1295 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -5,7 +5,7 @@ from __future__ import annotations from logging import getLogger import mimetypes -from aiohttp.web import HTTPNotFound, Request, Response +from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse from aioimmich.exceptions import ImmichError from homeassistant.components.http import HomeAssistantView @@ -20,6 +20,7 @@ from homeassistant.components.media_source import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator from .const import DOMAIN from .coordinator import ImmichConfigEntry @@ -136,7 +137,7 @@ class ImmichMediaSource(MediaSource): except ImmichError: return [] - return [ + ret = [ BrowseMediaSource( domain=DOMAIN, identifier=( @@ -156,6 +157,28 @@ class ImmichMediaSource(MediaSource): if asset.mime_type.startswith("image/") ] + ret.extend( + BrowseMediaSource( + domain=DOMAIN, + identifier=( + f"{identifier.unique_id}/" + f"{identifier.album_id}/" + f"{asset.asset_id}/" + f"{asset.file_name}" + ), + media_class=MediaClass.VIDEO, + media_content_type=asset.mime_type, + title=asset.file_name, + can_play=True, + can_expand=False, + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail", + ) + for asset in album_info.assets + if asset.mime_type.startswith("video/") + ) + + return ret + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" identifier = ImmichMediaSourceIdentifier(item.identifier) @@ -184,12 +207,12 @@ class ImmichMediaView(HomeAssistantView): async def get( self, request: Request, source_dir_id: str, location: str - ) -> Response: + ) -> Response | StreamResponse: """Start a GET request.""" if not self.hass.config_entries.async_loaded_entries(DOMAIN): raise HTTPNotFound - asset_id, file_name, size = location.split("/") + asset_id, file_name, size = location.split("/") mime_type, _ = mimetypes.guess_type(file_name) if not isinstance(mime_type, str): raise HTTPNotFound @@ -202,6 +225,20 @@ class ImmichMediaView(HomeAssistantView): assert entry immich_api = entry.runtime_data.api + # stream response for videos + if mime_type.startswith("video/"): + try: + resp = await immich_api.assets.async_play_video_stream(asset_id) + except ImmichError as exc: + raise HTTPNotFound from exc + stream = ChunkAsyncStreamIterator(resp) + response = StreamResponse() + await response.prepare(request) + async for chunk in stream: + await response.write(chunk) + return response + + # web response for images try: image = await immich_api.assets.async_view_asset(asset_id, size) except ImmichError as exc: diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 5571861f417..aad9771d963 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -37,6 +37,9 @@ class MockPayloadWriter: async def write_headers(self, *args: Any, **kwargs: Any) -> None: """Write headers.""" + async def write(self, *args: Any, **kwargs: Any) -> None: + """Write payload.""" + _MOCK_PAYLOAD_WRITER = MockPayloadWriter() diff --git a/tests/components/immich/__init__.py b/tests/components/immich/__init__.py index 604ab84d68d..3a48c2cd725 100644 --- a/tests/components/immich/__init__.py +++ b/tests/components/immich/__init__.py @@ -1,10 +1,19 @@ """Tests for the Immich integration.""" from homeassistant.core import HomeAssistant +from homeassistant.util.aiohttp import MockStreamReader from tests.common import MockConfigEntry +class MockStreamReaderChunked(MockStreamReader): + """Mock a stream reader with simulated chunked data.""" + + async def readchunk(self) -> tuple[bytes, bool]: + """Read bytes.""" + return (self._content.read(), False) + + async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Fixture for setting up the component.""" config_entry.add_to_hass(hass) diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py index d26eddfd55e..5a957870f07 100644 --- a/tests/components/immich/conftest.py +++ b/tests/components/immich/conftest.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import MockStreamReaderChunked from .const import MOCK_ALBUM_WITH_ASSETS, MOCK_ALBUM_WITHOUT_ASSETS from tests.common import MockConfigEntry @@ -69,6 +70,7 @@ def mock_immich_assets() -> AsyncMock: """Mock the Immich server.""" mock = AsyncMock(spec=ImmichAssests) mock.async_view_asset.return_value = b"xxxx" + mock.async_play_video_stream.return_value = MockStreamReaderChunked(b"xxxx") return mock diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py index aeec4764732..ac0b221f721 100644 --- a/tests/components/immich/const.py +++ b/tests/components/immich/const.py @@ -41,5 +41,12 @@ MOCK_ALBUM_WITH_ASSETS = ImmichAlbum( "This is my first great album", "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", 1, - [ImmichAsset("2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg")], + [ + ImmichAsset( + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg" + ), + ImmichAsset( + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", "filename.mp4", "video/mp4" + ), + ], ) diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index 772f0535f02..ae7201f5e70 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.aiohttp import MockRequest -from . import setup_integration +from . import MockStreamReaderChunked, setup_integration from .const import MOCK_ALBUM_WITHOUT_ASSETS from tests.common import MockConfigEntry @@ -255,7 +255,7 @@ async def test_browse_media_get_items( result = await source.async_browse_media(item) assert result - assert len(result.children) == 1 + assert len(result.children) == 2 media_file = result.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( @@ -273,6 +273,23 @@ async def test_browse_media_get_items( "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail" ) + media_file = result.children[1] + assert isinstance(media_file, BrowseMedia) + assert media_file.identifier == ( + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" + ) + assert media_file.title == "filename.mp4" + assert media_file.media_class == MediaClass.VIDEO + assert media_file.media_content_type == "video/mp4" + assert media_file.can_play is True + assert not media_file.can_expand + assert media_file.thumbnail == ( + "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail.jpg/thumbnail" + ) + async def test_media_view( hass: HomeAssistant, @@ -317,6 +334,22 @@ async def test_media_view( "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", ) + # exception in async_play_video_stream() + mock_immich.assets.async_play_video_stream.side_effect = ImmichError( + { + "message": "Not found or no asset.read access", + "error": "Bad Request", + "statusCode": 400, + "correlationId": "e0hlizyl", + } + ) + with pytest.raises(web.HTTPNotFound): + await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + ) + # success mock_immich.assets.async_view_asset.side_effect = None mock_immich.assets.async_view_asset.return_value = b"xxxx" @@ -334,3 +367,15 @@ async def test_media_view( "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize", ) assert isinstance(result, web.Response) + + mock_immich.assets.async_play_video_stream.side_effect = None + mock_immich.assets.async_play_video_stream.return_value = MockStreamReaderChunked( + b"xxxx" + ) + with patch.object(tempfile, "tempdir", tmp_path): + result = await view.get( + request, + "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + ) + assert isinstance(result, web.StreamResponse) From 8ec5472b79c67710f8ded30270c9208a4e351e46 Mon Sep 17 00:00:00 2001 From: Lode Smets <31108717+lodesmets@users.noreply.github.com> Date: Tue, 20 May 2025 21:17:43 +0200 Subject: [PATCH 0710/1175] Added support for shared spaces in Synology DSM (Photo Station) (#144044) * Added shared space to the list of all the albums * Added tests * added more tests * Apply suggestions from code review --------- Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .../components/synology_dsm/media_source.py | 34 ++++++++++++--- .../synology_dsm/test_media_source.py | 41 +++++++++++++++++-- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 6234f5e8dd0..7fafe1fecb3 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -145,6 +145,17 @@ class SynologyPhotosMediaSource(MediaSource): can_expand=True, ) ] + ret += [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{item.identifier}/shared", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title="Shared space", + can_play=False, + can_expand=True, + ) + ] ret.extend( BrowseMediaSource( domain=DOMAIN, @@ -162,13 +173,24 @@ class SynologyPhotosMediaSource(MediaSource): # Request items of album # Get Items - album = SynoPhotosAlbum(int(identifier.album_id), "", 0, identifier.passphrase) - try: - album_items = await diskstation.api.photos.get_items_from_album( - album, 0, 1000 + if identifier.album_id == "shared": + # Get items from shared space + try: + album_items = await diskstation.api.photos.get_items_from_shared_space( + 0, 1000 + ) + except SynologyDSMException: + return [] + else: + album = SynoPhotosAlbum( + int(identifier.album_id), "", 0, identifier.passphrase ) - except SynologyDSMException: - return [] + try: + album_items = await diskstation.api.photos.get_items_from_album( + album, 0, 1000 + ) + except SynologyDSMException: + return [] assert album_items is not None ret = [] diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index dd454f92137..d66688575bc 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -61,6 +61,11 @@ def dsm_with_photos() -> MagicMock: SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True, ""), ] ) + dsm.photos.get_items_from_shared_space = AsyncMock( + return_value=[ + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True, ""), + ] + ) dsm.photos.get_item_thumbnail_url = AsyncMock( return_value="http://my.thumbnail.url" ) @@ -257,13 +262,16 @@ async def test_browse_media_get_albums( result = await source.async_browse_media(item) assert result - assert len(result.children) == 2 + assert len(result.children) == 3 assert isinstance(result.children[0], BrowseMedia) assert result.children[0].identifier == "mocked_syno_dsm_entry/0" assert result.children[0].title == "All images" assert isinstance(result.children[1], BrowseMedia) - assert result.children[1].identifier == "mocked_syno_dsm_entry/1_" - assert result.children[1].title == "Album 1" + assert result.children[1].identifier == "mocked_syno_dsm_entry/shared" + assert result.children[1].title == "Shared space" + assert isinstance(result.children[2], BrowseMedia) + assert result.children[2].identifier == "mocked_syno_dsm_entry/1_" + assert result.children[2].title == "Album 1" @pytest.mark.usefixtures("setup_media_source") @@ -315,6 +323,17 @@ async def test_browse_media_get_items_error( assert result.identifier is None assert len(result.children) == 0 + # exception in get_items_from_shared_space() + dsm_with_photos.photos.get_items_from_shared_space = AsyncMock( + side_effect=SynologyDSMException("", None) + ) + item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/shared", None) + result = await source.async_browse_media(item) + + assert result + assert result.identifier is None + assert len(result.children) == 0 + @pytest.mark.usefixtures("setup_media_source") async def test_browse_media_get_items_thumbnail_error( @@ -411,6 +430,22 @@ async def test_browse_media_get_items( assert not item.can_expand assert item.thumbnail == "http://my.thumbnail.url" + item = MediaSourceItem(hass, DOMAIN, "mocked_syno_dsm_entry/shared", None) + result = await source.async_browse_media(item) + assert result + assert len(result.children) == 1 + item = result.children[0] + assert ( + item.identifier + == "mocked_syno_dsm_entry/shared_/10_1298753/filename.jpg_shared" + ) + assert item.title == "filename.jpg" + assert item.media_class == MediaClass.IMAGE + assert item.media_content_type == "image/jpeg" + assert item.can_play + assert not item.can_expand + assert item.thumbnail == "http://my.thumbnail.url" + @pytest.mark.usefixtures("setup_media_source") async def test_media_view( From 3ff3cb975b6122a1c58d01eda0efd75f453e2858 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 20 May 2025 15:47:45 -0400 Subject: [PATCH 0711/1175] Add date sensors to Rehlko (#145314) * feat: add datetime sensors * fix: constants * fix: constants * fix: move tz conversion to api * fix: update typing --- homeassistant/components/rehlko/__init__.py | 3 +- homeassistant/components/rehlko/const.py | 1 + homeassistant/components/rehlko/entity.py | 10 +- homeassistant/components/rehlko/sensor.py | 64 ++++- homeassistant/components/rehlko/strings.json | 15 ++ .../components/rehlko/fixtures/generator.json | 6 +- .../rehlko/snapshots/test_sensor.ambr | 240 ++++++++++++++++++ 7 files changed, 324 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index bda2704a206..3f255f23085 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import dt as dt_util from .const import ( CONF_REFRESH_TOKEN, @@ -28,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bool: """Set up Rehlko from a config entry.""" websession = async_get_clientsession(hass) - rehlko = AioKem(session=websession) + rehlko = AioKem(session=websession, home_timezone=dt_util.get_default_time_zone()) # If requests take more than 20 seconds; timeout and let the setup retry. rehlko.set_timeout(20) diff --git a/homeassistant/components/rehlko/const.py b/homeassistant/components/rehlko/const.py index f63c0872d46..6dced0ccda6 100644 --- a/homeassistant/components/rehlko/const.py +++ b/homeassistant/components/rehlko/const.py @@ -18,6 +18,7 @@ DEVICE_DATA_IS_CONNECTED = "isConnected" KOHLER = "Kohler" GENERATOR_DATA_DEVICE = "device" +GENERATOR_DATA_EXERCISE = "exercise" CONNECTION_EXCEPTIONS = ( TimeoutError, diff --git a/homeassistant/components/rehlko/entity.py b/homeassistant/components/rehlko/entity.py index 94d384e1949..274562e6a41 100644 --- a/homeassistant/components/rehlko/entity.py +++ b/homeassistant/components/rehlko/entity.py @@ -43,7 +43,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): device_id: int, device_data: dict, description: EntityDescription, - use_device_key: bool = False, + document_key: str | None = None, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -61,7 +61,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): manufacturer=KOHLER, connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]), ) - self._use_device_key = use_device_key + self._document_key = document_key @property def _device_data(self) -> dict[str, Any]: @@ -71,8 +71,10 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): @property def _rehlko_value(self) -> str: """Return the sensor value.""" - if self._use_device_key: - return self._device_data[self.entity_description.key] + if self._document_key: + return self.coordinator.data[self._document_key][ + self.entity_description.key + ] return self.coordinator.data[self.entity_description.key] @property diff --git a/homeassistant/components/rehlko/sensor.py b/homeassistant/components/rehlko/sensor.py index 9186f0e0c9f..6ff45b1a464 100644 --- a/homeassistant/components/rehlko/sensor.py +++ b/homeassistant/components/rehlko/sensor.py @@ -2,7 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -25,7 +27,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from .const import DEVICE_DATA_DEVICES, DEVICE_DATA_ID +from .const import ( + DEVICE_DATA_DEVICES, + DEVICE_DATA_ID, + GENERATOR_DATA_DEVICE, + GENERATOR_DATA_EXERCISE, +) from .coordinator import RehlkoConfigEntry from .entity import RehlkoEntity @@ -37,7 +44,8 @@ PARALLEL_UPDATES = 0 class RehlkoSensorEntityDescription(SensorEntityDescription): """Class describing Rehlko sensor entities.""" - use_device_key: bool = False + document_key: str | None = None + value_fn: Callable[[str], datetime | None] | None = None SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( @@ -116,7 +124,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="runtimeSinceLastMaintenanceHours", @@ -132,7 +140,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( translation_key="device_ip_address", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="serverIpAddress", @@ -171,7 +179,7 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( RehlkoSensorEntityDescription( key="status", translation_key="generator_status", - use_device_key=True, + document_key=GENERATOR_DATA_DEVICE, ), RehlkoSensorEntityDescription( key="engineState", @@ -181,6 +189,44 @@ SENSORS: tuple[RehlkoSensorEntityDescription, ...] = ( key="powerSource", translation_key="power_source", ), + RehlkoSensorEntityDescription( + key="lastRanTimestamp", + translation_key="last_run", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=datetime.fromisoformat, + ), + RehlkoSensorEntityDescription( + key="lastMaintenanceTimestamp", + translation_key="last_maintainance", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_DEVICE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="nextMaintenanceTimestamp", + translation_key="next_maintainance", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_DEVICE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="lastStartTimestamp", + translation_key="last_exercise", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_EXERCISE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), + RehlkoSensorEntityDescription( + key="nextStartTimestamp", + translation_key="next_exercise", + device_class=SensorDeviceClass.TIMESTAMP, + document_key=GENERATOR_DATA_EXERCISE, + value_fn=datetime.fromisoformat, + entity_registry_enabled_default=False, + ), ) @@ -199,7 +245,7 @@ async def async_setup_entry( device_data[DEVICE_DATA_ID], device_data, sensor_description, - sensor_description.use_device_key, + sensor_description.document_key, ) for home_data in homes for device_data in home_data[DEVICE_DATA_DEVICES] @@ -210,7 +256,11 @@ async def async_setup_entry( class RehlkoSensorEntity(RehlkoEntity, SensorEntity): """Representation of a Rehlko sensor.""" + entity_description: RehlkoSensorEntityDescription + @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the sensor state.""" + if self.entity_description.value_fn: + return self.entity_description.value_fn(self._rehlko_value) return self._rehlko_value diff --git a/homeassistant/components/rehlko/strings.json b/homeassistant/components/rehlko/strings.json index 6b842173558..d98ae04d5c8 100644 --- a/homeassistant/components/rehlko/strings.json +++ b/homeassistant/components/rehlko/strings.json @@ -91,6 +91,21 @@ }, "generator_status": { "name": "Generator status" + }, + "last_run": { + "name": "Last run" + }, + "last_maintainance": { + "name": "Last maintainance" + }, + "next_maintainance": { + "name": "Next maintainance" + }, + "next_exercise": { + "name": "Next exercise" + }, + "last_exercise": { + "name": "Last exercise" } } }, diff --git a/tests/components/rehlko/fixtures/generator.json b/tests/components/rehlko/fixtures/generator.json index fa1d4d0b45b..5741b470bc6 100644 --- a/tests/components/rehlko/fixtures/generator.json +++ b/tests/components/rehlko/fixtures/generator.json @@ -54,8 +54,8 @@ "alertCount": 0, "model": "Model20KW", "modelDisplayName": "20 KW", - "lastMaintenanceTimestamp": "2025-04-10T09:12:59", - "nextMaintenanceTimestamp": "2026-04-10T09:12:59", + "lastMaintenanceTimestamp": "2025-04-10T09:12:59-04:00", + "nextMaintenanceTimestamp": "2026-04-10T09:12:59-04:00", "maintenancePeriodDays": 365, "hasServiceAgreement": null, "totalRuntimeHours": 120.2 @@ -74,7 +74,7 @@ }, "exercise": { "frequency": "Weekly", - "nextStartTimestamp": "2025-04-19T10:00:00", + "nextStartTimestamp": "2025-04-19T10:00:00-04:00", "mode": "Unloaded", "runningMode": null, "durationMinutes": 20, diff --git a/tests/components/rehlko/snapshots/test_sensor.ambr b/tests/components/rehlko/snapshots/test_sensor.ambr index 3973996ba80..3f0334ec7b8 100644 --- a/tests/components/rehlko/snapshots/test_sensor.ambr +++ b/tests/components/rehlko/snapshots/test_sensor.ambr @@ -609,6 +609,150 @@ 'state': 'ReadyToRun', }) # --- +# name: test_sensors[sensor.generator_1_last_exercise-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_exercise', + '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 exercise', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_exercise', + 'unique_id': 'myemail@email.com_12345_lastStartTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_exercise-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last exercise', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_exercise', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-12T14:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_last_maintainance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_maintainance', + '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 maintainance', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_maintainance', + 'unique_id': 'myemail@email.com_12345_lastMaintenanceTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_maintainance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last maintainance', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_maintainance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-10T13:12:59+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_last_run-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_last_run', + '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 run', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_run', + 'unique_id': 'myemail@email.com_12345_lastRanTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_last_run-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Last run', + }), + 'context': , + 'entity_id': 'sensor.generator_1_last_run', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-12T14:00:00+00:00', + }) +# --- # name: test_sensors[sensor.generator_1_lube_oil_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -661,6 +805,102 @@ 'state': '6.0', }) # --- +# name: test_sensors[sensor.generator_1_next_exercise-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_next_exercise', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Next exercise', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'next_exercise', + 'unique_id': 'myemail@email.com_12345_nextStartTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_next_exercise-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Next exercise', + }), + 'context': , + 'entity_id': 'sensor.generator_1_next_exercise', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-04-19T14:00:00+00:00', + }) +# --- +# name: test_sensors[sensor.generator_1_next_maintainance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.generator_1_next_maintainance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Next maintainance', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'next_maintainance', + 'unique_id': 'myemail@email.com_12345_nextMaintenanceTimestamp', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.generator_1_next_maintainance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Generator 1 Next maintainance', + }), + 'context': , + 'entity_id': 'sensor.generator_1_next_maintainance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2026-04-10T13:12:59+00:00', + }) +# --- # name: test_sensors[sensor.generator_1_power_source-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 73811eac0a11c2f228c89b538f14fb934dd51aca Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 20 May 2025 15:48:33 -0400 Subject: [PATCH 0712/1175] Add support for music library folder to Sonos (#139554) * initial prototype * use constants * make playing work * remove unneeded code * remove unneeded code * fix regressions issues * refactor add_to_queue * refactor add_to_queue * refactor add_to_queue * simplify * add tests * remove bad test * rename constants * comments * comments * comments * use snapshot * refactor to use add_to_queue * refactor to use add_to_queue * add comments, redo snapshots * update comment * merge formatting * code review changes * fix: merge issue * fix: update snapshot to include new can_search field --- homeassistant/components/sonos/const.py | 10 +++ .../components/sonos/media_browser.py | 55 +++++++++++--- .../components/sonos/media_player.py | 24 ++++++ tests/components/sonos/conftest.py | 40 ++++++++++ .../sonos/snapshots/test_media_browser.ambr | 76 +++++++++++++++++++ tests/components/sonos/test_media_browser.py | 44 ++++++++++- tests/components/sonos/test_media_player.py | 19 +++++ 7 files changed, 258 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index cda40729dbc..614be2b1817 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -31,9 +31,12 @@ SONOS_ALBUM_ARTIST = "album_artists" SONOS_TRACKS = "tracks" SONOS_COMPOSER = "composers" SONOS_RADIO = "radio" +SONOS_SHARE = "share" SONOS_OTHER_ITEM = "other items" SONOS_AUDIO_BOOK = "audio book" +MEDIA_TYPE_DIRECTORY = MediaClass.DIRECTORY + SONOS_STATE_PLAYING = "PLAYING" SONOS_STATE_TRANSITIONING = "TRANSITIONING" @@ -43,12 +46,14 @@ EXPANDABLE_MEDIA_TYPES = [ MediaType.COMPOSER, MediaType.GENRE, MediaType.PLAYLIST, + MEDIA_TYPE_DIRECTORY, SONOS_ALBUM, SONOS_ALBUM_ARTIST, SONOS_ARTIST, SONOS_GENRE, SONOS_COMPOSER, SONOS_PLAYLISTS, + SONOS_SHARE, ] SONOS_TO_MEDIA_CLASSES = { @@ -59,6 +64,8 @@ SONOS_TO_MEDIA_CLASSES = { SONOS_GENRE: MediaClass.GENRE, SONOS_PLAYLISTS: MediaClass.PLAYLIST, SONOS_TRACKS: MediaClass.TRACK, + SONOS_SHARE: MediaClass.DIRECTORY, + "object.container": MediaClass.DIRECTORY, "object.container.album.musicAlbum": MediaClass.ALBUM, "object.container.genre.musicGenre": MediaClass.PLAYLIST, "object.container.person.composer": MediaClass.PLAYLIST, @@ -79,6 +86,7 @@ SONOS_TO_MEDIA_TYPES = { SONOS_GENRE: MediaType.GENRE, SONOS_PLAYLISTS: MediaType.PLAYLIST, SONOS_TRACKS: MediaType.TRACK, + "object.container": MEDIA_TYPE_DIRECTORY, "object.container.album.musicAlbum": MediaType.ALBUM, "object.container.genre.musicGenre": MediaType.PLAYLIST, "object.container.person.composer": MediaType.PLAYLIST, @@ -97,6 +105,7 @@ MEDIA_TYPES_TO_SONOS: dict[MediaType | str, str] = { MediaType.GENRE: SONOS_GENRE, MediaType.PLAYLIST: SONOS_PLAYLISTS, MediaType.TRACK: SONOS_TRACKS, + MEDIA_TYPE_DIRECTORY: SONOS_SHARE, } SONOS_TYPES_MAPPING = { @@ -127,6 +136,7 @@ LIBRARY_TITLES_MAPPING = { "A:GENRE": "Genres", "A:PLAYLISTS": "Playlists", "A:TRACKS": "Tracks", + "S:": "Folders", } PLAYABLE_MEDIA_TYPES = [ diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 16b425dae50..255daf22829 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -9,7 +9,7 @@ import logging from typing import cast import urllib.parse -from soco.data_structures import DidlObject +from soco.data_structures import DidlContainer, DidlObject from soco.ms_data_structures import MusicServiceItem from soco.music_library import MusicLibrary @@ -32,6 +32,7 @@ from .const import ( SONOS_ALBUM, SONOS_ALBUM_ARTIST, SONOS_GENRE, + SONOS_SHARE, SONOS_TO_MEDIA_CLASSES, SONOS_TO_MEDIA_TYPES, SONOS_TRACKS, @@ -105,6 +106,24 @@ def media_source_filter(item: BrowseMedia) -> bool: return item.media_content_type.startswith("audio/") +def _get_title(id_string: str) -> str: + """Extract a suitable title from the content id string.""" + if id_string.startswith("S:"): + # Format is S://server/share/folder + # If just S: this will be in the mappings; otherwise use the last folder in path. + title = LIBRARY_TITLES_MAPPING.get( + id_string, urllib.parse.unquote(id_string.split("/")[-1]) + ) + else: + parts = id_string.split("/") + title = ( + urllib.parse.unquote(parts[1]) + if len(parts) > 1 + else LIBRARY_TITLES_MAPPING.get(id_string, id_string) + ) + return title + + async def async_browse_media( hass: HomeAssistant, speaker: SonosSpeaker, @@ -240,10 +259,7 @@ def build_item_response( thumbnail = get_thumbnail_url(search_type, payload["idstring"]) if not title: - try: - title = urllib.parse.unquote(payload["idstring"].split("/")[1]) - except IndexError: - title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + title = _get_title(id_string=payload["idstring"]) try: media_class = SONOS_TO_MEDIA_CLASSES[ @@ -288,12 +304,12 @@ def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia: thumbnail = get_thumbnail_url(media_class, content_id, item=item) return BrowseMedia( - title=item.title, + title=_get_title(item.item_id) if item.title is None else item.title, thumbnail=thumbnail, media_class=media_class, media_content_id=content_id, media_content_type=SONOS_TO_MEDIA_TYPES[media_type], - can_play=can_play(item.item_class), + can_play=can_play(item.item_class, item_id=content_id), can_expand=can_expand(item), ) @@ -396,6 +412,10 @@ def library_payload(media_library: MusicLibrary, get_thumbnail_url=None) -> Brow with suppress(UnknownMediaType): children.append(item_payload(item, get_thumbnail_url)) + # Add entry for Folders at the top level of the music library. + didl_item = DidlContainer(title="Folders", parent_id="", item_id="S:") + children.append(item_payload(didl_item, get_thumbnail_url)) + return BrowseMedia( title="Music Library", media_class=MediaClass.DIRECTORY, @@ -508,12 +528,16 @@ def get_media_type(item: DidlObject) -> str: return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) -def can_play(item: DidlObject) -> bool: +def can_play(item_class: str, item_id: str | None = None) -> bool: """Test if playable. Used by async_browse_media. """ - return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES + # Folders are playable once we reach the folder level. + # Format is S://server_address/share/folder + if item_id and item_id.startswith("S:") and item_class == "object.container": + return item_id.count("/") >= 4 + return SONOS_TO_MEDIA_TYPES.get(item_class) in PLAYABLE_MEDIA_TYPES def can_expand(item: DidlObject) -> bool: @@ -565,6 +589,19 @@ def get_media( matches = media_library.get_music_library_information( search_type, search_term=search_term, full_album_art_uri=True ) + elif search_type == SONOS_SHARE: + # In order to get the MusicServiceItem, we browse the parent folder + # and find one that matches on item_id. + parts = item_id.rstrip("/").split("/") + parent_folder = "/".join(parts[:-1]) + matches = media_library.browse_by_idstring( + search_type, parent_folder, full_album_art_uri=True + ) + result = next( + (item for item in matches if (item_id == item.item_id)), + None, + ) + matches = [result] 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 diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 573c28d700a..f1f95659469 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -53,6 +53,7 @@ from . import UnjoinData, media_browser from .const import ( DATA_SONOS, DOMAIN, + MEDIA_TYPE_DIRECTORY, MEDIA_TYPES_TO_SONOS, MODELS_LINEIN_AND_TV, MODELS_LINEIN_ONLY, @@ -656,6 +657,10 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_id, timeout=LONG_SERVICE_TIMEOUT ) soco.play_from_queue(0) + elif media_type == MEDIA_TYPE_DIRECTORY: + self._play_media_directory( + soco=soco, media_type=media_type, media_id=media_id, enqueue=enqueue + ) elif media_type in {MediaType.MUSIC, MediaType.TRACK}: # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) @@ -738,6 +743,25 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) + def _play_media_directory( + self, + soco: SoCo, + media_type: MediaType | str, + media_id: str, + enqueue: MediaPlayerEnqueue, + ): + """Play a directory from a music library share.""" + item = media_browser.get_media(self.media.library, media_id, media_type) + if not item: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_media", + translation_placeholders={ + "media_id": media_id, + }, + ) + self._play_media_queue(soco, item, enqueue) + @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 b33151678a5..5043c9331fc 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -21,6 +21,7 @@ from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.const import SONOS_SHARE from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceInfo @@ -501,6 +502,45 @@ def mock_browse_by_idstring( return list_from_json_fixture("music_library_tracks.json") if search_type == "albums" and idstring == "A:ALBUM": return list_from_json_fixture("music_library_albums.json") + if search_type == SONOS_SHARE and idstring == "S:": + return [ + MockMusicServiceItem( + None, + "S://192.168.1.1/music", + "S:", + "object.container", + ) + ] + if search_type == SONOS_SHARE and idstring == "S://192.168.1.1/music": + return [ + MockMusicServiceItem( + None, + "S://192.168.1.1/music/beatles", + "S://192.168.1.1/music", + "object.container", + ), + MockMusicServiceItem( + None, + "S://192.168.1.1/music/elton%20john", + "S://192.168.1.1/music", + "object.container", + ), + ] + if search_type == SONOS_SHARE and idstring == "S://192.168.1.1/music/elton%20john": + return [ + MockMusicServiceItem( + None, + "S://192.168.1.1/music/elton%20john/Greatest%20Hits", + "S://192.168.1.1/music/elton%20john", + "object.container", + ), + MockMusicServiceItem( + None, + "S://192.168.1.1/music/elton%20john/Good%20Bye%20Yellow%20Brick%20Road", + "S://192.168.1.1/music/elton%20john", + "object.container", + ), + ] return [] diff --git a/tests/components/sonos/snapshots/test_media_browser.ambr b/tests/components/sonos/snapshots/test_media_browser.ambr index 07992c4474c..ddf03ca3b37 100644 --- a/tests/components/sonos/snapshots/test_media_browser.ambr +++ b/tests/components/sonos/snapshots/test_media_browser.ambr @@ -192,6 +192,17 @@ 'thumbnail': None, 'title': 'Playlists', }), + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': 'S:', + 'media_content_type': 'directory', + 'thumbnail': None, + 'title': 'Folders', + }), ]) # --- # name: test_browse_media_library_albums @@ -242,6 +253,71 @@ }), ]) # --- +# name: test_browse_media_library_folders[S://192.168.1.1/music] + dict({ + 'can_expand': False, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': 'S://192.168.1.1/music/beatles', + 'media_content_type': 'directory', + 'thumbnail': None, + 'title': 'beatles', + }), + dict({ + 'can_expand': True, + 'can_play': True, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': 'S://192.168.1.1/music/elton%20john', + 'media_content_type': 'directory', + 'thumbnail': None, + 'title': 'elton john', + }), + ]), + 'children_media_class': 'directory', + 'media_class': 'directory', + 'media_content_id': 'S://192.168.1.1/music', + 'media_content_type': 'directory', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'music', + }) +# --- +# name: test_browse_media_library_folders[S:] + dict({ + 'can_expand': False, + 'can_play': False, + 'can_search': False, + 'children': list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'can_search': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': 'S://192.168.1.1/music', + 'media_content_type': 'directory', + 'thumbnail': None, + 'title': 'music', + }), + ]), + 'children_media_class': 'directory', + 'media_class': 'directory', + 'media_content_id': 'S:', + 'media_content_type': 'directory', + 'not_shown': 0, + 'thumbnail': None, + 'title': 'Folders', + }) +# --- # name: test_browse_media_root list([ dict({ diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index 669e9168297..3be0767ca99 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -5,11 +5,19 @@ from functools import partial import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + BrowseMedia, + MediaClass, + MediaType, +) +from homeassistant.components.sonos.const import MEDIA_TYPE_DIRECTORY from homeassistant.components.sonos.media_browser import ( build_item_response, get_thumbnail_url_full, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from .conftest import SoCoMockFactory @@ -217,3 +225,37 @@ async def test_browse_media_favorites( response = await client.receive_json() assert response["success"] assert response["result"] == snapshot + + +@pytest.mark.parametrize( + "media_content_id", + [ + ("S:"), + ("S://192.168.1.1/music"), + ], +) +async def test_browse_media_library_folders( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + media_content_id: str, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the async_browse_media method.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + ATTR_ENTITY_ID: "media_player.zone_a", + ATTR_MEDIA_CONTENT_ID: media_content_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_DIRECTORY, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == snapshot + assert soco_mock.music_library.browse_by_idstring.call_count == 1 diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index aaaaac6a4ba..37ce119b0de 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -28,6 +28,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.sonos.const import ( DOMAIN as SONOS_DOMAIN, + MEDIA_TYPE_DIRECTORY, SOURCE_LINEIN, SOURCE_TV, ) @@ -182,6 +183,19 @@ async def test_entity_basic( "play_pos": 0, }, ), + ( + MEDIA_TYPE_DIRECTORY, + "S://192.168.1.1/music/elton%20john", + MediaPlayerEnqueue.REPLACE, + { + "title": None, + "item_id": "S://192.168.1.1/music/elton%20john", + "clear_queue": 1, + "position": None, + "play": 1, + "play_pos": 0, + }, + ), ], ) async def test_play_media_library( @@ -247,6 +261,11 @@ async def test_play_media_library( "A:ALBUM/UnknowAlbum", "Sonos does not support media content type: UnknownContent", ), + ( + MEDIA_TYPE_DIRECTORY, + "S://192.168.1.1/music/error", + "Could not find media in library: S://192.168.1.1/music/error", + ), ], ) async def test_play_media_library_content_error( From ba44986524c912ecc96252afcc347487a9ce4d95 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 20 May 2025 23:11:03 +0300 Subject: [PATCH 0713/1175] Remove the old ZWave controller from the list of migration targets (#145281) * Remove the old ZWave controller from the list of migration targets * ensure addon device path is serial/by_id * Use executor --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/config_flow.py | 9 +++++++++ tests/components/zwave_js/test_config_flow.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 324011a3009..67e67fbec60 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -1163,6 +1163,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to get USB ports: %s", err) return self.async_abort(reason="usb_ports_failed") + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options + old_usb_path = addon_config.get(CONF_ADDON_DEVICE, "") + # Remove the old controller from the ports list. + ports.pop( + await self.hass.async_add_executor_job(usb.get_serial_by_id, old_usb_path), + None, + ) + data_schema = vol.Schema( { vol.Required(CONF_USB_PATH): vol.In(ports), diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 68489b304d2..c5b0f506dac 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -13,6 +13,7 @@ from aiohasupervisor.models import AddonsOptions, Discovery import aiohttp import pytest from serial.tools.list_ports_common import ListPortInfo +from voluptuous import InInvalid from zwave_js_server.exceptions import FailedCommand from zwave_js_server.version import VersionInfo @@ -3694,6 +3695,7 @@ async def test_reconfigure_migrate_with_addon( integration, addon_running, restart_addon, + addon_options, set_addon_options, get_addon_discovery_info, get_server_version: AsyncMock, @@ -3717,6 +3719,7 @@ async def test_reconfigure_migrate_with_addon( "usb_path": "/dev/ttyUSB0", }, ) + addon_options["device"] = "/dev/ttyUSB0" async def mock_backup_nvm_raw(): await asyncio.sleep(0) @@ -3793,6 +3796,9 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" assert result["data_schema"].schema[CONF_USB_PATH] + # Ensure the old usb path is not in the list of options + with pytest.raises(InInvalid): + result["data_schema"].schema[CONF_USB_PATH](addon_options["device"]) # Reset side effect before starting the add-on. get_server_version.side_effect = None From c60f19b35bf614cbb56d1e7a45cadf9093333854 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Tue, 20 May 2025 22:37:27 +0200 Subject: [PATCH 0714/1175] Bump xiaomi-ble to 0.39.0 (#145348) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 3f13c7921a8..2b87da630a0 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "iot_class": "local_push", - "requirements": ["xiaomi-ble==0.38.0"] + "requirements": ["xiaomi-ble==0.39.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cade178bdd6..7c3cdb369dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3104,7 +3104,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.38.0 +xiaomi-ble==0.39.0 # homeassistant.components.knx xknx==3.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac538873a10..a5e95f6f551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2512,7 +2512,7 @@ wyoming==1.5.4 xbox-webapi==2.1.0 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.38.0 +xiaomi-ble==0.39.0 # homeassistant.components.knx xknx==3.8.0 From 46fe132e831288d88c3dcffc01333caae43f0fdd Mon Sep 17 00:00:00 2001 From: Joris Drenth Date: Tue, 20 May 2025 23:02:07 +0200 Subject: [PATCH 0715/1175] Add sensors to Wallbox (#145247) --- homeassistant/components/wallbox/const.py | 2 ++ homeassistant/components/wallbox/sensor.py | 18 ++++++++++++++++++ homeassistant/components/wallbox/strings.json | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index dfa7fd5a4c1..d978e1ec7c9 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -11,6 +11,8 @@ CODE_KEY = "code" CONF_STATION = "station" CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" CHARGER_ADDED_ENERGY_KEY = "added_energy" +CHARGER_ADDED_GREEN_ENERGY_KEY = "added_green_energy" +CHARGER_ADDED_GRID_ENERGY_KEY = "added_grid_energy" CHARGER_ADDED_RANGE_KEY = "added_range" CHARGER_CHARGING_POWER_KEY = "charging_power" CHARGER_CHARGING_SPEED_KEY = "charging_speed" diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 78b26520bec..4b0ec8175e3 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -27,6 +27,8 @@ from homeassistant.helpers.typing import StateType from .const import ( CHARGER_ADDED_DISCHARGED_ENERGY_KEY, CHARGER_ADDED_ENERGY_KEY, + CHARGER_ADDED_GREEN_ENERGY_KEY, + CHARGER_ADDED_GRID_ENERGY_KEY, CHARGER_ADDED_RANGE_KEY, CHARGER_CHARGING_POWER_KEY, CHARGER_CHARGING_SPEED_KEY, @@ -99,6 +101,22 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + CHARGER_ADDED_GREEN_ENERGY_KEY: WallboxSensorEntityDescription( + key=CHARGER_ADDED_GREEN_ENERGY_KEY, + translation_key=CHARGER_ADDED_GREEN_ENERGY_KEY, + precision=2, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + CHARGER_ADDED_GRID_ENERGY_KEY: WallboxSensorEntityDescription( + key=CHARGER_ADDED_GRID_ENERGY_KEY, + translation_key=CHARGER_ADDED_GRID_ENERGY_KEY, + precision=2, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), CHARGER_ADDED_DISCHARGED_ENERGY_KEY: WallboxSensorEntityDescription( key=CHARGER_ADDED_DISCHARGED_ENERGY_KEY, translation_key=CHARGER_ADDED_DISCHARGED_ENERGY_KEY, diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json index 7f401981286..68602a960c2 100644 --- a/homeassistant/components/wallbox/strings.json +++ b/homeassistant/components/wallbox/strings.json @@ -59,6 +59,12 @@ "added_energy": { "name": "Added energy" }, + "added_green_energy": { + "name": "Added green energy" + }, + "added_grid_energy": { + "name": "Added grid energy" + }, "added_discharged_energy": { "name": "Discharged energy" }, From 69a4d2107fee096a86bdf8371774ba5a522bee35 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Tue, 20 May 2025 22:29:55 +0100 Subject: [PATCH 0716/1175] Add initial coordinator refresh for players in Squeezebox (#145347) * initial * add test for new player --- .../components/squeezebox/__init__.py | 1 + .../squeezebox/test_media_player.py | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index d29e7287340..2fcb17b9781 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -152,6 +152,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) - player_coordinator = SqueezeBoxPlayerUpdateCoordinator( hass, entry, player, lms.uuid ) + await player_coordinator.async_refresh() known_players.append(player.player_id) async_dispatcher_send( hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index bbdad374bcf..f71a7db23ba 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -72,7 +72,12 @@ from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util.dt import utcnow -from .conftest import FAKE_VALID_ITEM_ID, TEST_MAC, TEST_VOLUME_STEP +from .conftest import ( + FAKE_VALID_ITEM_ID, + TEST_MAC, + TEST_VOLUME_STEP, + configure_squeezebox_media_player_platform, +) from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -100,6 +105,33 @@ async def test_entity_registry( await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +async def test_squeezebox_new_player_discovery( + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms: MagicMock, + player_factory: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test discovery of a new squeezebox player.""" + # Initial setup with one player (from the 'lms' fixture) + await configure_squeezebox_media_player_platform(hass, config_entry, lms) + await hass.async_block_till_done(wait_background_tasks=True) + assert hass.states.get("media_player.test_player") is not None + assert hass.states.get("media_player.test_player_2") is None + + # Simulate a new player appearing + new_player_mock = player_factory(TEST_MAC[1]) + lms.async_get_players.return_value = [ + lms.async_get_players.return_value[0], + new_player_mock, + ] + + freezer.tick(timedelta(seconds=DISCOVERY_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("media_player.test_player_2") is not None + + async def test_squeezebox_player_rediscovery( hass: HomeAssistant, configured_player: MagicMock, freezer: FrozenDateTimeFactory ) -> None: From 3f72030d5fb90afaaba5d856168b18f6c41d7d1c Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 21 May 2025 16:08:32 +0800 Subject: [PATCH 0717/1175] Bump pyswitchbot to 0.64.1 (#145360) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/test_lock.py | 11 ++++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 064ebf5e2f4..dfbfd9335a5 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -40,5 +40,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.62.2"] + "requirements": ["PySwitchbot==0.64.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7c3cdb369dc..ed049044440 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.62.2 +PySwitchbot==0.64.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5e95f6f551..0244b601e9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.62.2 +PySwitchbot==0.64.1 # homeassistant.components.syncthru PySyncThru==0.8.0 diff --git a/tests/components/switchbot/test_lock.py b/tests/components/switchbot/test_lock.py index ea8939c8e41..859c818a6e3 100644 --- a/tests/components/switchbot/test_lock.py +++ b/tests/components/switchbot/test_lock.py @@ -45,9 +45,12 @@ async def test_lock_services( entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) - with patch( - f"homeassistant.components.switchbot.lock.switchbot.SwitchbotLock.{mock_method}", - ) as mocked_instance: + mocked_instance = AsyncMock(return_value=True) + with patch.multiple( + "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", + update=AsyncMock(return_value=None), + **{mock_method: mocked_instance}, + ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -90,6 +93,7 @@ async def test_lock_services_with_night_latch_enabled( with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", is_night_latch_enabled=MagicMock(return_value=True), + update=AsyncMock(return_value=None), **{mock_method: mocked_instance}, ): assert await hass.config_entries.async_setup(entry.entry_id) @@ -142,6 +146,7 @@ async def test_exception_handling_lock_service( with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", is_night_latch_enabled=MagicMock(return_value=True), + update=AsyncMock(return_value=None), **{mock_method: AsyncMock(side_effect=exception)}, ): assert await hass.config_entries.async_setup(entry.entry_id) From eb851850726b847946cd0a2f36707c1a133db50b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 May 2025 10:19:53 +0200 Subject: [PATCH 0718/1175] Minor code deduplication in backup manager (#145366) --- homeassistant/components/backup/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 39a7c60c3f1..f51c2a14b47 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1406,19 +1406,19 @@ class BackupManager: # No issues to report, clear previous error ir.async_delete_issue(self.hass, DOMAIN, "automatic_backup_failed") return - if (agent_errors or unavailable_agents) and not (addon_errors or folder_errors): + if failed_agents and not (addon_errors or folder_errors): # No issues with add-ons or folders, but issues with agents self._create_automatic_backup_failed_issue( "automatic_backup_failed_upload_agents", {"failed_agents": ", ".join(failed_agents)}, ) - elif addon_errors and not (agent_errors or unavailable_agents or folder_errors): + elif addon_errors and not (failed_agents or folder_errors): # No issues with agents or folders, but issues with add-ons self._create_automatic_backup_failed_issue( "automatic_backup_failed_addons", {"failed_addons": ", ".join(val.name for val in addon_errors.values())}, ) - elif folder_errors and not (agent_errors or unavailable_agents or addon_errors): + elif folder_errors and not (failed_agents or addon_errors): # No issues with agents or add-ons, but issues with folders self._create_automatic_backup_failed_issue( "automatic_backup_failed_folders", From 5e25bbba2d4979296cc9fc50f9a8276dc8280fd9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 May 2025 10:22:47 +0200 Subject: [PATCH 0719/1175] Fix limit of shown backups on Synology DSM location (#145342) --- homeassistant/components/synology_dsm/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/backup.py b/homeassistant/components/synology_dsm/backup.py index 46e47ebde16..b3279db1cac 100644 --- a/homeassistant/components/synology_dsm/backup.py +++ b/homeassistant/components/synology_dsm/backup.py @@ -236,7 +236,7 @@ class SynologyDSMBackupAgent(BackupAgent): raise BackupAgentError("Failed to read meta data") from err try: - files = await self._file_station.get_files(path=self.path) + files = await self._file_station.get_files(path=self.path, limit=1000) except SynologyDSMAPIErrorException as err: raise BackupAgentError("Failed to list backups") from err From 08c453581c169c4fb91deb5a96e3e6f65d99124a Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 21 May 2025 17:12:08 +0800 Subject: [PATCH 0720/1175] Add hub3 support for switchbot integration (#145371) add support for hub3 --- .../components/switchbot/__init__.py | 1 + homeassistant/components/switchbot/const.py | 2 + tests/components/switchbot/__init__.py | 27 ++++++++ tests/components/switchbot/test_sensor.py | 61 +++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 22119a5442e..56629764f66 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -79,6 +79,7 @@ PLATFORMS_BY_TYPE = { SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], + SupportedModels.HUB3.value: [Platform.SENSOR, Platform.BINARY_SENSOR], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 327b6e704a0..b19af0afe94 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -43,6 +43,7 @@ class SupportedModels(StrEnum): K10_VACUUM = "k10_vacuum" K10_PRO_VACUUM = "k10_pro_vacuum" K10_PRO_COMBO_VACUUM = "k10_pro_combo_vacumm" + HUB3 = "hub3" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -78,6 +79,7 @@ NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.LEAK: SupportedModels.LEAK, SwitchbotModel.REMOTE: SupportedModels.REMOTE, SwitchbotModel.HUBMINI_MATTER: SupportedModels.HUBMINI_MATTER, + SwitchbotModel.HUB3: SupportedModels.HUB3, } SUPPORTED_MODEL_TYPES = ( diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 8ba242823f6..e858d5a71c0 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -688,3 +688,30 @@ S10_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + + +HUB3_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Hub3", + manufacturer_data={ + 2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Hub3", + manufacturer_data={ + 2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80", + }, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@" + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Hub3"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index 8b1e6c83f21..a04bff75c2d 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -23,6 +23,7 @@ from homeassistant.setup import async_setup_component from . import ( CIRCULATOR_FAN_SERVICE_INFO, + HUB3_SERVICE_INFO, HUBMINI_MATTER_SERVICE_INFO, LEAK_SERVICE_INFO, REMOTE_SERVICE_INFO, @@ -385,3 +386,63 @@ async def test_fan_sensors(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_hub3_sensor(hass: HomeAssistant) -> None: + """Test setting up creates the sensor for Hub3.""" + await async_setup_component(hass, DOMAIN, {}) + inject_bluetooth_service_info(hass, HUB3_SERVICE_INFO) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", + CONF_NAME: "test-name", + CONF_SENSOR_TYPE: "hub3", + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 5 + + temperature_sensor = hass.states.get("sensor.test_name_temperature") + temperature_sensor_attrs = temperature_sensor.attributes + assert temperature_sensor.state == "25.3" + assert temperature_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Temperature" + assert temperature_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temperature_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + humidity_sensor = hass.states.get("sensor.test_name_humidity") + humidity_sensor_attrs = humidity_sensor.attributes + assert humidity_sensor.state == "52" + assert humidity_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Humidity" + assert humidity_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert humidity_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal") + rssi_sensor_attrs = rssi_sensor.attributes + assert rssi_sensor.state == "-60" + assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal" + assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm" + + light_level_sensor = hass.states.get("sensor.test_name_light_level") + light_level_sensor_attrs = light_level_sensor.attributes + assert light_level_sensor.state == "3" + assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level" + assert light_level_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "Level" + assert light_level_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + illuminance_sensor = hass.states.get("sensor.test_name_illuminance") + illuminance_sensor_attrs = illuminance_sensor.attributes + assert illuminance_sensor.state == "90" + assert illuminance_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Illuminance" + assert illuminance_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illuminance_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 291499d5e17769195989bb53f48b3b4d2f4ef0e2 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 21 May 2025 11:57:20 +0200 Subject: [PATCH 0721/1175] Update links to user docs: Connect-ZBT-1, Green, Yellow (#145374) --- homeassistant/components/homeassistant_green/hardware.py | 2 +- .../components/homeassistant_sky_connect/hardware.py | 2 +- homeassistant/components/homeassistant_yellow/hardware.py | 2 +- tests/components/homeassistant_green/test_hardware.py | 2 +- tests/components/homeassistant_sky_connect/test_hardware.py | 4 ++-- tests/components/homeassistant_yellow/test_hardware.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant_green/hardware.py b/homeassistant/components/homeassistant_green/hardware.py index 0537d17620b..bf0decb9d05 100644 --- a/homeassistant/components/homeassistant_green/hardware.py +++ b/homeassistant/components/homeassistant_green/hardware.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN BOARD_NAME = "Home Assistant Green" -DOCUMENTATION_URL = "https://green.home-assistant.io/documentation/" +DOCUMENTATION_URL = "https://support.nabucasa.com/hc/en-us/categories/24638797677853-Home-Assistant-Green" MANUFACTURER = "homeassistant" MODEL = "green" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 9bfa5d16655..bf4ffefdc75 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -9,7 +9,7 @@ from .config_flow import HomeAssistantSkyConnectConfigFlow from .const import DOMAIN from .util import get_hardware_variant -DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" +DOCUMENTATION_URL = "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1" EXPECTED_ENTRY_VERSION = ( HomeAssistantSkyConnectConfigFlow.VERSION, HomeAssistantSkyConnectConfigFlow.MINOR_VERSION, diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index 2b9ee0673db..2064f33484c 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN BOARD_NAME = "Home Assistant Yellow" -DOCUMENTATION_URL = "https://yellow.home-assistant.io/documentation/" +DOCUMENTATION_URL = "https://support.nabucasa.com/hc/en-us/categories/24734575925149-Home-Assistant-Yellow" MANUFACTURER = "homeassistant" MODEL = "yellow" diff --git a/tests/components/homeassistant_green/test_hardware.py b/tests/components/homeassistant_green/test_hardware.py index ab91514b297..4ede532d326 100644 --- a/tests/components/homeassistant_green/test_hardware.py +++ b/tests/components/homeassistant_green/test_hardware.py @@ -58,7 +58,7 @@ async def test_hardware_info( "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Green", - "url": "https://green.home-assistant.io/documentation/", + "url": "https://support.nabucasa.com/hc/en-us/categories/24638797677853-Home-Assistant-Green", } ] } diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index e59a1e7df06..2a594ebcdad 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -97,7 +97,7 @@ async def test_hardware_info( "description": "SkyConnect v1.0", }, "name": "Home Assistant SkyConnect", - "url": "https://skyconnect.home-assistant.io/documentation/", + "url": "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1", }, { "board": None, @@ -110,7 +110,7 @@ async def test_hardware_info( "description": "Home Assistant Connect ZBT-1", }, "name": "Home Assistant Connect ZBT-1", - "url": "https://skyconnect.home-assistant.io/documentation/", + "url": "https://support.nabucasa.com/hc/en-us/categories/24734620813469-Home-Assistant-Connect-ZBT-1", }, # Bad entry is skipped ] diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 4fd2eddb704..8de03891ae1 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -59,7 +59,7 @@ async def test_hardware_info( "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Yellow", - "url": "https://yellow.home-assistant.io/documentation/", + "url": "https://support.nabucasa.com/hc/en-us/categories/24734575925149-Home-Assistant-Yellow", } ] } From 3ada93b29381961b9c1361261fb7abf7c284e75c Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Wed, 21 May 2025 12:35:10 +0200 Subject: [PATCH 0722/1175] Bump eheimdigital to 1.2.0 (#145372) --- homeassistant/components/eheimdigital/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eheimdigital/manifest.json b/homeassistant/components/eheimdigital/manifest.json index c3c8a251300..99f2a0a9c56 100644 --- a/homeassistant/components/eheimdigital/manifest.json +++ b/homeassistant/components/eheimdigital/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["eheimdigital"], "quality_scale": "bronze", - "requirements": ["eheimdigital==1.1.0"], + "requirements": ["eheimdigital==1.2.0"], "zeroconf": [ { "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." } ] diff --git a/requirements_all.txt b/requirements_all.txt index ed049044440..6ec444b3bcf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -833,7 +833,7 @@ ebusdpy==0.0.17 ecoaliface==0.4.0 # homeassistant.components.eheimdigital -eheimdigital==1.1.0 +eheimdigital==1.2.0 # homeassistant.components.electric_kiwi electrickiwi-api==0.9.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0244b601e9b..3b02d0e653a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ eagle100==0.1.1 easyenergy==2.1.2 # homeassistant.components.eheimdigital -eheimdigital==1.1.0 +eheimdigital==1.2.0 # homeassistant.components.electric_kiwi electrickiwi-api==0.9.14 From 630c43883472370603ae98fb620df1a56a9d777b Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 21 May 2025 18:37:47 +0800 Subject: [PATCH 0723/1175] Add lock ultra and lock lite for switchbot integration (#145373) --- .../components/switchbot/__init__.py | 12 +++++ homeassistant/components/switchbot/const.py | 8 ++++ tests/components/switchbot/__init__.py | 44 +++++++++++++++++++ tests/components/switchbot/test_lock.py | 24 +++++++--- 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 56629764f66..ee7d0b7e658 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -80,6 +80,16 @@ PLATFORMS_BY_TYPE = { SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.HUB3.value: [Platform.SENSOR, Platform.BINARY_SENSOR], + SupportedModels.LOCK_LITE.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], + SupportedModels.LOCK_ULTRA.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, @@ -101,6 +111,8 @@ CLASS_BY_DEVICE = { SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.K10_PRO_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum, + SupportedModels.LOCK_LITE.value: switchbot.SwitchbotLock, + SupportedModels.LOCK_ULTRA.value: switchbot.SwitchbotLock, } diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index b19af0afe94..aae189be2e1 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -44,6 +44,8 @@ class SupportedModels(StrEnum): K10_PRO_VACUUM = "k10_pro_vacuum" K10_PRO_COMBO_VACUUM = "k10_pro_combo_vacumm" HUB3 = "hub3" + LOCK_LITE = "lock_lite" + LOCK_ULTRA = "lock_ultra" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -67,6 +69,8 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.K10_VACUUM: SupportedModels.K10_VACUUM, SwitchbotModel.K10_PRO_VACUUM: SupportedModels.K10_PRO_VACUUM, SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM, + SwitchbotModel.LOCK_LITE: SupportedModels.LOCK_LITE, + SwitchbotModel.LOCK_ULTRA: SupportedModels.LOCK_ULTRA, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -91,6 +95,8 @@ ENCRYPTED_MODELS = { SwitchbotModel.RELAY_SWITCH_1PM, SwitchbotModel.LOCK, SwitchbotModel.LOCK_PRO, + SwitchbotModel.LOCK_LITE, + SwitchbotModel.LOCK_ULTRA, } ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ @@ -100,6 +106,8 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ SwitchbotModel.LOCK_PRO: switchbot.SwitchbotLock, SwitchbotModel.RELAY_SWITCH_1PM: switchbot.SwitchbotRelaySwitch, SwitchbotModel.RELAY_SWITCH_1: switchbot.SwitchbotRelaySwitch, + SwitchbotModel.LOCK_LITE: switchbot.SwitchbotLock, + SwitchbotModel.LOCK_ULTRA: switchbot.SwitchbotLock, } HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index e858d5a71c0..1e90b0bf1fe 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -715,3 +715,47 @@ HUB3_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + + +LOCK_LITE_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Lock Lite", + manufacturer_data={2409: b"\xe9\xd5\x11\xb2kS\x17\x93\x08 "}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"-\x80d"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Lock Lite", + manufacturer_data={2409: b"\xe9\xd5\x11\xb2kS\x17\x93\x08 "}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"-\x80d"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Lock Lite"), + time=0, + connectable=True, + tx_power=-127, +) + + +LOCK_ULTRA_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Lock Ultra", + manufacturer_data={2409: b"\xb0\xe9\xfe\xb6j=%\x8204\x00\x04"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x804\x00\x10\xa5\xb8"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Lock Ultra", + manufacturer_data={2409: b"\xb0\xe9\xfe\xb6j=%\x8204\x00\x04"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x804\x00\x10\xa5\xb8" + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Lock Ultra"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_lock.py b/tests/components/switchbot/test_lock.py index 859c818a6e3..38b8d24523b 100644 --- a/tests/components/switchbot/test_lock.py +++ b/tests/components/switchbot/test_lock.py @@ -17,7 +17,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import LOCK_SERVICE_INFO, WOLOCKPRO_SERVICE_INFO +from . import ( + LOCK_LITE_SERVICE_INFO, + LOCK_SERVICE_INFO, + LOCK_ULTRA_SERVICE_INFO, + WOLOCKPRO_SERVICE_INFO, +) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -25,7 +30,12 @@ from tests.components.bluetooth import inject_bluetooth_service_info @pytest.mark.parametrize( ("sensor_type", "service_info"), - [("lock_pro", WOLOCKPRO_SERVICE_INFO), ("lock", LOCK_SERVICE_INFO)], + [ + ("lock_pro", WOLOCKPRO_SERVICE_INFO), + ("lock", LOCK_SERVICE_INFO), + ("lock_lite", LOCK_LITE_SERVICE_INFO), + ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), + ], ) @pytest.mark.parametrize( ("service", "mock_method"), @@ -44,8 +54,8 @@ async def test_lock_services( entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) - mocked_instance = AsyncMock(return_value=True) + with patch.multiple( "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", update=AsyncMock(return_value=None), @@ -68,7 +78,12 @@ async def test_lock_services( @pytest.mark.parametrize( ("sensor_type", "service_info"), - [("lock_pro", WOLOCKPRO_SERVICE_INFO), ("lock", LOCK_SERVICE_INFO)], + [ + ("lock_pro", WOLOCKPRO_SERVICE_INFO), + ("lock", LOCK_SERVICE_INFO), + ("lock_lite", LOCK_LITE_SERVICE_INFO), + ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), + ], ) @pytest.mark.parametrize( ("service", "mock_method"), @@ -87,7 +102,6 @@ async def test_lock_services_with_night_latch_enabled( entry = mock_entry_encrypted_factory(sensor_type=sensor_type) entry.add_to_hass(hass) - mocked_instance = AsyncMock(return_value=True) with patch.multiple( From 00a1d9d1b05b6e6eab66ec60da7cf4a484dc9a04 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 May 2025 13:22:05 +0200 Subject: [PATCH 0724/1175] Improve comment explaining planned backup store version bump (#145368) --- homeassistant/components/backup/store.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index 6472f8ae151..c220ab0731e 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -77,7 +77,10 @@ class _BackupStore(Store[StoredBackupData]): for agent in data["config"]["agents"]: data["config"]["agents"][agent]["retention"] = None - # Note: We allow reading data with major version 2. + # Note: We allow reading data with major version 2 in which the unused key + # data["config"]["schedule"]["state"] will be removed. The bump to 2 is + # planned to happen after a 6 month quiet period with no minor version + # changes. # Reject if major version is higher than 2. if old_major_version > 2: raise NotImplementedError From efa7fe0dc997da38f13d5b2516bcaaddedd7ec36 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 21 May 2025 14:30:59 +0300 Subject: [PATCH 0725/1175] Recommended installation option for Z-Wave (#145327) Recommended installation option for ZWave --- .../components/zwave_js/config_flow.py | 134 +++++-- .../components/zwave_js/strings.json | 8 + tests/components/zwave_js/test_config_flow.py | 370 +++++++++++++++++- 3 files changed, 467 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 67e67fbec60..b539c747c4f 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -197,6 +197,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self._migrating = False self._reconfigure_config_entry: ConfigEntry | None = None self._usb_discovery = False + self._recommended_install = False async def async_step_install_addon( self, user_input: dict[str, Any] | None = None @@ -372,10 +373,22 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" if is_hassio(self.hass): - return await self.async_step_on_supervisor() + return await self.async_step_installation_type() return await self.async_step_manual() + async def async_step_installation_type( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the installation type step.""" + return self.async_show_menu( + step_id="installation_type", + menu_options=[ + "intent_recommended", + "intent_custom", + ], + ) + async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -516,7 +529,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="addon_required") return await self.async_step_intent_migrate() - return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) + return await self.async_step_installation_type() async def async_step_manual( self, user_input: dict[str, Any] | None = None @@ -593,6 +606,21 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="hassio_confirm") + async def async_step_intent_recommended( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Select recommended installation type.""" + self._recommended_install = True + return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) + + async def async_step_intent_custom( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Select custom installation type.""" + if self._usb_discovery: + return await self.async_step_on_supervisor({CONF_USE_ADDON: True}) + return await self.async_step_on_supervisor() + async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -641,31 +669,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): addon_info = await self._async_get_addon_info() addon_config = addon_info.options - if user_input is not None: - self.s0_legacy_key = user_input[CONF_S0_LEGACY_KEY] - self.s2_access_control_key = user_input[CONF_S2_ACCESS_CONTROL_KEY] - self.s2_authenticated_key = user_input[CONF_S2_AUTHENTICATED_KEY] - self.s2_unauthenticated_key = user_input[CONF_S2_UNAUTHENTICATED_KEY] - self.lr_s2_access_control_key = user_input[CONF_LR_S2_ACCESS_CONTROL_KEY] - self.lr_s2_authenticated_key = user_input[CONF_LR_S2_AUTHENTICATED_KEY] - if not self._usb_discovery: - self.usb_path = user_input[CONF_USB_PATH] - - addon_config_updates = { - CONF_ADDON_DEVICE: self.usb_path, - CONF_ADDON_S0_LEGACY_KEY: self.s0_legacy_key, - CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, - CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, - CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, - CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, - CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, - } - - await self._async_set_addon_config(addon_config_updates) - - return await self.async_step_start_addon() - - usb_path = self.usb_path or addon_config.get(CONF_ADDON_DEVICE) or "" s0_legacy_key = addon_config.get( CONF_ADDON_S0_LEGACY_KEY, self.s0_legacy_key or "" ) @@ -685,22 +688,67 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" ) - schema: VolDictType = { - vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, - vol.Optional( - CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key - ): str, - vol.Optional(CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key): str, - vol.Optional( - CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key - ): str, - vol.Optional( - CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key - ): str, - vol.Optional( - CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key - ): str, - } + if self._recommended_install and self._usb_discovery: + # Recommended installation with USB discovery, skip asking for keys + user_input = {} + + if user_input is not None: + self.s0_legacy_key = user_input.get(CONF_S0_LEGACY_KEY, s0_legacy_key) + self.s2_access_control_key = user_input.get( + CONF_S2_ACCESS_CONTROL_KEY, s2_access_control_key + ) + self.s2_authenticated_key = user_input.get( + CONF_S2_AUTHENTICATED_KEY, s2_authenticated_key + ) + self.s2_unauthenticated_key = user_input.get( + CONF_S2_UNAUTHENTICATED_KEY, s2_unauthenticated_key + ) + self.lr_s2_access_control_key = user_input.get( + CONF_LR_S2_ACCESS_CONTROL_KEY, lr_s2_access_control_key + ) + self.lr_s2_authenticated_key = user_input.get( + CONF_LR_S2_AUTHENTICATED_KEY, lr_s2_authenticated_key + ) + if not self._usb_discovery: + self.usb_path = user_input[CONF_USB_PATH] + + addon_config_updates = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_S0_LEGACY_KEY: self.s0_legacy_key, + CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, + CONF_ADDON_S2_AUTHENTICATED_KEY: self.s2_authenticated_key, + CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, + } + + await self._async_set_addon_config(addon_config_updates) + + return await self.async_step_start_addon() + + usb_path = self.usb_path or addon_config.get(CONF_ADDON_DEVICE) or "" + schema: VolDictType = ( + {} + if self._recommended_install + else { + vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, + vol.Optional( + CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key + ): str, + vol.Optional( + CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key + ): str, + vol.Optional( + CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key + ): str, + vol.Optional( + CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key + ): str, + vol.Optional( + CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key + ): str, + } + ) if not self._usb_discovery: try: diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 2a8e2c6ea2d..69465278a53 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -131,6 +131,14 @@ "usb_path": "[%key:common::config_flow::data::usb_path%]" }, "title": "Select your Z-Wave device" + }, + "installation_type": { + "title": "Set-up Z-Wave", + "description": "Choose the installation type for your Z-Wave integration.", + "menu_options": { + "intent_recommended": "Recommended installation", + "intent_custom": "Custom installation" + } } } }, diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index c5b0f506dac..fd26783e419 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -19,7 +19,24 @@ from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow from homeassistant.components.zwave_js.config_flow import TITLE, get_usb_ports -from homeassistant.components.zwave_js.const import ADDON_SLUG, CONF_USB_PATH, DOMAIN +from homeassistant.components.zwave_js.const import ( + ADDON_SLUG, + CONF_ADDON_DEVICE, + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, + CONF_ADDON_LR_S2_AUTHENTICATED_KEY, + CONF_ADDON_S0_LEGACY_KEY, + CONF_ADDON_S2_ACCESS_CONTROL_KEY, + CONF_ADDON_S2_AUTHENTICATED_KEY, + CONF_ADDON_S2_UNAUTHENTICATED_KEY, + CONF_LR_S2_ACCESS_CONTROL_KEY, + CONF_LR_S2_AUTHENTICATED_KEY, + CONF_S0_LEGACY_KEY, + CONF_S2_ACCESS_CONTROL_KEY, + CONF_S2_AUTHENTICATED_KEY, + CONF_S2_UNAUTHENTICATED_KEY, + CONF_USB_PATH, + DOMAIN, +) from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -455,6 +472,13 @@ async def test_clean_discovery_on_user_create( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -643,6 +667,14 @@ async def test_usb_discovery( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + assert result["menu_options"] == ["intent_recommended", "intent_custom"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "install_addon" @@ -756,6 +788,13 @@ async def test_usb_discovery_addon_not_running( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon_user" @@ -1511,6 +1550,13 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1589,6 +1635,13 @@ async def test_addon_running( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1700,6 +1753,13 @@ async def test_addon_running_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1763,6 +1823,13 @@ async def test_addon_running_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1810,6 +1877,13 @@ async def test_addon_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1913,6 +1987,13 @@ async def test_addon_installed_start_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -1998,6 +2079,13 @@ async def test_addon_installed_failures( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2079,6 +2167,13 @@ async def test_addon_installed_set_options_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2134,6 +2229,13 @@ async def test_addon_installed_usb_ports_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2194,6 +2296,13 @@ async def test_addon_installed_already_configured( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2280,6 +2389,13 @@ async def test_addon_not_installed( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2374,6 +2490,13 @@ async def test_install_addon_failure( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_custom"} + ) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "on_supervisor" @@ -2617,7 +2740,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( assert entry.state is config_entries.ConfigEntryState.LOADED assert setup_entry.call_count == 1 assert unload_entry.call_count == 1 - # avoid unload entry in teardown await hass.config_entries.async_unload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @@ -4695,3 +4817,247 @@ async def test_get_usb_ports_sorting(hass: HomeAssistant) -> None: "n/a - /dev/ttyUSB0, s/n: n/a", "N/A - /dev/ttyUSB2, s/n: n/a", ] + + +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_intent_recommended_user( + hass: HomeAssistant, + supervisor, + addon_not_installed, + install_addon, + start_addon, + addon_options, + set_addon_options, + get_addon_discovery_info, +) -> None: + """Test the intent_recommended step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert install_addon.call_args == call("core_zwave_js") + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "configure_addon_user" + assert result["data_schema"].schema[CONF_USB_PATH] is not None + assert result["data_schema"].schema.get(CONF_S0_LEGACY_KEY) is None + assert result["data_schema"].schema.get(CONF_S2_ACCESS_CONTROL_KEY) is None + assert result["data_schema"].schema.get(CONF_S2_AUTHENTICATED_KEY) is None + assert result["data_schema"].schema.get(CONF_S2_UNAUTHENTICATED_KEY) is None + assert result["data_schema"].schema.get(CONF_LR_S2_ACCESS_CONTROL_KEY) is None + assert result["data_schema"].schema.get(CONF_LR_S2_AUTHENTICATED_KEY) is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USB_PATH: "/test", + }, + ) + + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + CONF_ADDON_DEVICE: "/test", + CONF_ADDON_S0_LEGACY_KEY: "", + CONF_ADDON_S2_ACCESS_CONTROL_KEY: "", + CONF_ADDON_S2_AUTHENTICATED_KEY: "", + CONF_ADDON_S2_UNAUTHENTICATED_KEY: "", + CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: "", + CONF_ADDON_LR_S2_AUTHENTICATED_KEY: "", + } + ), + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call("core_zwave_js") + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "use_addon": True, + "integration_created_addon": True, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("usb_discovery_info", "device", "discovery_name"), + [ + ( + USB_DISCOVERY_INFO, + USB_DISCOVERY_INFO.device, + "zwave radio", + ), + ( + UsbServiceInfo( + device="/dev/zwa2", + pid="303A", + vid="4001", + serial_number="1234", + description="ZWA-2 - Nabu Casa ZWA-2", + manufacturer="Nabu Casa", + ), + "/dev/zwa2", + "Home Assistant Connect ZWA-2", + ), + ], +) +@pytest.mark.parametrize( + "discovery_info", + [ + [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + ], +) +async def test_recommended_usb_discovery( + hass: HomeAssistant, + supervisor, + addon_not_installed, + install_addon, + addon_options, + get_addon_discovery_info, + mock_usb_serial_by_id: MagicMock, + set_addon_options, + start_addon, + usb_discovery_info: UsbServiceInfo, + device: str, + discovery_name: str, +) -> None: + """Test usb discovery success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USB}, + data=usb_discovery_info, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "usb_confirm" + assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" + assert result["menu_options"] == ["intent_recommended", "intent_custom"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": "intent_recommended"} + ) + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "install_addon" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert install_addon.call_args == call("core_zwave_js") + + assert result["type"] is FlowResultType.SHOW_PROGRESS + assert result["step_id"] == "start_addon" + + assert set_addon_options.call_args == call( + "core_zwave_js", + AddonsOptions( + config={ + "device": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + } + ), + ) + + with ( + patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + assert start_addon.call_args == call("core_zwave_js") + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": device, + "s0_legacy_key": "", + "s2_access_control_key": "", + "s2_authenticated_key": "", + "s2_unauthenticated_key": "", + "lr_s2_access_control_key": "", + "lr_s2_authenticated_key": "", + "use_addon": True, + "integration_created_addon": True, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 61fd073a5cb1860f465161c97546de27e0085f5f Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 21 May 2025 14:19:37 +0200 Subject: [PATCH 0726/1175] Fix: Revert Ecovacs mower total_stats_area unit to square meters (#145380) --- homeassistant/components/ecovacs/sensor.py | 3 +-- tests/components/ecovacs/snapshots/test_sensor.ambr | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/sensor.py b/homeassistant/components/ecovacs/sensor.py index 67556606f3a..98f3783b231 100644 --- a/homeassistant/components/ecovacs/sensor.py +++ b/homeassistant/components/ecovacs/sensor.py @@ -98,9 +98,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = ( key="total_stats_area", translation_key="total_stats_area", device_class=SensorDeviceClass.AREA, - native_unit_of_measurement_fn=get_area_native_unit_of_measurement, + native_unit_of_measurement=UnitOfArea.SQUARE_METERS, state_class=SensorStateClass.TOTAL_INCREASING, - suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS, ), EcovacsSensorEntityDescription[TotalStatsEvent]( capability_fn=lambda caps: caps.stats.total, diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index c78df0e189a..468ff0a29f8 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -518,9 +518,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, @@ -546,7 +543,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0060', + 'state': '60', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_cleaning_duration:entity-registry] @@ -1269,9 +1266,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, @@ -1963,9 +1957,6 @@ }), 'name': None, 'options': dict({ - 'sensor.private': dict({ - 'suggested_unit_of_measurement': , - }), }), 'original_device_class': , 'original_icon': None, From 2209f0b88447e16b75d51bafd4ecf06b6b2c80b4 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Wed, 21 May 2025 09:17:51 -0400 Subject: [PATCH 0727/1175] Bump pysqueezebox to v0.12.1 (#145384) --- homeassistant/components/squeezebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index e9b89291749..49e1da860df 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/squeezebox", "iot_class": "local_polling", "loggers": ["pysqueezebox"], - "requirements": ["pysqueezebox==0.12.0"] + "requirements": ["pysqueezebox==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ec444b3bcf..79dd9b9e039 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2362,7 +2362,7 @@ pyspcwebgw==0.7.0 pyspeex-noise==1.0.2 # homeassistant.components.squeezebox -pysqueezebox==0.12.0 +pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b02d0e653a..04a62c19404 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1935,7 +1935,7 @@ pyspcwebgw==0.7.0 pyspeex-noise==1.0.2 # homeassistant.components.squeezebox -pysqueezebox==0.12.0 +pysqueezebox==0.12.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.1.0 From 77ec87d0acba958ac1406f1ee97c5e976beb2238 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 21 May 2025 16:10:25 +0200 Subject: [PATCH 0728/1175] Bump lcn-frontend to 0.2.5 (#144983) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 0031cbcc947..be5d6299f09 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/lcn", "iot_class": "local_push", "loggers": ["pypck"], - "requirements": ["pypck==0.8.6", "lcn-frontend==0.2.4"] + "requirements": ["pypck==0.8.6", "lcn-frontend==0.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 79dd9b9e039..b0950d345aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1312,7 +1312,7 @@ lakeside==0.13 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.4 +lcn-frontend==0.2.5 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04a62c19404..b1af0681bdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1112,7 +1112,7 @@ lacrosse-view==1.1.1 laundrify-aio==1.2.2 # homeassistant.components.lcn -lcn-frontend==0.2.4 +lcn-frontend==0.2.5 # homeassistant.components.ld2410_ble ld2410-ble==0.1.1 From dd00d0daad7fa72e38e883c325dcd24767d1ec43 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 21 May 2025 16:16:50 +0200 Subject: [PATCH 0729/1175] Improve failing backup repair messages (#145388) --- homeassistant/components/backup/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/backup/strings.json b/homeassistant/components/backup/strings.json index bdd338835aa..33a027d75e2 100644 --- a/homeassistant/components/backup/strings.json +++ b/homeassistant/components/backup/strings.json @@ -14,15 +14,15 @@ }, "automatic_backup_failed_addons": { "title": "Not all add-ons could be included in automatic backup", - "description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + "description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." }, "automatic_backup_failed_agents_addons_folders": { "title": "Automatic backup was created with errors", - "description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + "description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the core and supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." }, "automatic_backup_failed_folders": { "title": "Not all folders could be included in automatic backup", - "description": "Folders {failed_folders} could not be included in automatic backup. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." + "description": "Folders {failed_folders} could not be included in automatic backup. Please check the supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured." } }, "services": { From f76165e7611992d67346f01231d6531565284cd0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 May 2025 16:17:24 +0200 Subject: [PATCH 0730/1175] Prevent types-*/setuptools/wheel runtime requirements in dependencies (#145381) Prevent setuptools/wheel runtime requirements in dependencies --- script/hassfest/requirements.py | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 998593d20ec..dd5374461c3 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -28,6 +28,25 @@ PACKAGE_REGEX = re.compile( PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") +FORBIDDEN_PACKAGES = {"setuptools", "wheel"} +FORBIDDEN_PACKAGE_EXCEPTIONS = { + # Direct dependencies + "fitbit", # setuptools (fitbit) + "habitipy", # setuptools (habitica) + "influxdb-client", # setuptools (influxdb) + "microbeespy", # setuptools (microbees) + "pyefergy", # types-pytz (efergy) + "python-mystrom", # setuptools (mystrom) + # Transitive dependencies + "arrow", # types-python-dateutil (opower) + "asyncio-dgram", # setuptools (guardian / keba / minecraft_server) + "colorzero", # setuptools (remote_rpi_gpio / zha) + "incremental", # setuptools (azure_devops / lyric / ovo_energy / system_bridge) + "pbr", # setuptools (cmus / concord232 / mochad / nx584 / opnsense) + "pycountry-convert", # wheel (ecovacs) + "unasync", # setuptools (hive / osoenergy) +} + def validate(integrations: dict[str, Integration], config: Config) -> None: """Handle requirements for integrations.""" @@ -204,7 +223,21 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: ) continue - to_check.extend(item["dependencies"]) + dependencies: set[str] = item["dependencies"] + for pkg in dependencies: + if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES: + if package in FORBIDDEN_PACKAGE_EXCEPTIONS: + integration.add_warning( + "requirements", + f"Package {pkg} should not be a runtime dependency in {package}", + ) + else: + integration.add_error( + "requirements", + f"Package {pkg} should not be a runtime dependency in {package}", + ) + + to_check.extend(dependencies) return all_requirements From 4956cf3727111d82cc377fe0e49b43d3562b6b31 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 May 2025 17:04:48 +0200 Subject: [PATCH 0731/1175] Fix Z-Wave installation type string (#145390) --- homeassistant/components/zwave_js/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 69465278a53..ac5de91d6e8 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -133,7 +133,7 @@ "title": "Select your Z-Wave device" }, "installation_type": { - "title": "Set-up Z-Wave", + "title": "Set up Z-Wave", "description": "Choose the installation type for your Z-Wave integration.", "menu_options": { "intent_recommended": "Recommended installation", From cb717c0ec6b53f410c82662b901b8f322f6aaefe Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 May 2025 17:06:36 +0200 Subject: [PATCH 0732/1175] Improve Z-Wave config flow test fixtures (#145378) --- tests/components/zwave_js/test_config_flow.py | 738 +++++------------- 1 file changed, 185 insertions(+), 553 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index fd26783e419..e07caca3c6a 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -87,6 +87,31 @@ def platforms() -> list[str]: return [] +@pytest.fixture(name="discovery_info", autouse=True) +def discovery_info_fixture() -> list[Discovery]: + """Fixture to set up discovery info.""" + return [ + Discovery( + addon="core_zwave_js", + service="zwave_js", + uuid=uuid4(), + config=ADDON_DISCOVERY_INFO, + ) + ] + + +@pytest.fixture(name="discovery_info_side_effect", autouse=True) +def discovery_info_side_effect_fixture() -> Any | None: + """Return the discovery info from the supervisor.""" + return None + + +@pytest.fixture(name="get_addon_discovery_info", autouse=True) +def get_addon_discovery_info_fixture(get_addon_discovery_info: AsyncMock) -> AsyncMock: + """Get add-on discovery info.""" + return get_addon_discovery_info + + @pytest.fixture(name="setup_entry") def setup_entry_fixture() -> Generator[AsyncMock]: """Mock entry setup.""" @@ -226,6 +251,7 @@ async def slow_server_version(*args): await asyncio.sleep(0.1) +@pytest.mark.usefixtures("integration") @pytest.mark.parametrize( ("url", "server_version_side_effect", "server_version_timeout", "error"), [ @@ -249,7 +275,7 @@ async def slow_server_version(*args): ), ], ) -async def test_manual_errors(hass: HomeAssistant, integration, url, error) -> None: +async def test_manual_errors(hass: HomeAssistant, url: str, error: str) -> None: """Test all errors with a manual set up.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -293,7 +319,10 @@ async def test_manual_errors(hass: HomeAssistant, integration, url, error) -> No ], ) async def test_reconfigure_manual_errors( - hass: HomeAssistant, integration, url, error + hass: HomeAssistant, + integration: MockConfigEntry, + url: str, + error: str, ) -> None: """Test all errors with a manual set up in a reconfigure flow.""" entry = integration @@ -354,13 +383,10 @@ async def test_manual_already_configured(hass: HomeAssistant) -> None: assert entry.data["integration_created_addon"] is False -@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_supervisor_discovery( hass: HomeAssistant, - supervisor, - addon_running, - addon_options, - get_addon_discovery_info, + addon_options: dict[str, Any], ) -> None: """Test flow started from Supervisor discovery.""" @@ -413,13 +439,9 @@ async def test_supervisor_discovery( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - ("discovery_info", "server_version_side_effect"), - [({"config": ADDON_DISCOVERY_INFO}, TimeoutError())], -) -async def test_supervisor_discovery_cannot_connect( - hass: HomeAssistant, supervisor, get_addon_discovery_info -) -> None: +@pytest.mark.usefixtures("supervisor") +@pytest.mark.parametrize("server_version_side_effect", [TimeoutError()]) +async def test_supervisor_discovery_cannot_connect(hass: HomeAssistant) -> None: """Test Supervisor discovery and cannot connect.""" result = await hass.config_entries.flow.async_init( @@ -437,13 +459,11 @@ async def test_supervisor_discovery_cannot_connect( assert result["reason"] == "cannot_connect" -@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_clean_discovery_on_user_create( hass: HomeAssistant, supervisor, addon_running, addon_options, - get_addon_discovery_info, ) -> None: """Test discovery flow is cleaned up when a user flow is finished.""" @@ -525,8 +545,10 @@ async def test_clean_discovery_on_user_create( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_abort_discovery_with_existing_entry( - hass: HomeAssistant, supervisor, addon_running, addon_options + hass: HomeAssistant, + addon_options: dict[str, Any], ) -> None: """Test discovery flow is aborted if an entry already exists.""" @@ -555,9 +577,8 @@ async def test_abort_discovery_with_existing_entry( assert entry.data["url"] == "ws://host1:3001" -async def test_abort_hassio_discovery_with_existing_flow( - hass: HomeAssistant, supervisor, addon_installed, addon_options -) -> None: +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") +async def test_abort_hassio_discovery_with_existing_flow(hass: HomeAssistant) -> None: """Test hassio discovery flow is aborted when another discovery has happened.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -582,9 +603,8 @@ async def test_abort_hassio_discovery_with_existing_flow( assert result2["reason"] == "already_in_progress" -async def test_abort_hassio_discovery_for_other_addon( - hass: HomeAssistant, supervisor, addon_installed, addon_options -) -> None: +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") +async def test_abort_hassio_discovery_for_other_addon(hass: HomeAssistant) -> None: """Test hassio discovery flow is aborted for a non official add-on discovery.""" result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -605,6 +625,7 @@ async def test_abort_hassio_discovery_for_other_addon( assert result2["reason"] == "not_zwave_js_addon" +@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") @pytest.mark.parametrize( ("usb_discovery_info", "device", "discovery_name"), [ @@ -627,26 +648,9 @@ async def test_abort_hassio_discovery_for_other_addon( ), ], ) -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) async def test_usb_discovery( hass: HomeAssistant, - supervisor, - addon_not_installed, install_addon, - addon_options, - get_addon_discovery_info, mock_usb_serial_by_id: MagicMock, set_addon_options, start_addon, @@ -751,28 +755,13 @@ async def test_usb_discovery( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_installed") async def test_usb_discovery_addon_not_running( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, + addon_options: dict[str, Any], mock_usb_serial_by_id: MagicMock, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test usb discovery when add-on is installed but not running.""" addon_options["device"] = "/dev/incorrect_device" @@ -872,20 +861,7 @@ async def test_usb_discovery_addon_not_running( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_usb_discovery_migration( hass: HomeAssistant, addon_options: dict[str, Any], @@ -1022,20 +998,7 @@ async def test_usb_discovery_migration( assert entry.unique_id == "5678" -@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_usb_discovery_migration_restore_driver_ready_timeout( hass: HomeAssistant, addon_options: dict[str, Any], @@ -1166,13 +1129,12 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert integration.data["use_addon"] is True +@pytest.mark.usefixtures("supervisor", "addon_installed") async def test_discovery_addon_not_running( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test discovery with add-on already installed but not running.""" addon_options["device"] = None @@ -1260,14 +1222,12 @@ async def test_discovery_addon_not_running( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") async def test_discovery_addon_not_installed( hass: HomeAssistant, - supervisor, - addon_not_installed, - install_addon, - addon_options, - set_addon_options, - start_addon, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test discovery with add-on not installed.""" result = await hass.config_entries.flow.async_init( @@ -1362,9 +1322,8 @@ async def test_discovery_addon_not_installed( assert len(mock_setup_entry.mock_calls) == 1 -async def test_abort_usb_discovery_with_existing_flow( - hass: HomeAssistant, supervisor, addon_options -) -> None: +@pytest.mark.usefixtures("supervisor", "addon_info") +async def test_abort_usb_discovery_with_existing_flow(hass: HomeAssistant) -> None: """Test usb discovery flow is aborted when another discovery has happened.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1429,9 +1388,8 @@ async def test_usb_discovery_with_existing_usb_flow(hass: HomeAssistant) -> None assert len(hass.config_entries.flow.async_progress()) == 0 -async def test_abort_usb_discovery_addon_required( - hass: HomeAssistant, supervisor, addon_options -) -> None: +@pytest.mark.usefixtures("supervisor", "addon_info") +async def test_abort_usb_discovery_addon_required(hass: HomeAssistant) -> None: """Test usb discovery aborted when existing entry not using add-on.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1526,24 +1484,27 @@ async def test_usb_discovery_same_device( assert mock_usb_serial_by_id.call_count == 2 +@pytest.mark.usefixtures("supervisor", "addon_info") @pytest.mark.parametrize( - "discovery_info", + "usb_discovery_info", [CP2652_ZIGBEE_DISCOVERY_INFO], ) async def test_abort_usb_discovery_aborts_specific_devices( - hass: HomeAssistant, supervisor, addon_options, discovery_info + hass: HomeAssistant, + usb_discovery_info: UsbServiceInfo, ) -> None: """Test usb discovery flow is aborted on specific devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USB}, - data=discovery_info, + data=usb_discovery_info, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_zwave_device" -async def test_not_addon(hass: HomeAssistant, supervisor) -> None: +@pytest.mark.usefixtures("supervisor") +async def test_not_addon(hass: HomeAssistant) -> None: """Test opting out of add-on on Supervisor.""" result = await hass.config_entries.flow.async_init( @@ -1602,25 +1563,10 @@ async def test_not_addon(hass: HomeAssistant, supervisor) -> None: assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_addon_running( hass: HomeAssistant, - supervisor, - addon_running, addon_options, - get_addon_discovery_info, ) -> None: """Test add-on already running on Supervisor.""" addon_options["device"] = "/test" @@ -1677,6 +1623,7 @@ async def test_addon_running( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( ( "discovery_info", @@ -1739,11 +1686,8 @@ async def test_addon_running( ) async def test_addon_running_failures( hass: HomeAssistant, - supervisor, - addon_running, - addon_options, - get_addon_discovery_info, - abort_reason, + addon_options: dict[str, Any], + abort_reason: str, ) -> None: """Test all failures when add-on is running.""" addon_options["device"] = "/test" @@ -1771,25 +1715,10 @@ async def test_addon_running_failures( assert result["reason"] == abort_reason -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_addon_running_already_configured( hass: HomeAssistant, - supervisor, - addon_running, - addon_options, - get_addon_discovery_info, + addon_options: dict[str, Any], ) -> None: """Test that only one unique instance is allowed when add-on is running.""" addon_options["device"] = "/test_new" @@ -1849,27 +1778,11 @@ async def test_addon_running_already_configured( assert entry.data["lr_s2_authenticated_key"] == "new321" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") async def test_addon_installed( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test add-on already installed but not running on Supervisor.""" @@ -1958,28 +1871,12 @@ async def test_addon_installed( assert len(mock_setup_entry.mock_calls) == 1 -@pytest.mark.parametrize( - ("discovery_info", "start_addon_side_effect"), - [ - ( - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ), - SupervisorError(), - ) - ], -) +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") +@pytest.mark.parametrize("start_addon_side_effect", [SupervisorError()]) async def test_addon_installed_start_failure( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test add-on start failure when add-on is installed.""" @@ -2044,6 +1941,7 @@ async def test_addon_installed_start_failure( assert result["reason"] == "addon_start_failed" +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") @pytest.mark.parametrize( ("discovery_info", "server_version_side_effect"), [ @@ -2066,12 +1964,8 @@ async def test_addon_installed_start_failure( ) async def test_addon_installed_failures( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test all failures when add-on is installed.""" @@ -2136,30 +2030,12 @@ async def test_addon_installed_failures( assert result["reason"] == "addon_start_failed" -@pytest.mark.parametrize( - ("set_addon_options_side_effect", "discovery_info"), - [ - ( - SupervisorError(), - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], - ) - ], -) +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") +@pytest.mark.parametrize("set_addon_options_side_effect", [SupervisorError()]) async def test_addon_installed_set_options_failure( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test all failures when add-on is installed.""" @@ -2218,11 +2094,8 @@ async def test_addon_installed_set_options_failure( assert start_addon.call_count == 0 -async def test_addon_installed_usb_ports_failure( - hass: HomeAssistant, - supervisor, - addon_installed, -) -> None: +@pytest.mark.usefixtures("supervisor", "addon_installed") +async def test_addon_installed_usb_ports_failure(hass: HomeAssistant) -> None: """Test usb ports failure when add-on is installed.""" result = await hass.config_entries.flow.async_init( @@ -2251,27 +2124,11 @@ async def test_addon_installed_usb_ports_failure( assert result["reason"] == "usb_ports_failed" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info") async def test_addon_installed_already_configured( hass: HomeAssistant, - supervisor, - addon_installed, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test that only one unique instance is allowed when add-on is installed.""" entry = MockConfigEntry( @@ -2361,28 +2218,12 @@ async def test_addon_installed_already_configured( assert entry.data["lr_s2_authenticated_key"] == "new321" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") async def test_addon_not_installed( hass: HomeAssistant, - supervisor, - addon_not_installed, - install_addon, - addon_options, - set_addon_options, - start_addon, - get_addon_discovery_info, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, ) -> None: """Test add-on not installed.""" result = await hass.config_entries.flow.async_init( @@ -2480,8 +2321,10 @@ async def test_addon_not_installed( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_not_installed") async def test_install_addon_failure( - hass: HomeAssistant, supervisor, addon_not_installed, install_addon + hass: HomeAssistant, + install_addon: AsyncMock, ) -> None: """Test add-on install failure.""" install_addon.side_effect = SupervisorError() @@ -2517,7 +2360,11 @@ async def test_install_addon_failure( assert result["reason"] == "addon_install_failed" -async def test_reconfigure_manual(hass: HomeAssistant, client, integration) -> None: +async def test_reconfigure_manual( + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, +) -> None: """Test manual settings in reconfigure flow.""" entry = integration hass.config_entries.async_update_entry(entry, unique_id="1234") @@ -2552,7 +2399,8 @@ async def test_reconfigure_manual(hass: HomeAssistant, client, integration) -> N async def test_reconfigure_manual_different_device( - hass: HomeAssistant, integration + hass: HomeAssistant, + integration: MockConfigEntry, ) -> None: """Test reconfigure flow manual step connecting to different device.""" entry = integration @@ -2579,8 +2427,11 @@ async def test_reconfigure_manual_different_device( assert result["reason"] == "different_device" +@pytest.mark.usefixtures("supervisor") async def test_reconfigure_not_addon( - hass: HomeAssistant, client, supervisor, integration + hass: HomeAssistant, + client: MagicMock, + integration: MockConfigEntry, ) -> None: """Test reconfigure flow and opting out of add-on on Supervisor.""" entry = integration @@ -2745,9 +2596,9 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( assert entry.state is config_entries.ConfigEntryState.NOT_LOADED +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( ( - "discovery_info", "entry_data", "old_addon_options", "new_addon_options", @@ -2755,14 +2606,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( ), [ ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -2788,14 +2631,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( 0, ), ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {"use_addon": True}, { "device": "/test", @@ -2825,14 +2660,10 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( async def test_reconfigure_addon_running( hass: HomeAssistant, client, - supervisor, integration, - addon_running, addon_options, set_addon_options, restart_addon, - get_addon_discovery_info, - discovery_info, entry_data, old_addon_options, new_addon_options, @@ -2919,18 +2750,11 @@ async def test_reconfigure_addon_running( assert client.disconnect.call_count == 1 +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( - ("discovery_info", "entry_data", "old_addon_options", "new_addon_options"), + ("entry_data", "old_addon_options", "new_addon_options"), [ ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -2961,14 +2785,10 @@ async def test_reconfigure_addon_running( async def test_reconfigure_addon_running_no_changes( hass: HomeAssistant, client, - supervisor, integration, - addon_running, addon_options, set_addon_options, restart_addon, - get_addon_discovery_info, - discovery_info, entry_data, old_addon_options, new_addon_options, @@ -3054,9 +2874,9 @@ async def different_device_server_version(*args): ) +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( ( - "discovery_info", "entry_data", "old_addon_options", "new_addon_options", @@ -3065,14 +2885,6 @@ async def different_device_server_version(*args): ), [ ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -3101,14 +2913,6 @@ async def different_device_server_version(*args): different_device_server_version, ), ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -3140,19 +2944,14 @@ async def different_device_server_version(*args): async def test_reconfigure_different_device( hass: HomeAssistant, client, - supervisor, integration, - addon_running, addon_options, set_addon_options, restart_addon, - get_addon_discovery_info, - discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, - server_version_side_effect, ) -> None: """Test reconfigure flow and configuring a different device.""" addon_options.update(old_addon_options) @@ -3233,9 +3032,9 @@ async def test_reconfigure_different_device( assert client.disconnect.call_count == 1 +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( ( - "discovery_info", "entry_data", "old_addon_options", "new_addon_options", @@ -3244,14 +3043,6 @@ async def test_reconfigure_different_device( ), [ ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -3280,14 +3071,6 @@ async def test_reconfigure_different_device( [SupervisorError(), None], ), ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -3323,19 +3106,14 @@ async def test_reconfigure_different_device( async def test_reconfigure_addon_restart_failed( hass: HomeAssistant, client, - supervisor, integration, - addon_running, addon_options, set_addon_options, restart_addon, - get_addon_discovery_info, - discovery_info, entry_data, old_addon_options, new_addon_options, disconnect_calls, - restart_addon_side_effect, ) -> None: """Test reconfigure flow and add-on restart failure.""" addon_options.update(old_addon_options) @@ -3413,76 +3191,42 @@ async def test_reconfigure_addon_restart_failed( assert client.disconnect.call_count == 1 -@pytest.mark.parametrize( - ( - "discovery_info", - "entry_data", - "old_addon_options", - "new_addon_options", - "disconnect_calls", - "server_version_side_effect", - ), - [ - ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], - {}, - { - "device": "/test", - "network_key": "abc123", - "s0_legacy_key": "abc123", - "s2_access_control_key": "old456", - "s2_authenticated_key": "old789", - "s2_unauthenticated_key": "old987", - "lr_s2_access_control_key": "old654", - "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, - }, - { - "usb_path": "/test", - "s0_legacy_key": "abc123", - "s2_access_control_key": "old456", - "s2_authenticated_key": "old789", - "s2_unauthenticated_key": "old987", - "lr_s2_access_control_key": "old654", - "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, - }, - 0, - aiohttp.ClientError("Boom"), - ), - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running", "restart_addon") +@pytest.mark.parametrize("server_version_side_effect", [aiohttp.ClientError("Boom")]) async def test_reconfigure_addon_running_server_info_failure( hass: HomeAssistant, - client, - supervisor, - integration, - addon_running, - addon_options, - set_addon_options, - restart_addon, - get_addon_discovery_info, - discovery_info, - entry_data, - old_addon_options, - new_addon_options, - disconnect_calls, - server_version_side_effect, + client: MagicMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, ) -> None: """Test reconfigure flow and add-on already running with server info failure.""" + old_addon_options = { + "device": "/test", + "network_key": "abc123", + "s0_legacy_key": "abc123", + "s2_access_control_key": "old456", + "s2_authenticated_key": "old789", + "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", + "log_level": "info", + "emulate_hardware": False, + } + new_addon_options = { + "usb_path": "/test", + "s0_legacy_key": "abc123", + "s2_access_control_key": "old456", + "s2_authenticated_key": "old789", + "s2_unauthenticated_key": "old987", + "lr_s2_access_control_key": "old654", + "lr_s2_authenticated_key": "old321", + "log_level": "info", + "emulate_hardware": False, + } addon_options.update(old_addon_options) entry = integration - data = {**entry.data, **entry_data} - hass.config_entries.async_update_entry(entry, data=data, unique_id="1234") + hass.config_entries.async_update_entry(entry, unique_id="1234") assert entry.data["url"] == "ws://test.org" @@ -3516,14 +3260,15 @@ async def test_reconfigure_addon_running_server_info_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" - assert entry.data == data + assert entry.data["url"] == "ws://test.org" + assert set_addon_options.call_count == 0 assert client.connect.call_count == 2 assert client.disconnect.call_count == 1 +@pytest.mark.usefixtures("supervisor", "addon_not_installed") @pytest.mark.parametrize( ( - "discovery_info", "entry_data", "old_addon_options", "new_addon_options", @@ -3531,14 +3276,6 @@ async def test_reconfigure_addon_running_server_info_failure( ), [ ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {}, { "device": "/test", @@ -3564,14 +3301,6 @@ async def test_reconfigure_addon_running_server_info_failure( 0, ), ( - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ], {"use_addon": True}, { "device": "/test", @@ -3601,15 +3330,11 @@ async def test_reconfigure_addon_running_server_info_failure( async def test_reconfigure_addon_not_installed( hass: HomeAssistant, client, - supervisor, - addon_not_installed, install_addon, integration, addon_options, set_addon_options, start_addon, - get_addon_discovery_info, - discovery_info, entry_data, old_addon_options, new_addon_options, @@ -3783,19 +3508,7 @@ async def test_reconfigure_migrate_low_sdk_version( assert result["reason"] == "migration_low_sdk_version" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") @pytest.mark.parametrize( ( "reset_server_version_side_effect", @@ -3813,13 +3526,10 @@ async def test_reconfigure_migrate_low_sdk_version( async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client, - supervisor, integration, - addon_running, restart_addon, addon_options, set_addon_options, - get_addon_discovery_info, get_server_version: AsyncMock, reset_server_version_side_effect: Exception | None, reset_unique_id: str, @@ -3971,28 +3681,13 @@ async def test_reconfigure_migrate_with_addon( assert entry.unique_id == final_unique_id -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_reset_driver_ready_timeout( hass: HomeAssistant, client, - supervisor, integration, - addon_running, restart_addon, set_addon_options, - get_addon_discovery_info, get_server_version: AsyncMock, ) -> None: """Test migration flow with driver ready timeout after controller reset.""" @@ -4133,28 +3828,13 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert entry.unique_id == "5678" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_restore_driver_ready_timeout( hass: HomeAssistant, client, - supervisor, integration, - addon_running, restart_addon, set_addon_options, - get_addon_discovery_info, ) -> None: """Test migration flow with driver ready timeout after nvm restore.""" entry = integration @@ -4286,7 +3966,9 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( async def test_reconfigure_migrate_backup_failure( - hass: HomeAssistant, integration, client + hass: HomeAssistant, + integration: MockConfigEntry, + client: MagicMock, ) -> None: """Test backup failure.""" entry = integration @@ -4317,7 +3999,9 @@ async def test_reconfigure_migrate_backup_failure( async def test_reconfigure_migrate_backup_file_failure( - hass: HomeAssistant, integration, client + hass: HomeAssistant, + integration: MockConfigEntry, + client: MagicMock, ) -> None: """Test backup file failure.""" entry = integration @@ -4360,20 +4044,7 @@ async def test_reconfigure_migrate_backup_file_failure( assert result["reason"] == "backup_failed" -@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info") -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_start_addon_failure( hass: HomeAssistant, client: MagicMock, @@ -4458,28 +4129,12 @@ async def test_reconfigure_migrate_start_addon_failure( assert result["reason"] == "addon_start_failed" -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_running", "restart_addon") async def test_reconfigure_migrate_restore_failure( hass: HomeAssistant, - client, - supervisor, - integration, - addon_running, - restart_addon, - set_addon_options, - get_addon_discovery_info, + client: MagicMock, + integration: MockConfigEntry, + set_addon_options: AsyncMock, ) -> None: """Test restore failure.""" entry = integration @@ -4545,6 +4200,7 @@ async def test_reconfigure_migrate_restore_failure( }, ) + assert set_addon_options.call_count == 1 assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "start_addon" @@ -4656,7 +4312,11 @@ async def test_get_driver_failure_instruct_unplug( assert result["reason"] == "config_entry_not_loaded" -async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> None: +async def test_hard_reset_failure( + hass: HomeAssistant, + integration: MockConfigEntry, + client: MagicMock, +) -> None: """Test hard reset failure.""" entry = integration hass.config_entries.async_update_entry( @@ -4703,7 +4363,9 @@ async def test_hard_reset_failure(hass: HomeAssistant, integration, client) -> N async def test_choose_serial_port_usb_ports_failure( - hass: HomeAssistant, integration, client + hass: HomeAssistant, + integration: MockConfigEntry, + client: MagicMock, ) -> None: """Test choose serial port usb ports failure.""" entry = integration @@ -4763,8 +4425,10 @@ async def test_choose_serial_port_usb_ports_failure( assert result["reason"] == "usb_ports_failed" +@pytest.mark.usefixtures("supervisor", "addon_installed") async def test_configure_addon_usb_ports_failure( - hass: HomeAssistant, integration, addon_installed, supervisor + hass: HomeAssistant, + integration: MockConfigEntry, ) -> None: """Test configure addon usb ports failure.""" entry = integration @@ -4791,7 +4455,7 @@ async def test_configure_addon_usb_ports_failure( assert result["reason"] == "usb_ports_failed" -async def test_get_usb_ports_sorting(hass: HomeAssistant) -> None: +async def test_get_usb_ports_sorting() -> None: """Test that get_usb_ports sorts ports with 'n/a' descriptions last.""" mock_ports = [ ListPortInfo("/dev/ttyUSB0"), @@ -4819,28 +4483,12 @@ async def test_get_usb_ports_sorting(hass: HomeAssistant) -> None: ] -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) +@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") async def test_intent_recommended_user( hass: HomeAssistant, - supervisor, - addon_not_installed, - install_addon, - start_addon, - addon_options, - set_addon_options, - get_addon_discovery_info, + install_addon: AsyncMock, + start_addon: AsyncMock, + set_addon_options: AsyncMock, ) -> None: """Test the intent_recommended step.""" result = await hass.config_entries.flow.async_init( @@ -4932,6 +4580,7 @@ async def test_intent_recommended_user( assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info") @pytest.mark.parametrize( ("usb_discovery_info", "device", "discovery_name"), [ @@ -4954,29 +4603,12 @@ async def test_intent_recommended_user( ), ], ) -@pytest.mark.parametrize( - "discovery_info", - [ - [ - Discovery( - addon="core_zwave_js", - service="zwave_js", - uuid=uuid4(), - config=ADDON_DISCOVERY_INFO, - ) - ] - ], -) async def test_recommended_usb_discovery( hass: HomeAssistant, - supervisor, - addon_not_installed, - install_addon, - addon_options, - get_addon_discovery_info, + install_addon: AsyncMock, mock_usb_serial_by_id: MagicMock, - set_addon_options, - start_addon, + set_addon_options: AsyncMock, + start_addon: AsyncMock, usb_discovery_info: UsbServiceInfo, device: str, discovery_name: str, From bbd223af1ff8174f9ae79b767710f62b6854719e Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 21 May 2025 18:07:36 +0300 Subject: [PATCH 0733/1175] Jewish Calendar: Make exception translatable (#145376) --- homeassistant/components/jewish_calendar/service.py | 5 ++++- homeassistant/components/jewish_calendar/strings.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/service.py b/homeassistant/components/jewish_calendar/service.py index 06d537b168d..a065ee9c969 100644 --- a/homeassistant/components/jewish_calendar/service.py +++ b/homeassistant/components/jewish_calendar/service.py @@ -16,6 +16,7 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.selector import LanguageSelector, LanguageSelectorConfig from homeassistant.helpers.sun import get_astral_event_date @@ -48,7 +49,9 @@ def async_setup_services(hass: HomeAssistant) -> None: event_date = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) if event_date is None: _LOGGER.error("Can't get sunset event date for %s", today) - raise ValueError("Can't get sunset event date") + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="sunset_event" + ) sunset = dt_util.as_local(event_date) _LOGGER.debug("Now: %s Sunset: %s", now, sunset) return now > sunset diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json index b76127604c7..adfce661538 100644 --- a/homeassistant/components/jewish_calendar/strings.json +++ b/homeassistant/components/jewish_calendar/strings.json @@ -185,5 +185,8 @@ } } } + }, + "exceptions": { + "sunset_event": { "message": "Can't get sunset event date" } } } From 743abadfcf988b21b78d04755a7da59ca71eeb79 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 21 May 2025 17:11:19 +0200 Subject: [PATCH 0734/1175] OTBR: remove links to obsolete multiprotocol docs (#145394) --- homeassistant/components/otbr/util.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/otbr/util.py b/homeassistant/components/otbr/util.py index 30e456e11a8..363b1385327 100644 --- a/homeassistant/components/otbr/util.py +++ b/homeassistant/components/otbr/util.py @@ -19,9 +19,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon MultiprotocolAddonManager, get_multiprotocol_addon_manager, is_multiprotocol_url, - multi_pan_addon_using_device, ) -from homeassistant.components.homeassistant_yellow import RADIO_DEVICE as YELLOW_RADIO from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -34,10 +32,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -INFO_URL_SKY_CONNECT = ( - "https://skyconnect.home-assistant.io/multiprotocol-channel-missmatch" -) -INFO_URL_YELLOW = "https://yellow.home-assistant.io/multiprotocol-channel-missmatch" INSECURE_NETWORK_KEYS = ( # Thread web UI default @@ -208,16 +202,12 @@ async def _warn_on_channel_collision( delete_issue() return - yellow = await multi_pan_addon_using_device(hass, YELLOW_RADIO) - learn_more_url = INFO_URL_YELLOW if yellow else INFO_URL_SKY_CONNECT - ir.async_create_issue( hass, DOMAIN, f"otbr_zha_channel_collision_{otbrdata.entry_id}", is_fixable=False, is_persistent=False, - learn_more_url=learn_more_url, severity=ir.IssueSeverity.WARNING, translation_key="otbr_zha_channel_collision", translation_placeholders={ From 1dbe1955eb289a317661dd792e24ee5a2f350714 Mon Sep 17 00:00:00 2001 From: TheOneValen <4579392+TheOneValen@users.noreply.github.com> Date: Wed, 21 May 2025 17:18:34 +0200 Subject: [PATCH 0735/1175] Allow image send with read-only access (matrix notify) (#144819) --- homeassistant/components/matrix/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 8640aa4d074..5123436a397 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -475,7 +475,7 @@ class MatrixBot: file_stat = await aiofiles.os.stat(image_path) _LOGGER.debug("Uploading file from path, %s", image_path) - async with aiofiles.open(image_path, "r+b") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: response, _ = await self._client.upload( image_file, content_type=mime_type, From 4cd3527761016263a9eb601dc3e701cedcec356f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 21 May 2025 17:37:51 +0200 Subject: [PATCH 0736/1175] Enable B009 (#144192) --- homeassistant/components/dsmr/sensor.py | 2 +- homeassistant/components/mqtt/client.py | 6 +++--- homeassistant/components/netatmo/diagnostics.py | 2 +- homeassistant/components/netatmo/entity.py | 5 +++-- homeassistant/components/netatmo/select.py | 15 +++++++++------ homeassistant/components/profiler/__init__.py | 2 +- homeassistant/components/ssdp/scanner.py | 3 ++- homeassistant/components/ssdp/server.py | 3 ++- homeassistant/config.py | 2 +- homeassistant/core.py | 2 +- homeassistant/helpers/entity.py | 2 +- homeassistant/util/__init__.py | 2 +- pyproject.toml | 1 + tests/components/flexit_bacnet/test_number.py | 6 +++--- tests/components/flexit_bacnet/test_switch.py | 8 ++++---- .../components/husqvarna_automower/test_button.py | 4 +--- tests/components/litterrobot/test_init.py | 2 +- tests/components/media_source/test_init.py | 2 +- tests/components/motionblinds_ble/test_entity.py | 2 +- tests/helpers/test_event.py | 2 +- tests/test_core_config.py | 2 +- 21 files changed, 40 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index ba528271824..918d4e33971 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -572,7 +572,7 @@ def device_class_and_uom( ) -> tuple[SensorDeviceClass | None, str | None]: """Get native unit of measurement from telegram,.""" dsmr_object = getattr(data, entity_description.obis_reference) - uom: str | None = getattr(dsmr_object, "unit") or None + uom: str | None = dsmr_object.unit or None with suppress(ValueError): if entity_description.device_class == SensorDeviceClass.GAS and ( enery_uom := UnitOfEnergy(str(uom)) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index f6f53599363..c2bcb306d0b 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -839,9 +839,9 @@ class MQTT: """Return a string with the exception message.""" # if msg_callback is a partial we return the name of the first argument if isinstance(msg_callback, partial): - call_back_name = getattr(msg_callback.args[0], "__name__") + call_back_name = msg_callback.args[0].__name__ else: - call_back_name = getattr(msg_callback, "__name__") + call_back_name = msg_callback.__name__ return ( f"Exception in {call_back_name} when handling msg on " f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe] @@ -1109,7 +1109,7 @@ class MQTT: # decoding the same topic multiple times. topic = msg.topic except UnicodeDecodeError: - bare_topic: bytes = getattr(msg, "_topic") + bare_topic: bytes = msg._topic # noqa: SLF001 _LOGGER.warning( "Skipping received%s message on invalid topic %s (qos=%s): %s", " retained" if msg.retain else "", diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index 4901ef6bd55..8cb07d1f9d8 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -49,7 +49,7 @@ async def async_get_config_entry_diagnostics( ), "data": { ACCOUNT: async_redact_data( - getattr(data_handler.account, "raw_data"), + data_handler.account.raw_data, TO_REDACT, ) }, diff --git a/homeassistant/components/netatmo/entity.py b/homeassistant/components/netatmo/entity.py index 6fdebcf0c3f..b519c75ae55 100644 --- a/homeassistant/components/netatmo/entity.py +++ b/homeassistant/components/netatmo/entity.py @@ -178,7 +178,8 @@ class NetatmoWeatherModuleEntity(NetatmoModuleEntity): def __init__(self, device: NetatmoDevice) -> None: """Set up a Netatmo weather module entity.""" super().__init__(device) - category = getattr(self.device.device_category, "name") + assert self.device.device_category + category = self.device.device_category.name self._publishers.extend( [ { @@ -189,7 +190,7 @@ class NetatmoWeatherModuleEntity(NetatmoModuleEntity): ) if hasattr(self.device, "place"): - place = cast(Place, getattr(self.device, "place")) + place = cast(Place, self.device.place) if hasattr(place, "location") and place.location is not None: self._attr_extra_state_attributes.update( { diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index e8637c90584..cb6675e4129 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -72,7 +72,9 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): self._attr_unique_id = f"{self.home.entity_id}-schedule-select" - self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") + schedule = self.home.get_selected_schedule() + assert schedule + self._attr_current_option = schedule.name self._attr_options = [ schedule.name for schedule in self.home.schedules.values() if schedule.name ] @@ -98,12 +100,11 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._attr_current_option = getattr( + self._attr_current_option = ( self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id].get( data["schedule_id"] - ), - "name", - ) + ) + ).name self.async_write_ha_state() async def async_select_option(self, option: str) -> None: @@ -125,7 +126,9 @@ class NetatmoScheduleSelect(NetatmoBaseEntity, SelectEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_current_option = getattr(self.home.get_selected_schedule(), "name") + schedule = self.home.get_selected_schedule() + assert schedule + self._attr_current_option = schedule.name self.hass.data[DOMAIN][DATA_SCHEDULES][self.home.entity_id] = ( self.home.schedules ) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 04dc6d76a5e..de14dc30d54 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -256,7 +256,7 @@ async def async_setup_entry( # noqa: C901 """Log all scheduled in the event loop.""" with _increase_repr_limit(): handle: asyncio.Handle - for handle in getattr(hass.loop, "_scheduled"): + for handle in getattr(hass.loop, "_scheduled"): # noqa: B009 if not handle.cancelled(): _LOGGER.critical("Scheduled: %s", handle) diff --git a/homeassistant/components/ssdp/scanner.py b/homeassistant/components/ssdp/scanner.py index d42c879e76a..1b7d69a3214 100644 --- a/homeassistant/components/ssdp/scanner.py +++ b/homeassistant/components/ssdp/scanner.py @@ -260,11 +260,12 @@ class Scanner: for source_ip in await async_build_source_set(self.hass): source_ip_str = str(source_ip) if source_ip.version == 6: + assert source_ip.scope_id is not None source_tuple: AddressTupleVXType = ( source_ip_str, 0, 0, - int(getattr(source_ip, "scope_id")), + int(source_ip.scope_id), ) else: source_tuple = (source_ip_str, 0) diff --git a/homeassistant/components/ssdp/server.py b/homeassistant/components/ssdp/server.py index 6d89263ab20..3a164fa374b 100644 --- a/homeassistant/components/ssdp/server.py +++ b/homeassistant/components/ssdp/server.py @@ -170,11 +170,12 @@ class Server: for source_ip in await async_build_source_set(self.hass): source_ip_str = str(source_ip) if source_ip.version == 6: + assert source_ip.scope_id is not None source_tuple: AddressTupleVXType = ( source_ip_str, 0, 0, - int(getattr(source_ip, "scope_id")), + int(source_ip.scope_id), ) else: source_tuple = (source_ip_str, 0) diff --git a/homeassistant/config.py b/homeassistant/config.py index e9089f27662..c3f02539f7d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -378,7 +378,7 @@ def _get_annotation(item: Any) -> tuple[str, int | str] | None: if not hasattr(item, "__config_file__"): return None - return (getattr(item, "__config_file__"), getattr(item, "__line__", "?")) + return (item.__config_file__, getattr(item, "__line__", "?")) def _get_by_path(data: dict | list, items: list[Hashable]) -> Any: diff --git a/homeassistant/core.py b/homeassistant/core.py index d7535907dfc..afffb883741 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -452,7 +452,7 @@ class HomeAssistant: self.import_executor = InterruptibleThreadPoolExecutor( max_workers=1, thread_name_prefix="ImportExecutor" ) - self.loop_thread_id = getattr(self.loop, "_thread_id") + self.loop_thread_id = self.loop._thread_id # type: ignore[attr-defined] # noqa: SLF001 def verify_event_loop_thread(self, what: str) -> None: """Report and raise if we are not running in the event loop thread.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a3edf6bb64f..8b13ee2409a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -381,7 +381,7 @@ class CachedProperties(type): for parent in cls.__mro__[:0:-1]: if "_CachedProperties__cached_properties" not in parent.__dict__: continue - cached_properties = getattr(parent, "_CachedProperties__cached_properties") + cached_properties = getattr(parent, "_CachedProperties__cached_properties") # noqa: B009 for property_name in cached_properties: if property_name in seen_props: continue diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index c2d825a1676..19515fd7945 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -160,7 +160,7 @@ class Throttle: If we cannot acquire the lock, it is running so return None. """ if hasattr(method, "__self__"): - host = getattr(method, "__self__") + host = method.__self__ elif is_func: host = wrapper else: diff --git a/pyproject.toml b/pyproject.toml index 183ef236ef1..30ca8efa7c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -707,6 +707,7 @@ select = [ "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 + "B009", # Do not call getattr with a constant attribute value. It is not any safer than normal property access. "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 diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py index f566b623f12..1053521dc2d 100644 --- a/tests/components/flexit_bacnet/test_number.py +++ b/tests/components/flexit_bacnet/test_number.py @@ -60,7 +60,7 @@ async def test_numbers_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + mocked_method = mock_flexit_bacnet.set_fan_setpoint_supply_air_fire assert len(mocked_method.mock_calls) == 1 assert hass.states.get(ENTITY_ID).state == "60" @@ -76,7 +76,7 @@ async def test_numbers_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + mocked_method = mock_flexit_bacnet.set_fan_setpoint_supply_air_fire assert len(mocked_method.mock_calls) == 2 assert hass.states.get(ENTITY_ID).state == "40" @@ -94,7 +94,7 @@ async def test_numbers_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + mocked_method = mock_flexit_bacnet.set_fan_setpoint_supply_air_fire assert len(mocked_method.mock_calls) == 3 mock_flexit_bacnet.set_fan_setpoint_supply_air_fire.side_effect = None diff --git a/tests/components/flexit_bacnet/test_switch.py b/tests/components/flexit_bacnet/test_switch.py index 8ce0bf11977..434e5fe1968 100644 --- a/tests/components/flexit_bacnet/test_switch.py +++ b/tests/components/flexit_bacnet/test_switch.py @@ -59,7 +59,7 @@ async def test_switches_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "disable_electric_heater") + mocked_method = mock_flexit_bacnet.disable_electric_heater assert len(mocked_method.mock_calls) == 1 assert hass.states.get(ENTITY_ID).state == STATE_OFF @@ -73,7 +73,7 @@ async def test_switches_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "enable_electric_heater") + mocked_method = mock_flexit_bacnet.enable_electric_heater assert len(mocked_method.mock_calls) == 1 assert hass.states.get(ENTITY_ID).state == STATE_ON @@ -88,7 +88,7 @@ async def test_switches_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "disable_electric_heater") + mocked_method = mock_flexit_bacnet.disable_electric_heater assert len(mocked_method.mock_calls) == 2 mock_flexit_bacnet.disable_electric_heater.side_effect = None @@ -114,7 +114,7 @@ async def test_switches_implementation( blocking=True, ) - mocked_method = getattr(mock_flexit_bacnet, "enable_electric_heater") + mocked_method = mock_flexit_bacnet.enable_electric_heater assert len(mocked_method.mock_calls) == 2 mock_flexit_bacnet.enable_electric_heater.side_effect = None diff --git a/tests/components/husqvarna_automower/test_button.py b/tests/components/husqvarna_automower/test_button.py index 1674c356f73..9fb5ad28c89 100644 --- a/tests/components/husqvarna_automower/test_button.py +++ b/tests/components/husqvarna_automower/test_button.py @@ -68,9 +68,7 @@ async def test_button_states_and_commands( await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == "2023-06-05T00:16:00+00:00" - getattr(mock_automower_client.commands, "error_confirm").side_effect = ApiError( - "Test error" - ) + mock_automower_client.commands.error_confirm.side_effect = ApiError("Test error") with pytest.raises( HomeAssistantError, match="Failed to send command: Test error", diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index e42bdb048b7..9ba4acaa935 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -37,7 +37,7 @@ async def test_unload_entry(hass: HomeAssistant, mock_account: MagicMock) -> Non {ATTR_ENTITY_ID: VACUUM_ENTITY_ID}, blocking=True, ) - getattr(mock_account.robots[0], "start_cleaning").assert_called_once() + mock_account.robots[0].start_cleaning.assert_called_once() assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 2c2952068ee..1849fbc09ab 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -241,7 +241,7 @@ async def test_websocket_resolve_media( # Validate url is relative and signed. assert msg["result"]["url"][0] == "/" parsed = yarl.URL(msg["result"]["url"]) - assert parsed.path == getattr(media, "url") + assert parsed.path == media.url assert "authSig" in parsed.query with patch( diff --git a/tests/components/motionblinds_ble/test_entity.py b/tests/components/motionblinds_ble/test_entity.py index 00369ba1e22..eee234a03be 100644 --- a/tests/components/motionblinds_ble/test_entity.py +++ b/tests/components/motionblinds_ble/test_entity.py @@ -52,4 +52,4 @@ async def test_entity_update( {ATTR_ENTITY_ID: f"{platform.name.lower()}.{name}_{entity}"}, blocking=True, ) - getattr(mock_motion_device, "status_query").assert_called_once_with() + mock_motion_device.status_query.assert_called_once_with() diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index b8bc89e29d7..465d1b1778b 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -3605,7 +3605,7 @@ async def test_track_time_interval_name(hass: HomeAssistant) -> None: timedelta(seconds=10), name=unique_string, ) - scheduled = getattr(hass.loop, "_scheduled") + scheduled = hass.loop._scheduled assert any(handle for handle in scheduled if unique_string in str(handle)) unsub() diff --git a/tests/test_core_config.py b/tests/test_core_config.py index 7fbd10db206..bbf7027e7ef 100644 --- a/tests/test_core_config.py +++ b/tests/test_core_config.py @@ -832,7 +832,7 @@ async def test_configuration_legacy_template_is_removed(hass: HomeAssistant) -> }, ) - assert not getattr(hass.config, "legacy_templates") + assert not hass.config.legacy_templates async def test_config_defaults() -> None: From 34c5f799836c851fa5b552b9ba391aed6fd5390e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 May 2025 18:40:18 +0200 Subject: [PATCH 0737/1175] Update bluetooth-auto-recovery to 1.5.2 (#145395) --- 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 f9377443296..4fc835e4532 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -18,7 +18,7 @@ "bleak==0.22.3", "bleak-retry-connector==3.9.0", "bluetooth-adapters==0.21.4", - "bluetooth-auto-recovery==1.5.1", + "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", "habluetooth==3.48.2" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fcb23c346a2..6ef8613ad96 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ bcrypt==4.2.0 bleak-retry-connector==3.9.0 bleak==0.22.3 bluetooth-adapters==0.21.4 -bluetooth-auto-recovery==1.5.1 +bluetooth-auto-recovery==1.5.2 bluetooth-data-tools==1.28.1 cached-ipaddress==0.10.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index b0950d345aa..1b0e3dcdfdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.1 +bluetooth-auto-recovery==1.5.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1af0681bdf..496612e6e19 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ bluemaestro-ble==0.4.1 bluetooth-adapters==0.21.4 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.5.1 +bluetooth-auto-recovery==1.5.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From b1da60026972db5b265a209acf25d5e18772e4a4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 May 2025 18:40:24 +0200 Subject: [PATCH 0738/1175] Update inkbird-ble to 0.16.2 (#145396) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 38d406da62e..9c73c4d970f 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -53,5 +53,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/inkbird", "iot_class": "local_push", - "requirements": ["inkbird-ble==0.16.1"] + "requirements": ["inkbird-ble==0.16.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b0e3dcdfdf..a076910345a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1239,7 +1239,7 @@ influxdb-client==1.48.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.16.1 +inkbird-ble==0.16.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 496612e6e19..f720e6ab536 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1054,7 +1054,7 @@ influxdb-client==1.48.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.16.1 +inkbird-ble==0.16.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.5.0 From 980f19173fabd794572a5c2c2e079adb8e00bff5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 May 2025 18:40:32 +0200 Subject: [PATCH 0739/1175] Update sensorpro-ble to 0.7.1 (#145397) --- homeassistant/components/sensorpro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json index ccf042245ea..1a6ec5527a0 100644 --- a/homeassistant/components/sensorpro/manifest.json +++ b/homeassistant/components/sensorpro/manifest.json @@ -18,5 +18,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/sensorpro", "iot_class": "local_push", - "requirements": ["sensorpro-ble==0.7.0"] + "requirements": ["sensorpro-ble==0.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index a076910345a..602de390ccc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2722,7 +2722,7 @@ sense-energy==0.13.8 sensirion-ble==0.1.1 # homeassistant.components.sensorpro -sensorpro-ble==0.7.0 +sensorpro-ble==0.7.1 # homeassistant.components.sensorpush_cloud sensorpush-api==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f720e6ab536..309462c55cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2205,7 +2205,7 @@ sense-energy==0.13.8 sensirion-ble==0.1.1 # homeassistant.components.sensorpro -sensorpro-ble==0.7.0 +sensorpro-ble==0.7.1 # homeassistant.components.sensorpush_cloud sensorpush-api==2.1.2 From 3df993b9a434d0e7b8669b19b1ed34e8e66bbc95 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 21 May 2025 19:42:13 +0200 Subject: [PATCH 0740/1175] Update igloohome-api to 0.1.1 (#145401) --- homeassistant/components/igloohome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/igloohome/manifest.json b/homeassistant/components/igloohome/manifest.json index 35c58479d75..7bfb8f690c7 100644 --- a/homeassistant/components/igloohome/manifest.json +++ b/homeassistant/components/igloohome/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/igloohome", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["igloohome-api==0.1.0"] + "requirements": ["igloohome-api==0.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 602de390ccc..8cf9ad612e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ ifaddr==0.2.0 iglo==1.2.7 # homeassistant.components.igloohome -igloohome-api==0.1.0 +igloohome-api==0.1.1 # homeassistant.components.ihc ihcsdk==2.8.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 309462c55cd..a58d837646b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1036,7 +1036,7 @@ idasen-ha==2.6.3 ifaddr==0.2.0 # homeassistant.components.igloohome -igloohome-api==0.1.0 +igloohome-api==0.1.1 # homeassistant.components.imeon_inverter imeon_inverter_api==0.3.12 From c8ceea4be85f2a70d1f6f4c2cab9d16984f08944 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 21 May 2025 20:01:12 +0200 Subject: [PATCH 0741/1175] Add SmartThings capability for Washer spin level (#145039) --- .../components/smartthings/icons.json | 3 + .../components/smartthings/select.py | 29 +++ .../components/smartthings/strings.json | 22 ++ .../smartthings/snapshots/test_select.ambr | 196 ++++++++++++++++++ 4 files changed, 250 insertions(+) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index f0c688b2ddc..15526dc7d88 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -59,6 +59,9 @@ }, "flexible_detergent_amount": { "default": "mdi:car-coolant-level" + }, + "spin_level": { + "default": "mdi:rotate-right" } }, "sensor": { diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index 39a49da2bbe..b5fb27610c2 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -20,6 +20,26 @@ LAMP_TO_HA = { "extraHigh": "extra_high", } +WASHER_SPIN_LEVEL_TO_HA = { + "none": "none", + "rinseHold": "rinse_hold", + "noSpin": "no_spin", + "low": "low", + "extraLow": "extra_low", + "delicate": "delicate", + "medium": "medium", + "high": "high", + "extraHigh": "extra_high", + "200": "200", + "400": "400", + "600": "600", + "800": "800", + "1000": "1000", + "1200": "1200", + "1400": "1400", + "1600": "1600", +} + @dataclass(frozen=True, kw_only=True) class SmartThingsSelectDescription(SelectEntityDescription): @@ -93,6 +113,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { extra_components=["hood"], capability_ignore_list=[Capability.SAMSUNG_CE_CONNECTION_STATE], ), + Capability.CUSTOM_WASHER_SPIN_LEVEL: SmartThingsSelectDescription( + key=Capability.CUSTOM_WASHER_SPIN_LEVEL, + translation_key="spin_level", + options_attribute=Attribute.SUPPORTED_WASHER_SPIN_LEVEL, + status_attribute=Attribute.WASHER_SPIN_LEVEL, + command=Command.SET_WASHER_SPIN_LEVEL, + options_map=WASHER_SPIN_LEVEL_TO_HA, + entity_category=EntityCategory.CONFIG, + ), } diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 607583c8941..2ce72dc0c95 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -160,6 +160,28 @@ "extra": "[%key:component::smartthings::entity::select::detergent_amount::state::extra%]", "custom": "[%key:component::smartthings::entity::select::detergent_amount::state::custom%]" } + }, + "spin_level": { + "name": "Spin level", + "state": { + "none": "None", + "rinse_hold": "Rinse hold", + "no_spin": "No spin", + "low": "[%key:common::state::low%]", + "extra_low": "Extra low", + "delicate": "Delicate", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]", + "extra_high": "Extra high", + "200": "200", + "400": "400", + "600": "600", + "800": "800", + "1000": "1000", + "1200": "1200", + "1400": "1400", + "1600": "1600" + } } }, "sensor": { diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index c1093bbd209..58a206f109c 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -459,6 +459,70 @@ 'state': 'stop', }) # --- +# name: test_all_entities[da_wm_wm_000001][select.washer_spin_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'rinse_hold', + 'no_spin', + 'low', + 'medium', + 'high', + 'extra_high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.washer_spin_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Spin level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spin_level', + 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_000001][select.washer_spin_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer Spin level', + 'options': list([ + 'rinse_hold', + 'no_spin', + 'low', + 'medium', + 'high', + 'extra_high', + ]), + }), + 'context': , + 'entity_id': 'select.washer_spin_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'high', + }) +# --- # name: test_all_entities[da_wm_wm_000001_1][select.washing_machine-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -517,6 +581,72 @@ 'state': 'run', }) # --- +# name: test_all_entities[da_wm_wm_000001_1][select.washing_machine_spin_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'rinse_hold', + 'no_spin', + '400', + '800', + '1000', + '1200', + '1400', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.washing_machine_spin_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Spin level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spin_level', + 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_000001_1][select.washing_machine_spin_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing Machine Spin level', + 'options': list([ + 'rinse_hold', + 'no_spin', + '400', + '800', + '1000', + '1200', + '1400', + ]), + }), + 'context': , + 'entity_id': 'select.washing_machine_spin_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1400', + }) +# --- # name: test_all_entities[da_wm_wm_01011][select.machine_a_laver-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -695,6 +825,72 @@ 'state': 'standard', }) # --- +# name: test_all_entities[da_wm_wm_01011][select.machine_a_laver_spin_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'rinse_hold', + 'no_spin', + '400', + '800', + '1000', + '1200', + '1400', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.machine_a_laver_spin_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Spin level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spin_level', + 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_01011][select.machine_a_laver_spin_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Machine à Laver Spin level', + 'options': list([ + 'rinse_hold', + 'no_spin', + '400', + '800', + '1000', + '1200', + '1400', + ]), + }), + 'context': , + 'entity_id': 'select.machine_a_laver_spin_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- # name: test_all_entities[da_wm_wm_100001][select.washer-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ea9fc6052d3b6f52dcc84a601c9f85e73c42f5ce Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 21 May 2025 20:14:13 +0200 Subject: [PATCH 0742/1175] Add power cool and power freeze to SmartThings (#145102) --- .../components/smartthings/icons.json | 6 + .../components/smartthings/strings.json | 6 + .../components/smartthings/switch.py | 28 +- .../device_status/da_ref_normal_000001.json | 2 +- .../smartthings/snapshots/test_switch.ambr | 284 +++++++++++++++++- tests/components/smartthings/test_switch.py | 32 ++ 6 files changed, 353 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index 15526dc7d88..f1034d1a55f 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -112,6 +112,12 @@ "ice_maker": { "default": "mdi:delete-variant" }, + "power_cool": { + "default": "mdi:snowflake-alert" + }, + "power_freeze": { + "default": "mdi:snowflake" + }, "sanitize": { "default": "mdi:lotion" }, diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 2ce72dc0c95..27c0eafe811 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -578,6 +578,12 @@ "sabbath_mode": { "name": "Sabbath mode" }, + "power_cool": { + "name": "Power cool" + }, + "power_freeze": { + "name": "Power freeze" + }, "auto_cycle_link": { "name": "Auto cycle link" }, diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 61ebc56699b..56096dc6ab5 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -48,6 +48,9 @@ class SmartThingsSwitchEntityDescription(SwitchEntityDescription): status_attribute: Attribute component_translation_key: dict[str, str] | None = None + on_key: str = "on" + on_command: Command = Command.ON + off_command: Command = Command.OFF @dataclass(frozen=True, kw_only=True) @@ -98,6 +101,25 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio key=Capability.SAMSUNG_CE_SABBATH_MODE, translation_key="sabbath_mode", status_attribute=Attribute.STATUS, + entity_category=EntityCategory.CONFIG, + ), + Capability.SAMSUNG_CE_POWER_COOL: SmartThingsSwitchEntityDescription( + key=Capability.SAMSUNG_CE_POWER_COOL, + translation_key="power_cool", + status_attribute=Attribute.ACTIVATED, + on_key="True", + on_command=Command.ACTIVATE, + off_command=Command.DEACTIVATE, + entity_category=EntityCategory.CONFIG, + ), + Capability.SAMSUNG_CE_POWER_FREEZE: SmartThingsSwitchEntityDescription( + key=Capability.SAMSUNG_CE_POWER_FREEZE, + translation_key="power_freeze", + status_attribute=Attribute.ACTIVATED, + on_key="True", + on_command=Command.ACTIVATE, + off_command=Command.DEACTIVATE, + entity_category=EntityCategory.CONFIG, ), Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE: SmartThingsSwitchEntityDescription( key=Capability.SAMSUNG_CE_STEAM_CLOSET_SANITIZE_MODE, @@ -239,14 +261,14 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): """Turn the switch off.""" await self.execute_device_command( self.switch_capability, - Command.OFF, + self.entity_description.off_command, ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.execute_device_command( self.switch_capability, - Command.ON, + self.entity_description.on_command, ) @property @@ -256,7 +278,7 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchEntity): self.get_attribute_value( self.switch_capability, self.entity_description.status_attribute ) - == "on" + == self.entity_description.on_key ) diff --git a/tests/components/smartthings/fixtures/device_status/da_ref_normal_000001.json b/tests/components/smartthings/fixtures/device_status/da_ref_normal_000001.json index 0c5a883b4f9..57dba2e0259 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ref_normal_000001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ref_normal_000001.json @@ -574,7 +574,7 @@ }, "samsungce.powerCool": { "activated": { - "value": false, + "value": true, "timestamp": "2025-01-19T21:07:55.725Z" } }, diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index be9253dd388..6d0be8b3c89 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -93,6 +93,100 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_power_cool-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.refrigerator_power_cool', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power cool', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_cool', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_samsungce.powerCool_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_power_cool-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Power cool', + }), + 'context': , + 'entity_id': 'switch.refrigerator_power_cool', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_power_freeze-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.refrigerator_power_freeze', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power freeze', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_freeze', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_samsungce.powerFreeze_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_000001][switch.refrigerator_power_freeze-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Power freeze', + }), + 'context': , + 'entity_id': 'switch.refrigerator_power_freeze', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ref_normal_000001][switch.refrigerator_sabbath_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -105,7 +199,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'switch', - 'entity_category': None, + 'entity_category': , 'entity_id': 'switch.refrigerator_sabbath_mode', 'has_entity_name': True, 'hidden_by': None, @@ -187,6 +281,194 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_power_cool-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.refrigerator_power_cool', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power cool', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_cool', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_samsungce.powerCool_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_power_cool-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Power cool', + }), + 'context': , + 'entity_id': 'switch.refrigerator_power_cool', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_power_freeze-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.refrigerator_power_freeze', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power freeze', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_freeze', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_samsungce.powerFreeze_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][switch.refrigerator_power_freeze-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Power freeze', + }), + 'context': , + 'entity_id': 'switch.refrigerator_power_freeze', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][switch.frigo_power_cool-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.frigo_power_cool', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power cool', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_cool', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_samsungce.powerCool_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][switch.frigo_power_cool-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Frigo Power cool', + }), + 'context': , + 'entity_id': 'switch.frigo_power_cool', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][switch.frigo_power_freeze-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.frigo_power_freeze', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power freeze', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_freeze', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_samsungce.powerFreeze_activated_activated', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][switch.frigo_power_freeze-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Frigo Power freeze', + }), + 'context': , + 'entity_id': 'switch.frigo_power_freeze', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 09f710366d0..59790abe07d 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -110,6 +110,38 @@ async def test_command_switch_turn_on_off( ) +@pytest.mark.parametrize("device_fixture", ["da_ref_normal_000001"]) +@pytest.mark.parametrize( + ("action", "command"), + [ + (SERVICE_TURN_ON, Command.ACTIVATE), + (SERVICE_TURN_OFF, Command.DEACTIVATE), + ], +) +async def test_custom_commands( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + action: str, + command: Command, +) -> None: + """Test switch turn on and off command.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SWITCH_DOMAIN, + action, + {ATTR_ENTITY_ID: "switch.refrigerator_power_cool"}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "7db87911-7dce-1cf2-7119-b953432a2f09", + Capability.SAMSUNG_CE_POWER_COOL, + command, + MAIN, + ) + + @pytest.mark.parametrize("device_fixture", ["c2c_arlo_pro_3_switch"]) async def test_state_update( hass: HomeAssistant, From b3ba506e6c9ac6cbbf3a2b70927e1456d3367956 Mon Sep 17 00:00:00 2001 From: Jeremiah Paige Date: Wed, 21 May 2025 11:15:26 -0700 Subject: [PATCH 0743/1175] wsdot component adopts wsdot package (#144914) * wsdot component adopts wsdot package * update generated files * format code * move wsdot to async_setup_platform * Fix tests * cast wsdot travel id * bump wsdot to 0.0.1 --------- Co-authored-by: Joostlek --- homeassistant/components/wsdot/manifest.json | 4 +- homeassistant/components/wsdot/sensor.py | 91 ++++++-------------- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wsdot/conftest.py | 24 ++++++ tests/components/wsdot/test_sensor.py | 53 ++++-------- 6 files changed, 73 insertions(+), 105 deletions(-) create mode 100644 tests/components/wsdot/conftest.py diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index 9b7746eea74..7956897b982 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -4,5 +4,7 @@ "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/wsdot", "iot_class": "cloud_polling", - "quality_scale": "legacy" + "loggers": ["wsdot"], + "quality_scale": "legacy", + "requirements": ["wsdot==0.0.1"] } diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index b3eb2715562..ce1f775eb03 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -2,44 +2,32 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone -from http import HTTPStatus +from datetime import timedelta import logging -import re from typing import Any -import requests import voluptuous as vol +from wsdot import TravelTime, WsdotTravelError, WsdotTravelTimes from homeassistant.components.sensor import ( PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime +from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTR_ACCESS_CODE = "AccessCode" -ATTR_AVG_TIME = "AverageTime" -ATTR_CURRENT_TIME = "CurrentTime" -ATTR_DESCRIPTION = "Description" -ATTR_TIME_UPDATED = "TimeUpdated" -ATTR_TRAVEL_TIME_ID = "TravelTimeID" - ATTRIBUTION = "Data provided by WSDOT" CONF_TRAVEL_TIMES = "travel_time" ICON = "mdi:car" - -RESOURCE = ( - "http://www.wsdot.wa.gov/Traffic/api/TravelTimes/" - "TravelTimesREST.svc/GetTravelTimeAsJson" -) +DOMAIN = "wsdot" SCAN_INTERVAL = timedelta(minutes=3) @@ -53,7 +41,7 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, @@ -61,12 +49,14 @@ def setup_platform( ) -> None: """Set up the WSDOT sensor.""" sensors = [] + session = async_get_clientsession(hass) + api_key = config[CONF_API_KEY] + wsdot_travel = WsdotTravelTimes(api_key=api_key, session=session) for travel_time in config[CONF_TRAVEL_TIMES]: name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID) + travel_time_id = int(travel_time[CONF_ID]) sensors.append( - WashingtonStateTravelTimeSensor( - name, config[CONF_API_KEY], travel_time.get(CONF_ID) - ) + WashingtonStateTravelTimeSensor(name, wsdot_travel, travel_time_id) ) add_entities(sensors, True) @@ -82,10 +72,8 @@ class WashingtonStateTransportSensor(SensorEntity): _attr_icon = ICON - def __init__(self, name: str, access_code: str) -> None: + def __init__(self, name: str) -> None: """Initialize the sensor.""" - self._data: dict[str, str | int | None] = {} - self._access_code = access_code self._name = name self._state: int | None = None @@ -106,57 +94,28 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = UnitOfTime.MINUTES - def __init__(self, name: str, access_code: str, travel_time_id: str) -> None: + def __init__( + self, name: str, wsdot_travel: WsdotTravelTimes, travel_time_id: int + ) -> None: """Construct a travel time sensor.""" + super().__init__(name) + self._data: TravelTime | None = None self._travel_time_id = travel_time_id - WashingtonStateTransportSensor.__init__(self, name, access_code) + self._wsdot_travel = wsdot_travel - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data from WSDOT.""" - params = { - ATTR_ACCESS_CODE: self._access_code, - ATTR_TRAVEL_TIME_ID: self._travel_time_id, - } - - response = requests.get(RESOURCE, params, timeout=10) - if response.status_code != HTTPStatus.OK: + try: + travel_time = await self._wsdot_travel.get_travel_time(self._travel_time_id) + except WsdotTravelError: _LOGGER.warning("Invalid response from WSDOT API") else: - self._data = response.json() - _state = self._data.get(ATTR_CURRENT_TIME) - if not isinstance(_state, int): - self._state = None - else: - self._state = _state + self._data = travel_time + self._state = travel_time.CurrentTime @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return other details about the sensor state.""" if self._data is not None: - attrs: dict[str, str | int | None | datetime] = {} - for key in ( - ATTR_AVG_TIME, - ATTR_NAME, - ATTR_DESCRIPTION, - ATTR_TRAVEL_TIME_ID, - ): - attrs[key] = self._data.get(key) - attrs[ATTR_TIME_UPDATED] = _parse_wsdot_timestamp( - self._data.get(ATTR_TIME_UPDATED) - ) - return attrs + return self._data.model_dump() return None - - -def _parse_wsdot_timestamp(timestamp: Any) -> datetime | None: - """Convert WSDOT timestamp to datetime.""" - if not isinstance(timestamp, str): - return None - # ex: Date(1485040200000-0800) - timestamp_parts = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp) - if timestamp_parts is None: - return None - milliseconds, tzone = timestamp_parts.groups() - return datetime.fromtimestamp( - int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone))) - ) diff --git a/requirements_all.txt b/requirements_all.txt index 8cf9ad612e6..a93cddb559f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3097,6 +3097,9 @@ wled==0.21.0 # homeassistant.components.wolflink wolf-comm==0.0.23 +# homeassistant.components.wsdot +wsdot==0.0.1 + # homeassistant.components.wyoming wyoming==1.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58d837646b..5450daf5f8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2505,6 +2505,9 @@ wled==0.21.0 # homeassistant.components.wolflink wolf-comm==0.0.23 +# homeassistant.components.wsdot +wsdot==0.0.1 + # homeassistant.components.wyoming wyoming==1.5.4 diff --git a/tests/components/wsdot/conftest.py b/tests/components/wsdot/conftest.py new file mode 100644 index 00000000000..48e2f0a90f7 --- /dev/null +++ b/tests/components/wsdot/conftest.py @@ -0,0 +1,24 @@ +"""Provide common WSDOT fixtures.""" + +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest +from wsdot import TravelTime + +from homeassistant.components.wsdot.sensor import DOMAIN + +from tests.common import load_json_object_fixture + + +@pytest.fixture +def mock_travel_time() -> AsyncGenerator[TravelTime]: + """WsdotTravelTimes.get_travel_time is mocked to return a TravelTime data based on test fixture payload.""" + with patch( + "homeassistant.components.wsdot.sensor.WsdotTravelTimes", autospec=True + ) as mock: + client = mock.return_value + client.get_travel_time.return_value = TravelTime( + **load_json_object_fixture("wsdot.json", DOMAIN) + ) + yield mock diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index ff3d4960735..60d28991b56 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -1,64 +1,41 @@ """The tests for the WSDOT platform.""" from datetime import datetime, timedelta, timezone -import re +from unittest.mock import AsyncMock -import requests_mock - -from homeassistant.components.wsdot import sensor as wsdot from homeassistant.components.wsdot.sensor import ( - ATTR_DESCRIPTION, - ATTR_TIME_UPDATED, CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TRAVEL_TIMES, - RESOURCE, - SCAN_INTERVAL, + DOMAIN, ) +from homeassistant.const import CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import load_fixture - config = { CONF_API_KEY: "foo", - SCAN_INTERVAL: timedelta(seconds=120), CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I90 EB"}], } -async def test_setup_with_config(hass: HomeAssistant) -> None: +async def test_setup_with_config( + hass: HomeAssistant, mock_travel_time: AsyncMock +) -> None: """Test the platform setup with configuration.""" - assert await async_setup_component(hass, "sensor", {"wsdot": config}) + assert await async_setup_component( + hass, "sensor", {"sensor": [{CONF_PLATFORM: DOMAIN, **config}]} + ) - -async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None: - """Test for operational WSDOT sensor with proper attributes.""" - entities = [] - - def add_entities(new_entities, update_before_add=False): - """Mock add entities.""" - for entity in new_entities: - entity.hass = hass - - if update_before_add: - for entity in new_entities: - entity.update() - - entities.extend(new_entities) - - uri = re.compile(RESOURCE + "*") - requests_mock.get(uri, text=load_fixture("wsdot/wsdot.json")) - wsdot.setup_platform(hass, config, add_entities) - assert len(entities) == 1 - sensor = entities[0] - assert sensor.name == "I90 EB" - assert sensor.state == 11 + state = hass.states.get("sensor.i90_eb") + assert state is not None + assert state.name == "I90 EB" + assert state.state == "11" assert ( - sensor.extra_state_attributes[ATTR_DESCRIPTION] + state.attributes["Description"] == "Downtown Seattle to Downtown Bellevue via I-90" ) - assert sensor.extra_state_attributes[ATTR_TIME_UPDATED] == datetime( + assert state.attributes["TimeUpdated"] == datetime( 2017, 1, 21, 15, 10, tzinfo=timezone(timedelta(hours=-8)) ) From 13ce4322ac25d792d9d287fb0bad4f4704a6aaf4 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 21 May 2025 21:21:06 +0300 Subject: [PATCH 0744/1175] Reword sunset event exception (#145400) --- homeassistant/components/jewish_calendar/strings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/jewish_calendar/strings.json b/homeassistant/components/jewish_calendar/strings.json index adfce661538..ecfb6a472e6 100644 --- a/homeassistant/components/jewish_calendar/strings.json +++ b/homeassistant/components/jewish_calendar/strings.json @@ -187,6 +187,8 @@ } }, "exceptions": { - "sunset_event": { "message": "Can't get sunset event date" } + "sunset_event": { + "message": "Sunset event cannot be calculated for the provided date and location" + } } } From 3c93f6e3f9bc8a5321239061d8ea7fe801d22830 Mon Sep 17 00:00:00 2001 From: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com> Date: Wed, 21 May 2025 20:23:05 +0200 Subject: [PATCH 0745/1175] ZHA repairs: remove links to obsolete docs (#145398) --- .../components/zha/repairs/wrong_silabs_firmware.py | 11 ----------- tests/components/zha/test_repairs.py | 11 ++--------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py index 566158eff56..5b1eed18014 100644 --- a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py +++ b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py @@ -37,16 +37,6 @@ class HardwareType(enum.StrEnum): OTHER = "other" -DISABLE_MULTIPAN_URL = { - HardwareType.YELLOW: ( - "https://yellow.home-assistant.io/guides/disable-multiprotocol/#flash-the-silicon-labs-radio-firmware" - ), - HardwareType.SKYCONNECT: ( - "https://skyconnect.home-assistant.io/procedures/disable-multiprotocol/#step-flash-the-silicon-labs-radio-firmware" - ), - HardwareType.OTHER: None, -} - ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED = "wrong_silabs_firmware_installed" @@ -99,7 +89,6 @@ async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> boo issue_id=ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, is_fixable=False, is_persistent=True, - learn_more_url=DISABLE_MULTIPAN_URL[hardware_type], severity=ir.IssueSeverity.ERROR, translation_key=( ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index 0ff863f0c45..059210968df 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -18,7 +18,6 @@ from homeassistant.components.zha.repairs.network_settings_inconsistent import ( ISSUE_INCONSISTENT_NETWORK_SETTINGS, ) from homeassistant.components.zha.repairs.wrong_silabs_firmware import ( - DISABLE_MULTIPAN_URL, ISSUE_WRONG_SILABS_FIRMWARE_INSTALLED, HardwareType, _detect_radio_hardware, @@ -110,17 +109,12 @@ def test_detect_radio_hardware_failure(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - ("detected_hardware", "expected_learn_more_url"), - [ - (HardwareType.SKYCONNECT, DISABLE_MULTIPAN_URL[HardwareType.SKYCONNECT]), - (HardwareType.YELLOW, DISABLE_MULTIPAN_URL[HardwareType.YELLOW]), - (HardwareType.OTHER, None), - ], + ("detected_hardware"), + [HardwareType.SKYCONNECT, HardwareType.YELLOW, HardwareType.OTHER], ) async def test_multipan_firmware_repair( hass: HomeAssistant, detected_hardware: HardwareType, - expected_learn_more_url: str, config_entry: MockConfigEntry, mock_zigpy_connect: ControllerApplication, issue_registry: ir.IssueRegistry, @@ -159,7 +153,6 @@ async def test_multipan_firmware_repair( # The issue is created when we fail to probe assert issue is not None assert issue.translation_placeholders["firmware_type"] == "CPC" - assert issue.learn_more_url == expected_learn_more_url # If ZHA manages to start up normally after this, the issue will be deleted await hass.config_entries.async_setup(config_entry.entry_id) From 39a5341ab829520d18348786811f381277fdc78e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 21 May 2025 20:27:38 +0200 Subject: [PATCH 0746/1175] Add SmartThings capability for Washer soil level (#145041) --- .../components/smartthings/select.py | 20 ++++++ .../components/smartthings/strings.json | 13 ++++ .../smartthings/snapshots/test_select.ambr | 64 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/homeassistant/components/smartthings/select.py b/homeassistant/components/smartthings/select.py index b5fb27610c2..99dc7a09f87 100644 --- a/homeassistant/components/smartthings/select.py +++ b/homeassistant/components/smartthings/select.py @@ -20,6 +20,17 @@ LAMP_TO_HA = { "extraHigh": "extra_high", } +WASHER_SOIL_LEVEL_TO_HA = { + "none": "none", + "heavy": "heavy", + "normal": "normal", + "light": "light", + "extraLight": "extra_light", + "extraHeavy": "extra_heavy", + "up": "up", + "down": "down", +} + WASHER_SPIN_LEVEL_TO_HA = { "none": "none", "rinseHold": "rinse_hold", @@ -122,6 +133,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = { options_map=WASHER_SPIN_LEVEL_TO_HA, entity_category=EntityCategory.CONFIG, ), + Capability.CUSTOM_WASHER_SOIL_LEVEL: SmartThingsSelectDescription( + key=Capability.CUSTOM_WASHER_SOIL_LEVEL, + translation_key="soil_level", + options_attribute=Attribute.SUPPORTED_WASHER_SOIL_LEVEL, + status_attribute=Attribute.WASHER_SOIL_LEVEL, + command=Command.SET_WASHER_SOIL_LEVEL, + options_map=WASHER_SOIL_LEVEL_TO_HA, + entity_category=EntityCategory.CONFIG, + ), } diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 27c0eafe811..0d8e5feabc0 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -182,6 +182,19 @@ "1400": "1400", "1600": "1600" } + }, + "soil_level": { + "name": "Soil level", + "state": { + "none": "None", + "heavy": "Heavy", + "normal": "Normal", + "light": "Light", + "extra_light": "Extra light", + "extra_heavy": "Extra heavy", + "up": "Up", + "down": "Down" + } } }, "sensor": { diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index 58a206f109c..0ef12a3fe90 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -459,6 +459,70 @@ 'state': 'stop', }) # --- +# name: test_all_entities[da_wm_wm_000001][select.washer_soil_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'none', + 'extra_light', + 'light', + 'normal', + 'heavy', + 'extra_heavy', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.washer_soil_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Soil level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'soil_level', + 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_custom.washerSoilLevel_washerSoilLevel_washerSoilLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_wm_000001][select.washer_soil_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washer Soil level', + 'options': list([ + 'none', + 'extra_light', + 'light', + 'normal', + 'heavy', + 'extra_heavy', + ]), + }), + 'context': , + 'entity_id': 'select.washer_soil_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- # name: test_all_entities[da_wm_wm_000001][select.washer_spin_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From ca01bdc481fc82f2b75d99cdb9dff999ccd44d23 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 21 May 2025 21:12:43 +0200 Subject: [PATCH 0747/1175] Mark backflush binary sensor not supported for GS3 MP in lamarzocco (#145406) --- homeassistant/components/lamarzocco/binary_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index 9bf04129095..c108bdb02d8 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import cast from pylamarzocco import LaMarzoccoMachine -from pylamarzocco.const import BackFlushStatus, MachineState, WidgetType +from pylamarzocco.const import BackFlushStatus, MachineState, ModelName, WidgetType from pylamarzocco.models import BackFlush, MachineStatus from homeassistant.components.binary_sensor import ( @@ -66,6 +66,9 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( is BackFlushStatus.REQUESTED ), entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=lambda coordinator: ( + coordinator.device.dashboard.model_name != ModelName.GS3_MP + ), ), LaMarzoccoBinarySensorEntityDescription( key="websocket_connected", From cd9339903fb0d85ff39ec71073cf82b6fc64fdd9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 21 May 2025 21:17:03 +0200 Subject: [PATCH 0748/1175] Add thermostat fixture to SmartThings (#145407) --- tests/components/smartthings/conftest.py | 1 + .../device_status/sensi_thermostat.json | 106 ++++++++++++++++++ .../fixtures/devices/sensi_thermostat.json | 78 +++++++++++++ .../smartthings/snapshots/test_climate.ambr | 84 ++++++++++++++ .../smartthings/snapshots/test_init.ambr | 33 ++++++ .../smartthings/snapshots/test_sensor.ambr | 104 +++++++++++++++++ .../smartthings/snapshots/test_switch.ambr | 94 ++++++++-------- tests/components/smartthings/test_climate.py | 6 +- 8 files changed, 456 insertions(+), 50 deletions(-) create mode 100644 tests/components/smartthings/fixtures/device_status/sensi_thermostat.json create mode 100644 tests/components/smartthings/fixtures/devices/sensi_thermostat.json diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index ab6c6031d5e..e8cde67122b 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -146,6 +146,7 @@ def mock_smartthings() -> Generator[AsyncMock]: "ecobee_sensor", "ecobee_thermostat", "ecobee_thermostat_offline", + "sensi_thermostat", "fake_fan", "generic_fan_3_speed", "heatit_ztrm3_thermostat", diff --git a/tests/components/smartthings/fixtures/device_status/sensi_thermostat.json b/tests/components/smartthings/fixtures/device_status/sensi_thermostat.json new file mode 100644 index 00000000000..103e6631ab1 --- /dev/null +++ b/tests/components/smartthings/fixtures/device_status/sensi_thermostat.json @@ -0,0 +1,106 @@ +{ + "components": { + "main": { + "thermostatOperatingState": { + "supportedThermostatOperatingStates": { + "value": null + }, + "thermostatOperatingState": { + "value": "idle", + "timestamp": "2025-05-17T14:16:43.740Z" + } + }, + "relativeHumidityMeasurement": { + "humidity": { + "value": 49, + "unit": "%", + "timestamp": "2025-05-17T14:32:56.192Z" + } + }, + "healthCheck": { + "checkInterval": { + "value": 60, + "unit": "s", + "data": { + "deviceScheme": "UNTRACKED", + "protocol": "cloud" + }, + "timestamp": "2022-04-16T19:45:51.006Z" + }, + "healthStatus": { + "value": null + }, + "DeviceWatch-Enroll": { + "value": null + }, + "DeviceWatch-DeviceStatus": { + "value": "online", + "data": {}, + "timestamp": "2025-05-17T14:16:10.555Z" + } + }, + "temperatureMeasurement": { + "temperatureRange": { + "value": null + }, + "temperature": { + "value": 74.5, + "unit": "F", + "timestamp": "2025-05-17T14:32:56.192Z" + } + }, + "thermostatHeatingSetpoint": { + "heatingSetpoint": { + "value": 71, + "unit": "F", + "timestamp": "2025-05-17T14:16:12.093Z" + }, + "heatingSetpointRange": { + "value": null + } + }, + "thermostatFanMode": { + "thermostatFanMode": { + "value": "auto", + "data": { + "supportedThermostatFanModes": ["auto", "on", "circulate"] + }, + "timestamp": "2025-05-17T03:45:45.413Z" + }, + "supportedThermostatFanModes": { + "value": ["auto", "on", "circulate"], + "timestamp": "2025-05-17T03:45:45.413Z" + } + }, + "thermostatMode": { + "thermostatMode": { + "value": "auto", + "data": { + "supportedThermostatModes": [ + "off", + "heat", + "cool", + "emergency heat", + "auto" + ] + }, + "timestamp": "2025-05-17T05:45:53.597Z" + }, + "supportedThermostatModes": { + "value": ["off", "heat", "cool", "emergency heat", "auto"], + "timestamp": "2025-05-17T03:45:45.413Z" + } + }, + "thermostatCoolingSetpoint": { + "coolingSetpointRange": { + "value": null + }, + "coolingSetpoint": { + "value": 75, + "unit": "F", + "timestamp": "2025-05-17T14:16:13.677Z" + } + } + } + } +} diff --git a/tests/components/smartthings/fixtures/devices/sensi_thermostat.json b/tests/components/smartthings/fixtures/devices/sensi_thermostat.json new file mode 100644 index 00000000000..48d2a9c093d --- /dev/null +++ b/tests/components/smartthings/fixtures/devices/sensi_thermostat.json @@ -0,0 +1,78 @@ +{ + "items": [ + { + "deviceId": "2409a73c-918a-4d1f-b4f5-c27468c71d70", + "name": "Sensi Thermostat", + "label": "Thermostat", + "manufacturerName": "0AKf", + "presentationId": "sensi_thermostat", + "deviceManufacturerCode": "Emerson", + "locationId": "fc2fb744-4d34-4276-be33-56bbc6af266e", + "ownerId": "aecdb855-3ab7-9305-c0e3-0dced524e5dc", + "roomId": "025f6d30-c16c-4d11-8be2-03d5f4708d86", + "components": [ + { + "id": "main", + "label": "main", + "capabilities": [ + { + "id": "temperatureMeasurement", + "version": 1 + }, + { + "id": "thermostatCoolingSetpoint", + "version": 1 + }, + { + "id": "thermostatHeatingSetpoint", + "version": 1 + }, + { + "id": "thermostatOperatingState", + "version": 1 + }, + { + "id": "thermostatMode", + "version": 1 + }, + { + "id": "thermostatFanMode", + "version": 1 + }, + { + "id": "relativeHumidityMeasurement", + "version": 1 + }, + { + "id": "healthCheck", + "version": 1 + } + ], + "categories": [ + { + "name": "Thermostat", + "categoryType": "manufacturer" + } + ], + "optional": false + } + ], + "createTime": "2022-04-16T19:45:50.864Z", + "profile": { + "id": "923a86cc-983f-4cb1-98da-64fb5aa435ca" + }, + "viper": { + "manufacturerName": "Emerson", + "modelName": "1F95U-42WF", + "swVersion": "6004971003", + "endpointAppId": "viper_7722c3c0-dfc1-11e9-9149-4f2618178093" + }, + "type": "VIPER", + "restrictionTier": 0, + "allowed": null, + "executionContext": "CLOUD", + "relationships": [] + } + ], + "_links": {} +} diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index 6f4dd67d7f7..aef51b1c866 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -797,6 +797,90 @@ 'state': 'heat', }) # --- +# name: test_all_entities[sensi_thermostat][climate.thermostat-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_modes': list([ + 'auto', + 'on', + 'circulate', + ]), + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 7.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.thermostat', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensi_thermostat][climate.thermostat-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_humidity': 49, + 'current_temperature': 23.6, + 'fan_mode': 'auto', + 'fan_modes': list([ + 'auto', + 'on', + 'circulate', + ]), + 'friendly_name': 'Thermostat', + 'hvac_action': , + 'hvac_modes': list([ + , + , + , + , + , + ]), + 'max_temp': 35.0, + 'min_temp': 7.0, + 'supported_features': , + 'target_temp_high': 23.9, + 'target_temp_low': 21.7, + 'temperature': None, + }), + 'context': , + 'entity_id': 'climate.thermostat', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat_cool', + }) +# --- # name: test_all_entities[virtual_thermostat][climate.asd-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_init.ambr b/tests/components/smartthings/snapshots/test_init.ambr index 58b89099b11..446eca63fb2 100644 --- a/tests/components/smartthings/snapshots/test_init.ambr +++ b/tests/components/smartthings/snapshots/test_init.ambr @@ -1751,6 +1751,39 @@ 'via_device_id': None, }) # --- +# name: test_devices[sensi_thermostat] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': 'https://account.smartthings.com', + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'smartthings', + '2409a73c-918a-4d1f-b4f5-c27468c71d70', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Emerson', + 'model': '1F95U-42WF', + 'model_id': None, + 'name': 'Thermostat', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'suggested_area': None, + 'sw_version': '6004971003', + 'via_device_id': None, + }) +# --- # name: test_devices[sensibo_airconditioner_1] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index f5fe09cc4d5..7e9dd5c08da 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -11327,6 +11327,110 @@ 'state': '-1042', }) # --- +# name: test_all_entities[sensi_thermostat][sensor.thermostat_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.thermostat_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main_relativeHumidityMeasurement_humidity_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensi_thermostat][sensor.thermostat_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Thermostat Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.thermostat_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '49', + }) +# --- +# name: test_all_entities[sensi_thermostat][sensor.thermostat_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.thermostat_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensi_thermostat][sensor.thermostat_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Thermostat Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.thermostat_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '23.6', + }) +# --- # name: test_all_entities[sensibo_airconditioner_1][sensor.office_air_conditioner_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 6d0be8b3c89..3b5aa4114ea 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -516,53 +516,6 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'switch', - 'entity_category': , - 'entity_id': 'switch.airdresser_sanitize', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Sanitize', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'sanitize', - 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetSanitizeMode_status_status', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'AirDresser Sanitize', - }), - 'context': , - 'entity_id': 'switch.airdresser_sanitize', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -657,6 +610,53 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.airdresser_sanitize', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sanitize', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sanitize', + 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetSanitizeMode_status_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_wm_sc_000001][switch.airdresser_sanitize-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'AirDresser Sanitize', + }), + 'context': , + 'entity_id': 'switch.airdresser_sanitize', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_wd_000001][switch.dryer_wrinkle_prevent-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index ff8b5277e20..6332fbf905f 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -613,7 +613,7 @@ async def test_thermostat_set_fan_mode( ) -@pytest.mark.parametrize("device_fixture", ["virtual_thermostat"]) +@pytest.mark.parametrize("device_fixture", ["sensi_thermostat"]) async def test_thermostat_set_hvac_mode( hass: HomeAssistant, devices: AsyncMock, @@ -625,11 +625,11 @@ async def test_thermostat_set_hvac_mode( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.asd", ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, + {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVACMode.HEAT_COOL}, blocking=True, ) devices.execute_device_command.assert_called_once_with( - "2894dc93-0f11-49cc-8a81-3a684cebebf6", + "2409a73c-918a-4d1f-b4f5-c27468c71d70", Capability.THERMOSTAT_MODE, Command.SET_THERMOSTAT_MODE, MAIN, From 4f24d63de1e0d23935166960211cbe4870a8cc6d Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Wed, 21 May 2025 20:56:32 +0100 Subject: [PATCH 0749/1175] Update metoffice to use DataHub API (#131425) * Update metoffice to use DataHub API * Reauth test * Updated to datapoint 0.11.0 * Less hacky check for day/night in twice-daily forecasts * Updated to datapoint 0.12.1, added daily forecast * addressed review comments * one more nit * validate credewntials in reauth flow * Addressed review comments * Attempt to improve coverage * Addressed comments * Reverted unnecessary reordering * Update homeassistant/components/metoffice/sensor.py * Update tests/components/metoffice/test_sensor.py * Update homeassistant/components/metoffice/sensor.py --------- Co-authored-by: Franck Nijhof Co-authored-by: Joost Lekkerkerker --- .../components/metoffice/__init__.py | 69 +- .../components/metoffice/config_flow.py | 107 +- homeassistant/components/metoffice/const.py | 76 +- homeassistant/components/metoffice/data.py | 18 - homeassistant/components/metoffice/helpers.py | 57 +- .../components/metoffice/manifest.json | 2 +- homeassistant/components/metoffice/sensor.py | 154 +- .../components/metoffice/strings.json | 12 +- homeassistant/components/metoffice/weather.py | 200 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/metoffice/conftest.py | 5 +- tests/components/metoffice/const.py | 36 +- .../metoffice/fixtures/metoffice.json | 5763 ++++++++++++----- .../metoffice/snapshots/test_weather.ambr | 3706 +++++++---- .../components/metoffice/test_config_flow.py | 106 +- tests/components/metoffice/test_init.py | 142 +- tests/components/metoffice/test_sensor.py | 113 +- tests/components/metoffice/test_weather.py | 169 +- 19 files changed, 7328 insertions(+), 3411 deletions(-) delete mode 100644 homeassistant/components/metoffice/data.py diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 1d516bbc4f5..6977974c2e5 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -4,10 +4,10 @@ from __future__ import annotations import asyncio import logging -import re -from typing import Any import datapoint +import datapoint.Forecast +import datapoint.Manager from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -17,9 +17,8 @@ from homeassistant.const import ( CONF_NAME, Platform, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator @@ -30,11 +29,8 @@ from .const import ( METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_3HOURLY, - MODE_DAILY, ) -from .data import MetOfficeData -from .helpers import fetch_data, fetch_site +from .helpers import fetch_data _LOGGER = logging.getLogger(__name__) @@ -51,59 +47,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinates = f"{latitude}_{longitude}" - @callback - def update_unique_id( - entity_entry: er.RegistryEntry, - ) -> dict[str, Any] | None: - """Update unique ID of entity entry.""" + connection = datapoint.Manager.Manager(api_key=api_key) - if entity_entry.domain != Platform.SENSOR: - return None - - name_to_key = { - "Station Name": "name", - "Weather": "weather", - "Temperature": "temperature", - "Feels Like Temperature": "feels_like_temperature", - "Wind Speed": "wind_speed", - "Wind Direction": "wind_direction", - "Wind Gust": "wind_gust", - "Visibility": "visibility", - "Visibility Distance": "visibility_distance", - "UV Index": "uv", - "Probability of Precipitation": "precipitation", - "Humidity": "humidity", - } - - match = re.search(f"(?P.*)_{coordinates}.*", entity_entry.unique_id) - - if match is None: - return None - - if (name := match.group("name")) in name_to_key: - return { - "new_unique_id": entity_entry.unique_id.replace(name, name_to_key[name]) - } - return None - - await er.async_migrate_entries(hass, entry.entry_id, update_unique_id) - - connection = datapoint.connection(api_key=api_key) - - site = await hass.async_add_executor_job( - fetch_site, connection, latitude, longitude - ) - if site is None: - raise ConfigEntryNotReady - - async def async_update_3hourly() -> MetOfficeData: + async def async_update_hourly() -> datapoint.Forecast: return await hass.async_add_executor_job( - fetch_data, connection, site, MODE_3HOURLY + fetch_data, connection, latitude, longitude, "hourly" ) - async def async_update_daily() -> MetOfficeData: + async def async_update_daily() -> datapoint.Forecast: return await hass.async_add_executor_job( - fetch_data, connection, site, MODE_DAILY + fetch_data, connection, latitude, longitude, "daily" ) metoffice_hourly_coordinator = TimestampDataUpdateCoordinator( @@ -111,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER, config_entry=entry, name=f"MetOffice Hourly Coordinator for {site_name}", - update_method=async_update_3hourly, + update_method=async_update_hourly, update_interval=DEFAULT_SCAN_INTERVAL, ) diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index d46e537dadb..81369daf09a 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -2,10 +2,14 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any import datapoint +from datapoint.exceptions import APIException +import datapoint.Manager +from requests import HTTPError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -15,30 +19,41 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from .const import DOMAIN -from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: +async def validate_input( + hass: HomeAssistant, latitude: float, longitude: float, api_key: str +) -> dict[str, Any]: """Validate that the user input allows us to connect to DataPoint. Data has the keys from DATA_SCHEMA with values provided by the user. """ - latitude = data[CONF_LATITUDE] - longitude = data[CONF_LONGITUDE] - api_key = data[CONF_API_KEY] + errors = {} + connection = datapoint.Manager.Manager(api_key=api_key) - connection = datapoint.connection(api_key=api_key) + try: + forecast = await hass.async_add_executor_job( + connection.get_forecast, + latitude, + longitude, + "daily", + False, + ) - site = await hass.async_add_executor_job( - fetch_site, connection, latitude, longitude - ) + except (HTTPError, APIException) as err: + if isinstance(err, HTTPError) and err.response.status_code == 401: + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return {"site_name": forecast.name, "errors": errors} - if site is None: - raise CannotConnect - - return {"site_name": site.name} + return {"errors": errors} class MetOfficeConfigFlow(ConfigFlow, domain=DOMAIN): @@ -57,15 +72,17 @@ class MetOfficeConfigFlow(ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - user_input[CONF_NAME] = info["site_name"] + result = await validate_input( + self.hass, + latitude=user_input[CONF_LATITUDE], + longitude=user_input[CONF_LONGITUDE], + api_key=user_input[CONF_API_KEY], + ) + + errors = result["errors"] + + if not errors: + user_input[CONF_NAME] = result["site_name"] return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) @@ -83,7 +100,51 @@ class MetOfficeConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="user", + data_schema=data_schema, + errors=errors, + ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors = {} + + entry = self._get_reauth_entry() + if user_input is not None: + result = await validate_input( + self.hass, + latitude=entry.data[CONF_LATITUDE], + longitude=entry.data[CONF_LONGITUDE], + api_key=user_input[CONF_API_KEY], + ) + + errors = result["errors"] + + if not errors: + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + data_updates=user_input, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + description_placeholders={ + "docs_url": ("https://www.home-assistant.io/integrations/metoffice") + }, + errors=errors, ) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 966aec7d381..68c94f3d7a5 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -18,6 +18,17 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_APPARENT_TEMP, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_UV_INDEX, + ATTR_FORECAST_WIND_BEARING, ) DOMAIN = "metoffice" @@ -33,22 +44,19 @@ METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator" METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_NAME = "metoffice_name" -MODE_3HOURLY = "3hourly" -MODE_DAILY = "daily" - -CONDITION_CLASSES: dict[str, list[str]] = { - ATTR_CONDITION_CLEAR_NIGHT: ["0"], - ATTR_CONDITION_CLOUDY: ["7", "8"], - ATTR_CONDITION_FOG: ["5", "6"], - ATTR_CONDITION_HAIL: ["19", "20", "21"], - ATTR_CONDITION_LIGHTNING: ["30"], - ATTR_CONDITION_LIGHTNING_RAINY: ["28", "29"], - ATTR_CONDITION_PARTLYCLOUDY: ["2", "3"], - ATTR_CONDITION_POURING: ["13", "14", "15"], - ATTR_CONDITION_RAINY: ["9", "10", "11", "12"], - ATTR_CONDITION_SNOWY: ["22", "23", "24", "25", "26", "27"], - ATTR_CONDITION_SNOWY_RAINY: ["16", "17", "18"], - ATTR_CONDITION_SUNNY: ["1"], +CONDITION_CLASSES: dict[str, list[int]] = { + ATTR_CONDITION_CLEAR_NIGHT: [0], + ATTR_CONDITION_CLOUDY: [7, 8], + ATTR_CONDITION_FOG: [5, 6], + ATTR_CONDITION_HAIL: [19, 20, 21], + ATTR_CONDITION_LIGHTNING: [30], + ATTR_CONDITION_LIGHTNING_RAINY: [28, 29], + ATTR_CONDITION_PARTLYCLOUDY: [2, 3], + ATTR_CONDITION_POURING: [13, 14, 15], + ATTR_CONDITION_RAINY: [9, 10, 11, 12], + ATTR_CONDITION_SNOWY: [22, 23, 24, 25, 26, 27], + ATTR_CONDITION_SNOWY_RAINY: [16, 17, 18], + ATTR_CONDITION_SUNNY: [1], ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], @@ -59,20 +67,28 @@ CONDITION_MAP = { for cond_code in cond_codes } -VISIBILITY_CLASSES = { - "VP": "Very Poor", - "PO": "Poor", - "MO": "Moderate", - "GO": "Good", - "VG": "Very Good", - "EX": "Excellent", +HOURLY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = { + ATTR_FORECAST_CONDITION: "significantWeatherCode", + ATTR_FORECAST_NATIVE_APPARENT_TEMP: "feelsLikeTemperature", + ATTR_FORECAST_NATIVE_PRESSURE: "mslp", + ATTR_FORECAST_NATIVE_TEMP: "screenTemperature", + ATTR_FORECAST_PRECIPITATION: "totalPrecipAmount", + ATTR_FORECAST_PRECIPITATION_PROBABILITY: "probOfPrecipitation", + ATTR_FORECAST_UV_INDEX: "uvIndex", + ATTR_FORECAST_WIND_BEARING: "windDirectionFrom10m", + ATTR_FORECAST_NATIVE_WIND_SPEED: "windSpeed10m", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "windGustSpeed10m", } -VISIBILITY_DISTANCE_CLASSES = { - "VP": "<1", - "PO": "1-4", - "MO": "4-10", - "GO": "10-20", - "VG": "20-40", - "EX": ">40", +DAILY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = { + ATTR_FORECAST_CONDITION: "daySignificantWeatherCode", + ATTR_FORECAST_NATIVE_APPARENT_TEMP: "dayMaxFeelsLikeTemp", + ATTR_FORECAST_NATIVE_PRESSURE: "middayMslp", + ATTR_FORECAST_NATIVE_TEMP: "dayMaxScreenTemperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "nightMinScreenTemperature", + ATTR_FORECAST_PRECIPITATION_PROBABILITY: "dayProbabilityOfPrecipitation", + ATTR_FORECAST_UV_INDEX: "maxUvIndex", + ATTR_FORECAST_WIND_BEARING: "midday10MWindDirection", + ATTR_FORECAST_NATIVE_WIND_SPEED: "midday10MWindSpeed", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust", } diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py deleted file mode 100644 index 651e56c3adc..00000000000 --- a/homeassistant/components/metoffice/data.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Common Met Office Data class used by both sensor and entity.""" - -from __future__ import annotations - -from dataclasses import dataclass - -from datapoint.Forecast import Forecast -from datapoint.Site import Site -from datapoint.Timestep import Timestep - - -@dataclass -class MetOfficeData: - """Data structure for MetOffice weather and forecast.""" - - now: Forecast - forecast: list[Timestep] - site: Site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 56d4d8f971b..e6bb8a34020 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -3,51 +3,40 @@ from __future__ import annotations import logging +from typing import Any, Literal import datapoint -from datapoint.Site import Site +from datapoint.Forecast import Forecast +from requests import HTTPError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import UpdateFailed -from homeassistant.util.dt import utcnow - -from .const import MODE_3HOURLY -from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) -def fetch_site( - connection: datapoint.Manager, latitude: float, longitude: float -) -> Site | None: - """Fetch site information from Datapoint API.""" - try: - return connection.get_nearest_forecast_site( - latitude=latitude, longitude=longitude - ) - except datapoint.exceptions.APIException as err: - _LOGGER.error("Received error from Met Office Datapoint: %s", err) - return None - - -def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: +def fetch_data( + connection: datapoint.Manager, + latitude: float, + longitude: float, + frequency: Literal["daily", "twice-daily", "hourly"], +) -> Forecast: """Fetch weather and forecast from Datapoint API.""" try: - forecast = connection.get_forecast_for_site(site.location_id, mode) + return connection.get_forecast( + latitude, longitude, frequency, convert_weather_code=False + ) except (ValueError, datapoint.exceptions.APIException) as err: _LOGGER.error("Check Met Office connection: %s", err.args) raise UpdateFailed from err + except HTTPError as err: + if err.response.status_code == 401: + raise ConfigEntryAuthFailed from err + raise - time_now = utcnow() - return MetOfficeData( - now=forecast.now(), - forecast=[ - timestep - for day in forecast.days - for timestep in day.timesteps - if timestep.date > time_now - and ( - mode == MODE_3HOURLY or timestep.date.hour > 6 - ) # ensures only one result per day in MODE_DAILY - ], - site=site, - ) + +def get_attribute(data: dict[str, Any] | None, attr_name: str) -> Any | None: + """Get an attribute from weather data.""" + if data: + return data.get(attr_name, {}).get("value") + return None diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index 17643d7e061..730c75223fd 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/metoffice", "iot_class": "cloud_polling", "loggers": ["datapoint"], - "requirements": ["datapoint==0.9.9"] + "requirements": ["datapoint==0.12.1"] } diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 5a256144d11..77118ec382e 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -2,11 +2,13 @@ from __future__ import annotations +from dataclasses import dataclass from typing import Any -from datapoint.Element import Element +from datapoint.Forecast import Forecast from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -20,6 +22,7 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -33,105 +36,110 @@ from .const import ( CONDITION_MAP, DOMAIN, METOFFICE_COORDINATES, - METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_DAILY, - VISIBILITY_CLASSES, - VISIBILITY_DISTANCE_CLASSES, ) -from .data import MetOfficeData +from .helpers import get_attribute ATTR_LAST_UPDATE = "last_update" -ATTR_SENSOR_ID = "sensor_id" -ATTR_SITE_ID = "site_id" -ATTR_SITE_NAME = "site_name" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +@dataclass(frozen=True, kw_only=True) +class MetOfficeSensorEntityDescription(SensorEntityDescription): + """Entity description class for MetOffice sensors.""" + + native_attr_name: str + + +SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( + MetOfficeSensorEntityDescription( key="name", + native_attr_name="name", name="Station name", icon="mdi:label-outline", entity_registry_enabled_default=False, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="weather", + native_attr_name="significantWeatherCode", name="Weather", icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="temperature", + native_attr_name="screenTemperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, icon=None, entity_registry_enabled_default=True, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="feels_like_temperature", + native_attr_name="feelsLikeTemperature", name="Feels like temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, icon=None, entity_registry_enabled_default=False, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="wind_speed", + native_attr_name="windSpeed10m", name="Wind speed", - native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, # Hint mph because that's the preferred unit for wind speeds in UK # This can be removed if we add a mixed metric/imperial unit system for UK users suggested_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=True, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="wind_direction", + native_attr_name="windDirectionFrom10m", name="Wind direction", icon="mdi:compass-outline", entity_registry_enabled_default=False, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="wind_gust", + native_attr_name="windGustSpeed10m", name="Wind gust", - native_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, # Hint mph because that's the preferred unit for wind speeds in UK # This can be removed if we add a mixed metric/imperial unit system for UK users suggested_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, entity_registry_enabled_default=False, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="visibility", - name="Visibility", - icon="mdi:eye", - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="visibility_distance", + native_attr_name="visibility", name="Visibility distance", - native_unit_of_measurement=UnitOfLength.KILOMETERS, + native_unit_of_measurement=UnitOfLength.METERS, icon="mdi:eye", entity_registry_enabled_default=False, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="uv", + native_attr_name="uvIndex", name="UV index", native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", entity_registry_enabled_default=True, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="precipitation", + native_attr_name="probOfPrecipitation", name="Probability of precipitation", native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", entity_registry_enabled_default=True, ), - SensorEntityDescription( + MetOfficeSensorEntityDescription( key="humidity", + native_attr_name="screenRelativeHumidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, @@ -147,23 +155,37 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Met Office weather sensor platform.""" + entity_registry = er.async_get(hass) hass_data = hass.data[DOMAIN][entry.entry_id] + # Remove daily entities from legacy config entries + for description in SENSOR_TYPES: + if entity_id := entity_registry.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"{description.key}_{hass_data[METOFFICE_COORDINATES]}_daily", + ): + entity_registry.async_remove(entity_id) + + # Remove old visibility sensors + if entity_id := entity_registry.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"visibility_distance_{hass_data[METOFFICE_COORDINATES]}_daily", + ): + entity_registry.async_remove(entity_id) + if entity_id := entity_registry.async_get_entity_id( + SENSOR_DOMAIN, + DOMAIN, + f"visibility_distance_{hass_data[METOFFICE_COORDINATES]}", + ): + entity_registry.async_remove(entity_id) + async_add_entities( [ MetOfficeCurrentSensor( hass_data[METOFFICE_HOURLY_COORDINATOR], hass_data, - True, - description, - ) - for description in SENSOR_TYPES - ] - + [ - MetOfficeCurrentSensor( - hass_data[METOFFICE_DAILY_COORDINATOR], - hass_data, - False, description, ) for description in SENSOR_TYPES @@ -173,64 +195,43 @@ async def async_setup_entry( class MetOfficeCurrentSensor( - CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], SensorEntity + CoordinatorEntity[DataUpdateCoordinator[Forecast]], SensorEntity ): """Implementation of a Met Office current weather condition sensor.""" _attr_attribution = ATTRIBUTION _attr_has_entity_name = True + entity_description: MetOfficeSensorEntityDescription + def __init__( self, - coordinator: DataUpdateCoordinator[MetOfficeData], + coordinator: DataUpdateCoordinator[Forecast], hass_data: dict[str, Any], - use_3hourly: bool, - description: SensorEntityDescription, + description: MetOfficeSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - mode_label = "3-hourly" if use_3hourly else "daily" self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = f"{description.name} {mode_label}" self._attr_unique_id = f"{description.key}_{hass_data[METOFFICE_COORDINATES]}" - if not use_3hourly: - self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - self._attr_entity_registry_enabled_default = ( - self.entity_description.entity_registry_enabled_default and use_3hourly - ) @property def native_value(self) -> StateType: """Return the state of the sensor.""" - value = None + value = get_attribute( + self.coordinator.data.now(), self.entity_description.native_attr_name + ) - if self.entity_description.key == "visibility_distance" and hasattr( - self.coordinator.data.now, "visibility" + if ( + self.entity_description.native_attr_name == "significantWeatherCode" + and value ): - value = VISIBILITY_DISTANCE_CLASSES.get( - self.coordinator.data.now.visibility.value - ) - - if self.entity_description.key == "visibility" and hasattr( - self.coordinator.data.now, "visibility" - ): - value = VISIBILITY_CLASSES.get(self.coordinator.data.now.visibility.value) - - elif self.entity_description.key == "weather" and hasattr( - self.coordinator.data.now, self.entity_description.key - ): - value = CONDITION_MAP.get(self.coordinator.data.now.weather.value) - - elif hasattr(self.coordinator.data.now, self.entity_description.key): - value = getattr(self.coordinator.data.now, self.entity_description.key) - - if isinstance(value, Element): - value = value.value + value = CONDITION_MAP.get(value) return value @@ -238,7 +239,7 @@ class MetOfficeCurrentSensor( def icon(self) -> str | None: """Return the icon for the entity card.""" value = self.entity_description.icon - if self.entity_description.key == "weather": + if self.entity_description.native_attr_name == "significantWeatherCode": value = self.state if value is None: value = "sunny" @@ -252,8 +253,5 @@ class MetOfficeCurrentSensor( def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return { - ATTR_LAST_UPDATE: self.coordinator.data.now.date, - ATTR_SENSOR_ID: self.entity_description.key, - ATTR_SITE_ID: self.coordinator.data.site.location_id, - ATTR_SITE_NAME: self.coordinator.data.site.name, + ATTR_LAST_UPDATE: self.coordinator.data.now()["time"], } diff --git a/homeassistant/components/metoffice/strings.json b/homeassistant/components/metoffice/strings.json index 5a1c59bcfb7..b33cf9e3efc 100644 --- a/homeassistant/components/metoffice/strings.json +++ b/homeassistant/components/metoffice/strings.json @@ -2,21 +2,29 @@ "config": { "step": { "user": { - "description": "The latitude and longitude will be used to find the closest weather station.", "title": "Connect to the UK Met Office", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]" } + }, + "reauth_confirm": { + "title": "Reauthenticate with DataHub API", + "description": "Please re-enter you DataHub API key. If you are still using an old Datapoint API key, you need to sign up for DataHub API now, see [documentation]({docs_url}) for details.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index d3f1320c47e..c7ce0db6c50 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -2,15 +2,22 @@ from __future__ import annotations +from datetime import datetime from typing import Any, cast -from datapoint.Timestep import Timestep +from datapoint.Forecast import Forecast as ForecastData from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_APPARENT_TEMP, + ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_UV_INDEX, ATTR_FORECAST_WIND_BEARING, DOMAIN as WEATHER_DOMAIN, CoordinatorWeatherEntity, @@ -18,7 +25,12 @@ from homeassistant.components.weather import ( WeatherEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature +from homeassistant.const import ( + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -28,14 +40,15 @@ from . import get_device_info from .const import ( ATTRIBUTION, CONDITION_MAP, + DAILY_FORECAST_ATTRIBUTE_MAP, DOMAIN, + HOURLY_FORECAST_ATTRIBUTE_MAP, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_DAILY, ) -from .data import MetOfficeData +from .helpers import get_attribute async def async_setup_entry( @@ -47,11 +60,11 @@ async def async_setup_entry( entity_registry = er.async_get(hass) hass_data = hass.data[DOMAIN][entry.entry_id] - # Remove hourly entity from legacy config entries + # Remove daily entity from legacy config entries if entity_id := entity_registry.async_get_entity_id( WEATHER_DOMAIN, DOMAIN, - _calculate_unique_id(hass_data[METOFFICE_COORDINATES], True), + f"{hass_data[METOFFICE_COORDINATES]}_daily", ): entity_registry.async_remove(entity_id) @@ -67,54 +80,89 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep: Timestep) -> Forecast: - data = Forecast(datetime=timestep.date.isoformat()) - if timestep.weather: - data[ATTR_FORECAST_CONDITION] = CONDITION_MAP.get(timestep.weather.value) - if timestep.precipitation: - data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value - if timestep.temperature: - data[ATTR_FORECAST_NATIVE_TEMP] = timestep.temperature.value - if timestep.wind_direction: - data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value - if timestep.wind_speed: - data[ATTR_FORECAST_NATIVE_WIND_SPEED] = timestep.wind_speed.value +def _build_hourly_forecast_data(timestep: dict[str, Any]) -> Forecast: + data = Forecast(datetime=timestep["time"].isoformat()) + _populate_forecast_data(data, timestep, HOURLY_FORECAST_ATTRIBUTE_MAP) return data -def _calculate_unique_id(coordinates: str, use_3hourly: bool) -> str: - """Calculate unique ID.""" - if use_3hourly: - return coordinates - return f"{coordinates}_{MODE_DAILY}" +def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast: + data = Forecast(datetime=timestep["time"].isoformat()) + _populate_forecast_data(data, timestep, DAILY_FORECAST_ATTRIBUTE_MAP) + return data + + +def _populate_forecast_data( + forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str] +) -> None: + def get_mapped_attribute(attr: str) -> Any: + if attr not in mapping: + return None + return get_attribute(timestep, mapping[attr]) + + weather_code = get_mapped_attribute(ATTR_FORECAST_CONDITION) + if weather_code is not None: + forecast[ATTR_FORECAST_CONDITION] = CONDITION_MAP.get(weather_code) + forecast[ATTR_FORECAST_NATIVE_APPARENT_TEMP] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_APPARENT_TEMP + ) + forecast[ATTR_FORECAST_NATIVE_PRESSURE] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_PRESSURE + ) + forecast[ATTR_FORECAST_NATIVE_TEMP] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_TEMP + ) + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_TEMP_LOW + ) + forecast[ATTR_FORECAST_PRECIPITATION] = get_mapped_attribute( + ATTR_FORECAST_PRECIPITATION + ) + forecast[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = get_mapped_attribute( + ATTR_FORECAST_PRECIPITATION_PROBABILITY + ) + forecast[ATTR_FORECAST_UV_INDEX] = get_mapped_attribute(ATTR_FORECAST_UV_INDEX) + forecast[ATTR_FORECAST_WIND_BEARING] = get_mapped_attribute( + ATTR_FORECAST_WIND_BEARING + ) + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_WIND_SPEED + ) + forecast[ATTR_FORECAST_NATIVE_WIND_GUST_SPEED] = get_mapped_attribute( + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED + ) class MetOfficeWeather( CoordinatorWeatherEntity[ - TimestampDataUpdateCoordinator[MetOfficeData], - TimestampDataUpdateCoordinator[MetOfficeData], + TimestampDataUpdateCoordinator[ForecastData], + TimestampDataUpdateCoordinator[ForecastData], + TimestampDataUpdateCoordinator[ForecastData], ] ): """Implementation of a Met Office weather condition.""" _attr_attribution = ATTRIBUTION _attr_has_entity_name = True + _attr_name = None _attr_native_temperature_unit = UnitOfTemperature.CELSIUS - _attr_native_pressure_unit = UnitOfPressure.HPA - _attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR + _attr_native_pressure_unit = UnitOfPressure.PA + _attr_native_precipitation_unit = UnitOfLength.MILLIMETERS + _attr_native_visibility_unit = UnitOfLength.METERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_supported_features = ( WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY ) def __init__( self, - coordinator_daily: TimestampDataUpdateCoordinator[MetOfficeData], - coordinator_hourly: TimestampDataUpdateCoordinator[MetOfficeData], + coordinator_daily: TimestampDataUpdateCoordinator[ForecastData], + coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData], hass_data: dict[str, Any], ) -> None: """Initialise the platform with a data instance.""" - observation_coordinator = coordinator_daily + observation_coordinator = coordinator_hourly super().__init__( observation_coordinator, daily_coordinator=coordinator_daily, @@ -124,81 +172,99 @@ class MetOfficeWeather( self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = "Daily" - self._attr_unique_id = _calculate_unique_id( - hass_data[METOFFICE_COORDINATES], False - ) + self._attr_unique_id = hass_data[METOFFICE_COORDINATES] @property def condition(self) -> str | None: """Return the current condition.""" - if self.coordinator.data.now: - return CONDITION_MAP.get(self.coordinator.data.now.weather.value) + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "significantWeatherCode") + + if value: + return CONDITION_MAP.get(value) return None @property def native_temperature(self) -> float | None: """Return the platform temperature.""" - weather_now = self.coordinator.data.now - if weather_now.temperature: - value = weather_now.temperature.value - return float(value) if value is not None else None - return None + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "screenTemperature") + return float(value) if value is not None else None + + @property + def native_dew_point(self) -> float | None: + """Return the dew point.""" + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "screenDewPointTemperature") + return float(value) if value is not None else None @property def native_pressure(self) -> float | None: """Return the mean sea-level pressure.""" - weather_now = self.coordinator.data.now - if weather_now and weather_now.pressure: - value = weather_now.pressure.value - return float(value) if value is not None else None - return None + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "mslp") + return float(value) if value is not None else None @property def humidity(self) -> float | None: """Return the relative humidity.""" - weather_now = self.coordinator.data.now - if weather_now and weather_now.humidity: - value = weather_now.humidity.value - return float(value) if value is not None else None - return None + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "screenRelativeHumidity") + return float(value) if value is not None else None + + @property + def uv_index(self) -> float | None: + """Return the UV index.""" + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "uvIndex") + return float(value) if value is not None else None + + @property + def native_visibility(self) -> float | None: + """Return the visibility.""" + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "visibility") + return float(value) if value is not None else None @property def native_wind_speed(self) -> float | None: """Return the wind speed.""" - weather_now = self.coordinator.data.now - if weather_now and weather_now.wind_speed: - value = weather_now.wind_speed.value - return float(value) if value is not None else None - return None + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "windSpeed10m") + return float(value) if value is not None else None @property - def wind_bearing(self) -> str | None: + def wind_bearing(self) -> float | None: """Return the wind bearing.""" - weather_now = self.coordinator.data.now - if weather_now and weather_now.wind_direction: - value = weather_now.wind_direction.value - return str(value) if value is not None else None - return None + weather_now = self.coordinator.data.now() + value = get_attribute(weather_now, "windDirectionFrom10m") + return float(value) if value is not None else None @callback def _async_forecast_daily(self) -> list[Forecast] | None: - """Return the twice daily forecast in native units.""" + """Return the daily forecast in native units.""" coordinator = cast( - TimestampDataUpdateCoordinator[MetOfficeData], + TimestampDataUpdateCoordinator[ForecastData], self.forecast_coordinators["daily"], ) + timesteps = coordinator.data.timesteps return [ - _build_forecast_data(timestep) for timestep in coordinator.data.forecast + _build_daily_forecast_data(timestep) + for timestep in timesteps + if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo) ] @callback def _async_forecast_hourly(self) -> list[Forecast] | None: """Return the hourly forecast in native units.""" coordinator = cast( - TimestampDataUpdateCoordinator[MetOfficeData], + TimestampDataUpdateCoordinator[ForecastData], self.forecast_coordinators["hourly"], ) + + timesteps = coordinator.data.timesteps return [ - _build_forecast_data(timestep) for timestep in coordinator.data.forecast + _build_hourly_forecast_data(timestep) + for timestep in timesteps + if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo) ] diff --git a/requirements_all.txt b/requirements_all.txt index a93cddb559f..abb5fe26fbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ crownstone-uart==2.1.0 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.9.9 +datapoint==0.12.1 # homeassistant.components.bluetooth dbus-fast==2.43.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5450daf5f8a..12ea7fe76c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -644,7 +644,7 @@ crownstone-uart==2.1.0 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.9.9 +datapoint==0.12.1 # homeassistant.components.bluetooth dbus-fast==2.43.0 diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index 83c7e7853f7..dc64cc8dfb1 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -9,10 +9,9 @@ import pytest @pytest.fixture def mock_simple_manager_fail(): """Mock datapoint Manager with default values for testing in config_flow.""" - with patch("datapoint.Manager") as mock_manager: + with patch("datapoint.Manager.Manager") as mock_manager: instance = mock_manager.return_value - instance.get_nearest_forecast_site.side_effect = APIException() - instance.get_forecast_for_site.side_effect = APIException() + instance.get_forecast = APIException() instance.latitude = None instance.longitude = None instance.site = None diff --git a/tests/components/metoffice/const.py b/tests/components/metoffice/const.py index 8fe1b42ca59..2485b308981 100644 --- a/tests/components/metoffice/const.py +++ b/tests/components/metoffice/const.py @@ -3,7 +3,7 @@ from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -TEST_DATETIME_STRING = "2020-04-25T12:00:00+00:00" +TEST_DATETIME_STRING = "2024-11-23T12:00:00+00:00" TEST_API_KEY = "test-metoffice-api-key" @@ -34,31 +34,21 @@ METOFFICE_CONFIG_KINGSLYNN = { } KINGSLYNN_SENSOR_RESULTS = { - "weather": ("weather", "sunny"), - "visibility": ("visibility", "Very Good"), - "visibility_distance": ("visibility_distance", "20-40"), - "temperature": ("temperature", "14"), - "feels_like_temperature": ("feels_like_temperature", "13"), - "uv": ("uv_index", "6"), - "precipitation": ("probability_of_precipitation", "0"), - "wind_direction": ("wind_direction", "E"), - "wind_gust": ("wind_gust", "7"), - "wind_speed": ("wind_speed", "2"), - "humidity": ("humidity", "60"), + "weather": "rainy", + "temperature": "7.87", + "uv_index": "1", + "probability_of_precipitation": "67", + "pressure": "998.20", + "wind_speed": "22.21", } WAVERTREE_SENSOR_RESULTS = { - "weather": ("weather", "sunny"), - "visibility": ("visibility", "Good"), - "visibility_distance": ("visibility_distance", "10-20"), - "temperature": ("temperature", "17"), - "feels_like_temperature": ("feels_like_temperature", "14"), - "uv": ("uv_index", "5"), - "precipitation": ("probability_of_precipitation", "0"), - "wind_direction": ("wind_direction", "SSE"), - "wind_gust": ("wind_gust", "16"), - "wind_speed": ("wind_speed", "9"), - "humidity": ("humidity", "50"), + "weather": "rainy", + "temperature": "9.28", + "uv_index": "1", + "probability_of_precipitation": "61", + "pressure": "987.50", + "wind_speed": "17.60", } DEVICE_KEY_KINGSLYNN = {(DOMAIN, TEST_COORDINATES_KINGSLYNN)} diff --git a/tests/components/metoffice/fixtures/metoffice.json b/tests/components/metoffice/fixtures/metoffice.json index 68ba02b5429..70ed76e779c 100644 --- a/tests/components/metoffice/fixtures/metoffice.json +++ b/tests/components/metoffice/fixtures/metoffice.json @@ -23,1731 +23,4134 @@ ] } }, - "wavertree_hourly": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "F", - "units": "C", - "$": "Feels Like Temperature" - }, - { - "name": "G", - "units": "mph", - "$": "Wind Gust" - }, - { - "name": "H", - "units": "%", - "$": "Screen Relative Humidity" - }, - { - "name": "T", - "units": "C", - "$": "Temperature" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "Pp", - "units": "%", - "$": "Precipitation Probability" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "354107", - "lat": "53.3986", - "lon": "-2.9256", - "name": "WAVERTREE", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "47.0", - "Period": [ - { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SE", - "F": "7", - "G": "25", - "H": "63", - "Pp": "0", - "S": "9", - "T": "9", - "V": "VG", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "4", - "G": "22", - "H": "76", - "Pp": "0", - "S": "11", - "T": "7", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "SSE", - "F": "8", - "G": "18", - "H": "70", - "Pp": "0", - "S": "9", - "T": "10", - "V": "MO", - "W": "1", - "U": "3", - "$": "540" - }, - { - "D": "SSE", - "F": "14", - "G": "16", - "H": "50", - "Pp": "0", - "S": "9", - "T": "17", - "V": "GO", - "W": "1", - "U": "5", - "$": "720" - }, - { - "D": "S", - "F": "17", - "G": "9", - "H": "43", - "Pp": "1", - "S": "4", - "T": "19", - "V": "GO", - "W": "1", - "U": "2", - "$": "900" - }, - { - "D": "WNW", - "F": "15", - "G": "13", - "H": "55", - "Pp": "2", - "S": "7", - "T": "17", - "V": "GO", - "W": "3", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "14", - "G": "7", - "H": "64", - "Pp": "1", - "S": "2", - "T": "14", - "V": "GO", - "W": "2", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "WSW", - "F": "13", - "G": "4", - "H": "73", - "Pp": "1", - "S": "2", - "T": "13", - "V": "GO", - "W": "2", - "U": "0", - "$": "0" - }, - { - "D": "WNW", - "F": "12", - "G": "9", - "H": "77", - "Pp": "2", - "S": "4", - "T": "12", - "V": "GO", - "W": "2", - "U": "0", - "$": "180" - }, - - { - "D": "NW", - "F": "10", - "G": "9", - "H": "82", - "Pp": "5", - "S": "4", - "T": "11", - "V": "MO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "WNW", - "F": "11", - "G": "7", - "H": "79", - "Pp": "5", - "S": "4", - "T": "12", - "V": "MO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "WNW", - "F": "10", - "G": "18", - "H": "78", - "Pp": "6", - "S": "9", - "T": "12", - "V": "MO", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "NW", - "F": "10", - "G": "18", - "H": "71", - "Pp": "5", - "S": "9", - "T": "12", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "NW", - "F": "9", - "G": "16", - "H": "68", - "Pp": "9", - "S": "9", - "T": "11", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "8", - "G": "11", - "H": "68", - "Pp": "9", - "S": "7", - "T": "10", - "V": "VG", - "W": "8", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "WNW", - "F": "8", - "G": "9", - "H": "72", - "Pp": "11", - "S": "4", - "T": "9", - "V": "VG", - "W": "8", - "U": "0", - "$": "0" - }, - { - "D": "WNW", - "F": "7", - "G": "11", - "H": "77", - "Pp": "12", - "S": "7", - "T": "8", - "V": "VG", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "NW", - "F": "7", - "G": "9", - "H": "80", - "Pp": "14", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "NW", - "F": "7", - "G": "18", - "H": "73", - "Pp": "6", - "S": "9", - "T": "9", - "V": "VG", - "W": "3", - "U": "2", - "$": "540" - }, - { - "D": "NW", - "F": "8", - "G": "20", - "H": "59", - "Pp": "4", - "S": "9", - "T": "10", - "V": "VG", - "W": "3", - "U": "3", - "$": "720" - }, - { - "D": "NW", - "F": "8", - "G": "20", - "H": "58", - "Pp": "1", - "S": "9", - "T": "10", - "V": "VG", - "W": "1", - "U": "2", - "$": "900" - }, - { - "D": "NW", - "F": "8", - "G": "16", - "H": "57", - "Pp": "1", - "S": "7", - "T": "10", - "V": "VG", - "W": "1", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "8", - "G": "11", - "H": "67", - "Pp": "1", - "S": "4", - "T": "9", - "V": "VG", - "W": "0", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "NNW", - "F": "7", - "G": "7", - "H": "80", - "Pp": "2", - "S": "4", - "T": "8", - "V": "VG", - "W": "0", - "U": "0", - "$": "0" - }, - { - "D": "W", - "F": "6", - "G": "7", - "H": "86", - "Pp": "3", - "S": "4", - "T": "7", - "V": "GO", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "S", - "F": "5", - "G": "9", - "H": "86", - "Pp": "5", - "S": "4", - "T": "6", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "ENE", - "F": "7", - "G": "13", - "H": "72", - "Pp": "6", - "S": "7", - "T": "9", - "V": "GO", - "W": "3", - "U": "3", - "$": "540" - }, - { - "D": "ENE", - "F": "10", - "G": "16", - "H": "57", - "Pp": "10", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "N", - "F": "11", - "G": "16", - "H": "58", - "Pp": "10", - "S": "7", - "T": "12", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "N", - "F": "10", - "G": "16", - "H": "63", - "Pp": "10", - "S": "7", - "T": "11", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "NNE", - "F": "9", - "G": "11", - "H": "72", - "Pp": "9", - "S": "4", - "T": "10", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "E", - "F": "8", - "G": "9", - "H": "79", - "Pp": "6", - "S": "4", - "T": "9", - "V": "VG", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "SSE", - "F": "7", - "G": "11", - "H": "81", - "Pp": "3", - "S": "7", - "T": "8", - "V": "GO", - "W": "2", - "U": "0", - "$": "180" - }, - { - "D": "SE", - "F": "5", - "G": "16", - "H": "86", - "Pp": "9", - "S": "9", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "SE", - "F": "8", - "G": "22", - "H": "74", - "Pp": "12", - "S": "11", - "T": "10", - "V": "GO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "SE", - "F": "10", - "G": "27", - "H": "72", - "Pp": "47", - "S": "13", - "T": "12", - "V": "GO", - "W": "12", - "U": "3", - "$": "720" - }, - { - "D": "SSE", - "F": "10", - "G": "29", - "H": "73", - "Pp": "59", - "S": "13", - "T": "13", - "V": "GO", - "W": "14", - "U": "2", - "$": "900" - }, - { - "D": "SSE", - "F": "10", - "G": "20", - "H": "69", - "Pp": "39", - "S": "11", - "T": "12", - "V": "VG", - "W": "10", - "U": "1", - "$": "1080" - }, - { - "D": "SSE", - "F": "9", - "G": "22", - "H": "79", - "Pp": "19", - "S": "13", - "T": "11", - "V": "GO", - "W": "7", - "U": "0", - "$": "1260" - } - ] - } - ] - } - } - } - }, "wavertree_daily": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "FDm", - "units": "C", - "$": "Feels Like Day Maximum Temperature" + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-2.9256, 53.3986, 47] + }, + "properties": { + "location": { + "name": "Wavertree" }, - { - "name": "FNm", - "units": "C", - "$": "Feels Like Night Minimum Temperature" - }, - { - "name": "Dm", - "units": "C", - "$": "Day Maximum Temperature" - }, - { - "name": "Nm", - "units": "C", - "$": "Night Minimum Temperature" - }, - { - "name": "Gn", - "units": "mph", - "$": "Wind Gust Noon" - }, - { - "name": "Gm", - "units": "mph", - "$": "Wind Gust Midnight" - }, - { - "name": "Hn", - "units": "%", - "$": "Screen Relative Humidity Noon" - }, - { - "name": "Hm", - "units": "%", - "$": "Screen Relative Humidity Midnight" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "PPd", - "units": "%", - "$": "Precipitation Probability Day" - }, - { - "name": "PPn", - "units": "%", - "$": "Precipitation Probability Night" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "354107", - "lat": "53.3986", - "lon": "-2.9256", - "name": "WAVERTREE", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "47.0", - "Period": [ + "requestPointDistance": 1975.3601, + "modelRunDate": "2024-11-23T12:00Z", + "timeSeries": [ { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SSE", - "Gn": "16", - "Hn": "50", - "PPd": "2", - "S": "9", - "V": "GO", - "Dm": "19", - "FDm": "18", - "W": "1", - "U": "5", - "$": "Day" - }, - { - "D": "WSW", - "Gm": "4", - "Hm": "73", - "PPn": "2", - "S": "2", - "V": "GO", - "Nm": "11", - "FNm": "11", - "W": "2", - "$": "Night" - } - ] + "time": "2024-11-22T00:00Z", + "midday10MWindSpeed": 6.38, + "midnight10MWindSpeed": 2.78, + "midday10MWindDirection": 261, + "midnight10MWindDirection": 155, + "midday10MWindGust": 9.77, + "midnight10MWindGust": 8.75, + "middayVisibility": 29980, + "midnightVisibility": 18024, + "middayRelativeHumidity": 73.47, + "midnightRelativeHumidity": 86.1, + "middayMslp": 100790, + "midnightMslp": 101020, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 7.17, + "nightMinScreenTemperature": 2, + "dayUpperBoundMaxTemp": 7.78, + "nightUpperBoundMinTemp": 3.84, + "dayLowerBoundMaxTemp": 4.64, + "nightLowerBoundMinTemp": 1.18, + "nightMinFeelsLikeTemp": -3.07, + "dayUpperBoundMaxFeelsLikeTemp": 4.39, + "nightUpperBoundMinFeelsLikeTemp": -1.33, + "dayLowerBoundMaxFeelsLikeTemp": 2.49, + "nightLowerBoundMinFeelsLikeTemp": -4.04, + "nightProbabilityOfPrecipitation": 95, + "nightProbabilityOfSnow": 5, + "nightProbabilityOfHeavySnow": 0, + "nightProbabilityOfRain": 93, + "nightProbabilityOfHeavyRain": 90, + "nightProbabilityOfHail": 20, + "nightProbabilityOfSferics": 9 }, { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "WNW", - "Gn": "18", - "Hn": "78", - "PPd": "9", - "S": "9", - "V": "MO", - "Dm": "13", - "FDm": "11", - "W": "7", - "U": "4", - "$": "Day" - }, - { - "D": "WNW", - "Gm": "9", - "Hm": "72", - "PPn": "12", - "S": "4", - "V": "VG", - "Nm": "8", - "FNm": "7", - "W": "8", - "$": "Night" - } - ] + "time": "2024-11-23T00:00Z", + "midday10MWindSpeed": 7.87, + "midnight10MWindSpeed": 7.44, + "midday10MWindDirection": 176, + "midnight10MWindDirection": 171, + "midday10MWindGust": 15.43, + "midnight10MWindGust": 14.08, + "middayVisibility": 5106, + "midnightVisibility": 39734, + "middayRelativeHumidity": 95.13, + "midnightRelativeHumidity": 86.99, + "middayMslp": 98750, + "midnightMslp": 98490, + "maxUvIndex": 1, + "daySignificantWeatherCode": 12, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 12.56, + "nightMinScreenTemperature": 11.46, + "dayUpperBoundMaxTemp": 14.48, + "nightUpperBoundMinTemp": 13.92, + "dayLowerBoundMaxTemp": 11.63, + "nightLowerBoundMinTemp": 10.7, + "dayMaxFeelsLikeTemp": 9.81, + "nightMinFeelsLikeTemp": 9.53, + "dayUpperBoundMaxFeelsLikeTemp": 12.68, + "nightUpperBoundMinFeelsLikeTemp": 11.39, + "dayLowerBoundMaxFeelsLikeTemp": 9.81, + "nightLowerBoundMinFeelsLikeTemp": 9.53, + "dayProbabilityOfPrecipitation": 65, + "nightProbabilityOfPrecipitation": 74, + "dayProbabilityOfSnow": 3, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 65, + "nightProbabilityOfRain": 74, + "dayProbabilityOfHeavyRain": 41, + "nightProbabilityOfHeavyRain": 73, + "dayProbabilityOfHail": 3, + "nightProbabilityOfHail": 15, + "dayProbabilityOfSferics": 2, + "nightProbabilityOfSferics": 12 }, { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "NW", - "Gn": "20", - "Hn": "59", - "PPd": "14", - "S": "9", - "V": "VG", - "Dm": "11", - "FDm": "8", - "W": "3", - "U": "3", - "$": "Day" - }, - { - "D": "NNW", - "Gm": "7", - "Hm": "80", - "PPn": "3", - "S": "4", - "V": "VG", - "Nm": "6", - "FNm": "5", - "W": "0", - "$": "Night" - } - ] + "time": "2024-11-24T00:00Z", + "midday10MWindSpeed": 6.65, + "midnight10MWindSpeed": 7.33, + "midday10MWindDirection": 203, + "midnight10MWindDirection": 211, + "midday10MWindGust": 11.85, + "midnight10MWindGust": 13.11, + "middayVisibility": 36358, + "midnightVisibility": 51563, + "middayRelativeHumidity": 70.26, + "midnightRelativeHumidity": 72.97, + "middayMslp": 98748, + "midnightMslp": 98712, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 12.7, + "nightMinScreenTemperature": 8.21, + "dayUpperBoundMaxTemp": 15.19, + "nightUpperBoundMinTemp": 10.67, + "dayLowerBoundMaxTemp": 11.87, + "nightLowerBoundMinTemp": 7.03, + "dayMaxFeelsLikeTemp": 9.17, + "nightMinFeelsLikeTemp": 4.84, + "dayUpperBoundMaxFeelsLikeTemp": 12.63, + "nightUpperBoundMinFeelsLikeTemp": 7.25, + "dayLowerBoundMaxFeelsLikeTemp": 9.17, + "nightLowerBoundMinFeelsLikeTemp": 3.81, + "dayProbabilityOfPrecipitation": 26, + "nightProbabilityOfPrecipitation": 23, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 26, + "nightProbabilityOfRain": 23, + "dayProbabilityOfHeavyRain": 13, + "nightProbabilityOfHeavyRain": 16, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 3, + "dayProbabilityOfSferics": 3, + "nightProbabilityOfSferics": 2 }, { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "ENE", - "Gn": "16", - "Hn": "57", - "PPd": "10", - "S": "7", - "V": "GO", - "Dm": "12", - "FDm": "11", - "W": "7", - "U": "4", - "$": "Day" - }, - { - "D": "E", - "Gm": "9", - "Hm": "79", - "PPn": "9", - "S": "4", - "V": "VG", - "Nm": "7", - "FNm": "6", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-25T00:00Z", + "midday10MWindSpeed": 8.52, + "midnight10MWindSpeed": 8.12, + "midday10MWindDirection": 251, + "midnight10MWindDirection": 262, + "midday10MWindGust": 14.49, + "midnight10MWindGust": 13.33, + "middayVisibility": 32255, + "midnightVisibility": 36209, + "middayRelativeHumidity": 68.89, + "midnightRelativeHumidity": 72.82, + "middayMslp": 99488, + "midnightMslp": 100481, + "maxUvIndex": 1, + "daySignificantWeatherCode": 3, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 9.81, + "nightMinScreenTemperature": 7.71, + "dayUpperBoundMaxTemp": 10.98, + "nightUpperBoundMinTemp": 9.31, + "dayLowerBoundMaxTemp": 8.42, + "nightLowerBoundMinTemp": 4.42, + "dayMaxFeelsLikeTemp": 5.33, + "nightMinFeelsLikeTemp": 4.19, + "dayUpperBoundMaxFeelsLikeTemp": 7.12, + "nightUpperBoundMinFeelsLikeTemp": 5.29, + "dayLowerBoundMaxFeelsLikeTemp": 4.86, + "nightLowerBoundMinFeelsLikeTemp": 3.1, + "dayProbabilityOfPrecipitation": 5, + "nightProbabilityOfPrecipitation": 6, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 5, + "nightProbabilityOfRain": 6, + "dayProbabilityOfHeavyRain": 3, + "nightProbabilityOfHeavyRain": 5, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 1, + "nightProbabilityOfSferics": 1 }, { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "SE", - "Gn": "27", - "Hn": "72", - "PPd": "59", - "S": "13", - "V": "GO", - "Dm": "13", - "FDm": "10", - "W": "12", - "U": "3", - "$": "Day" - }, - { - "D": "SSE", - "Gm": "18", - "Hm": "85", - "PPn": "19", - "S": "11", - "V": "VG", - "Nm": "8", - "FNm": "6", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-26T00:00Z", + "midday10MWindSpeed": 5.68, + "midnight10MWindSpeed": 3.17, + "midday10MWindDirection": 265, + "midnight10MWindDirection": 74, + "midday10MWindGust": 9.58, + "midnight10MWindGust": 5.42, + "middayVisibility": 34027, + "midnightVisibility": 12383, + "middayRelativeHumidity": 70.41, + "midnightRelativeHumidity": 89.82, + "middayMslp": 101293, + "midnightMslp": 101390, + "maxUvIndex": 1, + "daySignificantWeatherCode": 3, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 8.72, + "nightMinScreenTemperature": 3.76, + "dayUpperBoundMaxTemp": 10.14, + "nightUpperBoundMinTemp": 7.47, + "dayLowerBoundMaxTemp": 6.46, + "nightLowerBoundMinTemp": -0.43, + "dayMaxFeelsLikeTemp": 5.9, + "nightMinFeelsLikeTemp": 1.31, + "dayUpperBoundMaxFeelsLikeTemp": 7.37, + "nightUpperBoundMinFeelsLikeTemp": 4.37, + "dayLowerBoundMaxFeelsLikeTemp": 3.99, + "nightLowerBoundMinFeelsLikeTemp": -3.09, + "dayProbabilityOfPrecipitation": 6, + "nightProbabilityOfPrecipitation": 44, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 1, + "dayProbabilityOfRain": 6, + "nightProbabilityOfRain": 44, + "dayProbabilityOfHeavyRain": 5, + "nightProbabilityOfHeavyRain": 24, + "dayProbabilityOfHail": 1, + "nightProbabilityOfHail": 2, + "dayProbabilityOfSferics": 1, + "nightProbabilityOfSferics": 1 + }, + { + "time": "2024-11-27T00:00Z", + "midday10MWindSpeed": 5.15, + "midnight10MWindSpeed": 3.29, + "midday10MWindDirection": 8, + "midnight10MWindDirection": 31, + "midday10MWindGust": 8.94, + "midnight10MWindGust": 5.54, + "middayVisibility": 25011, + "midnightVisibility": 31513, + "middayRelativeHumidity": 81.23, + "midnightRelativeHumidity": 86.67, + "middayMslp": 101439, + "midnightMslp": 102175, + "maxUvIndex": 1, + "daySignificantWeatherCode": 10, + "nightSignificantWeatherCode": 0, + "dayMaxScreenTemperature": 6.66, + "nightMinScreenTemperature": 2.36, + "dayUpperBoundMaxTemp": 11.14, + "nightUpperBoundMinTemp": 7.25, + "dayLowerBoundMaxTemp": 3.03, + "nightLowerBoundMinTemp": -3.02, + "dayMaxFeelsLikeTemp": 3.31, + "nightMinFeelsLikeTemp": 0.18, + "dayUpperBoundMaxFeelsLikeTemp": 9.03, + "nightUpperBoundMinFeelsLikeTemp": 3.85, + "dayLowerBoundMaxFeelsLikeTemp": 1.04, + "nightLowerBoundMinFeelsLikeTemp": -7.6, + "dayProbabilityOfPrecipitation": 43, + "nightProbabilityOfPrecipitation": 9, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 3, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 43, + "nightProbabilityOfRain": 8, + "dayProbabilityOfHeavyRain": 24, + "nightProbabilityOfHeavyRain": 7, + "dayProbabilityOfHail": 1, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 3, + "nightProbabilityOfSferics": 1 + }, + { + "time": "2024-11-28T00:00Z", + "midday10MWindSpeed": 3.51, + "midnight10MWindSpeed": 5.57, + "midday10MWindDirection": 104, + "midnight10MWindDirection": 131, + "midday10MWindGust": 6.21, + "midnight10MWindGust": 9.21, + "middayVisibility": 28173, + "midnightVisibility": 33839, + "middayRelativeHumidity": 85.35, + "midnightRelativeHumidity": 86.07, + "middayMslp": 102512, + "midnightMslp": 102382, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 5.73, + "nightMinScreenTemperature": 3.79, + "dayUpperBoundMaxTemp": 9.42, + "nightUpperBoundMinTemp": 8.18, + "dayLowerBoundMaxTemp": 1.26, + "nightLowerBoundMinTemp": -1.91, + "dayMaxFeelsLikeTemp": 2.95, + "nightMinFeelsLikeTemp": 1.63, + "dayUpperBoundMaxFeelsLikeTemp": 7.21, + "nightUpperBoundMinFeelsLikeTemp": 4.13, + "dayLowerBoundMaxFeelsLikeTemp": -0.81, + "nightLowerBoundMinFeelsLikeTemp": -5.94, + "dayProbabilityOfPrecipitation": 9, + "nightProbabilityOfPrecipitation": 9, + "dayProbabilityOfSnow": 2, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 9, + "nightProbabilityOfRain": 9, + "dayProbabilityOfHeavyRain": 3, + "nightProbabilityOfHeavyRain": 3, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 + }, + { + "time": "2024-11-29T00:00Z", + "midday10MWindSpeed": 6.39, + "midnight10MWindSpeed": 5.59, + "midday10MWindDirection": 137, + "midnight10MWindDirection": 151, + "midday10MWindGust": 10.72, + "midnight10MWindGust": 9.21, + "middayVisibility": 34870, + "midnightVisibility": 31318, + "middayRelativeHumidity": 83.78, + "midnightRelativeHumidity": 87.71, + "middayMslp": 101985, + "midnightMslp": 101688, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 8.21, + "nightMinScreenTemperature": 7.04, + "dayUpperBoundMaxTemp": 12.62, + "nightUpperBoundMinTemp": 10.76, + "dayLowerBoundMaxTemp": 4.15, + "nightLowerBoundMinTemp": -1.9, + "dayMaxFeelsLikeTemp": 4.88, + "nightMinFeelsLikeTemp": 4.95, + "dayUpperBoundMaxFeelsLikeTemp": 10.74, + "nightUpperBoundMinFeelsLikeTemp": 9.04, + "dayLowerBoundMaxFeelsLikeTemp": 0.63, + "nightLowerBoundMinFeelsLikeTemp": -6.49, + "dayProbabilityOfPrecipitation": 11, + "nightProbabilityOfPrecipitation": 13, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 11, + "nightProbabilityOfRain": 13, + "dayProbabilityOfHeavyRain": 4, + "nightProbabilityOfHeavyRain": 6, + "dayProbabilityOfHail": 1, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 1 } ] } } - } + ], + "parameters": [ + { + "daySignificantWeatherCode": { + "type": "Parameter", + "description": "Day Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "midnightRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midnight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midnight10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "nightUpperBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightUpperBoundMinTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnightVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midnight", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "dayUpperBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midday", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "nightLowerBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "middayMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midday", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midday10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "middayVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midday", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "midnight10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midnightMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midnight", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightSignificantWeatherCode": { + "type": "Parameter", + "description": "Night Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "dayProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayMaxScreenTemperature": { + "type": "Parameter", + "description": "Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinScreenTemperature": { + "type": "Parameter", + "description": "Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnight10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midnight", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "maxUvIndex": { + "type": "Parameter", + "description": "Day Maximum UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + }, + "dayProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayUpperBoundMaxTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "middayRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midday", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightLowerBoundMinTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + } + } + ] }, - "kingslynn_hourly": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "F", - "units": "C", - "$": "Feels Like Temperature" + "wavertree_hourly": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-2.9256, 53.3986, 47] + }, + "properties": { + "location": { + "name": "Wavertree" }, - { - "name": "G", - "units": "mph", - "$": "Wind Gust" - }, - { - "name": "H", - "units": "%", - "$": "Screen Relative Humidity" - }, - { - "name": "T", - "units": "C", - "$": "Temperature" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "Pp", - "units": "%", - "$": "Precipitation Probability" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "322380", - "lat": "52.7561", - "lon": "0.4019", - "name": "KING'S LYNN", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "5.0", - "Period": [ + "requestPointDistance": 1975.3601, + "modelRunDate": "2024-11-23T12:00Z", + "timeSeries": [ { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SSE", - "F": "4", - "G": "9", - "H": "88", - "Pp": "7", - "S": "9", - "T": "7", - "V": "GO", - "W": "8", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "5", - "G": "7", - "H": "86", - "Pp": "9", - "S": "4", - "T": "7", - "V": "GO", - "W": "8", - "U": "1", - "$": "360" - }, - { - "D": "ESE", - "F": "8", - "G": "4", - "H": "75", - "Pp": "9", - "S": "4", - "T": "9", - "V": "VG", - "W": "8", - "U": "3", - "$": "540" - }, - { - "D": "E", - "F": "13", - "G": "7", - "H": "60", - "Pp": "0", - "S": "2", - "T": "14", - "V": "VG", - "W": "1", - "U": "6", - "$": "720" - }, - { - "D": "NNW", - "F": "14", - "G": "9", - "H": "57", - "Pp": "0", - "S": "4", - "T": "15", - "V": "VG", - "W": "1", - "U": "3", - "$": "900" - }, - { - "D": "ENE", - "F": "14", - "G": "9", - "H": "58", - "Pp": "0", - "S": "4", - "T": "14", - "V": "VG", - "W": "1", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "8", - "G": "18", - "H": "76", - "Pp": "0", - "S": "9", - "T": "10", - "V": "VG", - "W": "0", - "U": "0", - "$": "1260" - } - ] + "time": "2024-11-23T12:00Z", + "screenTemperature": 9.28, + "maxScreenAirTemp": 9.28, + "minScreenAirTemp": 8.14, + "screenDewPointTemperature": 8.54, + "feelsLikeTemperature": 5.75, + "windSpeed10m": 7.87, + "windDirectionFrom10m": 176, + "windGustSpeed10m": 15.43, + "max10mWindGust": 19.04, + "visibility": 5106, + "screenRelativeHumidity": 95.13, + "mslp": 98750, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.53, + "totalPrecipAmount": 0.25, + "totalSnowAmount": 0, + "probOfPrecipitation": 61 }, { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "SSE", - "F": "5", - "G": "16", - "H": "84", - "Pp": "0", - "S": "7", - "T": "7", - "V": "VG", - "W": "0", - "U": "0", - "$": "0" - }, - { - "D": "S", - "F": "4", - "G": "16", - "H": "89", - "Pp": "0", - "S": "7", - "T": "6", - "V": "GO", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "S", - "F": "4", - "G": "16", - "H": "87", - "Pp": "0", - "S": "7", - "T": "7", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "SSW", - "F": "11", - "G": "13", - "H": "69", - "Pp": "0", - "S": "9", - "T": "13", - "V": "VG", - "W": "1", - "U": "4", - "$": "540" - }, - { - "D": "SW", - "F": "15", - "G": "18", - "H": "50", - "Pp": "8", - "S": "9", - "T": "17", - "V": "VG", - "W": "1", - "U": "5", - "$": "720" - }, - { - "D": "SW", - "F": "16", - "G": "16", - "H": "47", - "Pp": "8", - "S": "7", - "T": "18", - "V": "VG", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "SW", - "F": "15", - "G": "13", - "H": "56", - "Pp": "3", - "S": "7", - "T": "17", - "V": "VG", - "W": "3", - "U": "1", - "$": "1080" - }, - { - "D": "SW", - "F": "13", - "G": "11", - "H": "76", - "Pp": "4", - "S": "4", - "T": "13", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] + "time": "2024-11-23T13:00Z", + "screenTemperature": 9.93, + "maxScreenAirTemp": 9.93, + "minScreenAirTemp": 9.28, + "screenDewPointTemperature": 8.97, + "feelsLikeTemperature": 6.8, + "windSpeed10m": 7.06, + "windDirectionFrom10m": 178, + "windGustSpeed10m": 15.48, + "max10mWindGust": 18.1, + "visibility": 11368, + "screenRelativeHumidity": 93.78, + "mslp": 98683, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.82, + "totalPrecipAmount": 0.52, + "totalSnowAmount": 0, + "probOfPrecipitation": 65 }, { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "SSW", - "F": "10", - "G": "13", - "H": "75", - "Pp": "5", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "W", - "F": "9", - "G": "13", - "H": "84", - "Pp": "9", - "S": "7", - "T": "10", - "V": "GO", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "NW", - "F": "7", - "G": "16", - "H": "85", - "Pp": "50", - "S": "9", - "T": "9", - "V": "GO", - "W": "12", - "U": "1", - "$": "360" - }, - { - "D": "NW", - "F": "9", - "G": "11", - "H": "78", - "Pp": "36", - "S": "4", - "T": "10", - "V": "VG", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "WNW", - "F": "11", - "G": "11", - "H": "66", - "Pp": "9", - "S": "4", - "T": "12", - "V": "VG", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "W", - "F": "11", - "G": "13", - "H": "62", - "Pp": "9", - "S": "7", - "T": "13", - "V": "VG", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "E", - "F": "11", - "G": "11", - "H": "64", - "Pp": "10", - "S": "7", - "T": "12", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "9", - "G": "13", - "H": "78", - "Pp": "9", - "S": "7", - "T": "10", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] + "time": "2024-11-23T14:00Z", + "screenTemperature": 11.13, + "maxScreenAirTemp": 11.14, + "minScreenAirTemp": 9.93, + "screenDewPointTemperature": 9.99, + "feelsLikeTemperature": 8.41, + "windSpeed10m": 6.35, + "windDirectionFrom10m": 179, + "windGustSpeed10m": 13.61, + "max10mWindGust": 15.05, + "visibility": 18523, + "screenRelativeHumidity": 92.73, + "mslp": 98634, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 12 }, { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "SE", - "F": "7", - "G": "13", - "H": "85", - "Pp": "9", - "S": "7", - "T": "9", - "V": "VG", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "E", - "F": "7", - "G": "9", - "H": "91", - "Pp": "11", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "7", - "G": "9", - "H": "92", - "Pp": "12", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "ESE", - "F": "9", - "G": "13", - "H": "77", - "Pp": "14", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "ESE", - "F": "12", - "G": "16", - "H": "64", - "Pp": "14", - "S": "7", - "T": "13", - "V": "GO", - "W": "7", - "U": "3", - "$": "720" - }, - { - "D": "ESE", - "F": "12", - "G": "18", - "H": "66", - "Pp": "15", - "S": "9", - "T": "13", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "SSE", - "F": "11", - "G": "13", - "H": "73", - "Pp": "15", - "S": "7", - "T": "12", - "V": "GO", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "9", - "G": "13", - "H": "81", - "Pp": "13", - "S": "7", - "T": "10", - "V": "GO", - "W": "7", - "U": "0", - "$": "1260" - } - ] + "time": "2024-11-23T15:00Z", + "screenTemperature": 11.98, + "maxScreenAirTemp": 12.03, + "minScreenAirTemp": 11.13, + "screenDewPointTemperature": 10.75, + "feelsLikeTemperature": 9.81, + "windSpeed10m": 5.14, + "windDirectionFrom10m": 182, + "windGustSpeed10m": 11.14, + "max10mWindGust": 13.9, + "visibility": 17498, + "screenRelativeHumidity": 92.28, + "mslp": 98613, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.7, + "totalPrecipAmount": 0.09, + "totalSnowAmount": 0, + "probOfPrecipitation": 37 }, { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "SSE", - "F": "7", - "G": "13", - "H": "87", - "Pp": "11", - "S": "7", - "T": "9", - "V": "GO", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "SSE", - "F": "7", - "G": "13", - "H": "91", - "Pp": "15", - "S": "7", - "T": "9", - "V": "GO", - "W": "8", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "7", - "G": "13", - "H": "89", - "Pp": "8", - "S": "7", - "T": "9", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "SSE", - "F": "10", - "G": "20", - "H": "75", - "Pp": "8", - "S": "11", - "T": "12", - "V": "VG", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "S", - "F": "12", - "G": "22", - "H": "68", - "Pp": "11", - "S": "11", - "T": "14", - "V": "GO", - "W": "7", - "U": "3", - "$": "720" - }, - { - "D": "S", - "F": "12", - "G": "27", - "H": "68", - "Pp": "55", - "S": "13", - "T": "14", - "V": "GO", - "W": "12", - "U": "1", - "$": "900" - }, - { - "D": "SSE", - "F": "11", - "G": "22", - "H": "76", - "Pp": "34", - "S": "11", - "T": "13", - "V": "VG", - "W": "10", - "U": "1", - "$": "1080" - }, - { - "D": "SSE", - "F": "9", - "G": "20", - "H": "86", - "Pp": "20", - "S": "11", - "T": "11", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] + "time": "2024-11-23T16:00Z", + "screenTemperature": 12.56, + "maxScreenAirTemp": 12.59, + "minScreenAirTemp": 11.98, + "screenDewPointTemperature": 11.33, + "feelsLikeTemperature": 10.83, + "windSpeed10m": 4.29, + "windDirectionFrom10m": 197, + "windGustSpeed10m": 9.96, + "max10mWindGust": 10.5, + "visibility": 16335, + "screenRelativeHumidity": 92.27, + "mslp": 98660, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 1.23, + "totalPrecipAmount": 0.27, + "totalSnowAmount": 0, + "probOfPrecipitation": 36 + }, + { + "time": "2024-11-23T17:00Z", + "screenTemperature": 12.95, + "maxScreenAirTemp": 12.99, + "minScreenAirTemp": 12.56, + "screenDewPointTemperature": 11.75, + "feelsLikeTemperature": 11.27, + "windSpeed10m": 4.33, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 9.88, + "max10mWindGust": 10.47, + "visibility": 18682, + "screenRelativeHumidity": 92.39, + "mslp": 98710, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 11 + }, + { + "time": "2024-11-23T18:00Z", + "screenTemperature": 13, + "maxScreenAirTemp": 13.05, + "minScreenAirTemp": 12.9, + "screenDewPointTemperature": 11.56, + "feelsLikeTemperature": 11.32, + "windSpeed10m": 4.31, + "windDirectionFrom10m": 177, + "windGustSpeed10m": 8.67, + "max10mWindGust": 9.95, + "visibility": 19530, + "screenRelativeHumidity": 91, + "mslp": 98710, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 7 + }, + { + "time": "2024-11-23T19:00Z", + "screenTemperature": 13.02, + "maxScreenAirTemp": 13.16, + "minScreenAirTemp": 13, + "screenDewPointTemperature": 11.92, + "feelsLikeTemperature": 11.12, + "windSpeed10m": 4.85, + "windDirectionFrom10m": 177, + "windGustSpeed10m": 10.4, + "max10mWindGust": 11.01, + "visibility": 13803, + "screenRelativeHumidity": 93.07, + "mslp": 98682, + "uvIndex": 0, + "significantWeatherCode": 13, + "precipitationRate": 5.45, + "totalPrecipAmount": 0.51, + "totalSnowAmount": 0, + "probOfPrecipitation": 74 + }, + { + "time": "2024-11-23T20:00Z", + "screenTemperature": 13.67, + "maxScreenAirTemp": 13.72, + "minScreenAirTemp": 13.02, + "screenDewPointTemperature": 12.07, + "feelsLikeTemperature": 11.23, + "windSpeed10m": 6.31, + "windDirectionFrom10m": 187, + "windGustSpeed10m": 12.77, + "max10mWindGust": 13.53, + "visibility": 28855, + "screenRelativeHumidity": 90.06, + "mslp": 98692, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 11 + }, + { + "time": "2024-11-23T21:00Z", + "screenTemperature": 14.02, + "maxScreenAirTemp": 14.03, + "minScreenAirTemp": 13.67, + "screenDewPointTemperature": 11.71, + "feelsLikeTemperature": 11.65, + "windSpeed10m": 6.11, + "windDirectionFrom10m": 178, + "windGustSpeed10m": 12.31, + "max10mWindGust": 13.07, + "visibility": 34707, + "screenRelativeHumidity": 86.02, + "mslp": 98682, + "uvIndex": 0, + "significantWeatherCode": 9, + "precipitationRate": 0.35, + "totalPrecipAmount": 0.11, + "totalSnowAmount": 0, + "probOfPrecipitation": 30 + }, + { + "time": "2024-11-23T22:00Z", + "screenTemperature": 13.98, + "maxScreenAirTemp": 14.02, + "minScreenAirTemp": 13.9, + "screenDewPointTemperature": 11.78, + "feelsLikeTemperature": 11.43, + "windSpeed10m": 6.57, + "windDirectionFrom10m": 176, + "windGustSpeed10m": 13.29, + "max10mWindGust": 14.34, + "visibility": 37141, + "screenRelativeHumidity": 86.59, + "mslp": 98631, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 12 + }, + { + "time": "2024-11-23T23:00Z", + "screenTemperature": 14.28, + "maxScreenAirTemp": 14.29, + "minScreenAirTemp": 13.98, + "screenDewPointTemperature": 12.06, + "feelsLikeTemperature": 11.42, + "windSpeed10m": 7.38, + "windDirectionFrom10m": 176, + "windGustSpeed10m": 14.29, + "max10mWindGust": 15.45, + "visibility": 37580, + "screenRelativeHumidity": 86.56, + "mslp": 98571, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 10 + }, + { + "time": "2024-11-24T00:00Z", + "screenTemperature": 14.4, + "maxScreenAirTemp": 14.44, + "minScreenAirTemp": 14.28, + "screenDewPointTemperature": 12.25, + "feelsLikeTemperature": 11.52, + "windSpeed10m": 7.44, + "windDirectionFrom10m": 171, + "windGustSpeed10m": 14.08, + "max10mWindGust": 14.92, + "visibility": 39734, + "screenRelativeHumidity": 86.99, + "mslp": 98492, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 10 + }, + { + "time": "2024-11-24T01:00Z", + "screenTemperature": 14.38, + "maxScreenAirTemp": 14.42, + "minScreenAirTemp": 14.35, + "screenDewPointTemperature": 12.25, + "feelsLikeTemperature": 11.62, + "windSpeed10m": 7.16, + "windDirectionFrom10m": 170, + "windGustSpeed10m": 13.92, + "max10mWindGust": 14.5, + "visibility": 39173, + "screenRelativeHumidity": 87.03, + "mslp": 98422, + "uvIndex": 0, + "significantWeatherCode": 9, + "precipitationRate": 1.24, + "totalPrecipAmount": 0.17, + "totalSnowAmount": 0, + "probOfPrecipitation": 40 + }, + { + "time": "2024-11-24T02:00Z", + "screenTemperature": 14.19, + "maxScreenAirTemp": 14.38, + "minScreenAirTemp": 14.16, + "screenDewPointTemperature": 12.49, + "feelsLikeTemperature": 11.33, + "windSpeed10m": 7.47, + "windDirectionFrom10m": 176, + "windGustSpeed10m": 14.46, + "max10mWindGust": 15.43, + "visibility": 31444, + "screenRelativeHumidity": 89.63, + "mslp": 98351, + "uvIndex": 0, + "significantWeatherCode": 13, + "precipitationRate": 2.07, + "totalPrecipAmount": 0.21, + "totalSnowAmount": 0, + "probOfPrecipitation": 74 + }, + { + "time": "2024-11-24T03:00Z", + "screenTemperature": 14.44, + "maxScreenAirTemp": 14.48, + "minScreenAirTemp": 14.19, + "screenDewPointTemperature": 12.35, + "feelsLikeTemperature": 11.65, + "windSpeed10m": 7.25, + "windDirectionFrom10m": 187, + "windGustSpeed10m": 14.32, + "max10mWindGust": 15.51, + "visibility": 20239, + "screenRelativeHumidity": 87.4, + "mslp": 98310, + "uvIndex": 0, + "significantWeatherCode": 13, + "precipitationRate": 2.63, + "totalPrecipAmount": 0.34, + "totalSnowAmount": 0, + "probOfPrecipitation": 73 + }, + { + "time": "2024-11-24T04:00Z", + "screenTemperature": 14.42, + "maxScreenAirTemp": 14.45, + "minScreenAirTemp": 14.37, + "screenDewPointTemperature": 12.28, + "feelsLikeTemperature": 11.68, + "windSpeed10m": 7.09, + "windDirectionFrom10m": 189, + "windGustSpeed10m": 13.8, + "max10mWindGust": 15.24, + "visibility": 24690, + "screenRelativeHumidity": 87.07, + "mslp": 98310, + "uvIndex": 0, + "significantWeatherCode": 9, + "precipitationRate": 1.32, + "totalPrecipAmount": 0.28, + "totalSnowAmount": 0, + "probOfPrecipitation": 50 + }, + { + "time": "2024-11-24T05:00Z", + "screenTemperature": 14.31, + "maxScreenAirTemp": 14.42, + "minScreenAirTemp": 14.11, + "screenDewPointTemperature": 12.17, + "feelsLikeTemperature": 11.79, + "windSpeed10m": 6.58, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 12.7, + "max10mWindGust": 14.06, + "visibility": 25995, + "screenRelativeHumidity": 87.01, + "mslp": 98330, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.65, + "totalPrecipAmount": 0.25, + "totalSnowAmount": 0, + "probOfPrecipitation": 47 + }, + { + "time": "2024-11-24T06:00Z", + "screenTemperature": 13.43, + "maxScreenAirTemp": 14.31, + "minScreenAirTemp": 13.41, + "screenDewPointTemperature": 10.33, + "feelsLikeTemperature": 10.74, + "windSpeed10m": 6.71, + "windDirectionFrom10m": 216, + "windGustSpeed10m": 12.73, + "max10mWindGust": 13.79, + "visibility": 27446, + "screenRelativeHumidity": 81.67, + "mslp": 98396, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 1.04, + "totalPrecipAmount": 0.3, + "totalSnowAmount": 0, + "probOfPrecipitation": 42 + }, + { + "time": "2024-11-24T07:00Z", + "screenTemperature": 12.48, + "maxScreenAirTemp": 13.43, + "minScreenAirTemp": 12.47, + "screenDewPointTemperature": 9.48, + "feelsLikeTemperature": 10.09, + "windSpeed10m": 5.72, + "windDirectionFrom10m": 214, + "windGustSpeed10m": 11.03, + "max10mWindGust": 12.54, + "visibility": 24289, + "screenRelativeHumidity": 81.94, + "mslp": 98458, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 1.17, + "totalPrecipAmount": 0.16, + "totalSnowAmount": 0, + "probOfPrecipitation": 40 + }, + { + "time": "2024-11-24T08:00Z", + "screenTemperature": 11.88, + "maxScreenAirTemp": 12.48, + "minScreenAirTemp": 11.86, + "screenDewPointTemperature": 8.86, + "feelsLikeTemperature": 9.53, + "windSpeed10m": 5.48, + "windDirectionFrom10m": 209, + "windGustSpeed10m": 10.3, + "max10mWindGust": 11.11, + "visibility": 30442, + "screenRelativeHumidity": 81.73, + "mslp": 98548, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.29, + "totalPrecipAmount": 0.08, + "totalSnowAmount": 0, + "probOfPrecipitation": 38 + }, + { + "time": "2024-11-24T09:00Z", + "screenTemperature": 11.46, + "maxScreenAirTemp": 11.88, + "minScreenAirTemp": 11.45, + "screenDewPointTemperature": 8.21, + "feelsLikeTemperature": 9.06, + "windSpeed10m": 5.44, + "windDirectionFrom10m": 201, + "windGustSpeed10m": 9.99, + "max10mWindGust": 10.31, + "visibility": 28370, + "screenRelativeHumidity": 80.35, + "mslp": 98638, + "uvIndex": 1, + "significantWeatherCode": 10, + "precipitationRate": 0.28, + "totalPrecipAmount": 0.04, + "totalSnowAmount": 0, + "probOfPrecipitation": 26 + }, + { + "time": "2024-11-24T10:00Z", + "screenTemperature": 11.54, + "maxScreenAirTemp": 11.56, + "minScreenAirTemp": 11.46, + "screenDewPointTemperature": 7.52, + "feelsLikeTemperature": 9.03, + "windSpeed10m": 5.72, + "windDirectionFrom10m": 199, + "windGustSpeed10m": 10.28, + "max10mWindGust": 10.83, + "visibility": 29181, + "screenRelativeHumidity": 76.29, + "mslp": 98696, + "uvIndex": 1, + "significantWeatherCode": 10, + "precipitationRate": 0.28, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 25 + }, + { + "time": "2024-11-24T11:00Z", + "screenTemperature": 11.66, + "maxScreenAirTemp": 11.67, + "minScreenAirTemp": 11.54, + "screenDewPointTemperature": 7.29, + "feelsLikeTemperature": 9.17, + "windSpeed10m": 5.68, + "windDirectionFrom10m": 199, + "windGustSpeed10m": 10.06, + "max10mWindGust": 11.06, + "visibility": 33278, + "screenRelativeHumidity": 74.39, + "mslp": 98755, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 6 + }, + { + "time": "2024-11-24T12:00Z", + "screenTemperature": 11.82, + "maxScreenAirTemp": 11.84, + "minScreenAirTemp": 11.66, + "screenDewPointTemperature": 6.61, + "feelsLikeTemperature": 8.98, + "windSpeed10m": 6.65, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 11.85, + "max10mWindGust": 12.49, + "visibility": 36358, + "screenRelativeHumidity": 70.26, + "mslp": 98748, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T13:00Z", + "screenTemperature": 11.84, + "maxScreenAirTemp": 11.87, + "minScreenAirTemp": 11.82, + "screenDewPointTemperature": 6.06, + "feelsLikeTemperature": 8.85, + "windSpeed10m": 7.07, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 12.6, + "max10mWindGust": 14.16, + "visibility": 38017, + "screenRelativeHumidity": 67.6, + "mslp": 98757, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T14:00Z", + "screenTemperature": 11.73, + "maxScreenAirTemp": 11.84, + "minScreenAirTemp": 11.72, + "screenDewPointTemperature": 5.74, + "feelsLikeTemperature": 8.64, + "windSpeed10m": 7.33, + "windDirectionFrom10m": 201, + "windGustSpeed10m": 13.04, + "max10mWindGust": 14.33, + "visibility": 36175, + "screenRelativeHumidity": 66.62, + "mslp": 98737, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T15:00Z", + "screenTemperature": 11.61, + "maxScreenAirTemp": 11.73, + "minScreenAirTemp": 11.57, + "screenDewPointTemperature": 5.89, + "feelsLikeTemperature": 8.53, + "windSpeed10m": 7.32, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 13.02, + "max10mWindGust": 15, + "visibility": 35510, + "screenRelativeHumidity": 67.73, + "mslp": 98727, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2024-11-24T16:00Z", + "screenTemperature": 11.25, + "maxScreenAirTemp": 11.61, + "minScreenAirTemp": 11.24, + "screenDewPointTemperature": 5.8, + "feelsLikeTemperature": 8.25, + "windSpeed10m": 7.05, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 12.84, + "max10mWindGust": 14.78, + "visibility": 34357, + "screenRelativeHumidity": 68.9, + "mslp": 98708, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T17:00Z", + "screenTemperature": 11.03, + "maxScreenAirTemp": 11.25, + "minScreenAirTemp": 11.02, + "screenDewPointTemperature": 5.9, + "feelsLikeTemperature": 8.03, + "windSpeed10m": 7.04, + "windDirectionFrom10m": 194, + "windGustSpeed10m": 12.69, + "max10mWindGust": 14.44, + "visibility": 37801, + "screenRelativeHumidity": 70.45, + "mslp": 98689, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2024-11-24T18:00Z", + "screenTemperature": 10.86, + "maxScreenAirTemp": 11.03, + "minScreenAirTemp": 10.8, + "screenDewPointTemperature": 5.96, + "feelsLikeTemperature": 7.85, + "windSpeed10m": 7.04, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 12.82, + "max10mWindGust": 14.25, + "visibility": 39237, + "screenRelativeHumidity": 71.58, + "mslp": 98670, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T19:00Z", + "screenTemperature": 10.79, + "maxScreenAirTemp": 10.86, + "minScreenAirTemp": 10.75, + "screenDewPointTemperature": 5.92, + "feelsLikeTemperature": 7.81, + "windSpeed10m": 6.93, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 12.62, + "max10mWindGust": 13.94, + "visibility": 40795, + "screenRelativeHumidity": 71.71, + "mslp": 98669, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T20:00Z", + "screenTemperature": 10.65, + "maxScreenAirTemp": 10.79, + "minScreenAirTemp": 10.62, + "screenDewPointTemperature": 5.78, + "feelsLikeTemperature": 7.7, + "windSpeed10m": 6.82, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 12.52, + "max10mWindGust": 13.63, + "visibility": 41929, + "screenRelativeHumidity": 71.7, + "mslp": 98678, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T21:00Z", + "screenTemperature": 10.53, + "maxScreenAirTemp": 10.65, + "minScreenAirTemp": 10.5, + "screenDewPointTemperature": 5.84, + "feelsLikeTemperature": 7.48, + "windSpeed10m": 7.08, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 12.89, + "max10mWindGust": 13.18, + "visibility": 44628, + "screenRelativeHumidity": 72.53, + "mslp": 98677, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-24T22:00Z", + "screenTemperature": 10.47, + "maxScreenAirTemp": 10.53, + "minScreenAirTemp": 10.42, + "screenDewPointTemperature": 5.65, + "feelsLikeTemperature": 7.32, + "windSpeed10m": 7.41, + "windDirectionFrom10m": 204, + "windGustSpeed10m": 13.4, + "max10mWindGust": 13.81, + "visibility": 47105, + "screenRelativeHumidity": 71.84, + "mslp": 98704, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2024-11-24T23:00Z", + "screenTemperature": 10.32, + "maxScreenAirTemp": 10.47, + "minScreenAirTemp": 10.26, + "screenDewPointTemperature": 5.54, + "feelsLikeTemperature": 7.08, + "windSpeed10m": 7.7, + "windDirectionFrom10m": 207, + "windGustSpeed10m": 14.01, + "max10mWindGust": 14.01, + "visibility": 52166, + "screenRelativeHumidity": 72.03, + "mslp": 98704, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-25T00:00Z", + "screenTemperature": 10.22, + "maxScreenAirTemp": 10.32, + "minScreenAirTemp": 10.06, + "screenDewPointTemperature": 5.64, + "feelsLikeTemperature": 7.09, + "windSpeed10m": 7.33, + "windDirectionFrom10m": 211, + "windGustSpeed10m": 13.11, + "max10mWindGust": 13.65, + "visibility": 51563, + "screenRelativeHumidity": 72.97, + "mslp": 98712, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 23 + }, + { + "time": "2024-11-25T01:00Z", + "screenTemperature": 9.98, + "maxScreenAirTemp": 10.22, + "minScreenAirTemp": 9.94, + "screenDewPointTemperature": 5.98, + "feelsLikeTemperature": 6.88, + "windSpeed10m": 7.04, + "windDirectionFrom10m": 215, + "windGustSpeed10m": 12.51, + "max10mWindGust": 12.51, + "visibility": 52180, + "screenRelativeHumidity": 76.02, + "mslp": 98741, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 11 + }, + { + "time": "2024-11-25T02:00Z", + "screenTemperature": 9.59, + "maxScreenAirTemp": 9.98, + "minScreenAirTemp": 9.53, + "screenDewPointTemperature": 5.22, + "feelsLikeTemperature": 6.37, + "windSpeed10m": 7.14, + "windDirectionFrom10m": 222, + "windGustSpeed10m": 13.02, + "max10mWindGust": 13.02, + "visibility": 41536, + "screenRelativeHumidity": 74.07, + "mslp": 98788, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 7 + }, + { + "time": "2024-11-25T03:00Z", + "screenTemperature": 9.27, + "maxScreenAirTemp": 9.59, + "minScreenAirTemp": 9.25, + "screenDewPointTemperature": 5.16, + "feelsLikeTemperature": 6.06, + "windSpeed10m": 6.91, + "windDirectionFrom10m": 226, + "windGustSpeed10m": 12.42, + "max10mWindGust": 12.88, + "visibility": 38854, + "screenRelativeHumidity": 75.45, + "mslp": 98816, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-25T04:00Z", + "screenTemperature": 9.09, + "maxScreenAirTemp": 9.27, + "minScreenAirTemp": 9.04, + "screenDewPointTemperature": 4.8, + "feelsLikeTemperature": 5.8, + "windSpeed10m": 7.04, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 12.56, + "max10mWindGust": 12.8, + "visibility": 36196, + "screenRelativeHumidity": 74.38, + "mslp": 98858, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2024-11-25T05:00Z", + "screenTemperature": 8.82, + "maxScreenAirTemp": 9.09, + "minScreenAirTemp": 8.81, + "screenDewPointTemperature": 4.54, + "feelsLikeTemperature": 5.36, + "windSpeed10m": 7.26, + "windDirectionFrom10m": 232, + "windGustSpeed10m": 13.12, + "max10mWindGust": 14.39, + "visibility": 42056, + "screenRelativeHumidity": 74.58, + "mslp": 98910, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T06:00Z", + "screenTemperature": 8.66, + "maxScreenAirTemp": 8.88, + "minScreenAirTemp": 8.63, + "screenDewPointTemperature": 4.28, + "feelsLikeTemperature": 5.14, + "windSpeed10m": 7.32, + "windDirectionFrom10m": 235, + "windGustSpeed10m": 13.39, + "max10mWindGust": 15.94, + "visibility": 41207, + "screenRelativeHumidity": 74.14, + "mslp": 98961, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T07:00Z", + "screenTemperature": 8.58, + "maxScreenAirTemp": 8.69, + "minScreenAirTemp": 8.56, + "screenDewPointTemperature": 4.21, + "feelsLikeTemperature": 5.01, + "windSpeed10m": 7.44, + "windDirectionFrom10m": 240, + "windGustSpeed10m": 13.28, + "max10mWindGust": 14.8, + "visibility": 38861, + "screenRelativeHumidity": 74.26, + "mslp": 99061, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T08:00Z", + "screenTemperature": 8.42, + "maxScreenAirTemp": 8.58, + "minScreenAirTemp": 8.42, + "screenDewPointTemperature": 3.99, + "feelsLikeTemperature": 4.84, + "windSpeed10m": 7.46, + "windDirectionFrom10m": 243, + "windGustSpeed10m": 13.21, + "max10mWindGust": 14.59, + "visibility": 36897, + "screenRelativeHumidity": 73.86, + "mslp": 99161, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T09:00Z", + "screenTemperature": 8.4, + "maxScreenAirTemp": 8.42, + "minScreenAirTemp": 8.27, + "screenDewPointTemperature": 3.83, + "feelsLikeTemperature": 4.77, + "windSpeed10m": 7.59, + "windDirectionFrom10m": 243, + "windGustSpeed10m": 13.29, + "max10mWindGust": 13.29, + "visibility": 36152, + "screenRelativeHumidity": 73.17, + "mslp": 99252, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T10:00Z", + "screenTemperature": 8.66, + "maxScreenAirTemp": 8.66, + "minScreenAirTemp": 8.4, + "screenDewPointTemperature": 3.94, + "feelsLikeTemperature": 4.96, + "windSpeed10m": 8, + "windDirectionFrom10m": 245, + "windGustSpeed10m": 13.83, + "max10mWindGust": 13.83, + "visibility": 36320, + "screenRelativeHumidity": 72.24, + "mslp": 99342, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T11:00Z", + "screenTemperature": 8.83, + "maxScreenAirTemp": 8.83, + "minScreenAirTemp": 8.66, + "screenDewPointTemperature": 3.7, + "feelsLikeTemperature": 5.05, + "windSpeed10m": 8.44, + "windDirectionFrom10m": 249, + "windGustSpeed10m": 14.47, + "max10mWindGust": 14.47, + "visibility": 32194, + "screenRelativeHumidity": 69.92, + "mslp": 99424, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 3 + }, + { + "time": "2024-11-25T12:00Z", + "screenTemperature": 8.94, + "screenDewPointTemperature": 3.65, + "feelsLikeTemperature": 5.18, + "windSpeed10m": 8.52, + "windDirectionFrom10m": 251, + "windGustSpeed10m": 14.49, + "visibility": 32255, + "screenRelativeHumidity": 68.89, + "mslp": 99488, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "probOfPrecipitation": 2 } ] } } - } + ], + "parameters": [ + { + "totalSnowAmount": { + "type": "Parameter", + "description": "Total Snow Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "screenTemperature": { + "type": "Parameter", + "description": "Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "visibility": { + "type": "Parameter", + "description": "Visibility", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "windDirectionFrom10m": { + "type": "Parameter", + "description": "10m Wind From Direction", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "precipitationRate": { + "type": "Parameter", + "description": "Precipitation Rate", + "unit": { + "label": "millimetres per hour", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm/h" + } + } + }, + "maxScreenAirTemp": { + "type": "Parameter", + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "feelsLikeTemperature": { + "type": "Parameter", + "description": "Feels Like Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenDewPointTemperature": { + "type": "Parameter", + "description": "Screen Dew Point Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenRelativeHumidity": { + "type": "Parameter", + "description": "Screen Relative Humidity", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windSpeed10m": { + "type": "Parameter", + "description": "10m Wind Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "probOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "max10mWindGust": { + "type": "Parameter", + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "significantWeatherCode": { + "type": "Parameter", + "description": "Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "minScreenAirTemp": { + "type": "Parameter", + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "totalPrecipAmount": { + "type": "Parameter", + "description": "Total Precipitation Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "mslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "windGustSpeed10m": { + "type": "Parameter", + "description": "10m Wind Gust Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "uvIndex": { + "type": "Parameter", + "description": "UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + } + } + ] }, "kingslynn_daily": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "FDm", - "units": "C", - "$": "Feels Like Day Maximum Temperature" + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0.40190000000000003, 52.7561, 5] + }, + "properties": { + "location": { + "name": "King's Lynn" }, - { - "name": "FNm", - "units": "C", - "$": "Feels Like Night Minimum Temperature" - }, - { - "name": "Dm", - "units": "C", - "$": "Day Maximum Temperature" - }, - { - "name": "Nm", - "units": "C", - "$": "Night Minimum Temperature" - }, - { - "name": "Gn", - "units": "mph", - "$": "Wind Gust Noon" - }, - { - "name": "Gm", - "units": "mph", - "$": "Wind Gust Midnight" - }, - { - "name": "Hn", - "units": "%", - "$": "Screen Relative Humidity Noon" - }, - { - "name": "Hm", - "units": "%", - "$": "Screen Relative Humidity Midnight" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "PPd", - "units": "%", - "$": "Precipitation Probability Day" - }, - { - "name": "PPn", - "units": "%", - "$": "Precipitation Probability Night" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "322380", - "lat": "52.7561", - "lon": "0.4019", - "name": "KING'S LYNN", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "5.0", - "Period": [ + "requestPointDistance": 2720.9208, + "modelRunDate": "2024-11-23T12:00Z", + "timeSeries": [ { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "ESE", - "Gn": "4", - "Hn": "75", - "PPd": "9", - "S": "4", - "V": "VG", - "Dm": "9", - "FDm": "8", - "W": "8", - "U": "3", - "$": "Day" - }, - { - "D": "SSE", - "Gm": "16", - "Hm": "84", - "PPn": "0", - "S": "7", - "V": "VG", - "Nm": "7", - "FNm": "5", - "W": "0", - "$": "Night" - } - ] + "time": "2024-11-22T00:00Z", + "midday10MWindSpeed": 6.74, + "midnight10MWindSpeed": 2.98, + "midday10MWindDirection": 288, + "midnight10MWindDirection": 188, + "midday10MWindGust": 11.32, + "midnight10MWindGust": 7.72, + "middayVisibility": 25304, + "midnightVisibility": 16924, + "middayRelativeHumidity": 68.93, + "midnightRelativeHumidity": 94.01, + "middayMslp": 100530, + "midnightMslp": 101290, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 5.24, + "nightMinScreenTemperature": -0.4, + "dayUpperBoundMaxTemp": 6.17, + "nightUpperBoundMinTemp": 1.91, + "dayLowerBoundMaxTemp": 4.13, + "nightLowerBoundMinTemp": -1.1, + "nightMinFeelsLikeTemp": -4.12, + "dayUpperBoundMaxFeelsLikeTemp": 2.08, + "nightUpperBoundMinFeelsLikeTemp": -1.75, + "dayLowerBoundMaxFeelsLikeTemp": 0.48, + "nightLowerBoundMinFeelsLikeTemp": -4.12, + "nightProbabilityOfPrecipitation": 89, + "nightProbabilityOfSnow": 6, + "nightProbabilityOfHeavySnow": 2, + "nightProbabilityOfRain": 86, + "nightProbabilityOfHeavyRain": 84, + "nightProbabilityOfHail": 18, + "nightProbabilityOfSferics": 9 }, { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "SSW", - "Gn": "13", - "Hn": "69", - "PPd": "0", - "S": "9", - "V": "VG", - "Dm": "13", - "FDm": "11", - "W": "1", - "U": "4", - "$": "Day" - }, - { - "D": "SSW", - "Gm": "13", - "Hm": "75", - "PPn": "5", - "S": "7", - "V": "GO", - "Nm": "11", - "FNm": "10", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-23T00:00Z", + "midday10MWindSpeed": 9.93, + "midnight10MWindSpeed": 8.72, + "midday10MWindDirection": 180, + "midnight10MWindDirection": 199, + "midday10MWindGust": 18, + "midnight10MWindGust": 16.6, + "middayVisibility": 7478, + "midnightVisibility": 42290, + "middayRelativeHumidity": 97.5, + "midnightRelativeHumidity": 90.27, + "middayMslp": 99820, + "midnightMslp": 99340, + "maxUvIndex": 1, + "daySignificantWeatherCode": 15, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 10.16, + "nightMinScreenTemperature": 9.3, + "dayUpperBoundMaxTemp": 13, + "nightUpperBoundMinTemp": 13.01, + "dayLowerBoundMaxTemp": 9.51, + "nightLowerBoundMinTemp": 9.3, + "dayMaxFeelsLikeTemp": 5.14, + "nightMinFeelsLikeTemp": 6.38, + "dayUpperBoundMaxFeelsLikeTemp": 9.42, + "nightUpperBoundMinFeelsLikeTemp": 9.42, + "dayLowerBoundMaxFeelsLikeTemp": 5.14, + "nightLowerBoundMinFeelsLikeTemp": 6.38, + "dayProbabilityOfPrecipitation": 97, + "nightProbabilityOfPrecipitation": 95, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 97, + "nightProbabilityOfRain": 95, + "dayProbabilityOfHeavyRain": 96, + "nightProbabilityOfHeavyRain": 93, + "dayProbabilityOfHail": 19, + "nightProbabilityOfHail": 19, + "dayProbabilityOfSferics": 10, + "nightProbabilityOfSferics": 11 }, { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "NW", - "Gn": "11", - "Hn": "78", - "PPd": "36", - "S": "4", - "V": "VG", - "Dm": "10", - "FDm": "9", - "W": "7", - "U": "3", - "$": "Day" - }, - { - "D": "SE", - "Gm": "13", - "Hm": "85", - "PPn": "9", - "S": "7", - "V": "VG", - "Nm": "9", - "FNm": "7", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-24T00:00Z", + "midday10MWindSpeed": 10.03, + "midnight10MWindSpeed": 6.3, + "midday10MWindDirection": 200, + "midnight10MWindDirection": 214, + "midday10MWindGust": 19, + "midnight10MWindGust": 12.27, + "middayVisibility": 19911, + "midnightVisibility": 44678, + "middayRelativeHumidity": 82.47, + "midnightRelativeHumidity": 84.49, + "middayMslp": 99220, + "midnightMslp": 99277, + "maxUvIndex": 1, + "daySignificantWeatherCode": 12, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 15.66, + "nightMinScreenTemperature": 9.75, + "dayUpperBoundMaxTemp": 16.88, + "nightUpperBoundMinTemp": 10.72, + "dayLowerBoundMaxTemp": 13.97, + "nightLowerBoundMinTemp": 8.25, + "dayMaxFeelsLikeTemp": 11.45, + "nightMinFeelsLikeTemp": 7.13, + "dayUpperBoundMaxFeelsLikeTemp": 12.2, + "nightUpperBoundMinFeelsLikeTemp": 8, + "dayLowerBoundMaxFeelsLikeTemp": 10.46, + "nightLowerBoundMinFeelsLikeTemp": 5.07, + "dayProbabilityOfPrecipitation": 81, + "nightProbabilityOfPrecipitation": 86, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 81, + "nightProbabilityOfRain": 86, + "dayProbabilityOfHeavyRain": 78, + "nightProbabilityOfHeavyRain": 82, + "dayProbabilityOfHail": 15, + "nightProbabilityOfHail": 16, + "dayProbabilityOfSferics": 8, + "nightProbabilityOfSferics": 8 }, { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "ESE", - "Gn": "13", - "Hn": "77", - "PPd": "14", - "S": "7", - "V": "GO", - "Dm": "11", - "FDm": "9", - "W": "7", - "U": "3", - "$": "Day" - }, - { - "D": "SSE", - "Gm": "13", - "Hm": "87", - "PPn": "11", - "S": "7", - "V": "GO", - "Nm": "9", - "FNm": "7", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-25T00:00Z", + "midday10MWindSpeed": 6.91, + "midnight10MWindSpeed": 5.14, + "midday10MWindDirection": 233, + "midnight10MWindDirection": 228, + "midday10MWindGust": 12.61, + "midnight10MWindGust": 9.33, + "middayVisibility": 38960, + "midnightVisibility": 39029, + "middayRelativeHumidity": 70.02, + "midnightRelativeHumidity": 84, + "middayMslp": 99715, + "midnightMslp": 100666, + "maxUvIndex": 1, + "daySignificantWeatherCode": 1, + "nightSignificantWeatherCode": 0, + "dayMaxScreenTemperature": 10.94, + "nightMinScreenTemperature": 4.7, + "dayUpperBoundMaxTemp": 11.7, + "nightUpperBoundMinTemp": 7.14, + "dayLowerBoundMaxTemp": 9.36, + "nightLowerBoundMinTemp": 2.09, + "dayMaxFeelsLikeTemp": 7.72, + "nightMinFeelsLikeTemp": 1.4, + "dayUpperBoundMaxFeelsLikeTemp": 8.79, + "nightUpperBoundMinFeelsLikeTemp": 3.27, + "dayLowerBoundMaxFeelsLikeTemp": 6.22, + "nightLowerBoundMinFeelsLikeTemp": -0.99, + "dayProbabilityOfPrecipitation": 3, + "nightProbabilityOfPrecipitation": 4, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 3, + "nightProbabilityOfRain": 4, + "dayProbabilityOfHeavyRain": 1, + "nightProbabilityOfHeavyRain": 2, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 }, { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "SSE", - "Gn": "20", - "Hn": "75", - "PPd": "8", - "S": "11", - "V": "VG", - "Dm": "12", - "FDm": "10", - "W": "7", - "U": "3", - "$": "Day" - }, - { - "D": "SSE", - "Gm": "20", - "Hm": "86", - "PPn": "20", - "S": "11", - "V": "VG", - "Nm": "9", - "FNm": "7", - "W": "7", - "$": "Night" - } - ] + "time": "2024-11-26T00:00Z", + "midday10MWindSpeed": 4.33, + "midnight10MWindSpeed": 2.83, + "midday10MWindDirection": 241, + "midnight10MWindDirection": 179, + "midday10MWindGust": 8.23, + "midnight10MWindGust": 4.92, + "middayVisibility": 40528, + "midnightVisibility": 14079, + "middayRelativeHumidity": 77.2, + "midnightRelativeHumidity": 94.47, + "middayMslp": 101355, + "midnightMslp": 101517, + "maxUvIndex": 1, + "daySignificantWeatherCode": 1, + "nightSignificantWeatherCode": 9, + "dayMaxScreenTemperature": 7.93, + "nightMinScreenTemperature": 2.68, + "dayUpperBoundMaxTemp": 10.02, + "nightUpperBoundMinTemp": 9.62, + "dayLowerBoundMaxTemp": 6.28, + "nightLowerBoundMinTemp": -1.11, + "dayMaxFeelsLikeTemp": 5.22, + "nightMinFeelsLikeTemp": 1.74, + "dayUpperBoundMaxFeelsLikeTemp": 7.33, + "nightUpperBoundMinFeelsLikeTemp": 5.97, + "dayLowerBoundMaxFeelsLikeTemp": 4.13, + "nightLowerBoundMinFeelsLikeTemp": -3.64, + "dayProbabilityOfPrecipitation": 3, + "nightProbabilityOfPrecipitation": 52, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 3, + "nightProbabilityOfRain": 52, + "dayProbabilityOfHeavyRain": 2, + "nightProbabilityOfHeavyRain": 48, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 10, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 9 + }, + { + "time": "2024-11-27T00:00Z", + "midday10MWindSpeed": 7.99, + "midnight10MWindSpeed": 5.7, + "midday10MWindDirection": 280, + "midnight10MWindDirection": 304, + "midday10MWindGust": 14.53, + "midnight10MWindGust": 9.97, + "middayVisibility": 12470, + "midnightVisibility": 31017, + "middayRelativeHumidity": 89.2, + "midnightRelativeHumidity": 86.45, + "middayMslp": 100836, + "midnightMslp": 101855, + "maxUvIndex": 1, + "daySignificantWeatherCode": 10, + "nightSignificantWeatherCode": 0, + "dayMaxScreenTemperature": 8.41, + "nightMinScreenTemperature": 4.04, + "dayUpperBoundMaxTemp": 12.97, + "nightUpperBoundMinTemp": 8.08, + "dayLowerBoundMaxTemp": 4.19, + "nightLowerBoundMinTemp": -1.57, + "dayMaxFeelsLikeTemp": 4.11, + "nightMinFeelsLikeTemp": 1.3, + "dayUpperBoundMaxFeelsLikeTemp": 10.56, + "nightUpperBoundMinFeelsLikeTemp": 5.08, + "dayLowerBoundMaxFeelsLikeTemp": 1.68, + "nightLowerBoundMinFeelsLikeTemp": -4.13, + "dayProbabilityOfPrecipitation": 49, + "nightProbabilityOfPrecipitation": 37, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 49, + "nightProbabilityOfRain": 37, + "dayProbabilityOfHeavyRain": 45, + "nightProbabilityOfHeavyRain": 24, + "dayProbabilityOfHail": 9, + "nightProbabilityOfHail": 2, + "dayProbabilityOfSferics": 9, + "nightProbabilityOfSferics": 4 + }, + { + "time": "2024-11-28T00:00Z", + "midday10MWindSpeed": 3.52, + "midnight10MWindSpeed": 3.01, + "midday10MWindDirection": 314, + "midnight10MWindDirection": 98, + "midday10MWindGust": 6.7, + "midnight10MWindGust": 5.08, + "middayVisibility": 38659, + "midnightVisibility": 12067, + "middayRelativeHumidity": 80.63, + "midnightRelativeHumidity": 92.04, + "middayMslp": 102495, + "midnightMslp": 102655, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 7.26, + "nightMinScreenTemperature": 2.84, + "dayUpperBoundMaxTemp": 10.28, + "nightUpperBoundMinTemp": 7.53, + "dayLowerBoundMaxTemp": 4.63, + "nightLowerBoundMinTemp": -1.27, + "dayMaxFeelsLikeTemp": 5.08, + "nightMinFeelsLikeTemp": 1.66, + "dayUpperBoundMaxFeelsLikeTemp": 7.29, + "nightUpperBoundMinFeelsLikeTemp": 4.94, + "dayLowerBoundMaxFeelsLikeTemp": 1.7, + "nightLowerBoundMinFeelsLikeTemp": -3.19, + "dayProbabilityOfPrecipitation": 7, + "nightProbabilityOfPrecipitation": 8, + "dayProbabilityOfSnow": 1, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 7, + "nightProbabilityOfRain": 7, + "dayProbabilityOfHeavyRain": 2, + "nightProbabilityOfHeavyRain": 2, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 + }, + { + "time": "2024-11-29T00:00Z", + "midday10MWindSpeed": 4.61, + "midnight10MWindSpeed": 4.68, + "midday10MWindDirection": 143, + "midnight10MWindDirection": 160, + "midday10MWindGust": 8.48, + "midnight10MWindGust": 8.27, + "middayVisibility": 28001, + "midnightVisibility": 32845, + "middayRelativeHumidity": 83.1, + "midnightRelativeHumidity": 90.51, + "middayMslp": 102395, + "midnightMslp": 102078, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 8, + "dayMaxScreenTemperature": 8.34, + "nightMinScreenTemperature": 5.65, + "dayUpperBoundMaxTemp": 13.38, + "nightUpperBoundMinTemp": 11.7, + "dayLowerBoundMaxTemp": 4.49, + "nightLowerBoundMinTemp": -1.92, + "dayMaxFeelsLikeTemp": 5.77, + "nightMinFeelsLikeTemp": 3.8, + "dayUpperBoundMaxFeelsLikeTemp": 11.34, + "nightUpperBoundMinFeelsLikeTemp": 9.44, + "dayLowerBoundMaxFeelsLikeTemp": 2.35, + "nightLowerBoundMinFeelsLikeTemp": -4.87, + "dayProbabilityOfPrecipitation": 8, + "nightProbabilityOfPrecipitation": 12, + "dayProbabilityOfSnow": 1, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 8, + "nightProbabilityOfRain": 12, + "dayProbabilityOfHeavyRain": 3, + "nightProbabilityOfHeavyRain": 2, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 } ] } } - } + ], + "parameters": [ + { + "daySignificantWeatherCode": { + "type": "Parameter", + "description": "Day Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "midnightRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midnight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midnight10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "nightUpperBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightUpperBoundMinTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnightVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midnight", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "dayUpperBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midday", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "nightLowerBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "middayMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midday", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midday10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "middayVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midday", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "midnight10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midnightMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midnight", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightSignificantWeatherCode": { + "type": "Parameter", + "description": "Night Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "dayProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayMaxScreenTemperature": { + "type": "Parameter", + "description": "Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinScreenTemperature": { + "type": "Parameter", + "description": "Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnight10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midnight", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "maxUvIndex": { + "type": "Parameter", + "description": "Day Maximum UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + }, + "dayProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayUpperBoundMaxTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "middayRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midday", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightLowerBoundMinTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + } + } + ] + }, + "kingslynn_hourly": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0.40190000000000003, 52.7561, 5] + }, + "properties": { + "location": { + "name": "King's Lynn" + }, + "requestPointDistance": 2720.9208, + "modelRunDate": "2024-11-23T12:00Z", + "timeSeries": [ + { + "time": "2024-11-23T12:00Z", + "screenTemperature": 7.87, + "maxScreenAirTemp": 7.87, + "minScreenAirTemp": 7.48, + "screenDewPointTemperature": 7.51, + "feelsLikeTemperature": 3.39, + "windSpeed10m": 9.93, + "windDirectionFrom10m": 180, + "windGustSpeed10m": 18, + "max10mWindGust": 18.11, + "visibility": 7478, + "screenRelativeHumidity": 97.5, + "mslp": 99820, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.75, + "totalPrecipAmount": 0.84, + "totalSnowAmount": 0, + "probOfPrecipitation": 67 + }, + { + "time": "2024-11-23T13:00Z", + "screenTemperature": 7.87, + "maxScreenAirTemp": 7.9, + "minScreenAirTemp": 7.84, + "screenDewPointTemperature": 7.1, + "feelsLikeTemperature": 3.25, + "windSpeed10m": 10.52, + "windDirectionFrom10m": 178, + "windGustSpeed10m": 19.06, + "max10mWindGust": 19.16, + "visibility": 8196, + "screenRelativeHumidity": 94.78, + "mslp": 99680, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.86, + "totalPrecipAmount": 0.29, + "totalSnowAmount": 0, + "probOfPrecipitation": 57 + }, + { + "time": "2024-11-23T14:00Z", + "screenTemperature": 8.34, + "maxScreenAirTemp": 8.34, + "minScreenAirTemp": 7.87, + "screenDewPointTemperature": 7.32, + "feelsLikeTemperature": 4, + "windSpeed10m": 10, + "windDirectionFrom10m": 182, + "windGustSpeed10m": 18.66, + "max10mWindGust": 18.98, + "visibility": 9417, + "screenRelativeHumidity": 93.17, + "mslp": 99550, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.23, + "totalPrecipAmount": 0.06, + "totalSnowAmount": 0, + "probOfPrecipitation": 62 + }, + { + "time": "2024-11-23T15:00Z", + "screenTemperature": 9.11, + "maxScreenAirTemp": 9.13, + "minScreenAirTemp": 8.34, + "screenDewPointTemperature": 8.03, + "feelsLikeTemperature": 5.14, + "windSpeed10m": 9.45, + "windDirectionFrom10m": 183, + "windGustSpeed10m": 17.94, + "max10mWindGust": 18.36, + "visibility": 8865, + "screenRelativeHumidity": 92.81, + "mslp": 99406, + "uvIndex": 1, + "significantWeatherCode": 15, + "precipitationRate": 1.87, + "totalPrecipAmount": 0.48, + "totalSnowAmount": 0, + "probOfPrecipitation": 93 + }, + { + "time": "2024-11-23T16:00Z", + "screenTemperature": 10.16, + "maxScreenAirTemp": 10.17, + "minScreenAirTemp": 9.11, + "screenDewPointTemperature": 9.02, + "feelsLikeTemperature": 6.38, + "windSpeed10m": 9.8, + "windDirectionFrom10m": 186, + "windGustSpeed10m": 18.67, + "max10mWindGust": 19.04, + "visibility": 16945, + "screenRelativeHumidity": 92.66, + "mslp": 99301, + "uvIndex": 0, + "significantWeatherCode": 15, + "precipitationRate": 4.03, + "totalPrecipAmount": 1.14, + "totalSnowAmount": 0, + "probOfPrecipitation": 95 + }, + { + "time": "2024-11-23T17:00Z", + "screenTemperature": 11.07, + "maxScreenAirTemp": 11.08, + "minScreenAirTemp": 10.16, + "screenDewPointTemperature": 9.94, + "feelsLikeTemperature": 7.46, + "windSpeed10m": 9.41, + "windDirectionFrom10m": 193, + "windGustSpeed10m": 18.09, + "max10mWindGust": 18.86, + "visibility": 9798, + "screenRelativeHumidity": 92.69, + "mslp": 99270, + "uvIndex": 0, + "significantWeatherCode": 15, + "precipitationRate": 2.26, + "totalPrecipAmount": 0.24, + "totalSnowAmount": 0, + "probOfPrecipitation": 93 + }, + { + "time": "2024-11-23T18:00Z", + "screenTemperature": 11.94, + "maxScreenAirTemp": 11.95, + "minScreenAirTemp": 11.07, + "screenDewPointTemperature": 10.9, + "feelsLikeTemperature": 8.72, + "windSpeed10m": 8.19, + "windDirectionFrom10m": 200, + "windGustSpeed10m": 16.15, + "max10mWindGust": 17.4, + "visibility": 10545, + "screenRelativeHumidity": 93.31, + "mslp": 99260, + "uvIndex": 0, + "significantWeatherCode": 15, + "precipitationRate": 2.51, + "totalPrecipAmount": 0.88, + "totalSnowAmount": 0, + "probOfPrecipitation": 93 + }, + { + "time": "2024-11-23T19:00Z", + "screenTemperature": 13.3, + "maxScreenAirTemp": 13.31, + "minScreenAirTemp": 11.94, + "screenDewPointTemperature": 11.95, + "feelsLikeTemperature": 10.09, + "windSpeed10m": 8.35, + "windDirectionFrom10m": 208, + "windGustSpeed10m": 16.37, + "max10mWindGust": 16.41, + "visibility": 36868, + "screenRelativeHumidity": 91.45, + "mslp": 99264, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 11 + }, + { + "time": "2024-11-23T20:00Z", + "screenTemperature": 13.56, + "maxScreenAirTemp": 13.58, + "minScreenAirTemp": 13.3, + "screenDewPointTemperature": 12.29, + "feelsLikeTemperature": 10.34, + "windSpeed10m": 8.42, + "windDirectionFrom10m": 205, + "windGustSpeed10m": 16.18, + "max10mWindGust": 16.75, + "visibility": 28041, + "screenRelativeHumidity": 91.94, + "mslp": 99304, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 27 + }, + { + "time": "2024-11-23T21:00Z", + "screenTemperature": 13.81, + "maxScreenAirTemp": 13.82, + "minScreenAirTemp": 13.56, + "screenDewPointTemperature": 12.5, + "feelsLikeTemperature": 10.53, + "windSpeed10m": 8.6, + "windDirectionFrom10m": 205, + "windGustSpeed10m": 16.28, + "max10mWindGust": 16.62, + "visibility": 29418, + "screenRelativeHumidity": 91.67, + "mslp": 99363, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 1.07, + "totalPrecipAmount": 0.21, + "totalSnowAmount": 0, + "probOfPrecipitation": 63 + }, + { + "time": "2024-11-23T22:00Z", + "screenTemperature": 14.07, + "maxScreenAirTemp": 14.08, + "minScreenAirTemp": 13.81, + "screenDewPointTemperature": 12.65, + "feelsLikeTemperature": 10.85, + "windSpeed10m": 8.42, + "windDirectionFrom10m": 204, + "windGustSpeed10m": 16.18, + "max10mWindGust": 16.85, + "visibility": 42192, + "screenRelativeHumidity": 91.08, + "mslp": 99382, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 14 + }, + { + "time": "2024-11-23T23:00Z", + "screenTemperature": 14.08, + "maxScreenAirTemp": 14.12, + "minScreenAirTemp": 14.05, + "screenDewPointTemperature": 12.78, + "feelsLikeTemperature": 10.96, + "windSpeed10m": 8.16, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 15.48, + "max10mWindGust": 16.29, + "visibility": 23225, + "screenRelativeHumidity": 91.85, + "mslp": 99372, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.89, + "totalPrecipAmount": 0.11, + "totalSnowAmount": 0, + "probOfPrecipitation": 61 + }, + { + "time": "2024-11-24T00:00Z", + "screenTemperature": 14.21, + "maxScreenAirTemp": 14.25, + "minScreenAirTemp": 14.08, + "screenDewPointTemperature": 12.64, + "feelsLikeTemperature": 10.87, + "windSpeed10m": 8.72, + "windDirectionFrom10m": 199, + "windGustSpeed10m": 16.6, + "max10mWindGust": 16.69, + "visibility": 42290, + "screenRelativeHumidity": 90.27, + "mslp": 99344, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 24 + }, + { + "time": "2024-11-24T01:00Z", + "screenTemperature": 14.28, + "maxScreenAirTemp": 14.3, + "minScreenAirTemp": 14.21, + "screenDewPointTemperature": 12.72, + "feelsLikeTemperature": 10.74, + "windSpeed10m": 9.29, + "windDirectionFrom10m": 199, + "windGustSpeed10m": 17.46, + "max10mWindGust": 17.85, + "visibility": 33325, + "screenRelativeHumidity": 90.21, + "mslp": 99303, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 19 + }, + { + "time": "2024-11-24T02:00Z", + "screenTemperature": 14.23, + "maxScreenAirTemp": 14.29, + "minScreenAirTemp": 14.19, + "screenDewPointTemperature": 12.69, + "feelsLikeTemperature": 10.54, + "windSpeed10m": 9.65, + "windDirectionFrom10m": 197, + "windGustSpeed10m": 18.14, + "max10mWindGust": 19.37, + "visibility": 20882, + "screenRelativeHumidity": 90.42, + "mslp": 99282, + "uvIndex": 0, + "significantWeatherCode": 13, + "precipitationRate": 0.89, + "totalPrecipAmount": 0.16, + "totalSnowAmount": 0, + "probOfPrecipitation": 70 + }, + { + "time": "2024-11-24T03:00Z", + "screenTemperature": 14.42, + "maxScreenAirTemp": 14.43, + "minScreenAirTemp": 14.23, + "screenDewPointTemperature": 12.72, + "feelsLikeTemperature": 10.6, + "windSpeed10m": 9.95, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 18.53, + "max10mWindGust": 19.32, + "visibility": 32364, + "screenRelativeHumidity": 89.41, + "mslp": 99242, + "uvIndex": 0, + "significantWeatherCode": 9, + "precipitationRate": 0.1, + "totalPrecipAmount": 0.06, + "totalSnowAmount": 0, + "probOfPrecipitation": 31 + }, + { + "time": "2024-11-24T04:00Z", + "screenTemperature": 14.51, + "maxScreenAirTemp": 14.58, + "minScreenAirTemp": 14.42, + "screenDewPointTemperature": 12.6, + "feelsLikeTemperature": 10.63, + "windSpeed10m": 10.05, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 18.86, + "max10mWindGust": 19.09, + "visibility": 15355, + "screenRelativeHumidity": 88.25, + "mslp": 99212, + "uvIndex": 0, + "significantWeatherCode": 9, + "precipitationRate": 0.38, + "totalPrecipAmount": 0.23, + "totalSnowAmount": 0, + "probOfPrecipitation": 40 + }, + { + "time": "2024-11-24T05:00Z", + "screenTemperature": 14.48, + "maxScreenAirTemp": 14.52, + "minScreenAirTemp": 14.47, + "screenDewPointTemperature": 12.37, + "feelsLikeTemperature": 10.53, + "windSpeed10m": 10.16, + "windDirectionFrom10m": 195, + "windGustSpeed10m": 18.76, + "max10mWindGust": 18.81, + "visibility": 29205, + "screenRelativeHumidity": 87.08, + "mslp": 99183, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 22 + }, + { + "time": "2024-11-24T06:00Z", + "screenTemperature": 14.53, + "maxScreenAirTemp": 14.57, + "minScreenAirTemp": 14.48, + "screenDewPointTemperature": 12.34, + "feelsLikeTemperature": 10.54, + "windSpeed10m": 10.23, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 18.81, + "max10mWindGust": 18.9, + "visibility": 25187, + "screenRelativeHumidity": 86.67, + "mslp": 99182, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 22 + }, + { + "time": "2024-11-24T07:00Z", + "screenTemperature": 14.72, + "maxScreenAirTemp": 14.73, + "minScreenAirTemp": 14.53, + "screenDewPointTemperature": 12.51, + "feelsLikeTemperature": 10.69, + "windSpeed10m": 10.33, + "windDirectionFrom10m": 194, + "windGustSpeed10m": 19, + "max10mWindGust": 19, + "visibility": 31443, + "screenRelativeHumidity": 86.55, + "mslp": 99173, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 15 + }, + { + "time": "2024-11-24T08:00Z", + "screenTemperature": 14.74, + "maxScreenAirTemp": 14.79, + "minScreenAirTemp": 14.71, + "screenDewPointTemperature": 12.36, + "feelsLikeTemperature": 10.7, + "windSpeed10m": 10.27, + "windDirectionFrom10m": 193, + "windGustSpeed10m": 18.91, + "max10mWindGust": 19.17, + "visibility": 24964, + "screenRelativeHumidity": 85.71, + "mslp": 99182, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.52, + "totalPrecipAmount": 0.04, + "totalSnowAmount": 0, + "probOfPrecipitation": 52 + }, + { + "time": "2024-11-24T09:00Z", + "screenTemperature": 14.78, + "maxScreenAirTemp": 14.81, + "minScreenAirTemp": 14.72, + "screenDewPointTemperature": 12.35, + "feelsLikeTemperature": 10.63, + "windSpeed10m": 10.56, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 19.44, + "max10mWindGust": 19.44, + "visibility": 16181, + "screenRelativeHumidity": 85.33, + "mslp": 99173, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.36, + "totalPrecipAmount": 0.2, + "totalSnowAmount": 0, + "probOfPrecipitation": 53 + }, + { + "time": "2024-11-24T10:00Z", + "screenTemperature": 14.88, + "maxScreenAirTemp": 14.91, + "minScreenAirTemp": 14.78, + "screenDewPointTemperature": 12.28, + "feelsLikeTemperature": 10.47, + "windSpeed10m": 11.1, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 20.32, + "max10mWindGust": 20.6, + "visibility": 22668, + "screenRelativeHumidity": 84.58, + "mslp": 99192, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.14, + "totalPrecipAmount": 0.05, + "totalSnowAmount": 0, + "probOfPrecipitation": 42 + }, + { + "time": "2024-11-24T11:00Z", + "screenTemperature": 15.3, + "maxScreenAirTemp": 15.33, + "minScreenAirTemp": 14.88, + "screenDewPointTemperature": 12.49, + "feelsLikeTemperature": 11.03, + "windSpeed10m": 10.55, + "windDirectionFrom10m": 201, + "windGustSpeed10m": 19.58, + "max10mWindGust": 19.77, + "visibility": 26957, + "screenRelativeHumidity": 83.56, + "mslp": 99220, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.2, + "totalPrecipAmount": 0.11, + "totalSnowAmount": 0, + "probOfPrecipitation": 44 + }, + { + "time": "2024-11-24T12:00Z", + "screenTemperature": 15.57, + "maxScreenAirTemp": 15.69, + "minScreenAirTemp": 15.3, + "screenDewPointTemperature": 12.54, + "feelsLikeTemperature": 11.45, + "windSpeed10m": 10.03, + "windDirectionFrom10m": 200, + "windGustSpeed10m": 19, + "max10mWindGust": 19, + "visibility": 19911, + "screenRelativeHumidity": 82.47, + "mslp": 99221, + "uvIndex": 1, + "significantWeatherCode": 15, + "precipitationRate": 0.83, + "totalPrecipAmount": 0.18, + "totalSnowAmount": 0, + "probOfPrecipitation": 81 + }, + { + "time": "2024-11-24T13:00Z", + "screenTemperature": 15.19, + "maxScreenAirTemp": 15.57, + "minScreenAirTemp": 15.16, + "screenDewPointTemperature": 12.23, + "feelsLikeTemperature": 10.93, + "windSpeed10m": 10.47, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 19.58, + "max10mWindGust": 19.58, + "visibility": 23634, + "screenRelativeHumidity": 82.75, + "mslp": 99230, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.66, + "totalPrecipAmount": 0.15, + "totalSnowAmount": 0, + "probOfPrecipitation": 59 + }, + { + "time": "2024-11-24T14:00Z", + "screenTemperature": 15.16, + "maxScreenAirTemp": 15.19, + "minScreenAirTemp": 15.08, + "screenDewPointTemperature": 11.92, + "feelsLikeTemperature": 10.99, + "windSpeed10m": 10.24, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 19.19, + "max10mWindGust": 19.19, + "visibility": 29843, + "screenRelativeHumidity": 81.2, + "mslp": 99230, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 12 + }, + { + "time": "2024-11-24T15:00Z", + "screenTemperature": 14.97, + "maxScreenAirTemp": 15.16, + "minScreenAirTemp": 14.96, + "screenDewPointTemperature": 11.65, + "feelsLikeTemperature": 10.99, + "windSpeed10m": 9.74, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 18.27, + "max10mWindGust": 18.82, + "visibility": 23608, + "screenRelativeHumidity": 80.72, + "mslp": 99239, + "uvIndex": 1, + "significantWeatherCode": 12, + "precipitationRate": 0.31, + "totalPrecipAmount": 0.07, + "totalSnowAmount": 0, + "probOfPrecipitation": 45 + }, + { + "time": "2024-11-24T16:00Z", + "screenTemperature": 14.76, + "maxScreenAirTemp": 14.97, + "minScreenAirTemp": 14.71, + "screenDewPointTemperature": 11.45, + "feelsLikeTemperature": 10.96, + "windSpeed10m": 9.42, + "windDirectionFrom10m": 200, + "windGustSpeed10m": 17.72, + "max10mWindGust": 17.84, + "visibility": 30385, + "screenRelativeHumidity": 80.72, + "mslp": 99229, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.18, + "totalPrecipAmount": 0.16, + "totalSnowAmount": 0, + "probOfPrecipitation": 48 + }, + { + "time": "2024-11-24T17:00Z", + "screenTemperature": 14.38, + "maxScreenAirTemp": 14.76, + "minScreenAirTemp": 14.31, + "screenDewPointTemperature": 11.36, + "feelsLikeTemperature": 10.87, + "windSpeed10m": 8.71, + "windDirectionFrom10m": 199, + "windGustSpeed10m": 16.39, + "max10mWindGust": 17.72, + "visibility": 26409, + "screenRelativeHumidity": 82.26, + "mslp": 99211, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.55, + "totalPrecipAmount": 0.51, + "totalSnowAmount": 0, + "probOfPrecipitation": 50 + }, + { + "time": "2024-11-24T18:00Z", + "screenTemperature": 14.27, + "maxScreenAirTemp": 14.38, + "minScreenAirTemp": 14.21, + "screenDewPointTemperature": 11.11, + "feelsLikeTemperature": 10.84, + "windSpeed10m": 8.56, + "windDirectionFrom10m": 196, + "windGustSpeed10m": 16.09, + "max10mWindGust": 16.09, + "visibility": 23645, + "screenRelativeHumidity": 81.33, + "mslp": 99164, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.43, + "totalPrecipAmount": 0.15, + "totalSnowAmount": 0, + "probOfPrecipitation": 55 + }, + { + "time": "2024-11-24T19:00Z", + "screenTemperature": 14.08, + "maxScreenAirTemp": 14.27, + "minScreenAirTemp": 14.07, + "screenDewPointTemperature": 10.51, + "feelsLikeTemperature": 10.35, + "windSpeed10m": 9.18, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 17.08, + "max10mWindGust": 17.08, + "visibility": 28936, + "screenRelativeHumidity": 79.25, + "mslp": 99127, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.3, + "totalPrecipAmount": 0.18, + "totalSnowAmount": 0, + "probOfPrecipitation": 43 + }, + { + "time": "2024-11-24T20:00Z", + "screenTemperature": 13, + "maxScreenAirTemp": 14.08, + "minScreenAirTemp": 12.95, + "screenDewPointTemperature": 10.35, + "feelsLikeTemperature": 9.56, + "windSpeed10m": 8.42, + "windDirectionFrom10m": 215, + "windGustSpeed10m": 15.63, + "max10mWindGust": 16.07, + "visibility": 12200, + "screenRelativeHumidity": 84.28, + "mslp": 99154, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.97, + "totalPrecipAmount": 0.21, + "totalSnowAmount": 0, + "probOfPrecipitation": 56 + }, + { + "time": "2024-11-24T21:00Z", + "screenTemperature": 11.88, + "maxScreenAirTemp": 13, + "minScreenAirTemp": 11.87, + "screenDewPointTemperature": 10.08, + "feelsLikeTemperature": 9.07, + "windSpeed10m": 6.7, + "windDirectionFrom10m": 221, + "windGustSpeed10m": 12.78, + "max10mWindGust": 13.87, + "visibility": 10227, + "screenRelativeHumidity": 88.76, + "mslp": 99182, + "uvIndex": 0, + "significantWeatherCode": 15, + "precipitationRate": 1.04, + "totalPrecipAmount": 0.46, + "totalSnowAmount": 0, + "probOfPrecipitation": 86 + }, + { + "time": "2024-11-24T22:00Z", + "screenTemperature": 11.28, + "maxScreenAirTemp": 11.88, + "minScreenAirTemp": 11.24, + "screenDewPointTemperature": 9.54, + "feelsLikeTemperature": 8.44, + "windSpeed10m": 6.56, + "windDirectionFrom10m": 218, + "windGustSpeed10m": 12.47, + "max10mWindGust": 12.47, + "visibility": 12135, + "screenRelativeHumidity": 89.13, + "mslp": 99229, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.45, + "totalPrecipAmount": 0.35, + "totalSnowAmount": 0, + "probOfPrecipitation": 58 + }, + { + "time": "2024-11-24T23:00Z", + "screenTemperature": 10.8, + "maxScreenAirTemp": 11.28, + "minScreenAirTemp": 10.78, + "screenDewPointTemperature": 8.75, + "feelsLikeTemperature": 7.88, + "windSpeed10m": 6.7, + "windDirectionFrom10m": 212, + "windGustSpeed10m": 12.96, + "max10mWindGust": 12.96, + "visibility": 36419, + "screenRelativeHumidity": 87.18, + "mslp": 99267, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.3, + "totalPrecipAmount": 0.43, + "totalSnowAmount": 0, + "probOfPrecipitation": 52 + }, + { + "time": "2024-11-25T00:00Z", + "screenTemperature": 10.58, + "maxScreenAirTemp": 10.8, + "minScreenAirTemp": 10.56, + "screenDewPointTemperature": 8.06, + "feelsLikeTemperature": 7.78, + "windSpeed10m": 6.3, + "windDirectionFrom10m": 214, + "windGustSpeed10m": 12.27, + "max10mWindGust": 12.27, + "visibility": 44678, + "screenRelativeHumidity": 84.49, + "mslp": 99278, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.25, + "totalPrecipAmount": 0.31, + "totalSnowAmount": 0, + "probOfPrecipitation": 43 + }, + { + "time": "2024-11-25T01:00Z", + "screenTemperature": 10.49, + "maxScreenAirTemp": 10.58, + "minScreenAirTemp": 10.48, + "screenDewPointTemperature": 7.77, + "feelsLikeTemperature": 7.63, + "windSpeed10m": 6.36, + "windDirectionFrom10m": 211, + "windGustSpeed10m": 12.3, + "max10mWindGust": 12.3, + "visibility": 43617, + "screenRelativeHumidity": 83.39, + "mslp": 99278, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.42, + "totalPrecipAmount": 0.23, + "totalSnowAmount": 0, + "probOfPrecipitation": 54 + }, + { + "time": "2024-11-25T02:00Z", + "screenTemperature": 10.18, + "maxScreenAirTemp": 10.49, + "minScreenAirTemp": 10.18, + "screenDewPointTemperature": 7.81, + "feelsLikeTemperature": 7.27, + "windSpeed10m": 6.43, + "windDirectionFrom10m": 204, + "windGustSpeed10m": 12.41, + "max10mWindGust": 12.41, + "visibility": 35252, + "screenRelativeHumidity": 85.21, + "mslp": 99287, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 23 + }, + { + "time": "2024-11-25T03:00Z", + "screenTemperature": 10.14, + "maxScreenAirTemp": 10.18, + "minScreenAirTemp": 10.12, + "screenDewPointTemperature": 7.49, + "feelsLikeTemperature": 7.27, + "windSpeed10m": 6.3, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 12.31, + "max10mWindGust": 12.85, + "visibility": 47099, + "screenRelativeHumidity": 83.6, + "mslp": 99279, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 8 + }, + { + "time": "2024-11-25T04:00Z", + "screenTemperature": 10.13, + "maxScreenAirTemp": 10.17, + "minScreenAirTemp": 10.11, + "screenDewPointTemperature": 7.42, + "feelsLikeTemperature": 7.26, + "windSpeed10m": 6.26, + "windDirectionFrom10m": 205, + "windGustSpeed10m": 12.08, + "max10mWindGust": 12.9, + "visibility": 44698, + "screenRelativeHumidity": 83.37, + "mslp": 99289, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 9 + }, + { + "time": "2024-11-25T05:00Z", + "screenTemperature": 10.09, + "maxScreenAirTemp": 10.13, + "minScreenAirTemp": 10.06, + "screenDewPointTemperature": 7.42, + "feelsLikeTemperature": 7.26, + "windSpeed10m": 6.12, + "windDirectionFrom10m": 206, + "windGustSpeed10m": 11.81, + "max10mWindGust": 12.36, + "visibility": 43814, + "screenRelativeHumidity": 83.54, + "mslp": 99299, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 6 + }, + { + "time": "2024-11-25T06:00Z", + "screenTemperature": 9.98, + "maxScreenAirTemp": 10.22, + "minScreenAirTemp": 9.97, + "screenDewPointTemperature": 7.16, + "feelsLikeTemperature": 7.23, + "windSpeed10m": 5.83, + "windDirectionFrom10m": 207, + "windGustSpeed10m": 11.26, + "max10mWindGust": 11.75, + "visibility": 41476, + "screenRelativeHumidity": 82.68, + "mslp": 99327, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 6 + }, + { + "time": "2024-11-25T07:00Z", + "screenTemperature": 9.89, + "maxScreenAirTemp": 9.98, + "minScreenAirTemp": 9.87, + "screenDewPointTemperature": 7.04, + "feelsLikeTemperature": 7.13, + "windSpeed10m": 5.82, + "windDirectionFrom10m": 211, + "windGustSpeed10m": 11.19, + "max10mWindGust": 11.19, + "visibility": 39207, + "screenRelativeHumidity": 82.5, + "mslp": 99379, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 7 + }, + { + "time": "2024-11-25T08:00Z", + "screenTemperature": 9.76, + "maxScreenAirTemp": 9.89, + "minScreenAirTemp": 9.76, + "screenDewPointTemperature": 6.73, + "feelsLikeTemperature": 6.95, + "windSpeed10m": 5.85, + "windDirectionFrom10m": 215, + "windGustSpeed10m": 11.33, + "max10mWindGust": 11.33, + "visibility": 38949, + "screenRelativeHumidity": 81.47, + "mslp": 99458, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, + { + "time": "2024-11-25T09:00Z", + "screenTemperature": 9.74, + "maxScreenAirTemp": 9.77, + "minScreenAirTemp": 9.74, + "screenDewPointTemperature": 6.68, + "feelsLikeTemperature": 6.87, + "windSpeed10m": 6.07, + "windDirectionFrom10m": 218, + "windGustSpeed10m": 11.44, + "max10mWindGust": 11.44, + "visibility": 38081, + "screenRelativeHumidity": 81.26, + "mslp": 99536, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, + { + "time": "2024-11-25T10:00Z", + "screenTemperature": 10.07, + "maxScreenAirTemp": 10.07, + "minScreenAirTemp": 9.74, + "screenDewPointTemperature": 6.4, + "feelsLikeTemperature": 7.15, + "windSpeed10m": 6.35, + "windDirectionFrom10m": 223, + "windGustSpeed10m": 11.73, + "max10mWindGust": 11.73, + "visibility": 37260, + "screenRelativeHumidity": 78.14, + "mslp": 99596, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T11:00Z", + "screenTemperature": 10.37, + "maxScreenAirTemp": 10.42, + "minScreenAirTemp": 10.07, + "screenDewPointTemperature": 5.91, + "feelsLikeTemperature": 7.4, + "windSpeed10m": 6.62, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 12.04, + "max10mWindGust": 12.04, + "visibility": 37321, + "screenRelativeHumidity": 74, + "mslp": 99664, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2024-11-25T12:00Z", + "screenTemperature": 10.72, + "screenDewPointTemperature": 5.47, + "feelsLikeTemperature": 7.72, + "windSpeed10m": 6.91, + "windDirectionFrom10m": 233, + "windGustSpeed10m": 12.61, + "visibility": 38960, + "screenRelativeHumidity": 70.02, + "mslp": 99715, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "probOfPrecipitation": 1 + } + ] + } + } + ], + "parameters": [ + { + "totalSnowAmount": { + "type": "Parameter", + "description": "Total Snow Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "screenTemperature": { + "type": "Parameter", + "description": "Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "visibility": { + "type": "Parameter", + "description": "Visibility", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "windDirectionFrom10m": { + "type": "Parameter", + "description": "10m Wind From Direction", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "precipitationRate": { + "type": "Parameter", + "description": "Precipitation Rate", + "unit": { + "label": "millimetres per hour", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm/h" + } + } + }, + "maxScreenAirTemp": { + "type": "Parameter", + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "feelsLikeTemperature": { + "type": "Parameter", + "description": "Feels Like Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenDewPointTemperature": { + "type": "Parameter", + "description": "Screen Dew Point Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenRelativeHumidity": { + "type": "Parameter", + "description": "Screen Relative Humidity", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windSpeed10m": { + "type": "Parameter", + "description": "10m Wind Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "probOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "max10mWindGust": { + "type": "Parameter", + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "significantWeatherCode": { + "type": "Parameter", + "description": "Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://datahub.metoffice.gov.uk/", + "type": "1" + } + } + }, + "minScreenAirTemp": { + "type": "Parameter", + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "totalPrecipAmount": { + "type": "Parameter", + "description": "Total Precipitation Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "mslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "windGustSpeed10m": { + "type": "Parameter", + "description": "10m Wind Gust Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "uvIndex": { + "type": "Parameter", + "description": "UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + } + } + ] } } diff --git a/tests/components/metoffice/snapshots/test_weather.ambr b/tests/components/metoffice/snapshots/test_weather.ambr index 0bbc0e06a0a..a567f9bde74 100644 --- a/tests/components/metoffice/snapshots/test_weather.ambr +++ b/tests/components/metoffice/snapshots/test_weather.ambr @@ -1,39 +1,91 @@ # serializer version: 1 # name: test_forecast_service[get_forecasts] dict({ - 'weather.met_office_wavertree_daily': dict({ + 'weather.met_office_wavertree': dict({ 'forecast': list([ dict({ + 'apparent_temperature': 9.2, 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 26, + 'pressure': 987.48, + 'temperature': 12.7, + 'templow': 8.2, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, }), dict({ + 'apparent_temperature': 5.3, 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 5, + 'pressure': 994.88, + 'temperature': 9.8, + 'templow': 7.7, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, + 'apparent_temperature': 5.9, + 'condition': 'partlycloudy', + 'datetime': '2024-11-26T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1012.93, + 'temperature': 8.7, + 'templow': 3.8, + 'uv_index': 1, + 'wind_bearing': 265, + 'wind_gust_speed': 34.49, + 'wind_speed': 20.45, }), dict({ + 'apparent_temperature': 3.3, 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, + 'datetime': '2024-11-27T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 43, + 'pressure': 1014.39, + 'temperature': 6.7, + 'templow': 2.4, + 'uv_index': 1, + 'wind_bearing': 8, + 'wind_gust_speed': 32.18, + 'wind_speed': 18.54, + }), + dict({ + 'apparent_temperature': 3.0, + 'condition': 'cloudy', + 'datetime': '2024-11-28T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1025.12, + 'temperature': 5.7, + 'templow': 3.8, + 'uv_index': 1, + 'wind_bearing': 104, + 'wind_gust_speed': 22.36, + 'wind_speed': 12.64, + }), + dict({ + 'apparent_temperature': 4.9, + 'condition': 'cloudy', + 'datetime': '2024-11-29T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 11, + 'pressure': 1019.85, + 'temperature': 8.2, + 'templow': 7.0, + 'uv_index': 1, + 'wind_bearing': 137, + 'wind_gust_speed': 38.59, + 'wind_speed': 23.0, }), ]), }), @@ -41,287 +93,631 @@ # --- # name: test_forecast_service[get_forecasts].1 dict({ - 'weather.met_office_wavertree_daily': dict({ + 'weather.met_office_wavertree': dict({ 'forecast': list([ dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, + 'apparent_temperature': 6.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T13:00:00+00:00', + 'precipitation': 0.52, + 'precipitation_probability': 65, + 'pressure': 986.83, + 'temperature': 9.9, + 'uv_index': 1, + 'wind_bearing': 178, + 'wind_gust_speed': 55.73, + 'wind_speed': 25.42, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, + 'apparent_temperature': 8.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.34, + 'temperature': 11.1, + 'uv_index': 1, + 'wind_bearing': 179, + 'wind_gust_speed': 49.0, + 'wind_speed': 22.86, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, + 'apparent_temperature': 9.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T15:00:00+00:00', + 'precipitation': 0.09, + 'precipitation_probability': 37, + 'pressure': 986.13, 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'uv_index': 1, + 'wind_bearing': 182, + 'wind_gust_speed': 40.1, + 'wind_speed': 18.5, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, + 'apparent_temperature': 10.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T16:00:00+00:00', + 'precipitation': 0.27, + 'precipitation_probability': 36, + 'pressure': 986.6, + 'temperature': 12.6, + 'uv_index': 0, + 'wind_bearing': 197, + 'wind_gust_speed': 35.86, + 'wind_speed': 15.44, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', + 'datetime': '2024-11-23T17:00:00+00:00', + 'precipitation': 0.0, 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'pressure': 987.1, + 'temperature': 12.9, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 35.57, + 'wind_speed': 15.59, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, + 'datetime': '2024-11-23T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.1, 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 31.21, + 'wind_speed': 15.52, }), dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, + 'apparent_temperature': 11.1, + 'condition': 'pouring', + 'datetime': '2024-11-23T19:00:00+00:00', + 'precipitation': 0.51, + 'precipitation_probability': 74, + 'pressure': 986.82, + 'temperature': 13.0, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 37.44, + 'wind_speed': 17.46, }), dict({ + 'apparent_temperature': 11.2, 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, + 'datetime': '2024-11-23T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 986.92, + 'temperature': 13.7, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 45.97, + 'wind_speed': 22.72, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-23T21:00:00+00:00', + 'precipitation': 0.11, + 'precipitation_probability': 30, + 'pressure': 986.82, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 178, + 'wind_gust_speed': 44.32, + 'wind_speed': 22.0, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.31, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 47.84, + 'wind_speed': 23.65, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 985.71, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 51.44, + 'wind_speed': 26.57, + }), + dict({ + 'apparent_temperature': 11.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 984.92, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 171, + 'wind_gust_speed': 50.69, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 11.6, + 'condition': 'rainy', + 'datetime': '2024-11-24T01:00:00+00:00', + 'precipitation': 0.17, + 'precipitation_probability': 40, + 'pressure': 984.22, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 170, + 'wind_gust_speed': 50.11, + 'wind_speed': 25.78, + }), + dict({ + 'apparent_temperature': 11.3, + 'condition': 'pouring', + 'datetime': '2024-11-24T02:00:00+00:00', + 'precipitation': 0.21, + 'precipitation_probability': 74, + 'pressure': 983.51, + 'temperature': 14.2, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 52.06, + 'wind_speed': 26.89, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'pouring', + 'datetime': '2024-11-24T03:00:00+00:00', + 'precipitation': 0.34, + 'precipitation_probability': 73, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 51.55, + 'wind_speed': 26.1, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T04:00:00+00:00', + 'precipitation': 0.28, + 'precipitation_probability': 50, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 189, + 'wind_gust_speed': 49.68, + 'wind_speed': 25.52, + }), + dict({ + 'apparent_temperature': 11.8, + 'condition': 'rainy', + 'datetime': '2024-11-24T05:00:00+00:00', + 'precipitation': 0.25, + 'precipitation_probability': 47, + 'pressure': 983.3, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.72, + 'wind_speed': 23.69, + }), + dict({ + 'apparent_temperature': 10.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T06:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 42, + 'pressure': 983.96, + 'temperature': 13.4, + 'uv_index': 0, + 'wind_bearing': 216, + 'wind_gust_speed': 45.83, + 'wind_speed': 24.16, + }), + dict({ + 'apparent_temperature': 10.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T07:00:00+00:00', + 'precipitation': 0.16, + 'precipitation_probability': 40, + 'pressure': 984.58, + 'temperature': 12.5, + 'uv_index': 0, + 'wind_bearing': 214, + 'wind_gust_speed': 39.71, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.5, + 'condition': 'rainy', + 'datetime': '2024-11-24T08:00:00+00:00', + 'precipitation': 0.08, + 'precipitation_probability': 38, + 'pressure': 985.48, + 'temperature': 11.9, + 'uv_index': 0, + 'wind_bearing': 209, + 'wind_gust_speed': 37.08, + 'wind_speed': 19.73, + }), + dict({ + 'apparent_temperature': 9.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T09:00:00+00:00', + 'precipitation': 0.04, + 'precipitation_probability': 26, + 'pressure': 986.38, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 35.96, + 'wind_speed': 19.58, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'rainy', + 'datetime': '2024-11-24T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'pressure': 986.96, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 37.01, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 6, + 'pressure': 987.55, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 36.22, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.48, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 8.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T13:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.57, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 45.36, + 'wind_speed': 25.45, + }), + dict({ + 'apparent_temperature': 8.6, + 'condition': 'cloudy', + 'datetime': '2024-11-24T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.37, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 46.94, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 8.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T15:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.27, + 'temperature': 11.6, + 'uv_index': 1, + 'wind_bearing': 198, + 'wind_gust_speed': 46.87, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 8.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T16:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.08, + 'temperature': 11.2, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.22, + 'wind_speed': 25.38, + }), + dict({ + 'apparent_temperature': 8.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T17:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 986.89, 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 194, + 'wind_gust_speed': 45.68, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.7, + 'temperature': 10.9, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.15, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T19:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.69, + 'temperature': 10.8, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 45.43, + 'wind_speed': 24.95, + }), + dict({ + 'apparent_temperature': 7.7, + 'condition': 'cloudy', + 'datetime': '2024-11-24T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.78, + 'temperature': 10.7, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.07, + 'wind_speed': 24.55, + }), + dict({ + 'apparent_temperature': 7.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T21:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.77, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 46.4, + 'wind_speed': 25.49, + }), + dict({ + 'apparent_temperature': 7.3, + 'condition': 'cloudy', + 'datetime': '2024-11-24T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.04, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 204, + 'wind_gust_speed': 48.24, + 'wind_speed': 26.68, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-24T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.04, + 'temperature': 10.3, + 'uv_index': 0, + 'wind_bearing': 207, + 'wind_gust_speed': 50.44, + 'wind_speed': 27.72, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.2, + 'uv_index': 0, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 6.9, + 'condition': 'cloudy', + 'datetime': '2024-11-25T01:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 987.41, + 'temperature': 10.0, + 'uv_index': 0, + 'wind_bearing': 215, + 'wind_gust_speed': 45.04, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 6.4, + 'condition': 'cloudy', + 'datetime': '2024-11-25T02:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.88, + 'temperature': 9.6, + 'uv_index': 0, + 'wind_bearing': 222, + 'wind_gust_speed': 46.87, + 'wind_speed': 25.7, + }), + dict({ + 'apparent_temperature': 6.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T03:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.16, + 'temperature': 9.3, + 'uv_index': 0, + 'wind_bearing': 226, + 'wind_gust_speed': 44.71, + 'wind_speed': 24.88, + }), + dict({ + 'apparent_temperature': 5.8, + 'condition': 'cloudy', + 'datetime': '2024-11-25T04:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.58, + 'temperature': 9.1, + 'uv_index': 0, + 'wind_bearing': 228, + 'wind_gust_speed': 45.22, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 5.4, + 'condition': 'clear-night', + 'datetime': '2024-11-25T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.1, + 'temperature': 8.8, + 'uv_index': 0, + 'wind_bearing': 232, + 'wind_gust_speed': 47.23, + 'wind_speed': 26.14, + }), + dict({ + 'apparent_temperature': 5.1, + 'condition': 'clear-night', + 'datetime': '2024-11-25T06:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.61, + 'temperature': 8.7, + 'uv_index': 0, + 'wind_bearing': 235, + 'wind_gust_speed': 48.2, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'clear-night', + 'datetime': '2024-11-25T07:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 990.61, + 'temperature': 8.6, + 'uv_index': 0, + 'wind_bearing': 240, + 'wind_gust_speed': 47.81, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'clear-night', + 'datetime': '2024-11-25T08:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 991.61, + 'temperature': 8.4, + 'uv_index': 0, + 'wind_bearing': 243, + 'wind_gust_speed': 47.56, + 'wind_speed': 26.86, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'sunny', + 'datetime': '2024-11-25T09:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 992.52, + 'temperature': 8.4, + 'uv_index': 1, + 'wind_bearing': 243, + 'wind_gust_speed': 47.84, + 'wind_speed': 27.32, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 993.42, + 'temperature': 8.7, + 'uv_index': 1, + 'wind_bearing': 245, + 'wind_gust_speed': 49.79, + 'wind_speed': 28.8, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'pressure': 994.24, + 'temperature': 8.8, + 'uv_index': 1, + 'wind_bearing': 249, + 'wind_gust_speed': 52.09, + 'wind_speed': 30.38, + }), + dict({ + 'apparent_temperature': 5.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 2, + 'pressure': 994.88, + 'temperature': 8.9, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), ]), }), @@ -329,39 +725,91 @@ # --- # name: test_forecast_service[get_forecasts].2 dict({ - 'weather.met_office_wavertree_daily': dict({ + 'weather.met_office_wavertree': dict({ 'forecast': list([ dict({ + 'apparent_temperature': 9.2, 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 26, + 'pressure': 987.48, + 'temperature': 12.7, + 'templow': 8.2, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, }), dict({ + 'apparent_temperature': 5.3, 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 5, + 'pressure': 994.88, + 'temperature': 9.8, + 'templow': 7.7, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, + 'apparent_temperature': 5.9, + 'condition': 'partlycloudy', + 'datetime': '2024-11-26T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1012.93, + 'temperature': 8.7, + 'templow': 3.8, + 'uv_index': 1, + 'wind_bearing': 265, + 'wind_gust_speed': 34.49, + 'wind_speed': 20.45, }), dict({ + 'apparent_temperature': 3.3, 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, + 'datetime': '2024-11-27T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 43, + 'pressure': 1014.39, + 'temperature': 6.7, + 'templow': 2.4, + 'uv_index': 1, + 'wind_bearing': 8, + 'wind_gust_speed': 32.18, + 'wind_speed': 18.54, + }), + dict({ + 'apparent_temperature': 3.0, + 'condition': 'cloudy', + 'datetime': '2024-11-28T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1025.12, + 'temperature': 5.7, + 'templow': 3.8, + 'uv_index': 1, + 'wind_bearing': 104, + 'wind_gust_speed': 22.36, + 'wind_speed': 12.64, + }), + dict({ + 'apparent_temperature': 4.9, + 'condition': 'cloudy', + 'datetime': '2024-11-29T00:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 11, + 'pressure': 1019.85, + 'temperature': 8.2, + 'templow': 7.0, + 'uv_index': 1, + 'wind_bearing': 137, + 'wind_gust_speed': 38.59, + 'wind_speed': 23.0, }), ]), }), @@ -369,937 +817,1889 @@ # --- # name: test_forecast_service[get_forecasts].3 dict({ - 'weather.met_office_wavertree_daily': dict({ + 'weather.met_office_wavertree': dict({ 'forecast': list([ dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, + 'apparent_temperature': 6.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T13:00:00+00:00', + 'precipitation': 0.52, + 'precipitation_probability': 65, + 'pressure': 986.83, + 'temperature': 9.9, + 'uv_index': 1, + 'wind_bearing': 178, + 'wind_gust_speed': 55.73, + 'wind_speed': 25.42, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, + 'apparent_temperature': 8.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.34, + 'temperature': 11.1, + 'uv_index': 1, + 'wind_bearing': 179, + 'wind_gust_speed': 49.0, + 'wind_speed': 22.86, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, + 'apparent_temperature': 9.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T15:00:00+00:00', + 'precipitation': 0.09, + 'precipitation_probability': 37, + 'pressure': 986.13, 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'uv_index': 1, + 'wind_bearing': 182, + 'wind_gust_speed': 40.1, + 'wind_speed': 18.5, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, + 'apparent_temperature': 10.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T16:00:00+00:00', + 'precipitation': 0.27, + 'precipitation_probability': 36, + 'pressure': 986.6, + 'temperature': 12.6, + 'uv_index': 0, + 'wind_bearing': 197, + 'wind_gust_speed': 35.86, + 'wind_speed': 15.44, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', + 'datetime': '2024-11-23T17:00:00+00:00', + 'precipitation': 0.0, 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'pressure': 987.1, + 'temperature': 12.9, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 35.57, + 'wind_speed': 15.59, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, + 'datetime': '2024-11-23T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.1, 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 31.21, + 'wind_speed': 15.52, }), dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, + 'apparent_temperature': 11.1, + 'condition': 'pouring', + 'datetime': '2024-11-23T19:00:00+00:00', + 'precipitation': 0.51, + 'precipitation_probability': 74, + 'pressure': 986.82, + 'temperature': 13.0, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 37.44, + 'wind_speed': 17.46, }), dict({ + 'apparent_temperature': 11.2, 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, + 'datetime': '2024-11-23T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 986.92, + 'temperature': 13.7, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 45.97, + 'wind_speed': 22.72, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-23T21:00:00+00:00', + 'precipitation': 0.11, + 'precipitation_probability': 30, + 'pressure': 986.82, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 178, + 'wind_gust_speed': 44.32, + 'wind_speed': 22.0, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.31, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 47.84, + 'wind_speed': 23.65, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 985.71, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 51.44, + 'wind_speed': 26.57, + }), + dict({ + 'apparent_temperature': 11.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 984.92, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 171, + 'wind_gust_speed': 50.69, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 11.6, + 'condition': 'rainy', + 'datetime': '2024-11-24T01:00:00+00:00', + 'precipitation': 0.17, + 'precipitation_probability': 40, + 'pressure': 984.22, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 170, + 'wind_gust_speed': 50.11, + 'wind_speed': 25.78, + }), + dict({ + 'apparent_temperature': 11.3, + 'condition': 'pouring', + 'datetime': '2024-11-24T02:00:00+00:00', + 'precipitation': 0.21, + 'precipitation_probability': 74, + 'pressure': 983.51, + 'temperature': 14.2, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 52.06, + 'wind_speed': 26.89, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'pouring', + 'datetime': '2024-11-24T03:00:00+00:00', + 'precipitation': 0.34, + 'precipitation_probability': 73, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 51.55, + 'wind_speed': 26.1, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T04:00:00+00:00', + 'precipitation': 0.28, + 'precipitation_probability': 50, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 189, + 'wind_gust_speed': 49.68, + 'wind_speed': 25.52, + }), + dict({ + 'apparent_temperature': 11.8, + 'condition': 'rainy', + 'datetime': '2024-11-24T05:00:00+00:00', + 'precipitation': 0.25, + 'precipitation_probability': 47, + 'pressure': 983.3, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.72, + 'wind_speed': 23.69, + }), + dict({ + 'apparent_temperature': 10.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T06:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 42, + 'pressure': 983.96, + 'temperature': 13.4, + 'uv_index': 0, + 'wind_bearing': 216, + 'wind_gust_speed': 45.83, + 'wind_speed': 24.16, + }), + dict({ + 'apparent_temperature': 10.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T07:00:00+00:00', + 'precipitation': 0.16, + 'precipitation_probability': 40, + 'pressure': 984.58, + 'temperature': 12.5, + 'uv_index': 0, + 'wind_bearing': 214, + 'wind_gust_speed': 39.71, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.5, + 'condition': 'rainy', + 'datetime': '2024-11-24T08:00:00+00:00', + 'precipitation': 0.08, + 'precipitation_probability': 38, + 'pressure': 985.48, + 'temperature': 11.9, + 'uv_index': 0, + 'wind_bearing': 209, + 'wind_gust_speed': 37.08, + 'wind_speed': 19.73, + }), + dict({ + 'apparent_temperature': 9.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T09:00:00+00:00', + 'precipitation': 0.04, + 'precipitation_probability': 26, + 'pressure': 986.38, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 35.96, + 'wind_speed': 19.58, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'rainy', + 'datetime': '2024-11-24T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'pressure': 986.96, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 37.01, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 6, + 'pressure': 987.55, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 36.22, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.48, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 8.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T13:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.57, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 45.36, + 'wind_speed': 25.45, + }), + dict({ + 'apparent_temperature': 8.6, + 'condition': 'cloudy', + 'datetime': '2024-11-24T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.37, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 46.94, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 8.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T15:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.27, + 'temperature': 11.6, + 'uv_index': 1, + 'wind_bearing': 198, + 'wind_gust_speed': 46.87, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 8.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T16:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.08, + 'temperature': 11.2, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.22, + 'wind_speed': 25.38, + }), + dict({ + 'apparent_temperature': 8.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T17:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 986.89, 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 194, + 'wind_gust_speed': 45.68, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.7, + 'temperature': 10.9, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.15, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T19:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.69, + 'temperature': 10.8, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 45.43, + 'wind_speed': 24.95, + }), + dict({ + 'apparent_temperature': 7.7, + 'condition': 'cloudy', + 'datetime': '2024-11-24T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.78, + 'temperature': 10.7, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.07, + 'wind_speed': 24.55, + }), + dict({ + 'apparent_temperature': 7.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T21:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.77, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 46.4, + 'wind_speed': 25.49, + }), + dict({ + 'apparent_temperature': 7.3, + 'condition': 'cloudy', + 'datetime': '2024-11-24T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.04, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 204, + 'wind_gust_speed': 48.24, + 'wind_speed': 26.68, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-24T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.04, + 'temperature': 10.3, + 'uv_index': 0, + 'wind_bearing': 207, + 'wind_gust_speed': 50.44, + 'wind_speed': 27.72, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.2, + 'uv_index': 0, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 6.9, + 'condition': 'cloudy', + 'datetime': '2024-11-25T01:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 987.41, + 'temperature': 10.0, + 'uv_index': 0, + 'wind_bearing': 215, + 'wind_gust_speed': 45.04, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 6.4, + 'condition': 'cloudy', + 'datetime': '2024-11-25T02:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.88, + 'temperature': 9.6, + 'uv_index': 0, + 'wind_bearing': 222, + 'wind_gust_speed': 46.87, + 'wind_speed': 25.7, + }), + dict({ + 'apparent_temperature': 6.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T03:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.16, + 'temperature': 9.3, + 'uv_index': 0, + 'wind_bearing': 226, + 'wind_gust_speed': 44.71, + 'wind_speed': 24.88, + }), + dict({ + 'apparent_temperature': 5.8, + 'condition': 'cloudy', + 'datetime': '2024-11-25T04:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.58, + 'temperature': 9.1, + 'uv_index': 0, + 'wind_bearing': 228, + 'wind_gust_speed': 45.22, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 5.4, + 'condition': 'clear-night', + 'datetime': '2024-11-25T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.1, + 'temperature': 8.8, + 'uv_index': 0, + 'wind_bearing': 232, + 'wind_gust_speed': 47.23, + 'wind_speed': 26.14, + }), + dict({ + 'apparent_temperature': 5.1, + 'condition': 'clear-night', + 'datetime': '2024-11-25T06:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.61, + 'temperature': 8.7, + 'uv_index': 0, + 'wind_bearing': 235, + 'wind_gust_speed': 48.2, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'clear-night', + 'datetime': '2024-11-25T07:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 990.61, + 'temperature': 8.6, + 'uv_index': 0, + 'wind_bearing': 240, + 'wind_gust_speed': 47.81, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'clear-night', + 'datetime': '2024-11-25T08:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 991.61, + 'temperature': 8.4, + 'uv_index': 0, + 'wind_bearing': 243, + 'wind_gust_speed': 47.56, + 'wind_speed': 26.86, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'sunny', + 'datetime': '2024-11-25T09:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 992.52, + 'temperature': 8.4, + 'uv_index': 1, + 'wind_bearing': 243, + 'wind_gust_speed': 47.84, + 'wind_speed': 27.32, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 993.42, + 'temperature': 8.7, + 'uv_index': 1, + 'wind_bearing': 245, + 'wind_gust_speed': 49.79, + 'wind_speed': 28.8, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'pressure': 994.24, + 'temperature': 8.8, + 'uv_index': 1, + 'wind_bearing': 249, + 'wind_gust_speed': 52.09, + 'wind_speed': 30.38, + }), + dict({ + 'apparent_temperature': 5.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 2, + 'pressure': 994.88, + 'temperature': 8.9, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), ]), }), }) # --- -# name: test_forecast_service[get_forecasts].4 - dict({ - 'weather.met_office_wavertree_daily': dict({ - 'forecast': list([ - ]), - }), - }) -# --- -# name: test_forecast_subscription[daily] +# name: test_forecast_subscription list([ dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ + 'apparent_temperature': 6.8, 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, + 'datetime': '2024-11-23T13:00:00+00:00', + 'precipitation': 0.52, + 'precipitation_probability': 65, + 'pressure': 986.83, + 'temperature': 9.9, + 'uv_index': 1, + 'wind_bearing': 178, + 'wind_gust_speed': 55.73, + 'wind_speed': 25.42, }), - ]) -# --- -# name: test_forecast_subscription[daily].1 - list([ dict({ + 'apparent_temperature': 8.4, 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 13.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, + 'datetime': '2024-11-23T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.34, + 'temperature': 11.1, + 'uv_index': 1, + 'wind_bearing': 179, + 'wind_gust_speed': 49.0, + 'wind_speed': 22.86, }), dict({ + 'apparent_temperature': 9.8, 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 59, - 'temperature': 13.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - ]) -# --- -# name: test_forecast_subscription[hourly] - list([ - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, + 'datetime': '2024-11-23T15:00:00+00:00', + 'precipitation': 0.09, + 'precipitation_probability': 37, + 'pressure': 986.13, 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'uv_index': 1, + 'wind_bearing': 182, + 'wind_gust_speed': 40.1, + 'wind_speed': 18.5, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, + 'apparent_temperature': 10.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T16:00:00+00:00', + 'precipitation': 0.27, + 'precipitation_probability': 36, + 'pressure': 986.6, + 'temperature': 12.6, + 'uv_index': 0, + 'wind_bearing': 197, + 'wind_gust_speed': 35.86, + 'wind_speed': 15.44, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', + 'datetime': '2024-11-23T17:00:00+00:00', + 'precipitation': 0.0, 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'pressure': 987.1, + 'temperature': 12.9, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 35.57, + 'wind_speed': 15.59, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, + 'datetime': '2024-11-23T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.1, 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 31.21, + 'wind_speed': 15.52, }), dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, + 'apparent_temperature': 11.1, + 'condition': 'pouring', + 'datetime': '2024-11-23T19:00:00+00:00', + 'precipitation': 0.51, + 'precipitation_probability': 74, + 'pressure': 986.82, + 'temperature': 13.0, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 37.44, + 'wind_speed': 17.46, }), dict({ + 'apparent_temperature': 11.2, 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, + 'datetime': '2024-11-23T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 986.92, + 'temperature': 13.7, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 45.97, + 'wind_speed': 22.72, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-23T21:00:00+00:00', + 'precipitation': 0.11, + 'precipitation_probability': 30, + 'pressure': 986.82, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 178, + 'wind_gust_speed': 44.32, + 'wind_speed': 22.0, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.31, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 47.84, + 'wind_speed': 23.65, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 985.71, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 51.44, + 'wind_speed': 26.57, + }), + dict({ + 'apparent_temperature': 11.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 984.92, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 171, + 'wind_gust_speed': 50.69, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 11.6, + 'condition': 'rainy', + 'datetime': '2024-11-24T01:00:00+00:00', + 'precipitation': 0.17, + 'precipitation_probability': 40, + 'pressure': 984.22, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 170, + 'wind_gust_speed': 50.11, + 'wind_speed': 25.78, + }), + dict({ + 'apparent_temperature': 11.3, + 'condition': 'pouring', + 'datetime': '2024-11-24T02:00:00+00:00', + 'precipitation': 0.21, + 'precipitation_probability': 74, + 'pressure': 983.51, + 'temperature': 14.2, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 52.06, + 'wind_speed': 26.89, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'pouring', + 'datetime': '2024-11-24T03:00:00+00:00', + 'precipitation': 0.34, + 'precipitation_probability': 73, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 51.55, + 'wind_speed': 26.1, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T04:00:00+00:00', + 'precipitation': 0.28, + 'precipitation_probability': 50, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 189, + 'wind_gust_speed': 49.68, + 'wind_speed': 25.52, + }), + dict({ + 'apparent_temperature': 11.8, + 'condition': 'rainy', + 'datetime': '2024-11-24T05:00:00+00:00', + 'precipitation': 0.25, + 'precipitation_probability': 47, + 'pressure': 983.3, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.72, + 'wind_speed': 23.69, + }), + dict({ + 'apparent_temperature': 10.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T06:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 42, + 'pressure': 983.96, + 'temperature': 13.4, + 'uv_index': 0, + 'wind_bearing': 216, + 'wind_gust_speed': 45.83, + 'wind_speed': 24.16, + }), + dict({ + 'apparent_temperature': 10.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T07:00:00+00:00', + 'precipitation': 0.16, + 'precipitation_probability': 40, + 'pressure': 984.58, + 'temperature': 12.5, + 'uv_index': 0, + 'wind_bearing': 214, + 'wind_gust_speed': 39.71, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.5, + 'condition': 'rainy', + 'datetime': '2024-11-24T08:00:00+00:00', + 'precipitation': 0.08, + 'precipitation_probability': 38, + 'pressure': 985.48, + 'temperature': 11.9, + 'uv_index': 0, + 'wind_bearing': 209, + 'wind_gust_speed': 37.08, + 'wind_speed': 19.73, + }), + dict({ + 'apparent_temperature': 9.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T09:00:00+00:00', + 'precipitation': 0.04, + 'precipitation_probability': 26, + 'pressure': 986.38, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 35.96, + 'wind_speed': 19.58, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'rainy', + 'datetime': '2024-11-24T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'pressure': 986.96, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 37.01, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 6, + 'pressure': 987.55, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 36.22, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.48, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 8.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T13:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.57, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 45.36, + 'wind_speed': 25.45, + }), + dict({ + 'apparent_temperature': 8.6, + 'condition': 'cloudy', + 'datetime': '2024-11-24T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.37, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 46.94, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 8.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T15:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.27, + 'temperature': 11.6, + 'uv_index': 1, + 'wind_bearing': 198, + 'wind_gust_speed': 46.87, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 8.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T16:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.08, + 'temperature': 11.2, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.22, + 'wind_speed': 25.38, + }), + dict({ + 'apparent_temperature': 8.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T17:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 986.89, 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 194, + 'wind_gust_speed': 45.68, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.7, + 'temperature': 10.9, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.15, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T19:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.69, + 'temperature': 10.8, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 45.43, + 'wind_speed': 24.95, + }), + dict({ + 'apparent_temperature': 7.7, + 'condition': 'cloudy', + 'datetime': '2024-11-24T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.78, + 'temperature': 10.7, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.07, + 'wind_speed': 24.55, + }), + dict({ + 'apparent_temperature': 7.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T21:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.77, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 46.4, + 'wind_speed': 25.49, + }), + dict({ + 'apparent_temperature': 7.3, + 'condition': 'cloudy', + 'datetime': '2024-11-24T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.04, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 204, + 'wind_gust_speed': 48.24, + 'wind_speed': 26.68, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-24T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.04, + 'temperature': 10.3, + 'uv_index': 0, + 'wind_bearing': 207, + 'wind_gust_speed': 50.44, + 'wind_speed': 27.72, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.2, + 'uv_index': 0, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 6.9, + 'condition': 'cloudy', + 'datetime': '2024-11-25T01:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 987.41, + 'temperature': 10.0, + 'uv_index': 0, + 'wind_bearing': 215, + 'wind_gust_speed': 45.04, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 6.4, + 'condition': 'cloudy', + 'datetime': '2024-11-25T02:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.88, + 'temperature': 9.6, + 'uv_index': 0, + 'wind_bearing': 222, + 'wind_gust_speed': 46.87, + 'wind_speed': 25.7, + }), + dict({ + 'apparent_temperature': 6.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T03:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.16, + 'temperature': 9.3, + 'uv_index': 0, + 'wind_bearing': 226, + 'wind_gust_speed': 44.71, + 'wind_speed': 24.88, + }), + dict({ + 'apparent_temperature': 5.8, + 'condition': 'cloudy', + 'datetime': '2024-11-25T04:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.58, + 'temperature': 9.1, + 'uv_index': 0, + 'wind_bearing': 228, + 'wind_gust_speed': 45.22, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 5.4, + 'condition': 'clear-night', + 'datetime': '2024-11-25T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.1, + 'temperature': 8.8, + 'uv_index': 0, + 'wind_bearing': 232, + 'wind_gust_speed': 47.23, + 'wind_speed': 26.14, + }), + dict({ + 'apparent_temperature': 5.1, + 'condition': 'clear-night', + 'datetime': '2024-11-25T06:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.61, + 'temperature': 8.7, + 'uv_index': 0, + 'wind_bearing': 235, + 'wind_gust_speed': 48.2, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'clear-night', + 'datetime': '2024-11-25T07:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 990.61, + 'temperature': 8.6, + 'uv_index': 0, + 'wind_bearing': 240, + 'wind_gust_speed': 47.81, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'clear-night', + 'datetime': '2024-11-25T08:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 991.61, + 'temperature': 8.4, + 'uv_index': 0, + 'wind_bearing': 243, + 'wind_gust_speed': 47.56, + 'wind_speed': 26.86, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'sunny', + 'datetime': '2024-11-25T09:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 992.52, + 'temperature': 8.4, + 'uv_index': 1, + 'wind_bearing': 243, + 'wind_gust_speed': 47.84, + 'wind_speed': 27.32, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 993.42, + 'temperature': 8.7, + 'uv_index': 1, + 'wind_bearing': 245, + 'wind_gust_speed': 49.79, + 'wind_speed': 28.8, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'pressure': 994.24, + 'temperature': 8.8, + 'uv_index': 1, + 'wind_bearing': 249, + 'wind_gust_speed': 52.09, + 'wind_speed': 30.38, + }), + dict({ + 'apparent_temperature': 5.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 2, + 'pressure': 994.88, + 'temperature': 8.9, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), ]) # --- -# name: test_forecast_subscription[hourly].1 +# name: test_forecast_subscription.1 list([ dict({ - 'condition': 'sunny', - 'datetime': '2020-04-25T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 19.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, + 'apparent_temperature': 6.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T13:00:00+00:00', + 'precipitation': 0.52, + 'precipitation_probability': 65, + 'pressure': 986.83, + 'temperature': 9.9, + 'uv_index': 1, + 'wind_bearing': 178, + 'wind_gust_speed': 55.73, + 'wind_speed': 25.42, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T18:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 17.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, + 'apparent_temperature': 8.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.34, + 'temperature': 11.1, + 'uv_index': 1, + 'wind_bearing': 179, + 'wind_gust_speed': 49.0, + 'wind_speed': 22.86, }), dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-25T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 14.0, - 'wind_bearing': 'NW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T00:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 13.0, - 'wind_bearing': 'WSW', - 'wind_speed': 3.22, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-26T03:00:00+00:00', - 'precipitation_probability': 2, + 'apparent_temperature': 9.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T15:00:00+00:00', + 'precipitation': 0.09, + 'precipitation_probability': 37, + 'pressure': 986.13, 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'uv_index': 1, + 'wind_bearing': 182, + 'wind_gust_speed': 40.1, + 'wind_speed': 18.5, }), dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, + 'apparent_temperature': 10.8, + 'condition': 'rainy', + 'datetime': '2024-11-23T16:00:00+00:00', + 'precipitation': 0.27, + 'precipitation_probability': 36, + 'pressure': 986.6, + 'temperature': 12.6, + 'uv_index': 0, + 'wind_bearing': 197, + 'wind_gust_speed': 35.86, + 'wind_speed': 15.44, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-26T09:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T12:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 12.0, - 'wind_bearing': 'WNW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T15:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 12.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T18:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 11.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-26T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T00:00:00+00:00', + 'datetime': '2024-11-23T17:00:00+00:00', + 'precipitation': 0.0, 'precipitation_probability': 11, - 'temperature': 9.0, - 'wind_bearing': 'WNW', - 'wind_speed': 6.44, + 'pressure': 987.1, + 'temperature': 12.9, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 35.57, + 'wind_speed': 15.59, }), dict({ + 'apparent_temperature': 11.3, 'condition': 'cloudy', - 'datetime': '2020-04-27T03:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 8.0, - 'wind_bearing': 'WNW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-27T06:00:00+00:00', - 'precipitation_probability': 14, - 'temperature': 8.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-27T12:00:00+00:00', - 'precipitation_probability': 4, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T15:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-27T18:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 10.0, - 'wind_bearing': 'NW', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-27T21:00:00+00:00', - 'precipitation_probability': 1, - 'temperature': 9.0, - 'wind_bearing': 'NW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T00:00:00+00:00', - 'precipitation_probability': 2, - 'temperature': 8.0, - 'wind_bearing': 'NNW', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'clear-night', - 'datetime': '2020-04-28T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 7.0, - 'wind_bearing': 'W', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'sunny', - 'datetime': '2020-04-28T06:00:00+00:00', - 'precipitation_probability': 5, - 'temperature': 6.0, - 'wind_bearing': 'S', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-28T09:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T12:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'ENE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T15:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 12.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T18:00:00+00:00', - 'precipitation_probability': 10, - 'temperature': 11.0, - 'wind_bearing': 'N', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-28T21:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 10.0, - 'wind_bearing': 'NNE', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T00:00:00+00:00', - 'precipitation_probability': 6, - 'temperature': 9.0, - 'wind_bearing': 'E', - 'wind_speed': 6.44, - }), - dict({ - 'condition': 'partlycloudy', - 'datetime': '2020-04-29T03:00:00+00:00', - 'precipitation_probability': 3, - 'temperature': 8.0, - 'wind_bearing': 'SSE', - 'wind_speed': 11.27, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T06:00:00+00:00', - 'precipitation_probability': 9, - 'temperature': 8.0, - 'wind_bearing': 'SE', - 'wind_speed': 14.48, - }), - dict({ - 'condition': 'cloudy', - 'datetime': '2020-04-29T09:00:00+00:00', - 'precipitation_probability': 12, - 'temperature': 10.0, - 'wind_bearing': 'SE', - 'wind_speed': 17.7, - }), - dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T12:00:00+00:00', - 'precipitation_probability': 47, - 'temperature': 12.0, - 'wind_bearing': 'SE', - 'wind_speed': 20.92, - }), - dict({ - 'condition': 'pouring', - 'datetime': '2020-04-29T15:00:00+00:00', - 'precipitation_probability': 59, + 'datetime': '2024-11-23T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.1, 'temperature': 13.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 31.21, + 'wind_speed': 15.52, }), dict({ - 'condition': 'rainy', - 'datetime': '2020-04-29T18:00:00+00:00', - 'precipitation_probability': 39, - 'temperature': 12.0, - 'wind_bearing': 'SSE', - 'wind_speed': 17.7, + 'apparent_temperature': 11.1, + 'condition': 'pouring', + 'datetime': '2024-11-23T19:00:00+00:00', + 'precipitation': 0.51, + 'precipitation_probability': 74, + 'pressure': 986.82, + 'temperature': 13.0, + 'uv_index': 0, + 'wind_bearing': 177, + 'wind_gust_speed': 37.44, + 'wind_speed': 17.46, }), dict({ + 'apparent_temperature': 11.2, 'condition': 'cloudy', - 'datetime': '2020-04-29T21:00:00+00:00', - 'precipitation_probability': 19, + 'datetime': '2024-11-23T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 986.92, + 'temperature': 13.7, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 45.97, + 'wind_speed': 22.72, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-23T21:00:00+00:00', + 'precipitation': 0.11, + 'precipitation_probability': 30, + 'pressure': 986.82, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 178, + 'wind_gust_speed': 44.32, + 'wind_speed': 22.0, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 12, + 'pressure': 986.31, + 'temperature': 14.0, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 47.84, + 'wind_speed': 23.65, + }), + dict({ + 'apparent_temperature': 11.4, + 'condition': 'cloudy', + 'datetime': '2024-11-23T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 985.71, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 51.44, + 'wind_speed': 26.57, + }), + dict({ + 'apparent_temperature': 11.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 10, + 'pressure': 984.92, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 171, + 'wind_gust_speed': 50.69, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 11.6, + 'condition': 'rainy', + 'datetime': '2024-11-24T01:00:00+00:00', + 'precipitation': 0.17, + 'precipitation_probability': 40, + 'pressure': 984.22, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 170, + 'wind_gust_speed': 50.11, + 'wind_speed': 25.78, + }), + dict({ + 'apparent_temperature': 11.3, + 'condition': 'pouring', + 'datetime': '2024-11-24T02:00:00+00:00', + 'precipitation': 0.21, + 'precipitation_probability': 74, + 'pressure': 983.51, + 'temperature': 14.2, + 'uv_index': 0, + 'wind_bearing': 176, + 'wind_gust_speed': 52.06, + 'wind_speed': 26.89, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'pouring', + 'datetime': '2024-11-24T03:00:00+00:00', + 'precipitation': 0.34, + 'precipitation_probability': 73, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 187, + 'wind_gust_speed': 51.55, + 'wind_speed': 26.1, + }), + dict({ + 'apparent_temperature': 11.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T04:00:00+00:00', + 'precipitation': 0.28, + 'precipitation_probability': 50, + 'pressure': 983.1, + 'temperature': 14.4, + 'uv_index': 0, + 'wind_bearing': 189, + 'wind_gust_speed': 49.68, + 'wind_speed': 25.52, + }), + dict({ + 'apparent_temperature': 11.8, + 'condition': 'rainy', + 'datetime': '2024-11-24T05:00:00+00:00', + 'precipitation': 0.25, + 'precipitation_probability': 47, + 'pressure': 983.3, + 'temperature': 14.3, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.72, + 'wind_speed': 23.69, + }), + dict({ + 'apparent_temperature': 10.7, + 'condition': 'rainy', + 'datetime': '2024-11-24T06:00:00+00:00', + 'precipitation': 0.3, + 'precipitation_probability': 42, + 'pressure': 983.96, + 'temperature': 13.4, + 'uv_index': 0, + 'wind_bearing': 216, + 'wind_gust_speed': 45.83, + 'wind_speed': 24.16, + }), + dict({ + 'apparent_temperature': 10.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T07:00:00+00:00', + 'precipitation': 0.16, + 'precipitation_probability': 40, + 'pressure': 984.58, + 'temperature': 12.5, + 'uv_index': 0, + 'wind_bearing': 214, + 'wind_gust_speed': 39.71, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.5, + 'condition': 'rainy', + 'datetime': '2024-11-24T08:00:00+00:00', + 'precipitation': 0.08, + 'precipitation_probability': 38, + 'pressure': 985.48, + 'temperature': 11.9, + 'uv_index': 0, + 'wind_bearing': 209, + 'wind_gust_speed': 37.08, + 'wind_speed': 19.73, + }), + dict({ + 'apparent_temperature': 9.1, + 'condition': 'rainy', + 'datetime': '2024-11-24T09:00:00+00:00', + 'precipitation': 0.04, + 'precipitation_probability': 26, + 'pressure': 986.38, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 35.96, + 'wind_speed': 19.58, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'rainy', + 'datetime': '2024-11-24T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 25, + 'pressure': 986.96, + 'temperature': 11.5, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 37.01, + 'wind_speed': 20.59, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 6, + 'pressure': 987.55, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 199, + 'wind_gust_speed': 36.22, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 9.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.48, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 8.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T13:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.57, + 'temperature': 11.8, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 45.36, + 'wind_speed': 25.45, + }), + dict({ + 'apparent_temperature': 8.6, + 'condition': 'cloudy', + 'datetime': '2024-11-24T14:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.37, + 'temperature': 11.7, + 'uv_index': 1, + 'wind_bearing': 201, + 'wind_gust_speed': 46.94, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 8.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T15:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.27, + 'temperature': 11.6, + 'uv_index': 1, + 'wind_bearing': 198, + 'wind_gust_speed': 46.87, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 8.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T16:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.08, + 'temperature': 11.2, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.22, + 'wind_speed': 25.38, + }), + dict({ + 'apparent_temperature': 8.0, + 'condition': 'cloudy', + 'datetime': '2024-11-24T17:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 986.89, 'temperature': 11.0, - 'wind_bearing': 'SSE', - 'wind_speed': 20.92, + 'uv_index': 0, + 'wind_bearing': 194, + 'wind_gust_speed': 45.68, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T18:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.7, + 'temperature': 10.9, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 46.15, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 7.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T19:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.69, + 'temperature': 10.8, + 'uv_index': 0, + 'wind_bearing': 196, + 'wind_gust_speed': 45.43, + 'wind_speed': 24.95, + }), + dict({ + 'apparent_temperature': 7.7, + 'condition': 'cloudy', + 'datetime': '2024-11-24T20:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.78, + 'temperature': 10.7, + 'uv_index': 0, + 'wind_bearing': 202, + 'wind_gust_speed': 45.07, + 'wind_speed': 24.55, + }), + dict({ + 'apparent_temperature': 7.5, + 'condition': 'cloudy', + 'datetime': '2024-11-24T21:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 986.77, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 203, + 'wind_gust_speed': 46.4, + 'wind_speed': 25.49, + }), + dict({ + 'apparent_temperature': 7.3, + 'condition': 'cloudy', + 'datetime': '2024-11-24T22:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 4, + 'pressure': 987.04, + 'temperature': 10.5, + 'uv_index': 0, + 'wind_bearing': 204, + 'wind_gust_speed': 48.24, + 'wind_speed': 26.68, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-24T23:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 987.04, + 'temperature': 10.3, + 'uv_index': 0, + 'wind_bearing': 207, + 'wind_gust_speed': 50.44, + 'wind_speed': 27.72, + }), + dict({ + 'apparent_temperature': 7.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.2, + 'uv_index': 0, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 6.9, + 'condition': 'cloudy', + 'datetime': '2024-11-25T01:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 11, + 'pressure': 987.41, + 'temperature': 10.0, + 'uv_index': 0, + 'wind_bearing': 215, + 'wind_gust_speed': 45.04, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 6.4, + 'condition': 'cloudy', + 'datetime': '2024-11-25T02:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 7, + 'pressure': 987.88, + 'temperature': 9.6, + 'uv_index': 0, + 'wind_bearing': 222, + 'wind_gust_speed': 46.87, + 'wind_speed': 25.7, + }), + dict({ + 'apparent_temperature': 6.1, + 'condition': 'cloudy', + 'datetime': '2024-11-25T03:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.16, + 'temperature': 9.3, + 'uv_index': 0, + 'wind_bearing': 226, + 'wind_gust_speed': 44.71, + 'wind_speed': 24.88, + }), + dict({ + 'apparent_temperature': 5.8, + 'condition': 'cloudy', + 'datetime': '2024-11-25T04:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 5, + 'pressure': 988.58, + 'temperature': 9.1, + 'uv_index': 0, + 'wind_bearing': 228, + 'wind_gust_speed': 45.22, + 'wind_speed': 25.34, + }), + dict({ + 'apparent_temperature': 5.4, + 'condition': 'clear-night', + 'datetime': '2024-11-25T05:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.1, + 'temperature': 8.8, + 'uv_index': 0, + 'wind_bearing': 232, + 'wind_gust_speed': 47.23, + 'wind_speed': 26.14, + }), + dict({ + 'apparent_temperature': 5.1, + 'condition': 'clear-night', + 'datetime': '2024-11-25T06:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 989.61, + 'temperature': 8.7, + 'uv_index': 0, + 'wind_bearing': 235, + 'wind_gust_speed': 48.2, + 'wind_speed': 26.35, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'clear-night', + 'datetime': '2024-11-25T07:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 990.61, + 'temperature': 8.6, + 'uv_index': 0, + 'wind_bearing': 240, + 'wind_gust_speed': 47.81, + 'wind_speed': 26.78, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'clear-night', + 'datetime': '2024-11-25T08:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 991.61, + 'temperature': 8.4, + 'uv_index': 0, + 'wind_bearing': 243, + 'wind_gust_speed': 47.56, + 'wind_speed': 26.86, + }), + dict({ + 'apparent_temperature': 4.8, + 'condition': 'sunny', + 'datetime': '2024-11-25T09:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 992.52, + 'temperature': 8.4, + 'uv_index': 1, + 'wind_bearing': 243, + 'wind_gust_speed': 47.84, + 'wind_speed': 27.32, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T10:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 1, + 'pressure': 993.42, + 'temperature': 8.7, + 'uv_index': 1, + 'wind_bearing': 245, + 'wind_gust_speed': 49.79, + 'wind_speed': 28.8, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T11:00:00+00:00', + 'precipitation': 0.0, + 'precipitation_probability': 3, + 'pressure': 994.24, + 'temperature': 8.8, + 'uv_index': 1, + 'wind_bearing': 249, + 'wind_gust_speed': 52.09, + 'wind_speed': 30.38, + }), + dict({ + 'apparent_temperature': 5.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'precipitation': None, + 'precipitation_probability': 2, + 'pressure': 994.88, + 'temperature': 8.9, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, }), ]) # --- diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index c2e75d89c1a..87d6e508da2 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -1,14 +1,18 @@ -"""Test the National Weather Service (NWS) config flow.""" +"""Test the MetOffice config flow.""" +import datetime import json from unittest.mock import patch +import pytest import requests_mock from homeassistant import config_entries from homeassistant.components.metoffice.const import DOMAIN +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr from .const import ( METOFFICE_CONFIG_WAVERTREE, @@ -28,8 +32,11 @@ async def test_form(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) - all_sites = json.dumps(mock_json["all_sites"]) - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -66,17 +73,10 @@ async def test_form_already_configured( # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) - - all_sites = json.dumps(mock_json["all_sites"]) - - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", - text="", - ) - requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=daily", - text="", + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, ) MockConfigEntry( @@ -102,7 +102,9 @@ async def test_form_cannot_connect( hass.config.latitude = TEST_LATITUDE_WAVERTREE hass.config.longitude = TEST_LONGITUDE_WAVERTREE - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="") + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", text="" + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -122,7 +124,7 @@ async def test_form_unknown_error( ) -> None: """Test we handle unknown error.""" mock_instance = mock_simple_manager_fail.return_value - mock_instance.get_nearest_forecast_site.side_effect = ValueError + mock_instance.get_forecast.side_effect = ValueError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -135,3 +137,77 @@ async def test_form_unknown_error( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + + +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) +async def test_reauth_flow( + hass: HomeAssistant, + requests_mock: requests_mock.Mocker, + device_registry: dr.DeviceRegistry, +) -> None: + """Test handling authentication errors and reauth flow.""" + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) + wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=wavertree_hourly, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(device_registry.devices) == 1 + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text="", + status_code=401, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text="", + status_code=401, + ) + + await entry.start_reauth_flow(hass) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=wavertree_hourly, + ) + + result = await hass.config_entries.flow.async_configure( + flows[0]["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/metoffice/test_init.py b/tests/components/metoffice/test_init.py index 159587ca7c1..2152742625b 100644 --- a/tests/components/metoffice/test_init.py +++ b/tests/components/metoffice/test_init.py @@ -1,129 +1,65 @@ """Tests for metoffice init.""" -from __future__ import annotations - import datetime +import json import pytest import requests_mock -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.metoffice.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr +from homeassistant.util import utcnow -from .const import DOMAIN, METOFFICE_CONFIG_WAVERTREE, TEST_COORDINATES_WAVERTREE +from .const import METOFFICE_CONFIG_WAVERTREE -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) -@pytest.mark.parametrize( - ("old_unique_id", "new_unique_id", "migration_needed"), - [ - ( - f"Station Name_{TEST_COORDINATES_WAVERTREE}", - f"name_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Weather_{TEST_COORDINATES_WAVERTREE}", - f"weather_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Temperature_{TEST_COORDINATES_WAVERTREE}", - f"temperature_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Feels Like Temperature_{TEST_COORDINATES_WAVERTREE}", - f"feels_like_temperature_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Wind Speed_{TEST_COORDINATES_WAVERTREE}", - f"wind_speed_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Wind Direction_{TEST_COORDINATES_WAVERTREE}", - f"wind_direction_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Wind Gust_{TEST_COORDINATES_WAVERTREE}", - f"wind_gust_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Visibility_{TEST_COORDINATES_WAVERTREE}", - f"visibility_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Visibility Distance_{TEST_COORDINATES_WAVERTREE}", - f"visibility_distance_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"UV Index_{TEST_COORDINATES_WAVERTREE}", - f"uv_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Probability of Precipitation_{TEST_COORDINATES_WAVERTREE}", - f"precipitation_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"Humidity_{TEST_COORDINATES_WAVERTREE}", - f"humidity_{TEST_COORDINATES_WAVERTREE}", - True, - ), - ( - f"name_{TEST_COORDINATES_WAVERTREE}", - f"name_{TEST_COORDINATES_WAVERTREE}", - False, - ), - ("abcde", "abcde", False), - ], -) -async def test_migrate_unique_id( +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) +async def test_reauth_on_auth_error( hass: HomeAssistant, - entity_registry: er.EntityRegistry, - old_unique_id: str, - new_unique_id: str, - migration_needed: bool, requests_mock: requests_mock.Mocker, + device_registry: dr.DeviceRegistry, ) -> None: - """Test unique id migration.""" + """Test handling authentication errors and reauth flow.""" + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) + wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=wavertree_hourly, + ) entry = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE, ) entry.add_to_hass(hass) - - entity: er.RegistryEntry = entity_registry.async_get_or_create( - suggested_object_id="my_sensor", - disabled_by=None, - domain=SENSOR_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - config_entry=entry, - ) - assert entity.unique_id == old_unique_id - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - if migration_needed: - assert ( - entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) - is None - ) + assert len(device_registry.devices) == 1 - assert ( - entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, new_unique_id) - == "sensor.my_sensor" + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text="", + status_code=401, ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text="", + status_code=401, + ) + + future_time = utcnow() + datetime.timedelta(minutes=40) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done(wait_background_tasks=True) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index db84e85075e..15a2acbf20b 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -2,13 +2,15 @@ import datetime import json +import re import pytest import requests_mock from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import ( DEVICE_KEY_KINGSLYNN, @@ -17,15 +19,15 @@ from .const import ( METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, TEST_DATETIME_STRING, - TEST_SITE_NAME_KINGSLYNN, - TEST_SITE_NAME_WAVERTREE, + TEST_LATITUDE_WAVERTREE, + TEST_LONGITUDE_WAVERTREE, WAVERTREE_SENSOR_RESULTS, ) from tests.common import MockConfigEntry, load_fixture -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_one_sensor_site_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -34,17 +36,15 @@ async def test_one_sensor_site_running( """Test the Met Office sensor platform.""" # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) - all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", text=wavertree_hourly, ) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=daily", + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", text=wavertree_daily, ) @@ -66,17 +66,15 @@ async def test_one_sensor_site_running( assert len(running_sensor_ids) > 0 for running_id in running_sensor_ids: sensor = hass.states.get(running_id) - sensor_id = sensor.attributes.get("sensor_id") - _, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] + sensor_id = re.search("met_office_wavertree_(.+?)$", running_id).group(1) + sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value assert sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING - assert sensor.attributes.get("site_id") == "354107" - assert sensor.attributes.get("site_name") == TEST_SITE_NAME_WAVERTREE assert sensor.attributes.get("attribution") == ATTRIBUTION -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_two_sensor_sites_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -86,24 +84,18 @@ async def test_two_sensor_sites_running( # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) - all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"]) kingslynn_daily = json.dumps(mock_json["kingslynn_daily"]) - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=wavertree_hourly, ) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=daily", text=wavertree_daily - ) - requests_mock.get( - "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly - ) - requests_mock.get( - "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, ) entry = MockConfigEntry( @@ -112,6 +104,16 @@ async def test_two_sensor_sites_running( ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=kingslynn_hourly, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=kingslynn_daily, + ) + entry2 = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN, @@ -134,25 +136,70 @@ async def test_two_sensor_sites_running( assert len(running_sensor_ids) > 0 for running_id in running_sensor_ids: sensor = hass.states.get(running_id) - sensor_id = sensor.attributes.get("sensor_id") - if sensor.attributes.get("site_id") == "354107": - _, sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] + if "wavertree" in running_id: + sensor_id = re.search("met_office_wavertree_(.+?)$", running_id).group(1) + sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value assert ( sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) - assert sensor.attributes.get("sensor_id") == sensor_id - assert sensor.attributes.get("site_id") == "354107" - assert sensor.attributes.get("site_name") == TEST_SITE_NAME_WAVERTREE assert sensor.attributes.get("attribution") == ATTRIBUTION else: - _, sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id] + sensor_id = re.search("met_office_king_s_lynn_(.+?)$", running_id).group(1) + sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id] assert sensor.state == sensor_value assert ( sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) - assert sensor.attributes.get("sensor_id") == sensor_id - assert sensor.attributes.get("site_id") == "322380" - assert sensor.attributes.get("site_name") == TEST_SITE_NAME_KINGSLYNN assert sensor.attributes.get("attribution") == ATTRIBUTION + + +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) +@pytest.mark.parametrize( + ("old_unique_id"), + [ + f"visibility_distance_{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}", + f"visibility_distance_{TEST_LATITUDE_WAVERTREE}_{TEST_LONGITUDE_WAVERTREE}_daily", + ], +) +async def test_legacy_entities_are_removed( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + requests_mock: requests_mock.Mocker, + old_unique_id: str, +) -> None: + """Test the expected entities are deleted.""" + mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) + wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) + wavertree_daily = json.dumps(mock_json["wavertree_daily"]) + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=wavertree_hourly, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=wavertree_daily, + ) + # Pre-create the entity + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + unique_id=old_unique_id, + suggested_object_id="met_office_wavertree_visibility_distance", + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 5176aff9e7d..f248ead3173 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -47,29 +47,24 @@ async def wavertree_data(requests_mock: requests_mock.Mocker) -> dict[str, _Matc """Mock data for the Wavertree location.""" # all metoffice test data encapsulated in here mock_json = json.loads(load_fixture("metoffice.json", "metoffice")) - all_sites = json.dumps(mock_json["all_sites"]) wavertree_hourly = json.dumps(mock_json["wavertree_hourly"]) wavertree_daily = json.dumps(mock_json["wavertree_daily"]) - sitelist_mock = requests_mock.get( - "/public/data/val/wxfcs/all/json/sitelist/", text=all_sites - ) wavertree_hourly_mock = requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", text=wavertree_hourly, ) wavertree_daily_mock = requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=daily", + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", text=wavertree_daily, ) return { - "sitelist_mock": sitelist_mock, "wavertree_hourly_mock": wavertree_hourly_mock, "wavertree_daily_mock": wavertree_daily_mock, } -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_site_cannot_connect( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -77,9 +72,14 @@ async def test_site_cannot_connect( ) -> None: """Test we handle cannot connect error.""" - requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="") - requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") - requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="") + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text="", + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text="", + ) entry = MockConfigEntry( domain=DOMAIN, @@ -91,15 +91,14 @@ async def test_site_cannot_connect( assert len(device_registry.devices) == 0 - assert hass.states.get("weather.met_office_wavertree_3hourly") is None - assert hass.states.get("weather.met_office_wavertree_daily") is None + assert hass.states.get("weather.met_office_wavertree") is None for sensor in WAVERTREE_SENSOR_RESULTS.values(): sensor_name = sensor[0] sensor = hass.states.get(f"sensor.wavertree_{sensor_name}") assert sensor is None -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_site_cannot_update( hass: HomeAssistant, requests_mock: requests_mock.Mocker, @@ -115,21 +114,43 @@ async def test_site_cannot_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - weather = hass.states.get("weather.met_office_wavertree_daily") + weather = hass.states.get("weather.met_office_wavertree") assert weather - requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") - requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=daily", text="") + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text="", + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text="", + ) - future_time = utcnow() + timedelta(minutes=20) + future_time = utcnow() + timedelta(minutes=40) async_fire_time_changed(hass, future_time) await hass.async_block_till_done(wait_background_tasks=True) - weather = hass.states.get("weather.met_office_wavertree_daily") + weather = hass.states.get("weather.met_office_wavertree") + assert weather.state == STATE_UNAVAILABLE + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + status_code=404, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + status_code=404, + ) + + future_time = utcnow() + timedelta(minutes=40) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done(wait_background_tasks=True) + + weather = hass.states.get("weather.met_office_wavertree") assert weather.state == STATE_UNAVAILABLE -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_one_weather_site_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -153,17 +174,17 @@ async def test_one_weather_site_running( assert device_wavertree.name == "Met Office Wavertree" # Wavertree daily weather platform expected results - weather = hass.states.get("weather.met_office_wavertree_daily") + weather = hass.states.get("weather.met_office_wavertree") assert weather - assert weather.state == "sunny" - assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 14.48 - assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("humidity") == 50 + assert weather.state == "rainy" + assert weather.attributes.get("temperature") == 9.3 + assert weather.attributes.get("wind_speed") == 28.33 + assert weather.attributes.get("wind_bearing") == 176.0 + assert weather.attributes.get("humidity") == 95 -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_two_weather_sites_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -177,19 +198,23 @@ async def test_two_weather_sites_running( kingslynn_hourly = json.dumps(mock_json["kingslynn_hourly"]) kingslynn_daily = json.dumps(mock_json["kingslynn_daily"]) - requests_mock.get( - "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly - ) - requests_mock.get( - "/public/data/val/wxfcs/all/json/322380?res=daily", text=kingslynn_daily - ) - entry = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly", + text=kingslynn_hourly, + ) + requests_mock.get( + "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily", + text=kingslynn_daily, + ) + entry2 = MockConfigEntry( domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN, @@ -209,29 +234,29 @@ async def test_two_weather_sites_running( assert device_wavertree.name == "Met Office Wavertree" # Wavertree daily weather platform expected results - weather = hass.states.get("weather.met_office_wavertree_daily") + weather = hass.states.get("weather.met_office_wavertree") assert weather - assert weather.state == "sunny" - assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 14.48 + assert weather.state == "rainy" + assert weather.attributes.get("temperature") == 9.3 + assert weather.attributes.get("wind_speed") == 28.33 assert weather.attributes.get("wind_speed_unit") == "km/h" - assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("humidity") == 50 + assert weather.attributes.get("wind_bearing") == 176.0 + assert weather.attributes.get("humidity") == 95 # King's Lynn daily weather platform expected results - weather = hass.states.get("weather.met_office_king_s_lynn_daily") + weather = hass.states.get("weather.met_office_king_s_lynn") assert weather - assert weather.state == "cloudy" - assert weather.attributes.get("temperature") == 9 - assert weather.attributes.get("wind_speed") == 6.44 + assert weather.state == "rainy" + assert weather.attributes.get("temperature") == 7.9 + assert weather.attributes.get("wind_speed") == 35.75 assert weather.attributes.get("wind_speed_unit") == "km/h" - assert weather.attributes.get("wind_bearing") == "ESE" - assert weather.attributes.get("humidity") == 75 + assert weather.attributes.get("wind_bearing") == 180.0 + assert weather.attributes.get("humidity") == 98 -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_new_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, no_sensor, wavertree_data ) -> None: @@ -250,7 +275,7 @@ async def test_new_config_entry( assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1 -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) @pytest.mark.parametrize( ("service"), [SERVICE_GET_FORECASTS], @@ -281,7 +306,7 @@ async def test_forecast_service( WEATHER_DOMAIN, service, { - "entity_id": "weather.met_office_wavertree_daily", + "entity_id": "weather.met_office_wavertree", "type": forecast_type, }, blocking=True, @@ -289,24 +314,17 @@ async def test_forecast_service( ) assert response == snapshot - # Calling the services should use cached data - assert wavertree_data["wavertree_daily_mock"].call_count == 1 - assert wavertree_data["wavertree_hourly_mock"].call_count == 1 - # Trigger data refetch freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) 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 - for forecast_type in ("daily", "hourly"): response = await hass.services.async_call( WEATHER_DOMAIN, service, { - "entity_id": "weather.met_office_wavertree_daily", + "entity_id": "weather.met_office_wavertree", "type": forecast_type, }, blocking=True, @@ -314,41 +332,18 @@ async def test_forecast_service( ) assert response == snapshot - # Calling the services should update the hourly forecast - assert wavertree_data["wavertree_daily_mock"].call_count == 2 - assert wavertree_data["wavertree_hourly_mock"].call_count == 2 - # Update fails - requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") - - freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - - response = await hass.services.async_call( - WEATHER_DOMAIN, - service, - { - "entity_id": "weather.met_office_wavertree_daily", - "type": "hourly", - }, - blocking=True, - return_response=True, - ) - assert response == snapshot - - -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_legacy_config_entry_is_removed( hass: HomeAssistant, entity_registry: er.EntityRegistry, no_sensor, wavertree_data ) -> None: """Test the expected entities are created.""" - # Pre-create the hourly entity + # Pre-create the daily entity entity_registry.async_get_or_create( WEATHER_DOMAIN, DOMAIN, "53.38374_-2.90929", - suggested_object_id="met_office_wavertree_3_hourly", + suggested_object_id="met_office_wavertree_daily", ) entry = MockConfigEntry( @@ -365,8 +360,7 @@ async def test_legacy_config_entry_is_removed( assert len(er.async_entries_for_config_entry(entity_registry, entry.entry_id)) == 1 -@pytest.mark.freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.UTC)) -@pytest.mark.parametrize("forecast_type", ["daily", "hourly"]) +@pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_forecast_subscription( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, @@ -374,7 +368,6 @@ async def test_forecast_subscription( snapshot: SnapshotAssertion, no_sensor, wavertree_data: dict[str, _Matcher], - forecast_type: str, ) -> None: """Test multiple forecast.""" client = await hass_ws_client(hass) @@ -391,8 +384,8 @@ async def test_forecast_subscription( await client.send_json_auto_id( { "type": "weather/subscribe_forecast", - "forecast_type": forecast_type, - "entity_id": "weather.met_office_wavertree_daily", + "forecast_type": "hourly", + "entity_id": "weather.met_office_wavertree", } ) msg = await client.receive_json() From d43371ed2f2c46734705393a470e00972ce3dfcb Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 21 May 2025 22:02:14 +0200 Subject: [PATCH 0750/1175] Bump pylamarzocco to 2.0.4 (#145402) --- homeassistant/components/lamarzocco/manifest.json | 2 +- homeassistant/components/lamarzocco/update.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/lamarzocco/test_update.py | 11 +++-------- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index d948d46ef1f..44ca31427c0 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.3"] + "requirements": ["pylamarzocco==2.0.4"] } diff --git a/homeassistant/components/lamarzocco/update.py b/homeassistant/components/lamarzocco/update.py index 632c66a8b66..33e64623256 100644 --- a/homeassistant/components/lamarzocco/update.py +++ b/homeassistant/components/lamarzocco/update.py @@ -4,7 +4,7 @@ import asyncio from dataclasses import dataclass from typing import Any -from pylamarzocco.const import FirmwareType, UpdateCommandStatus +from pylamarzocco.const import FirmwareType, UpdateStatus from pylamarzocco.exceptions import RequestNotSuccessful from homeassistant.components.update import ( @@ -125,7 +125,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity): await self.coordinator.device.update_firmware() while ( update_progress := await self.coordinator.device.get_firmware() - ).command_status is UpdateCommandStatus.IN_PROGRESS: + ).command_status is UpdateStatus.IN_PROGRESS: if counter >= MAX_UPDATE_WAIT: _raise_timeout_error() self._attr_update_percentage = update_progress.progress_percentage diff --git a/requirements_all.txt b/requirements_all.txt index abb5fe26fbf..bf451c260b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.3 +pylamarzocco==2.0.4 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12ea7fe76c7..7c3579c178e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.3 +pylamarzocco==2.0.4 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/tests/components/lamarzocco/test_update.py b/tests/components/lamarzocco/test_update.py index 46e466a3acc..99f85c21381 100644 --- a/tests/components/lamarzocco/test_update.py +++ b/tests/components/lamarzocco/test_update.py @@ -3,12 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from pylamarzocco.const import ( - FirmwareType, - UpdateCommandStatus, - UpdateProgressInfo, - UpdateStatus, -) +from pylamarzocco.const import FirmwareType, UpdateProgressInfo, UpdateStatus from pylamarzocco.exceptions import RequestNotSuccessful from pylamarzocco.models import UpdateDetails import pytest @@ -61,7 +56,7 @@ async def test_update_process( mock_lamarzocco.get_firmware.side_effect = [ UpdateDetails( status=UpdateStatus.TO_UPDATE, - command_status=UpdateCommandStatus.IN_PROGRESS, + command_status=UpdateStatus.IN_PROGRESS, progress_info=UpdateProgressInfo.STARTING_PROCESS, progress_percentage=0, ), @@ -139,7 +134,7 @@ async def test_update_times_out( """Test error during update.""" mock_lamarzocco.get_firmware.return_value = UpdateDetails( status=UpdateStatus.TO_UPDATE, - command_status=UpdateCommandStatus.IN_PROGRESS, + command_status=UpdateStatus.IN_PROGRESS, progress_info=UpdateProgressInfo.STARTING_PROCESS, progress_percentage=0, ) From 4a26352c50b4cb32507a79bb2eaf36bb477e101e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 21 May 2025 22:22:33 +0200 Subject: [PATCH 0751/1175] Bump py-synologydsm-api to 2.7.2 (#145403) bump py-synologydsm-api to 2.7.2 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 3804de7f3f1..cd054c7eb74 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.7.1"], + "requirements": ["py-synologydsm-api==2.7.2"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index bf451c260b7..fed31c6ec7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1771,7 +1771,7 @@ py-schluter==0.1.7 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.1 +py-synologydsm-api==2.7.2 # homeassistant.components.atome pyAtome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c3579c178e..0128c5df493 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ py-nightscout==1.2.2 py-sucks==0.9.10 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.1 +py-synologydsm-api==2.7.2 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From e2b9e21c6a9cbfaf3dc68c2acfcb5b891e72fb31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 21 May 2025 16:23:04 -0400 Subject: [PATCH 0752/1175] Bump ESPHome stable BLE version to 2025.5.0 (#144857) --- homeassistant/components/esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py index f793fd16bfe..2c9bee32734 100644 --- a/homeassistant/components/esphome/const.py +++ b/homeassistant/components/esphome/const.py @@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False DEFAULT_PORT: Final = 6053 -STABLE_BLE_VERSION_STR = "2025.2.2" +STABLE_BLE_VERSION_STR = "2025.5.0" STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR) PROJECT_URLS = { "esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/", From fbab6741afce0b3814f2c91c2da2418f3e153563 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Wed, 21 May 2025 21:37:07 +0100 Subject: [PATCH 0753/1175] Update exception handling for initialization for Squeezebox (#144674) * initial * tests * translate exceptions * updates * tests updates * remove bare exception * merge fix --- .../components/squeezebox/__init__.py | 59 ++++++++++++++++-- .../components/squeezebox/strings.json | 12 ++++ tests/components/squeezebox/test_init.py | 61 +++++++++++++++++++ 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 2fcb17b9781..18acd74efd7 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -3,6 +3,7 @@ from asyncio import timeout from dataclasses import dataclass from datetime import datetime +from http import HTTPStatus import logging from pysqueezebox import Player, Server @@ -16,7 +17,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import ( @@ -93,15 +98,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) - status = await lms.async_query( "serverstatus", "-", "-", "prefs:libraryname" ) - except Exception as err: + except TimeoutError as err: # Specifically catch timeout + _LOGGER.warning("Timeout connecting to LMS %s: %s", host, err) raise ConfigEntryNotReady( - f"Error communicating config not read for {host}" + translation_domain=DOMAIN, + translation_key="init_timeout", + translation_placeholders={ + "host": str(host), + }, ) from err if not status: - raise ConfigEntryNotReady(f"Error Config Not read for {host}") + # pysqueezebox's async_query returns None on various issues, + # including HTTP errors where it sets lms.http_status. + http_status = getattr(lms, "http_status", "N/A") + + if http_status == HTTPStatus.UNAUTHORIZED: + _LOGGER.warning("Authentication failed for Squeezebox server %s", host) + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="init_auth_failed", + translation_placeholders={ + "host": str(host), + }, + ) + + # For other errors where status is None (e.g., server error, connection refused by server) + _LOGGER.warning( + "LMS %s returned no status or an error (HTTP status: %s). Retrying setup", + host, + http_status, + ) + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="init_get_status_failed", + translation_placeholders={ + "host": str(host), + "http_status": str(http_status), + }, + ) + + # If we are here, status is a valid dictionary _LOGGER.debug("LMS Status for setup = %s", status) + # Check for essential keys in status before using them + if STATUS_QUERY_UUID not in status: + _LOGGER.error("LMS %s status response missing UUID", host) + # This is a non-recoverable error with the current server response + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="init_missing_uuid", + translation_placeholders={ + "host": str(host), + }, + ) + lms.uuid = status[STATUS_QUERY_UUID] _LOGGER.debug("LMS %s = '%s' with uuid = %s ", lms.name, host, lms.uuid) lms.name = ( diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 6a4e30119a0..593d637e0db 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -158,6 +158,18 @@ } }, "exceptions": { + "init_timeout": { + "message": "Timeout connecting to LMS {host}." + }, + "init_auth_failed": { + "message": "Authentication failed for {host)." + }, + "init_get_status_failed": { + "message": "Failed to get status from LMS {host} (HTTP status: {http_status}). Will retry." + }, + "init_missing_uuid": { + "message": "LMS {host} status response missing essential data (UUID)." + }, "invalid_announce_media_type": { "message": "Only type 'music' can be played as announcement (received type {media_type})." }, diff --git a/tests/components/squeezebox/test_init.py b/tests/components/squeezebox/test_init.py index 9074f57cdcb..f70782b13da 100644 --- a/tests/components/squeezebox/test_init.py +++ b/tests/components/squeezebox/test_init.py @@ -1,7 +1,9 @@ """Test squeezebox initialization.""" +from http import HTTPStatus from unittest.mock import patch +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -21,3 +23,62 @@ async def test_init_api_fail( ), ): assert not await hass.config_entries.async_setup(config_entry.entry_id) + + +async def test_init_timeout_error( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to TimeoutError.""" + + # Setup component to raise TimeoutError + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + side_effect=TimeoutError, + ), + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_init_unauthorized( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to unauthorized error.""" + + # Setup component to simulate unauthorized response + with ( + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=False, # async_query returns False on auth failure + ), + patch( + "homeassistant.components.squeezebox.Server", # Patch the Server class itself + autospec=True, + ) as mock_server_instance, + ): + mock_server_instance.return_value.http_status = HTTPStatus.UNAUTHORIZED + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_init_missing_uuid( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test init fail due to missing UUID in server status.""" + # A response that is truthy but does not contain STATUS_QUERY_UUID + mock_status_without_uuid = {"name": "Test Server"} + + with patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=mock_status_without_uuid, + ) as mock_async_query: + # ConfigEntryError is raised, caught by setup, and returns False + assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + mock_async_query.assert_called_once_with( + "serverstatus", "-", "-", "prefs:libraryname" + ) From 195e34cc09a72add8bd52b8b2dff863286d18238 Mon Sep 17 00:00:00 2001 From: "Lektri.co" <137074859+Lektrico@users.noreply.github.com> Date: Wed, 21 May 2025 23:43:42 +0300 Subject: [PATCH 0754/1175] Bump lektricowifi to 0.1 (#145393) Use lektricowifi 0.1: add a new command --- homeassistant/components/lektrico/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lektrico/manifest.json b/homeassistant/components/lektrico/manifest.json index d34915d66ba..1924f0a1fc8 100644 --- a/homeassistant/components/lektrico/manifest.json +++ b/homeassistant/components/lektrico/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/lektrico", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["lektricowifi==0.0.43"], + "requirements": ["lektricowifi==0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index fed31c6ec7c..49abbd90ef0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1324,7 +1324,7 @@ leaone-ble==0.3.0 led-ble==1.1.7 # homeassistant.components.lektrico -lektricowifi==0.0.43 +lektricowifi==0.1 # homeassistant.components.letpot letpot==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0128c5df493..5053d09f75e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1124,7 +1124,7 @@ leaone-ble==0.3.0 led-ble==1.1.7 # homeassistant.components.lektrico -lektricowifi==0.0.43 +lektricowifi==0.1 # homeassistant.components.letpot letpot==0.4.0 From 01b8f97201b2766bb96678e72d427fddec80accf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 May 2025 23:27:53 +0200 Subject: [PATCH 0755/1175] Mark cover methods and properties as mandatory in pylint plugin (#145308) --- pylint/plugins/hass_enforce_type_hints.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e92429d1620..4e0e63a5d72 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1390,66 +1390,77 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="supported_features", return_type="CoverEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="open_cover", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="close_cover", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="toggle", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_cover_position", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="stop_cover", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="open_cover_tilt", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="close_cover_tilt", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_cover_tilt_position", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="stop_cover_tilt", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="toggle_tilt", kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From 088cfc3576e0018ad1df373c08549092918e6530 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 21 May 2025 23:29:33 +0200 Subject: [PATCH 0756/1175] Mark fan methods and properties as mandatory in pylint plugin (#145311) --- pylint/plugins/hass_enforce_type_hints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 4e0e63a5d72..f618494d389 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1575,10 +1575,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="speed_count", return_type="int", + mandatory=True, ), TypeHintMatch( function_name="percentage_step", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="current_direction", @@ -1599,24 +1601,28 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="supported_features", return_type="FanEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="set_percentage", arg_types={1: "int"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_preset_mode", arg_types={1: "str"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_direction", arg_types={1: "str"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="turn_on", @@ -1627,12 +1633,14 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="oscillate", arg_types={1: "bool"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From 12376a2338692bf0579de2db647c42049ac992b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 May 2025 18:54:29 -0400 Subject: [PATCH 0757/1175] Mark LLMs that support streaming as such (#145405) --- homeassistant/components/anthropic/conversation.py | 1 + homeassistant/components/ollama/conversation.py | 9 ++++++--- .../components/openai_conversation/conversation.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py index 7e1fda467a8..bfdd4bfd361 100644 --- a/homeassistant/components/anthropic/conversation.py +++ b/homeassistant/components/anthropic/conversation.py @@ -326,6 +326,7 @@ class AnthropicConversationEntity( _attr_has_entity_name = True _attr_name = None + _attr_supports_streaming = True def __init__(self, entry: AnthropicConfigEntry) -> None: """Initialize the agent.""" diff --git a/homeassistant/components/ollama/conversation.py b/homeassistant/components/ollama/conversation.py index ab9e05b5fbe..6c507030ad3 100644 --- a/homeassistant/components/ollama/conversation.py +++ b/homeassistant/components/ollama/conversation.py @@ -89,9 +89,11 @@ def _parse_tool_args(arguments: dict[str, Any]) -> dict[str, Any]: def _convert_content( - chat_content: conversation.Content - | conversation.ToolResultContent - | conversation.AssistantContent, + chat_content: ( + conversation.Content + | conversation.ToolResultContent + | conversation.AssistantContent + ), ) -> ollama.Message: """Create tool response content.""" if isinstance(chat_content, conversation.ToolResultContent): @@ -172,6 +174,7 @@ class OllamaConversationEntity( """Ollama conversation agent.""" _attr_has_entity_name = True + _attr_supports_streaming = True def __init__(self, entry: ConfigEntry) -> None: """Initialize the agent.""" diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index d55ffc2df0c..a129400194b 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -231,6 +231,7 @@ class OpenAIConversationEntity( _attr_has_entity_name = True _attr_name = None + _attr_supports_streaming = True def __init__(self, entry: OpenAIConfigEntry) -> None: """Initialize the agent.""" From b407792bd17cc119870c7517e0023353db59862c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 00:57:39 +0200 Subject: [PATCH 0758/1175] Mark geo_location methods and properties as mandatory in pylint plugin (#145313) --- pylint/plugins/hass_enforce_type_hints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f618494d389..29fa1daf47c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1660,6 +1660,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="source", return_type="str", + mandatory=True, ), TypeHintMatch( function_name="distance", From 8b3bad1f54a966cbd80dbdde1961ea279087a5d4 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 22 May 2025 07:22:50 +0200 Subject: [PATCH 0759/1175] Bump habiticalib to v.0.4.0 (#145414) Bump habiticalib to v0.4.0 --- .../components/habitica/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 1 - tests/components/habitica/conftest.py | 12 --------- .../components/habitica/fixtures/content.json | 25 +++++++++++++++++++ .../habitica/snapshots/test_diagnostics.ambr | 2 ++ 8 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 48b6997239e..8b03e5efe01 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["habiticalib"], "quality_scale": "platinum", - "requirements": ["habiticalib==0.3.7"] + "requirements": ["habiticalib==0.4.0"] } diff --git a/pyproject.toml b/pyproject.toml index 30ca8efa7c6..955b2a707a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -604,8 +604,6 @@ filterwarnings = [ # - pkg_resources # https://pypi.org/project/aiomusiccast/ - v0.14.8 - 2023-03-20 "ignore:pkg_resources is deprecated as an API:DeprecationWarning:aiomusiccast", - # https://pypi.org/project/habitipy/ - v0.3.3 - 2024-10-28 - "ignore:pkg_resources is deprecated as an API:DeprecationWarning:habitipy.api", # 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.26 - 2025-02-26 diff --git a/requirements_all.txt b/requirements_all.txt index 49abbd90ef0..7777385f872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1115,7 +1115,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.3.7 +habiticalib==0.4.0 # homeassistant.components.bluetooth habluetooth==3.48.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5053d09f75e..5a8922a1c17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -957,7 +957,7 @@ ha-philipsjs==3.2.2 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.3.7 +habiticalib==0.4.0 # homeassistant.components.bluetooth habluetooth==3.48.2 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index dd5374461c3..464e94d918c 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -32,7 +32,6 @@ FORBIDDEN_PACKAGES = {"setuptools", "wheel"} FORBIDDEN_PACKAGE_EXCEPTIONS = { # Direct dependencies "fitbit", # setuptools (fitbit) - "habitipy", # setuptools (habitica) "influxdb-client", # setuptools (influxdb) "microbeespy", # setuptools (microbees) "pyefergy", # types-pytz (efergy) diff --git a/tests/components/habitica/conftest.py b/tests/components/habitica/conftest.py index 4ef14699e0b..fa2b65af6c3 100644 --- a/tests/components/habitica/conftest.py +++ b/tests/components/habitica/conftest.py @@ -155,18 +155,6 @@ async def mock_habiticalib() -> Generator[AsyncMock]: client.create_task.return_value = HabiticaTaskResponse.from_json( load_fixture("task.json", DOMAIN) ) - client.habitipy.return_value = { - "tasks": { - "user": { - "post": AsyncMock( - return_value={ - "text": "Use API from Home Assistant", - "type": "todo", - } - ) - } - } - } yield client diff --git a/tests/components/habitica/fixtures/content.json b/tests/components/habitica/fixtures/content.json index e26dbeb17cc..e66186860c7 100644 --- a/tests/components/habitica/fixtures/content.json +++ b/tests/components/habitica/fixtures/content.json @@ -657,6 +657,31 @@ "canDrop": false, "key": "Saddle" } + }, + "loginIncentives": { + "0": { + "nextRewardAt": 1 + }, + "1": { + "rewardKey": ["armor_special_bardRobes"], + "reward": [ + { + "text": "Bardic Robes", + "notes": "These colorful robes may be conspicuous, but you can sing your way out of any situation. Increases Perception by 3.", + "per": 3, + "value": 0, + "type": "armor", + "key": "armor_special_bardRobes", + "set": "special-bardRobes", + "klass": "special", + "index": "bardRobes", + "str": 0, + "int": 0, + "con": 0 + } + ], + "nextRewardAt": 2 + } } }, "appVersion": "5.29.2" diff --git a/tests/components/habitica/snapshots/test_diagnostics.ambr b/tests/components/habitica/snapshots/test_diagnostics.ambr index 718aea99ebc..e04edea3d94 100644 --- a/tests/components/habitica/snapshots/test_diagnostics.ambr +++ b/tests/components/habitica/snapshots/test_diagnostics.ambr @@ -541,6 +541,8 @@ 'quest': dict({ 'RSVPNeeded': True, 'key': 'dustbunnies', + 'members': dict({ + }), 'progress': dict({ 'collect': dict({ }), From f36ee88a878239cf02ecad6e657ee38a073066d6 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 01:23:44 -0400 Subject: [PATCH 0760/1175] Clean up AbstractTemplateEntity (#145409) Clean up abstract templates --- homeassistant/components/template/light.py | 28 +++++++++---------- .../components/template/template_entity.py | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 3b64cca26b4..ac751d46cf7 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -50,6 +50,7 @@ from homeassistant.util import color as color_util from . import TriggerUpdateCoordinator from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, @@ -272,16 +273,14 @@ async def async_setup_platform( ) -class AbstractTemplateLight(LightEntity): +class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): """Representation of a template lights features.""" - def __init__( + def __init__( # pylint: disable=super-init-not-called self, config: dict[str, Any], initial_state: bool | None = False ) -> None: """Initialize the features.""" - self._registered_scripts: list[str] = [] - # Template attributes self._template = config.get(CONF_STATE) self._level_template = config.get(CONF_LEVEL) @@ -312,7 +311,7 @@ class AbstractTemplateLight(LightEntity): self._color_mode: ColorMode | None = None self._supported_color_modes: set[ColorMode] | None = None - def _register_scripts( + def _iterate_scripts( self, config: dict[str, Any] ) -> Generator[tuple[str, Sequence[dict[str, Any]], ColorMode | None]]: for action_id, color_mode in ( @@ -327,7 +326,6 @@ class AbstractTemplateLight(LightEntity): (CONF_RGBWW_ACTION, ColorMode.RGBWW), ): if (action_config := config.get(action_id)) is not None: - self._registered_scripts.append(action_id) yield (action_id, action_config, color_mode) @property @@ -522,7 +520,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_COLOR_TEMP_KELVIN in kwargs - and (script := CONF_TEMPERATURE_ACTION) in self._registered_scripts + and (script := CONF_TEMPERATURE_ACTION) in self._action_scripts ): common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired( kwargs[ATTR_COLOR_TEMP_KELVIN] @@ -532,7 +530,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_EFFECT in kwargs - and (script := CONF_EFFECT_ACTION) in self._registered_scripts + and (script := CONF_EFFECT_ACTION) in self._action_scripts ): assert self._effect_list is not None effect = kwargs[ATTR_EFFECT] @@ -551,7 +549,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_HS_COLOR in kwargs - and (script := CONF_HS_ACTION) in self._registered_scripts + and (script := CONF_HS_ACTION) in self._action_scripts ): hs_value = kwargs[ATTR_HS_COLOR] common_params["hs"] = hs_value @@ -562,7 +560,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_RGBWW_COLOR in kwargs - and (script := CONF_RGBWW_ACTION) in self._registered_scripts + and (script := CONF_RGBWW_ACTION) in self._action_scripts ): rgbww_value = kwargs[ATTR_RGBWW_COLOR] common_params["rgbww"] = rgbww_value @@ -581,7 +579,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_RGBW_COLOR in kwargs - and (script := CONF_RGBW_ACTION) in self._registered_scripts + and (script := CONF_RGBW_ACTION) in self._action_scripts ): rgbw_value = kwargs[ATTR_RGBW_COLOR] common_params["rgbw"] = rgbw_value @@ -599,7 +597,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_RGB_COLOR in kwargs - and (script := CONF_RGB_ACTION) in self._registered_scripts + and (script := CONF_RGB_ACTION) in self._action_scripts ): rgb_value = kwargs[ATTR_RGB_COLOR] common_params["rgb"] = rgb_value @@ -611,7 +609,7 @@ class AbstractTemplateLight(LightEntity): if ( ATTR_BRIGHTNESS in kwargs - and (script := CONF_LEVEL_ACTION) in self._registered_scripts + and (script := CONF_LEVEL_ACTION) in self._action_scripts ): return (script, common_params) @@ -966,7 +964,7 @@ class LightTemplate(TemplateEntity, AbstractTemplateLight): assert name is not None color_modes = {ColorMode.ONOFF} - for action_id, action_config, color_mode in self._register_scripts(config): + for action_id, action_config, color_mode in self._iterate_scripts(config): self.add_script(action_id, action_config, name, DOMAIN) if color_mode: color_modes.add(color_mode) @@ -1180,7 +1178,7 @@ class TriggerLightEntity(TriggerEntity, AbstractTemplateLight): self._parse_result.add(key) color_modes = {ColorMode.ONOFF} - for action_id, action_config, color_mode in self._register_scripts(config): + for action_id, action_config, color_mode in self._iterate_scripts(config): self.add_script(action_id, action_config, name, DOMAIN) if color_mode: color_modes.add(color_mode) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 41ebf5bc1be..f879c60ed9e 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -280,7 +280,7 @@ class TemplateEntity(AbstractTemplateEntity): unique_id: str | None = None, ) -> None: """Template Entity.""" - super().__init__(hass) + AbstractTemplateEntity.__init__(self, hass) self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} self._template_result_info: TrackTemplateResultInfo | None = None self._attr_extra_state_attributes = {} From 9e7ae1daa452c3404f6b2951c1c5d1c69f18d74e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 07:56:18 +0200 Subject: [PATCH 0761/1175] Catch blocking version pinning in dependencies early (#145364) * Catch upper bindings in dependencies early * One more * Apply suggestions from code review --- script/hassfest/requirements.py | 73 +++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 464e94d918c..e183a87d9eb 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -22,6 +22,19 @@ from script.gen_requirements_all import ( from .model import Config, Integration +PACKAGE_CHECK_VERSION_RANGE = { + "aiohttp": "SemVer", + # https://github.com/iMicknl/python-overkiz-api/issues/1644 + # "attrs": "CalVer" + "grpcio": "SemVer", + "mashumaro": "SemVer", + "pydantic": "SemVer", + "pyjwt": "SemVer", + "pytz": "CalVer", + "typing_extensions": "SemVer", + "yarl": "SemVer", +} + PACKAGE_REGEX = re.compile( r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) @@ -175,7 +188,7 @@ def get_pipdeptree() -> dict[str, dict[str, Any]]: "key": "flake8-docstrings", "package_name": "flake8-docstrings", "installed_version": "1.5.0" - "dependencies": {"flake8"} + "dependencies": {"flake8": ">=1.2.3, <4.5.0"} } } """ @@ -191,7 +204,9 @@ def get_pipdeptree() -> dict[str, dict[str, Any]]: ): deptree[item["package"]["key"]] = { **item["package"], - "dependencies": {dep["key"] for dep in item["dependencies"]}, + "dependencies": { + dep["key"]: dep["required_version"] for dep in item["dependencies"] + }, } return deptree @@ -222,8 +237,8 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: ) continue - dependencies: set[str] = item["dependencies"] - for pkg in dependencies: + dependencies: dict[str, str] = item["dependencies"] + for pkg, version in dependencies.items(): if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES: if package in FORBIDDEN_PACKAGE_EXCEPTIONS: integration.add_warning( @@ -235,12 +250,62 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: "requirements", f"Package {pkg} should not be a runtime dependency in {package}", ) + check_dependency_version_range(integration, package, pkg, version) to_check.extend(dependencies) return all_requirements +def check_dependency_version_range( + integration: Integration, source: str, pkg: str, version: str +) -> None: + """Check requirement version range. + + We want to avoid upper version bounds that are too strict for common packages. + """ + if version == "Any" or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None: + return + + if not all( + _is_dependency_version_range_valid(version_part, convention) + for version_part in version.split(";", 1)[0].split(",") + ): + integration.add_error( + "requirements", + f"Version restrictions for {pkg} are too strict ({version}) in {source}", + ) + + +def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool: + version_match = PIP_VERSION_RANGE_SEPARATOR.match(version_part) + operator = version_match.group(1) + version = version_match.group(2) + + if operator in (">", ">=", "!="): + # Lower version binding and version exclusion are fine + return True + + if convention == "SemVer": + if operator == "==": + # Explicit version with wildcard is allowed only on major version + # e.g. ==1.* is allowed, but ==1.2.* is not + return version.endswith(".*") and version.count(".") == 1 + + awesome = AwesomeVersion(version) + if operator in ("<", "<="): + # Upper version binding only allowed on major version + # e.g. <=3 is allowed, but <=3.1 is not + return awesome.section(1) == 0 and awesome.section(2) == 0 + + if operator == "~=": + # Compatible release operator is only allowed on major or minor version + # e.g. ~=1.2 is allowed, but ~=1.2.3 is not + return awesome.section(2) == 0 + + return False + + def install_requirements(integration: Integration, requirements: set[str]) -> bool: """Install integration requirements. From 12b5dbdd8394c23def495a9948ee71d8ed92cf92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Thu, 22 May 2025 08:01:42 +0200 Subject: [PATCH 0762/1175] Add CancelBoost for Matter Water heater (#145316) * Update water_heater.py Add CancelBoost * Add test for CancelBoost * Update water_heater.py --- .../components/matter/water_heater.py | 5 ++++ tests/components/matter/test_water_heater.py | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/matter/water_heater.py b/homeassistant/components/matter/water_heater.py index 07c011554fa..e453a8be067 100644 --- a/homeassistant/components/matter/water_heater.py +++ b/homeassistant/components/matter/water_heater.py @@ -108,6 +108,11 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity): await self.send_device_command( clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info) ) + # Trigger CancelBoost command for other modes + else: + await self.send_device_command( + clusters.WaterHeaterManagement.Commands.CancelBoost() + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on water heater.""" diff --git a/tests/components/matter/test_water_heater.py b/tests/components/matter/test_water_heater.py index 2785dc9c778..a674c87c24b 100644 --- a/tests/components/matter/test_water_heater.py +++ b/tests/components/matter/test_water_heater.py @@ -166,6 +166,32 @@ async def test_water_heater_boostmode( command=clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info), ) + # disable water_heater boostmode + await hass.services.async_call( + "water_heater", + "set_operation_mode", + { + "entity_id": "water_heater.water_heater", + "operation_mode": STATE_ECO, + }, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 2 + assert matter_client.write_attribute.call_args == call( + node_id=matter_node.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=2, + attribute=clusters.Thermostat.Attributes.SystemMode, + ), + value=4, + ) + assert matter_client.send_device_command.call_count == 2 + assert matter_client.send_device_command.call_args == call( + node_id=matter_node.node_id, + endpoint_id=2, + command=clusters.WaterHeaterManagement.Commands.CancelBoost(), + ) + @pytest.mark.parametrize("node_fixture", ["silabs_water_heater"]) async def test_update_from_water_heater( From bffbd5607b3e5a563d77978ecde2c95951f4159e Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 22 May 2025 02:17:31 -0400 Subject: [PATCH 0763/1175] Remove unneeded parenthesis in comparison for Sonos (#145413) fix: remove unneeded paren --- homeassistant/components/sonos/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 322beaed092..e2e981b293c 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -86,7 +86,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): @property def available(self) -> bool: """Return whether this device is available.""" - return self.speaker.available and (self.speaker.charging is not None) + return self.speaker.available and self.speaker.charging is not None class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): From 66a6e5531092399f2cbc593d8e988dce6be80a4d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 22 May 2025 08:22:02 +0200 Subject: [PATCH 0764/1175] Centralise MockStreamReaderChunked helper (#145404) centralize MockStreamReaderChunked helper --- homeassistant/util/aiohttp.py | 8 ++++++++ tests/components/cloud/test_backup.py | 10 +--------- tests/components/immich/__init__.py | 9 --------- tests/components/immich/conftest.py | 2 +- tests/components/immich/test_media_source.py | 4 ++-- tests/components/synology_dsm/test_backup.py | 10 +--------- 6 files changed, 13 insertions(+), 30 deletions(-) diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index aad9771d963..5b6774a08a5 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -28,6 +28,14 @@ class MockStreamReader: return self._content.read(byte_count) +class MockStreamReaderChunked(MockStreamReader): + """Mock a stream reader with simulated chunked data.""" + + async def readchunk(self) -> tuple[bytes, bool]: + """Read bytes.""" + return (self._content.read(), False) + + class MockPayloadWriter: """Small mock to imitate payload writer.""" diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index 8399e69ab09..e75cf72332c 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -24,20 +24,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.backup import async_initialize_backup from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from homeassistant.util.aiohttp import MockStreamReader +from homeassistant.util.aiohttp import MockStreamReaderChunked from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator, MagicMock, WebSocketGenerator -class MockStreamReaderChunked(MockStreamReader): - """Mock a stream reader with simulated chunked data.""" - - async def readchunk(self) -> tuple[bytes, bool]: - """Read bytes.""" - return (self._content.read(), False) - - @pytest.fixture(autouse=True) async def setup_integration( hass: HomeAssistant, diff --git a/tests/components/immich/__init__.py b/tests/components/immich/__init__.py index 3a48c2cd725..604ab84d68d 100644 --- a/tests/components/immich/__init__.py +++ b/tests/components/immich/__init__.py @@ -1,19 +1,10 @@ """Tests for the Immich integration.""" from homeassistant.core import HomeAssistant -from homeassistant.util.aiohttp import MockStreamReader from tests.common import MockConfigEntry -class MockStreamReaderChunked(MockStreamReader): - """Mock a stream reader with simulated chunked data.""" - - async def readchunk(self) -> tuple[bytes, bool]: - """Read bytes.""" - return (self._content.read(), False) - - async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Fixture for setting up the component.""" config_entry.add_to_hass(hass) diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py index 5a957870f07..1b9a7df8df7 100644 --- a/tests/components/immich/conftest.py +++ b/tests/components/immich/conftest.py @@ -23,8 +23,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.aiohttp import MockStreamReaderChunked -from . import MockStreamReaderChunked from .const import MOCK_ALBUM_WITH_ASSETS, MOCK_ALBUM_WITHOUT_ASSETS from tests.common import MockConfigEntry diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index ae7201f5e70..c8da8d94eeb 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -23,9 +23,9 @@ from homeassistant.components.media_source import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.aiohttp import MockRequest +from homeassistant.util.aiohttp import MockRequest, MockStreamReaderChunked -from . import MockStreamReaderChunked, setup_integration +from . import setup_integration from .const import MOCK_ALBUM_WITHOUT_ASSETS from tests.common import MockConfigEntry diff --git a/tests/components/synology_dsm/test_backup.py b/tests/components/synology_dsm/test_backup.py index db0062b45bf..5d54377c202 100644 --- a/tests/components/synology_dsm/test_backup.py +++ b/tests/components/synology_dsm/test_backup.py @@ -34,7 +34,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.backup import async_initialize_backup from homeassistant.setup import async_setup_component -from homeassistant.util.aiohttp import MockStreamReader +from homeassistant.util.aiohttp import MockStreamReader, MockStreamReaderChunked from .common import mock_dsm_information from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME @@ -45,14 +45,6 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator BASE_FILENAME = "Automatic_backup_2025.2.0.dev0_2025-01-09_20.14_35457323" -class MockStreamReaderChunked(MockStreamReader): - """Mock a stream reader with simulated chunked data.""" - - async def readchunk(self) -> tuple[bytes, bool]: - """Read bytes.""" - return (self._content.read(), False) - - async def _mock_download_file(path: str, filename: str) -> MockStreamReader: if filename == f"{BASE_FILENAME}_meta.json": return MockStreamReader( From 4c6e854cad9ef8bd79e1b7a95c380f01dc9e652a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 08:25:41 +0200 Subject: [PATCH 0765/1175] Add valve position capability to SmartThings (#144923) * Add another EHS SmartThings fixture * Add another EHS * Add valve position capability to SmartThings * Fix * Fix --- .../components/smartthings/icons.json | 6 + .../components/smartthings/sensor.py | 10 + .../components/smartthings/strings.json | 7 + .../smartthings/snapshots/test_sensor.ambr | 171 ++++++++++++++++++ 4 files changed, 194 insertions(+) diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index f1034d1a55f..668dff961ee 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -74,6 +74,12 @@ "finished": "mdi:food-turkey" } }, + "diverter_valve_position": { + "state": { + "room": "mdi:sofa", + "tank": "mdi:water-boiler" + } + }, "manual_level": { "default": "mdi:radiator", "state": { diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 6c8c78b4d32..8ae479e58f5 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -433,6 +433,16 @@ CAPABILITY_TO_SENSORS: dict[ ) ], }, + Capability.SAMSUNG_CE_EHS_DIVERTER_VALVE: { + Attribute.POSITION: [ + SmartThingsSensorEntityDescription( + key=Attribute.POSITION, + translation_key="diverter_valve_position", + device_class=SensorDeviceClass.ENUM, + options=["room", "tank"], + ) + ] + }, Capability.ENERGY_METER: { Attribute.ENERGY: [ SmartThingsSensorEntityDescription( diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 0d8e5feabc0..c13fd0e7932 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -284,6 +284,13 @@ "completion_time": { "name": "Completion time" }, + "diverter_valve_position": { + "name": "Valve position", + "state": { + "room": "Room", + "tank": "Tank" + } + }, "dryer_mode": { "name": "Dryer mode" }, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 7e9dd5c08da..4197837112c 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -5945,6 +5945,63 @@ 'state': '1.08249458332857e-05', }) # --- +# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'room', + 'tank', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.eco_heating_system_valve_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve position', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diverter_valve_position', + 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_samsungce.ehsDiverterValve_position_position', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub][sensor.eco_heating_system_valve_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Eco Heating System Valve position', + 'options': list([ + 'room', + 'tank', + ]), + }), + 'context': , + 'entity_id': 'sensor.eco_heating_system_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'room', + }) +# --- # name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6222,6 +6279,63 @@ 'state': '4.50185416638851e-06', }) # --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'room', + 'tank', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.heat_pump_main_valve_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve position', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diverter_valve_position', + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_samsungce.ehsDiverterValve_position_position', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][sensor.heat_pump_main_valve_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Heat Pump Main Valve position', + 'options': list([ + 'room', + 'tank', + ]), + }), + 'context': , + 'entity_id': 'sensor.heat_pump_main_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'room', + }) +# --- # name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6499,6 +6613,63 @@ 'state': '0.000222076093320449', }) # --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'room', + 'tank', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.warmepumpe_valve_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve position', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'diverter_valve_position', + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_samsungce.ehsDiverterValve_position_position', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][sensor.warmepumpe_valve_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Wärmepumpe Valve position', + 'options': list([ + 'room', + 'tank', + ]), + }), + 'context': , + 'entity_id': 'sensor.warmepumpe_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'room', + }) +# --- # name: test_all_entities[da_wm_dw_000001][sensor.dishwasher_completion_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 613aa9b2cf7ea42dc59dd81d3f6236b5e9e4b05d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 08:27:01 +0200 Subject: [PATCH 0766/1175] Add climate entity for heatpump zones in SmartThings (#144991) * Add climate entity for heatpump zones in SmartThings * Fix * Fix * Fix * Fix * Fix * Fix * Sync SmartThings EHS fixture --- .../components/smartthings/__init__.py | 5 +- .../components/smartthings/climate.py | 176 ++++++++- .../smartthings/snapshots/test_climate.ambr | 333 ++++++++++++++++++ tests/components/smartthings/test_climate.py | 254 +++++++++++++ 4 files changed, 766 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 52ce07e06e2..e4259e4182c 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -289,7 +289,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry) for identifier in device_entry.identifiers if identifier[0] == DOMAIN ) - if device_id in device_status: + if any( + device_id.startswith(device_identifier) + for device_identifier in device_status + ): continue device_registry.async_update_device( device_entry.id, remove_config_entry_id=entry.entry_id diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index d063316e233..f87c9bbfcef 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -12,6 +12,8 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, SWING_BOTH, SWING_HORIZONTAL, SWING_OFF, @@ -23,10 +25,11 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry -from .const import MAIN, UNIT_MAP +from .const import DOMAIN, MAIN, UNIT_MAP from .entity import SmartThingsEntity ATTR_OPERATION_STATE = "operation_state" @@ -88,6 +91,14 @@ FAN_OSCILLATION_TO_SWING = { value: key for key, value in SWING_TO_FAN_OSCILLATION.items() } +HEAT_PUMP_AC_MODE_TO_HA = { + "auto": HVACMode.AUTO, + "cool": HVACMode.COOL, + "heat": HVACMode.HEAT, +} + +HA_MODE_TO_HEAT_PUMP_AC_MODE = {v: k for k, v in HEAT_PUMP_AC_MODE_TO_HA.items()} + WIND = "wind" FAN = "fan" WINDFREE = "windFree" @@ -110,6 +121,14 @@ THERMOSTAT_CAPABILITIES = [ Capability.THERMOSTAT_MODE, ] +HEAT_PUMP_CAPABILITIES = [ + Capability.TEMPERATURE_MEASUREMENT, + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Capability.AIR_CONDITIONER_MODE, + Capability.THERMOSTAT_COOLING_SETPOINT, + Capability.SWITCH, +] + async def async_setup_entry( hass: HomeAssistant, @@ -130,6 +149,16 @@ async def async_setup_entry( capability in device.status[MAIN] for capability in THERMOSTAT_CAPABILITIES ) ) + entities.extend( + SmartThingsHeatPumpZone(entry_data.client, device, component) + for device in entry_data.devices.values() + for component in device.status + if component in {"INDOOR", "INDOOR1", "INDOOR2"} + and all( + capability in device.status[component] + for capability in HEAT_PUMP_CAPABILITIES + ) + ) async_add_entities(entities) @@ -592,3 +621,148 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): if state not in modes ) return modes + + +class SmartThingsHeatPumpZone(SmartThingsEntity, ClimateEntity): + """Define a SmartThings heat pump zone.""" + + _attr_name = None + + def __init__(self, client: SmartThings, device: FullDevice, component: str) -> None: + """Init the class.""" + super().__init__( + client, + device, + { + Capability.AIR_CONDITIONER_MODE, + Capability.SWITCH, + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Capability.THERMOSTAT_COOLING_SETPOINT, + Capability.TEMPERATURE_MEASUREMENT, + }, + component=component, + ) + self._attr_hvac_modes = self._determine_hvac_modes() + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{device.device.device_id}_{component}")}, + via_device=(DOMAIN, device.device.device_id), + name=f"{device.device.label} {component}", + ) + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + features = ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + if ( + self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.AIR_CONDITIONER_MODE + ) + != "auto" + ): + features |= ClimateEntityFeature.TARGET_TEMPERATURE + return features + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_setpoint = self.get_attribute_value( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, Attribute.MINIMUM_SETPOINT + ) + if min_setpoint == -1000: + return DEFAULT_MIN_TEMP + return min_setpoint + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_setpoint = self.get_attribute_value( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, Attribute.MAXIMUM_SETPOINT + ) + if max_setpoint == -1000: + return DEFAULT_MAX_TEMP + return max_setpoint + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + if hvac_mode == HVACMode.OFF: + await self.async_turn_off() + return + if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "off": + await self.async_turn_on() + + await self.execute_device_command( + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + argument=HA_MODE_TO_HEAT_PUMP_AC_MODE[hvac_mode], + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self.execute_device_command( + Capability.THERMOSTAT_COOLING_SETPOINT, + Command.SET_COOLING_SETPOINT, + argument=kwargs[ATTR_TEMPERATURE], + ) + + async def async_turn_on(self) -> None: + """Turn device on.""" + await self.execute_device_command( + Capability.SWITCH, + Command.ON, + ) + + async def async_turn_off(self) -> None: + """Turn device off.""" + await self.execute_device_command( + Capability.SWITCH, + Command.OFF, + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.get_attribute_value( + Capability.TEMPERATURE_MEASUREMENT, Attribute.TEMPERATURE + ) + + @property + def hvac_mode(self) -> HVACMode | None: + """Return current operation ie. heat, cool, idle.""" + if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "off": + return HVACMode.OFF + return HEAT_PUMP_AC_MODE_TO_HA.get( + self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.AIR_CONDITIONER_MODE + ) + ) + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self.get_attribute_value( + Capability.THERMOSTAT_COOLING_SETPOINT, Attribute.COOLING_SETPOINT + ) + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][ + Attribute.TEMPERATURE + ].unit + assert unit + return UNIT_MAP[unit] + + def _determine_hvac_modes(self) -> list[HVACMode]: + """Determine the supported HVAC modes.""" + modes = [HVACMode.OFF] + if ( + ac_modes := self.get_attribute_value( + Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES + ) + ) is not None: + modes.extend( + state + for mode in ac_modes + if (state := HEAT_PUMP_AC_MODE_TO_HA.get(mode)) is not None + ) + return modes diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index aef51b1c866..a478605a3b1 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -128,6 +128,73 @@ 'state': 'heat', }) # --- +# name: test_all_entities[da_ac_ehs_01001][climate.heat_pump_indoor1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 26, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.heat_pump_indoor1', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_INDOOR1', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ac_ehs_01001][climate.heat_pump_indoor1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 18.5, + 'friendly_name': 'Heat pump INDOOR1', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 26, + 'supported_features': , + 'temperature': 35, + }), + 'context': , + 'entity_id': 'climate.heat_pump_indoor1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ac_rac_000001][climate.ac_office_granit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -528,6 +595,272 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_sac_ehs_000001_sub][climate.eco_heating_system_indoor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 25, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.eco_heating_system_indoor', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_INDOOR', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub][climate.eco_heating_system_indoor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 23.1, + 'friendly_name': 'Eco Heating System INDOOR', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 25, + 'supported_features': , + 'temperature': 25, + }), + 'context': , + 'entity_id': 'climate.eco_heating_system_indoor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][climate.heat_pump_main_indoor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 25, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.heat_pump_main_indoor', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_INDOOR', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000001_sub_1][climate.heat_pump_main_indoor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 31, + 'friendly_name': 'Heat Pump Main INDOOR', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 65, + 'min_temp': 25, + 'supported_features': , + 'temperature': 30, + }), + 'context': , + 'entity_id': 'climate.heat_pump_main_indoor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'heat', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][climate.warmepumpe_indoor1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.warmepumpe_indoor1', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_INDOOR1', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][climate.warmepumpe_indoor1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 31.2, + 'friendly_name': 'Wärmepumpe INDOOR1', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + }), + 'context': , + 'entity_id': 'climate.warmepumpe_indoor1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'auto', + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][climate.warmepumpe_indoor2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.warmepumpe_indoor2', + 'has_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': 'smartthings', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_INDOOR2', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_sac_ehs_000002_sub][climate.warmepumpe_indoor2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 29.1, + 'friendly_name': 'Wärmepumpe INDOOR2', + 'hvac_modes': list([ + , + , + , + , + ]), + 'max_temp': 35, + 'min_temp': 7, + 'supported_features': , + }), + 'context': , + 'entity_id': 'climate.warmepumpe_indoor2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[ecobee_thermostat][climate.main_floor-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 6332fbf905f..6f2325cad78 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -16,6 +16,8 @@ from homeassistant.components.climate import ( ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, @@ -865,6 +867,258 @@ async def test_thermostat_state_attributes_update( assert hass.states.get("climate.asd").attributes[state_attribute] == expected_value +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_heat_pump_hvac_mode( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test heat pump set HVAC mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.warmepumpe_indoor1", ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + "INDOOR1", + argument="heat", + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_heat_pump_hvac_mode_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test heat pump set HVAC mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.warmepumpe_indoor1", ATTR_HVAC_MODE: HVACMode.OFF}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + Command.OFF, + "INDOOR1", + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_heat_pump_hvac_mode_from_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test heat pump set HVAC mode.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.warmepumpe_indoor2", ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + assert devices.execute_device_command.mock_calls == [ + call( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + Command.ON, + "INDOOR2", + ), + call( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Command.SET_AIR_CONDITIONER_MODE, + "INDOOR2", + argument="heat", + ), + ] + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_heat_pump_set_temperature( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test heat pump set temperature.""" + set_attribute_value( + devices, + Capability.AIR_CONDITIONER_MODE, + Attribute.AIR_CONDITIONER_MODE, + "heat", + component="INDOOR1", + ) + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "climate.warmepumpe_indoor1", ATTR_TEMPERATURE: 35}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.THERMOSTAT_COOLING_SETPOINT, + Command.SET_COOLING_SETPOINT, + "INDOOR1", + argument=35, + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +@pytest.mark.parametrize( + ("service", "command"), + [ + (SERVICE_TURN_ON, Command.ON), + (SERVICE_TURN_OFF, Command.OFF), + ], +) +async def test_heat_pump_turn_on_off( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + service: str, + command: Command, +) -> None: + """Test heat pump turn on/off.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + CLIMATE_DOMAIN, + service, + {ATTR_ENTITY_ID: "climate.warmepumpe_indoor1"}, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.SWITCH, + command, + "INDOOR1", + ) + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) +async def test_heat_pump_hvac_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test state update.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("climate.warmepumpe_indoor1").state == HVACMode.AUTO + + await trigger_update( + hass, + devices, + "3810e5ad-5351-d9f9-12ff-000001200000", + Capability.AIR_CONDITIONER_MODE, + Attribute.AIR_CONDITIONER_MODE, + "cool", + component="INDOOR1", + ) + + assert hass.states.get("climate.warmepumpe_indoor1").state == HVACMode.COOL + + +@pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000001_sub"]) +@pytest.mark.parametrize( + ( + "capability", + "attribute", + "value", + "state_attribute", + "original_value", + "expected_value", + ), + [ + ( + Capability.TEMPERATURE_MEASUREMENT, + Attribute.TEMPERATURE, + 20, + ATTR_CURRENT_TEMPERATURE, + 23.1, + 20, + ), + ( + Capability.THERMOSTAT_COOLING_SETPOINT, + Attribute.COOLING_SETPOINT, + 20, + ATTR_TEMPERATURE, + 25, + 20, + ), + ( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Attribute.MINIMUM_SETPOINT, + 6, + ATTR_MIN_TEMP, + 25, + 6, + ), + ( + Capability.CUSTOM_THERMOSTAT_SETPOINT_CONTROL, + Attribute.MAXIMUM_SETPOINT, + 36, + ATTR_MAX_TEMP, + 65, + 36, + ), + ], + ids=[ + ATTR_CURRENT_TEMPERATURE, + ATTR_TEMPERATURE, + ATTR_MIN_TEMP, + ATTR_MAX_TEMP, + ], +) +async def test_heat_pump_state_attributes_update( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, + capability: Capability, + attribute: Attribute, + value: Any, + state_attribute: str, + original_value: Any, + expected_value: Any, +) -> None: + """Test state attributes update.""" + await setup_integration(hass, mock_config_entry) + + assert ( + hass.states.get("climate.eco_heating_system_indoor").attributes[state_attribute] + == original_value + ) + + await trigger_update( + hass, + devices, + "1f98ebd0-ac48-d802-7f62-000001200100", + capability, + attribute, + value, + component="INDOOR", + ) + + assert ( + hass.states.get("climate.eco_heating_system_indoor").attributes[state_attribute] + == expected_value + ) + + @pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) async def test_availability( hass: HomeAssistant, From 1db5c514e683ee2bb9df6f727caaef4598daccae Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Thu, 22 May 2025 03:07:38 -0400 Subject: [PATCH 0767/1175] Add binary_sensor platform to Rehlko (#145391) * feat: add binary_sensor platform to Rehlko * feat: add binary sensor platform * fix: simplify availability logic * fix: simplify availability logic * fix: simplify * fix: rename sensor * fix: rename sensor * fix: rename sensor * fix: remove unneeded type * fix: rename sensor to 'Auto run' * fix: use device_class name --- homeassistant/components/rehlko/__init__.py | 2 +- .../components/rehlko/binary_sensor.py | 108 +++++++++++++ homeassistant/components/rehlko/entity.py | 6 +- homeassistant/components/rehlko/strings.json | 8 + .../rehlko/snapshots/test_binary_sensor.ambr | 144 ++++++++++++++++++ tests/components/rehlko/test_binary_sensor.py | 93 +++++++++++ 6 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/rehlko/binary_sensor.py create mode 100644 tests/components/rehlko/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/rehlko/test_binary_sensor.py diff --git a/homeassistant/components/rehlko/__init__.py b/homeassistant/components/rehlko/__init__.py index 3f255f23085..d07289d256c 100644 --- a/homeassistant/components/rehlko/__init__.py +++ b/homeassistant/components/rehlko/__init__.py @@ -22,7 +22,7 @@ from .const import ( ) from .coordinator import RehlkoConfigEntry, RehlkoRuntimeData, RehlkoUpdateCoordinator -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rehlko/binary_sensor.py b/homeassistant/components/rehlko/binary_sensor.py new file mode 100644 index 00000000000..a2c0d694735 --- /dev/null +++ b/homeassistant/components/rehlko/binary_sensor.py @@ -0,0 +1,108 @@ +"""Binary sensor platform for Rehlko integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import ( + DEVICE_DATA_DEVICES, + DEVICE_DATA_ID, + DEVICE_DATA_IS_CONNECTED, + GENERATOR_DATA_DEVICE, +) +from .coordinator import RehlkoConfigEntry +from .entity import RehlkoEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class RehlkoBinarySensorEntityDescription(BinarySensorEntityDescription): + """Class describing Rehlko binary sensor entities.""" + + on_value: str | bool = True + off_value: str | bool = False + document_key: str | None = None + connectivity_key: str | None = DEVICE_DATA_IS_CONNECTED + + +BINARY_SENSORS: tuple[RehlkoBinarySensorEntityDescription, ...] = ( + RehlkoBinarySensorEntityDescription( + key=DEVICE_DATA_IS_CONNECTED, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + document_key=GENERATOR_DATA_DEVICE, + # Entity is available when the device is disconnected + connectivity_key=None, + ), + RehlkoBinarySensorEntityDescription( + key="switchState", + translation_key="auto_run", + on_value="Auto", + off_value="Off", + ), + RehlkoBinarySensorEntityDescription( + key="engineOilPressureOk", + translation_key="oil_pressure", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + on_value=False, + off_value=True, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: RehlkoConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the binary sensor platform.""" + homes = config_entry.runtime_data.homes + coordinators = config_entry.runtime_data.coordinators + async_add_entities( + RehlkoBinarySensorEntity( + coordinators[device_data[DEVICE_DATA_ID]], + device_data[DEVICE_DATA_ID], + device_data, + sensor_description, + document_key=sensor_description.document_key, + connectivity_key=sensor_description.connectivity_key, + ) + for home_data in homes + for device_data in home_data[DEVICE_DATA_DEVICES] + for sensor_description in BINARY_SENSORS + ) + + +class RehlkoBinarySensorEntity(RehlkoEntity, BinarySensorEntity): + """Representation of a Binary Sensor.""" + + entity_description: RehlkoBinarySensorEntityDescription + + @property + def is_on(self) -> bool | None: + """Return the state of the binary sensor.""" + if self._rehlko_value == self.entity_description.on_value: + return True + if self._rehlko_value == self.entity_description.off_value: + return False + _LOGGER.warning( + "Unexpected value for %s: %s", + self.entity_description.key, + self._rehlko_value, + ) + return None diff --git a/homeassistant/components/rehlko/entity.py b/homeassistant/components/rehlko/entity.py index 274562e6a41..d1c25742f42 100644 --- a/homeassistant/components/rehlko/entity.py +++ b/homeassistant/components/rehlko/entity.py @@ -44,6 +44,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): device_data: dict, description: EntityDescription, document_key: str | None = None, + connectivity_key: str | None = DEVICE_DATA_IS_CONNECTED, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -62,6 +63,7 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): connections=_get_device_connections(device_data[DEVICE_DATA_MAC_ADDRESS]), ) self._document_key = document_key + self._connectivity_key = connectivity_key @property def _device_data(self) -> dict[str, Any]: @@ -80,4 +82,6 @@ class RehlkoEntity(CoordinatorEntity[RehlkoUpdateCoordinator]): @property def available(self) -> bool: """Return if entity is available.""" - return super().available and self._device_data[DEVICE_DATA_IS_CONNECTED] + return super().available and ( + not self._connectivity_key or self._device_data[self._connectivity_key] + ) diff --git a/homeassistant/components/rehlko/strings.json b/homeassistant/components/rehlko/strings.json index d98ae04d5c8..bdf0e3de01c 100644 --- a/homeassistant/components/rehlko/strings.json +++ b/homeassistant/components/rehlko/strings.json @@ -31,6 +31,14 @@ } }, "entity": { + "binary_sensor": { + "auto_run": { + "name": "Auto run" + }, + "oil_pressure": { + "name": "Oil pressure" + } + }, "sensor": { "engine_speed": { "name": "Engine speed" diff --git a/tests/components/rehlko/snapshots/test_binary_sensor.ambr b/tests/components/rehlko/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..24284faa3cc --- /dev/null +++ b/tests/components/rehlko/snapshots/test_binary_sensor.ambr @@ -0,0 +1,144 @@ +# serializer version: 1 +# name: test_sensors[binary_sensor.generator_1_auto_run-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.generator_1_auto_run', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Auto run', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_run', + 'unique_id': 'myemail@email.com_12345_switchState', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[binary_sensor.generator_1_auto_run-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Generator 1 Auto run', + }), + 'context': , + 'entity_id': 'binary_sensor.generator_1_auto_run', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensors[binary_sensor.generator_1_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.generator_1_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': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'myemail@email.com_12345_isConnected', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[binary_sensor.generator_1_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Generator 1 Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.generator_1_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_sensors[binary_sensor.generator_1_oil_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.generator_1_oil_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Oil pressure', + 'platform': 'rehlko', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'oil_pressure', + 'unique_id': 'myemail@email.com_12345_engineOilPressureOk', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[binary_sensor.generator_1_oil_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Generator 1 Oil pressure', + }), + 'context': , + 'entity_id': 'binary_sensor.generator_1_oil_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/rehlko/test_binary_sensor.py b/tests/components/rehlko/test_binary_sensor.py new file mode 100644 index 00000000000..8834635f716 --- /dev/null +++ b/tests/components/rehlko/test_binary_sensor.py @@ -0,0 +1,93 @@ +"""Tests for the Rehlko binary sensors.""" + +from __future__ import annotations + +import logging +from typing import Any +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.rehlko.const import GENERATOR_DATA_DEVICE +from homeassistant.components.rehlko.coordinator import SCAN_INTERVAL_MINUTES +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.fixture(name="platform_binary_sensor", autouse=True) +async def platform_binary_sensor_fixture(): + """Patch Rehlko to only load binary_sensor platform.""" + with patch("homeassistant.components.rehlko.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensors( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + rehlko_config_entry: MockConfigEntry, + load_rehlko_config_entry: None, +) -> None: + """Test the Rehlko binary sensors.""" + await snapshot_platform( + hass, entity_registry, snapshot, rehlko_config_entry.entry_id + ) + + +async def test_binary_sensor_states( + hass: HomeAssistant, + generator: dict[str, Any], + mock_rehlko: AsyncMock, + load_rehlko_config_entry: None, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the Rehlko binary sensor state logic.""" + assert generator["engineOilPressureOk"] is True + state = hass.states.get("binary_sensor.generator_1_oil_pressure") + assert state.state == STATE_OFF + + generator["engineOilPressureOk"] = False + freezer.tick(SCAN_INTERVAL_MINUTES) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.generator_1_oil_pressure") + assert state.state == STATE_ON + + generator["engineOilPressureOk"] = "Unknown State" + with caplog.at_level(logging.WARNING): + caplog.clear() + freezer.tick(SCAN_INTERVAL_MINUTES) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.generator_1_oil_pressure") + assert state.state == STATE_UNKNOWN + assert "Unknown State" in caplog.text + assert "engineOilPressureOk" in caplog.text + + +async def test_binary_sensor_connectivity_availability( + hass: HomeAssistant, + generator: dict[str, Any], + mock_rehlko: AsyncMock, + load_rehlko_config_entry: None, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the connectivity entity availability when device is disconnected.""" + state = hass.states.get("binary_sensor.generator_1_connectivity") + assert state.state == STATE_ON + + # Entity should be available when device is disconnected + generator[GENERATOR_DATA_DEVICE]["isConnected"] = False + freezer.tick(SCAN_INTERVAL_MINUTES) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.generator_1_connectivity") + assert state.state == STATE_OFF From 8758a086c1f438581608dcc861f3bb372379f78d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 09:18:28 +0200 Subject: [PATCH 0768/1175] Improve type hints in doods (#145426) --- .../components/doods/image_processing.py | 66 +++++++++---------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index bcc6e7a8050..a00f942ec61 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -6,6 +6,7 @@ import io import logging import os import time +from typing import Any from PIL import Image, ImageDraw, UnidentifiedImageError from pydoods import PyDOODS @@ -88,10 +89,11 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Doods client.""" - url = config[CONF_URL] - auth_key = config[CONF_AUTH_KEY] - detector_name = config[CONF_DETECTOR] - timeout = config[CONF_TIMEOUT] + url: str = config[CONF_URL] + auth_key: str = config[CONF_AUTH_KEY] + detector_name: str = config[CONF_DETECTOR] + source: list[dict[str, str]] = config[CONF_SOURCE] + timeout: int = config[CONF_TIMEOUT] doods = PyDOODS(url, auth_key, timeout) response = doods.get_detectors() @@ -113,31 +115,35 @@ def setup_platform( add_entities( Doods( - hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), doods, detector, config, ) - for camera in config[CONF_SOURCE] + for camera in source ) class Doods(ImageProcessingEntity): """Doods image processing service client.""" - def __init__(self, hass, camera_entity, name, doods, detector, config): + def __init__( + self, + camera_entity: str, + name: str | None, + doods: PyDOODS, + detector: dict[str, Any], + config: dict[str, Any], + ) -> None: """Initialize the DOODS entity.""" - self.hass = hass - self._camera_entity = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - name = split_entity_id(camera_entity)[1] - self._name = f"Doods {name}" + self._attr_name = f"Doods {split_entity_id(camera_entity)[1]}" self._doods = doods - self._file_out = config[CONF_FILE_OUT] + self._file_out: list[template.Template] = config[CONF_FILE_OUT] self._detector_name = detector["name"] # detector config and aspect ratio @@ -150,16 +156,16 @@ class Doods(ImageProcessingEntity): self._aspect = self._width / self._height # the base confidence - dconfig = {} - confidence = config[CONF_CONFIDENCE] + dconfig: dict[str, float] = {} + confidence: float = config[CONF_CONFIDENCE] # handle labels and specific detection areas - labels = config[CONF_LABELS] + labels: list[str | dict[str, Any]] = config[CONF_LABELS] self._label_areas = {} self._label_covers = {} for label in labels: if isinstance(label, dict): - label_name = label[CONF_NAME] + label_name: str = label[CONF_NAME] if label_name not in detector["labels"] and label_name != "*": _LOGGER.warning("Detector does not support label %s", label_name) continue @@ -207,28 +213,18 @@ class Doods(ImageProcessingEntity): self._covers = area_config[CONF_COVERS] self._dconfig = dconfig - self._matches = {} + self._matches: dict[str, list[dict[str, Any]]] = {} self._total_matches = 0 self._last_image = None - self._process_time = 0 + self._process_time = 0.0 @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera_entity - - @property - def name(self): - """Return the name of the image processor.""" - return self._name - - @property - def state(self): + def state(self) -> int: """Return the state of the entity.""" return self._total_matches @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { ATTR_MATCHES: self._matches, @@ -281,7 +277,7 @@ class Doods(ImageProcessingEntity): os.makedirs(os.path.dirname(path), exist_ok=True) img.save(path) - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process the image.""" try: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") @@ -312,7 +308,7 @@ class Doods(ImageProcessingEntity): time.monotonic() - start, ) - matches = {} + matches: dict[str, list[dict[str, Any]]] = {} total_matches = 0 if not response or "error" in response: @@ -382,9 +378,7 @@ class Doods(ImageProcessingEntity): paths = [] for path_template in self._file_out: if isinstance(path_template, template.Template): - paths.append( - path_template.render(camera_entity=self._camera_entity) - ) + paths.append(path_template.render(camera_entity=self.camera_entity)) else: paths.append(path_template) self._save_image(image, matches, paths) From d48ca1d858835da8d1b70bcebab9499dbed46859 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Thu, 22 May 2025 09:07:10 +0100 Subject: [PATCH 0769/1175] Hotfix for incorrect bracket in messages for Squeezebox (#145418) hotfix for wrong bracket --- homeassistant/components/squeezebox/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 593d637e0db..b004234c327 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -162,7 +162,7 @@ "message": "Timeout connecting to LMS {host}." }, "init_auth_failed": { - "message": "Authentication failed for {host)." + "message": "Authentication failed with {host}." }, "init_get_status_failed": { "message": "Failed to get status from LMS {host} (HTTP status: {http_status}). Will retry." From e410977e6400d954dd80e8a508c02dd8a72ee944 Mon Sep 17 00:00:00 2001 From: "Lektri.co" <137074859+Lektrico@users.noreply.github.com> Date: Thu, 22 May 2025 11:34:14 +0300 Subject: [PATCH 0770/1175] Add new button to the Lektrico integration (#145420) --- homeassistant/components/lektrico/button.py | 6 +++ .../components/lektrico/strings.json | 3 ++ .../lektrico/snapshots/test_button.ambr | 47 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/homeassistant/components/lektrico/button.py b/homeassistant/components/lektrico/button.py index e598773321d..95913b33700 100644 --- a/homeassistant/components/lektrico/button.py +++ b/homeassistant/components/lektrico/button.py @@ -39,6 +39,12 @@ BUTTONS_FOR_CHARGERS: tuple[LektricoButtonEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, press_fn=lambda device: device.send_charge_stop(), ), + LektricoButtonEntityDescription( + key="charging_schedule_override", + translation_key="charging_schedule_override", + entity_category=EntityCategory.CONFIG, + press_fn=lambda device: device.send_charge_schedule_override(), + ), LektricoButtonEntityDescription( key="reboot", device_class=ButtonDeviceClass.RESTART, diff --git a/homeassistant/components/lektrico/strings.json b/homeassistant/components/lektrico/strings.json index 23aac0b3059..6664dd9672d 100644 --- a/homeassistant/components/lektrico/strings.json +++ b/homeassistant/components/lektrico/strings.json @@ -60,6 +60,9 @@ }, "charge_stop": { "name": "Charge stop" + }, + "charging_schedule_override": { + "name": "Charging schedule override" } }, "number": { diff --git a/tests/components/lektrico/snapshots/test_button.ambr b/tests/components/lektrico/snapshots/test_button.ambr index f9cb7189237..760a2f9fcdd 100644 --- a/tests/components/lektrico/snapshots/test_button.ambr +++ b/tests/components/lektrico/snapshots/test_button.ambr @@ -93,6 +93,53 @@ 'state': 'unknown', }) # --- +# name: test_all_entities[button.1p7k_500006_charging_schedule_override-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.1p7k_500006_charging_schedule_override', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Charging schedule override', + 'platform': 'lektrico', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'charging_schedule_override', + 'unique_id': '500006-charging_schedule_override', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.1p7k_500006_charging_schedule_override-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': '1p7k_500006 Charging schedule override', + }), + 'context': , + 'entity_id': 'button.1p7k_500006_charging_schedule_override', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_all_entities[button.1p7k_500006_restart-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 7893eaa389f19a5c08c966db6a12929cd014564e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 10:36:12 +0200 Subject: [PATCH 0771/1175] Improve type hints in microsoft_face_identify (#145419) --- .../image_processing.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/microsoft_face_identify/image_processing.py b/homeassistant/components/microsoft_face_identify/image_processing.py index 025a7eccdda..ed793580e1b 100644 --- a/homeassistant/components/microsoft_face_identify/image_processing.py +++ b/homeassistant/components/microsoft_face_identify/image_processing.py @@ -10,9 +10,10 @@ from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, + FaceInformation, ImageProcessingFaceEntity, ) -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE, MicrosoftFace from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError @@ -37,8 +38,9 @@ async def async_setup_platform( ) -> None: """Set up the Microsoft Face identify platform.""" api = hass.data[DATA_MICROSOFT_FACE] - face_group = config[CONF_GROUP] - confidence = config[CONF_CONFIDENCE] + face_group: str = config[CONF_GROUP] + confidence: float = config[CONF_CONFIDENCE] + source: list[dict[str, str]] = config[CONF_SOURCE] async_add_entities( MicrosoftFaceIdentifyEntity( @@ -48,43 +50,35 @@ async def async_setup_platform( confidence, camera.get(CONF_NAME), ) - for camera in config[CONF_SOURCE] + for camera in source ) class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Representation of the Microsoft Face API entity for identify.""" - def __init__(self, camera_entity, api, face_group, confidence, name=None): + def __init__( + self, + camera_entity: str, + api: MicrosoftFace, + face_group: str, + confidence: float, + name: str | None, + ) -> None: """Initialize the Microsoft Face API.""" super().__init__() self._api = api - self._camera = camera_entity - self._confidence = confidence + self._attr_camera_entity = camera_entity + self._attr_confidence = confidence self._face_group = face_group if name: - self._name = name + self._attr_name = name else: - self._name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}" + self._attr_name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}" - @property - def confidence(self): - """Return minimum confidence for send events.""" - return self._confidence - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - async def async_process_image(self, image): + async def async_process_image(self, image: bytes) -> None: """Process image. This method is a coroutine. @@ -106,7 +100,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): return # Parse data - known_faces = [] + known_faces: list[FaceInformation] = [] total = 0 for face in detect: total += 1 From a7f6a6f22c25e9e3c2085ed23b53c2854df1ede9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 10:37:03 +0200 Subject: [PATCH 0772/1175] Improve type hints in dlib_face_detect (#145422) --- .../dlib_face_detect/image_processing.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 80becdf9992..79f03ab3af7 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -25,37 +25,28 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) - for camera in config[CONF_SOURCE] + for camera in source ) class DlibFaceDetectEntity(ImageProcessingFaceEntity): """Dlib Face API entity for identify.""" - def __init__(self, camera_entity, name=None): + def __init__(self, camera_entity: str, name: str | None) -> None: """Initialize Dlib face entity.""" super().__init__() - self._camera = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - self._name = f"Dlib Face {split_entity_id(camera_entity)[1]}" + self._attr_name = f"Dlib Face {split_entity_id(camera_entity)[1]}" - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process image.""" fak_file = io.BytesIO(image) From 3d53bdc6c5888335f1770e65bf0f48403e84443e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 10:37:44 +0200 Subject: [PATCH 0773/1175] Improve type hints in dlib_face_identify (#145423) --- .../dlib_face_identify/image_processing.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index fee9f8dab3c..c41dad863d4 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, + FaceInformation, ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE @@ -38,31 +39,40 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + confidence: float = config[CONF_CONFIDENCE] + faces: dict[str, str] = config[CONF_FACES] + source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( DlibFaceIdentifyEntity( camera[CONF_ENTITY_ID], - config[CONF_FACES], + faces, camera.get(CONF_NAME), - config[CONF_CONFIDENCE], + confidence, ) - for camera in config[CONF_SOURCE] + for camera in source ) class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): """Dlib Face API entity for identify.""" - def __init__(self, camera_entity, faces, name, tolerance): + def __init__( + self, + camera_entity: str, + faces: dict[str, str], + name: str | None, + tolerance: float, + ) -> None: """Initialize Dlib face identify entry.""" super().__init__() - self._camera = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - self._name = f"Dlib Face {split_entity_id(camera_entity)[1]}" + self._attr_name = f"Dlib Face {split_entity_id(camera_entity)[1]}" self._faces = {} for face_name, face_file in faces.items(): @@ -74,17 +84,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): self._tolerance = tolerance - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process image.""" fak_file = io.BytesIO(image) @@ -94,7 +94,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): image = face_recognition.load_image_file(fak_file) unknowns = face_recognition.face_encodings(image) - found = [] + found: list[FaceInformation] = [] for unknown_face in unknowns: for name, face in self._faces.items(): result = face_recognition.compare_faces( From b5a3cedacd58c3c013aa9c5b016713a5de23f189 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 10:44:31 +0200 Subject: [PATCH 0774/1175] Move to explicit exports in test helpers (#145392) --- tests/common.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/common.py b/tests/common.py index a80027b2b7e..9aafba74aea 100644 --- a/tests/common.py +++ b/tests/common.py @@ -28,7 +28,7 @@ from types import FrameType, ModuleType from typing import Any, Literal, NoReturn from unittest.mock import AsyncMock, Mock, patch -from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 +from aiohttp.test_utils import unused_port as get_test_instance_port from annotatedyaml import load_yaml_dict, loader as yaml_loader import attr import pytest @@ -44,7 +44,7 @@ from homeassistant.auth import ( ) from homeassistant.auth.permissions import system_policies from homeassistant.components import device_automation, persistent_notification as pn -from homeassistant.components.device_automation import ( # noqa: F401 +from homeassistant.components.device_automation import ( _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) from homeassistant.components.logger import ( @@ -121,6 +121,11 @@ from .testing_config.custom_components.test_constant_deprecation import ( import_deprecated_constant, ) +__all__ = [ + "async_get_device_automation_capabilities", + "get_test_instance_port", +] + _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" From c007286fd680331c91b9cd0b969e7eeaeb9d107c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 22 May 2025 11:11:38 +0200 Subject: [PATCH 0775/1175] Improve Z-Wave config flow test typing (#145438) --- tests/components/zwave_js/test_config_flow.py | 166 ++++++++++-------- 1 file changed, 90 insertions(+), 76 deletions(-) diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index e07caca3c6a..5a1e7b217e0 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -246,7 +246,7 @@ async def test_manual(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "1234" -async def slow_server_version(*args): +async def slow_server_version(*args: Any) -> Any: """Simulate a slow server version.""" await asyncio.sleep(0.1) @@ -650,10 +650,10 @@ async def test_abort_hassio_discovery_for_other_addon(hass: HomeAssistant) -> No ) async def test_usb_discovery( hass: HomeAssistant, - install_addon, + install_addon: AsyncMock, mock_usb_serial_by_id: MagicMock, - set_addon_options, - start_addon, + set_addon_options: AsyncMock, + start_addon: AsyncMock, usb_discovery_info: UsbServiceInfo, device: str, discovery_name: str, @@ -789,6 +789,7 @@ async def test_usb_discovery_addon_not_running( # Make sure the discovered usb device is preferred. data_schema = result["data_schema"] + assert data_schema is not None assert data_schema({}) == { "s0_legacy_key": "", "s2_access_control_key": "", @@ -1566,7 +1567,7 @@ async def test_not_addon(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("supervisor", "addon_running") async def test_addon_running( hass: HomeAssistant, - addon_options, + addon_options: dict[str, Any], ) -> None: """Test add-on already running on Supervisor.""" addon_options["device"] = "/test" @@ -2659,15 +2660,15 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( ) async def test_reconfigure_addon_running( hass: HomeAssistant, - client, - integration, - addon_options, - set_addon_options, - restart_addon, - entry_data, - old_addon_options, - new_addon_options, - disconnect_calls, + client: MagicMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + entry_data: dict[str, Any], + old_addon_options: dict[str, Any], + new_addon_options: dict[str, Any], + disconnect_calls: int, ) -> None: """Test reconfigure flow and add-on already running on Supervisor.""" addon_options.update(old_addon_options) @@ -2784,14 +2785,14 @@ async def test_reconfigure_addon_running( ) async def test_reconfigure_addon_running_no_changes( hass: HomeAssistant, - client, - integration, - addon_options, - set_addon_options, - restart_addon, - entry_data, - old_addon_options, - new_addon_options, + client: MagicMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + entry_data: dict[str, Any], + old_addon_options: dict[str, Any], + new_addon_options: dict[str, Any], ) -> None: """Test reconfigure flow without changes, and add-on already running on Supervisor.""" addon_options.update(old_addon_options) @@ -2943,15 +2944,15 @@ async def different_device_server_version(*args): ) async def test_reconfigure_different_device( hass: HomeAssistant, - client, - integration, - addon_options, - set_addon_options, - restart_addon, - entry_data, - old_addon_options, - new_addon_options, - disconnect_calls, + client: MagicMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + entry_data: dict[str, Any], + old_addon_options: dict[str, Any], + new_addon_options: dict[str, Any], + disconnect_calls: int, ) -> None: """Test reconfigure flow and configuring a different device.""" addon_options.update(old_addon_options) @@ -3105,15 +3106,15 @@ async def test_reconfigure_different_device( ) async def test_reconfigure_addon_restart_failed( hass: HomeAssistant, - client, - integration, - addon_options, - set_addon_options, - restart_addon, - entry_data, - old_addon_options, - new_addon_options, - disconnect_calls, + client: MagicMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + restart_addon: AsyncMock, + entry_data: dict[str, Any], + old_addon_options: dict[str, Any], + new_addon_options: dict[str, Any], + disconnect_calls: int, ) -> None: """Test reconfigure flow and add-on restart failure.""" addon_options.update(old_addon_options) @@ -3329,16 +3330,16 @@ async def test_reconfigure_addon_running_server_info_failure( ) async def test_reconfigure_addon_not_installed( hass: HomeAssistant, - client, - install_addon, - integration, - addon_options, - set_addon_options, - start_addon, - entry_data, - old_addon_options, - new_addon_options, - disconnect_calls, + client: MagicMock, + install_addon: AsyncMock, + integration: MockConfigEntry, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, + start_addon: AsyncMock, + entry_data: dict[str, Any], + old_addon_options: dict[str, Any], + new_addon_options: dict[str, Any], + disconnect_calls: int, ) -> None: """Test reconfigure flow and add-on not installed on Supervisor.""" addon_options.update(old_addon_options) @@ -3464,7 +3465,10 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_reconfigure_migrate_no_addon(hass: HomeAssistant, integration) -> None: +async def test_reconfigure_migrate_no_addon( + hass: HomeAssistant, + integration: MockConfigEntry, +) -> None: """Test migration flow fails when not using add-on.""" entry = integration hass.config_entries.async_update_entry( @@ -3525,11 +3529,11 @@ async def test_reconfigure_migrate_low_sdk_version( ) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, - client, - integration, - restart_addon, - addon_options, - set_addon_options, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + addon_options: dict[str, Any], + set_addon_options: AsyncMock, get_server_version: AsyncMock, reset_server_version_side_effect: Exception | None, reset_unique_id: str, @@ -3627,10 +3631,12 @@ async def test_reconfigure_migrate_with_addon( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" - assert result["data_schema"].schema[CONF_USB_PATH] + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] # Ensure the old usb path is not in the list of options with pytest.raises(InInvalid): - result["data_schema"].schema[CONF_USB_PATH](addon_options["device"]) + data_schema.schema[CONF_USB_PATH](addon_options["device"]) # Reset side effect before starting the add-on. get_server_version.side_effect = None @@ -3684,10 +3690,10 @@ async def test_reconfigure_migrate_with_addon( @pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_reset_driver_ready_timeout( hass: HomeAssistant, - client, - integration, - restart_addon, - set_addon_options, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + set_addon_options: AsyncMock, get_server_version: AsyncMock, ) -> None: """Test migration flow with driver ready timeout after controller reset.""" @@ -3783,7 +3789,9 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" - assert result["data_schema"].schema[CONF_USB_PATH] + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -3831,10 +3839,10 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( @pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_restore_driver_ready_timeout( hass: HomeAssistant, - client, - integration, - restart_addon, - set_addon_options, + client: MagicMock, + integration: MockConfigEntry, + restart_addon: AsyncMock, + set_addon_options: AsyncMock, ) -> None: """Test migration flow with driver ready timeout after nvm restore.""" entry = integration @@ -3919,7 +3927,9 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "choose_serial_port" - assert result["data_schema"].schema[CONF_USB_PATH] + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -4218,7 +4228,9 @@ async def test_reconfigure_migrate_restore_failure( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "restore_failed" - assert result["description_placeholders"]["file_path"] + description_placeholders = result["description_placeholders"] + assert description_placeholders is not None + assert description_placeholders["file_path"] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -4514,13 +4526,15 @@ async def test_intent_recommended_user( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "configure_addon_user" - assert result["data_schema"].schema[CONF_USB_PATH] is not None - assert result["data_schema"].schema.get(CONF_S0_LEGACY_KEY) is None - assert result["data_schema"].schema.get(CONF_S2_ACCESS_CONTROL_KEY) is None - assert result["data_schema"].schema.get(CONF_S2_AUTHENTICATED_KEY) is None - assert result["data_schema"].schema.get(CONF_S2_UNAUTHENTICATED_KEY) is None - assert result["data_schema"].schema.get(CONF_LR_S2_ACCESS_CONTROL_KEY) is None - assert result["data_schema"].schema.get(CONF_LR_S2_AUTHENTICATED_KEY) is None + data_schema = result["data_schema"] + assert data_schema is not None + assert data_schema.schema[CONF_USB_PATH] is not None + assert data_schema.schema.get(CONF_S0_LEGACY_KEY) is None + assert data_schema.schema.get(CONF_S2_ACCESS_CONTROL_KEY) is None + assert data_schema.schema.get(CONF_S2_AUTHENTICATED_KEY) is None + assert data_schema.schema.get(CONF_S2_UNAUTHENTICATED_KEY) is None + assert data_schema.schema.get(CONF_LR_S2_ACCESS_CONTROL_KEY) is None + assert data_schema.schema.get(CONF_LR_S2_AUTHENTICATED_KEY) is None result = await hass.config_entries.flow.async_configure( result["flow_id"], From d35802a9966a079df3994511f625dc3b58e6f1d9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:21:45 +0200 Subject: [PATCH 0776/1175] Improve type hints in microsoft_face (#145417) * Improve type hints in microsoft_face * Remove hass from init --- .../components/microsoft_face/__init__.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 23c9885e0c5..5a8d9c3dae0 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify +from homeassistant.util.hass_dict import HassKey _LOGGER = logging.getLogger(__name__) @@ -31,9 +32,9 @@ ATTR_PERSON = "person" CONF_AZURE_REGION = "azure_region" -DATA_MICROSOFT_FACE = "microsoft_face" DEFAULT_TIMEOUT = 10 DOMAIN = "microsoft_face" +DATA_MICROSOFT_FACE: HassKey[MicrosoftFace] = HassKey(DOMAIN) FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" @@ -80,11 +81,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(__name__), DOMAIN, hass ) entities: dict[str, MicrosoftFaceGroupEntity] = {} + domain_config: dict[str, Any] = config[DOMAIN] + azure_region: str = domain_config[CONF_AZURE_REGION] + api_key: str = domain_config[CONF_API_KEY] + timeout: int = domain_config[CONF_TIMEOUT] face = MicrosoftFace( hass, - config[DOMAIN].get(CONF_AZURE_REGION), - config[DOMAIN].get(CONF_API_KEY), - config[DOMAIN].get(CONF_TIMEOUT), + azure_region, + api_key, + timeout, component, entities, ) @@ -110,7 +115,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if old_entity: await component.async_remove_entity(old_entity.entity_id) - entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) + entities[g_id] = MicrosoftFaceGroupEntity(face, g_id, name) await component.async_add_entities([entities[g_id]]) except HomeAssistantError as err: _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) @@ -219,30 +224,20 @@ class MicrosoftFaceGroupEntity(Entity): _attr_should_poll = False - def __init__(self, hass, api, g_id, name): + def __init__(self, api: MicrosoftFace, g_id: str, name: str) -> None: """Initialize person/group entity.""" - self.hass = hass + self.entity_id = f"{DOMAIN}.{g_id}" self._api = api self._id = g_id - self._name = name + self._attr_name = name @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def entity_id(self): - """Return entity id.""" - return f"{DOMAIN}.{self._id}" - - @property - def state(self): + def state(self) -> int: """Return the state of the entity.""" return len(self._api.store[self._id]) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return dict(self._api.store[self._id]) @@ -250,19 +245,27 @@ class MicrosoftFaceGroupEntity(Entity): class MicrosoftFace: """Microsoft Face api for Home Assistant.""" - def __init__(self, hass, server_loc, api_key, timeout, component, entities): + def __init__( + self, + hass: HomeAssistant, + server_loc: str, + api_key: str, + timeout: int, + component: EntityComponent[MicrosoftFaceGroupEntity], + entities: dict[str, MicrosoftFaceGroupEntity], + ) -> None: """Initialize Microsoft Face api.""" self.hass = hass self.websession = async_get_clientsession(hass) self.timeout = timeout self._api_key = api_key self._server_url = f"https://{server_loc}.{FACE_API_URL}" - self._store = {} - self._component: EntityComponent[MicrosoftFaceGroupEntity] = component + self._store: dict[str, dict[str, Any]] = {} + self._component = component self._entities = entities @property - def store(self): + def store(self) -> dict[str, dict[str, Any]]: """Store group/person data and IDs.""" return self._store @@ -281,9 +284,7 @@ class MicrosoftFace: self._component.async_remove_entity(old_entity.entity_id) ) - self._entities[g_id] = MicrosoftFaceGroupEntity( - self.hass, self, g_id, group["name"] - ) + self._entities[g_id] = MicrosoftFaceGroupEntity(self, g_id, group["name"]) new_entities.append(self._entities[g_id]) persons = await self.call_api("get", f"persongroups/{g_id}/persons") @@ -313,8 +314,8 @@ class MicrosoftFace: try: async with asyncio.timeout(self.timeout): - response = await getattr(self.websession, method)( - url, data=payload, headers=headers, params=params + response = await self.websession.request( + method, url, data=payload, headers=headers, params=params ) answer = await response.json() From 6e74b56649fd5b87701937d1802ea67c63ef4f6a Mon Sep 17 00:00:00 2001 From: marc7s <34547876+marc7s@users.noreply.github.com> Date: Thu, 22 May 2025 11:22:33 +0200 Subject: [PATCH 0777/1175] Catch invalid settings error in geocaching (#139944) Refactoring and preparation for other sensor types --- homeassistant/components/geocaching/coordinator.py | 6 +++++- homeassistant/components/geocaching/sensor.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/geocaching/coordinator.py b/homeassistant/components/geocaching/coordinator.py index fdf8f1340da..bfe82069650 100644 --- a/homeassistant/components/geocaching/coordinator.py +++ b/homeassistant/components/geocaching/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations -from geocachingapi.exceptions import GeocachingApiError +from geocachingapi.exceptions import GeocachingApiError, GeocachingInvalidSettingsError from geocachingapi.geocachingapi import GeocachingApi from geocachingapi.models import GeocachingStatus @@ -39,6 +39,7 @@ class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): return str(token) client_session = async_get_clientsession(hass) + self.geocaching = GeocachingApi( environment=ENVIRONMENT, token=session.token["access_token"], @@ -55,7 +56,10 @@ class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): ) async def _async_update_data(self) -> GeocachingStatus: + """Fetch the latest Geocaching status.""" try: return await self.geocaching.update() + except GeocachingInvalidSettingsError as error: + raise UpdateFailed(f"Invalid integration configuration: {error}") from error except GeocachingApiError as error: raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index a8008229c91..5ceef21dfbf 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -93,6 +93,7 @@ class GeocachingSensor( self._attr_unique_id = ( f"{coordinator.data.user.reference_code}_{description.key}" ) + self._attr_device_info = DeviceInfo( name=f"Geocaching {coordinator.data.user.username}", identifiers={(DOMAIN, cast(str, coordinator.data.user.reference_code))}, From 40267760fdb26d41e441974e9c77ebd879850a82 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:23:33 +0200 Subject: [PATCH 0778/1175] Improve type hints in tensorflow (#145433) * Improve type hints in tensorflow * Use ANTIALIAS again * Use Image.Resampling.LANCZOS --- .../components/tensorflow/image_processing.py | 113 ++++++++---------- 1 file changed, 52 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 15addd3513d..0fb069e8da8 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -7,6 +7,7 @@ import logging import os import sys import time +from typing import Any import numpy as np from PIL import Image, ImageDraw, UnidentifiedImageError @@ -54,6 +55,8 @@ CONF_MODEL_DIR = "model_dir" CONF_RIGHT = "right" CONF_TOP = "top" +_DEFAULT_AREA = (0.0, 0.0, 1.0, 1.0) + AREA_SCHEMA = vol.Schema( { vol.Optional(CONF_BOTTOM, default=1): cv.small_float, @@ -189,19 +192,21 @@ def setup_platform( hass.bus.listen_once(EVENT_HOMEASSISTANT_START, tensorflow_hass_start) - category_index = label_map_util.create_category_index_from_labelmap( - labels, use_display_name=True + category_index: dict[int, dict[str, Any]] = ( + label_map_util.create_category_index_from_labelmap( + labels, use_display_name=True + ) ) + source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( TensorFlowImageProcessor( - hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), category_index, config, ) - for camera in config[CONF_SOURCE] + for camera in source ) @@ -210,78 +215,66 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def __init__( self, - hass, - camera_entity, - name, - category_index, - config, - ): + camera_entity: str, + name: str | None, + category_index: dict[int, dict[str, Any]], + config: ConfigType, + ) -> None: """Initialize the TensorFlow entity.""" - model_config = config.get(CONF_MODEL) - self.hass = hass - self._camera_entity = camera_entity + model_config: dict[str, Any] = config[CONF_MODEL] + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - self._name = f"TensorFlow {split_entity_id(camera_entity)[1]}" + self._attr_name = f"TensorFlow {split_entity_id(camera_entity)[1]}" self._category_index = category_index self._min_confidence = config.get(CONF_CONFIDENCE) self._file_out = config.get(CONF_FILE_OUT) # handle categories and specific detection areas self._label_id_offset = model_config.get(CONF_LABEL_OFFSET) - categories = model_config.get(CONF_CATEGORIES) + categories: list[str | dict[str, Any]] = model_config[CONF_CATEGORIES] self._include_categories = [] - self._category_areas = {} + self._category_areas: dict[str, tuple[float, float, float, float]] = {} for category in categories: if isinstance(category, dict): - category_name = category.get(CONF_CATEGORY) + category_name: str = category[CONF_CATEGORY] category_area = category.get(CONF_AREA) self._include_categories.append(category_name) - self._category_areas[category_name] = [0, 0, 1, 1] + self._category_areas[category_name] = _DEFAULT_AREA if category_area: - self._category_areas[category_name] = [ - category_area.get(CONF_TOP), - category_area.get(CONF_LEFT), - category_area.get(CONF_BOTTOM), - category_area.get(CONF_RIGHT), - ] + self._category_areas[category_name] = ( + category_area[CONF_TOP], + category_area[CONF_LEFT], + category_area[CONF_BOTTOM], + category_area[CONF_RIGHT], + ) else: self._include_categories.append(category) - self._category_areas[category] = [0, 0, 1, 1] + self._category_areas[category] = _DEFAULT_AREA # Handle global detection area - self._area = [0, 0, 1, 1] + self._area = _DEFAULT_AREA if area_config := model_config.get(CONF_AREA): - self._area = [ - area_config.get(CONF_TOP), - area_config.get(CONF_LEFT), - area_config.get(CONF_BOTTOM), - area_config.get(CONF_RIGHT), - ] + self._area = ( + area_config[CONF_TOP], + area_config[CONF_LEFT], + area_config[CONF_BOTTOM], + area_config[CONF_RIGHT], + ) - self._matches = {} + self._matches: dict[str, list[dict[str, Any]]] = {} self._total_matches = 0 self._last_image = None - self._process_time = 0 + self._process_time = 0.0 @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera_entity - - @property - def name(self): - """Return the name of the image processor.""" - return self._name - - @property - def state(self): + def state(self) -> int: """Return the state of the entity.""" return self._total_matches @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { ATTR_MATCHES: self._matches, @@ -292,25 +285,25 @@ class TensorFlowImageProcessor(ImageProcessingEntity): ATTR_PROCESS_TIME: self._process_time, } - def _save_image(self, image, matches, paths): + def _save_image( + self, image: bytes, matches: dict[str, list[dict[str, Any]]], paths: list[str] + ) -> None: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img_width, img_height = img.size draw = ImageDraw.Draw(img) # Draw custom global region/area - if self._area != [0, 0, 1, 1]: + if self._area != _DEFAULT_AREA: draw_box( draw, self._area, img_width, img_height, "Detection Area", (0, 255, 255) ) for category, values in matches.items(): # Draw custom category regions/areas - if category in self._category_areas and self._category_areas[category] != [ - 0, - 0, - 1, - 1, - ]: + if ( + category in self._category_areas + and self._category_areas[category] != _DEFAULT_AREA + ): label = f"{category.capitalize()} Detection Area" draw_box( draw, @@ -333,7 +326,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): os.makedirs(os.path.dirname(path), exist_ok=True) img.save(path) - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process the image.""" if not (model := self.hass.data[DOMAIN][CONF_MODEL]): _LOGGER.debug("Model not yet ready") @@ -352,7 +345,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): except UnidentifiedImageError: _LOGGER.warning("Unable to process image, bad data") return - img.thumbnail((460, 460), Image.ANTIALIAS) + img.thumbnail((460, 460), Image.Resampling.LANCZOS) img_width, img_height = img.size inp = ( np.array(img.getdata()) @@ -371,7 +364,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): detections["detection_classes"][0].numpy() + self._label_id_offset ).astype(int) - matches = {} + matches: dict[str, list[dict[str, Any]]] = {} total_matches = 0 for box, score, obj_class in zip(boxes, scores, classes, strict=False): score = score * 100 @@ -416,9 +409,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): paths = [] for path_template in self._file_out: if isinstance(path_template, template.Template): - paths.append( - path_template.render(camera_entity=self._camera_entity) - ) + paths.append(path_template.render(camera_entity=self.camera_entity)) else: paths.append(path_template) self._save_image(image, matches, paths) From 69f0f38a09b28fea86ddf2e6f1f17f7374c879cd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:30:38 +0200 Subject: [PATCH 0779/1175] Improve type hints in qrcode (#145430) Co-authored-by: Joost Lekkerkerker --- .../components/qrcode/image_processing.py | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index bec0cea8c2f..f81969b63b6 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -21,48 +21,33 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the QR code image processing platform.""" + source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( - QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) - for camera in config[CONF_SOURCE] + QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) for camera in source ) class QrEntity(ImageProcessingEntity): """A QR image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str | None) -> None: """Initialize QR image processing entity.""" super().__init__() - self._camera = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - self._name = f"QR {split_entity_id(camera_entity)[1]}" - self._state = None + self._attr_name = f"QR {split_entity_id(camera_entity)[1]}" + self._attr_state = None - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process image.""" stream = io.BytesIO(image) img = Image.open(stream) barcodes = pyzbar.decode(img) if barcodes: - self._state = barcodes[0].data.decode("utf-8") + self._attr_state = barcodes[0].data.decode("utf-8") else: - self._state = None + self._attr_state = None From ab69223d75fa3387436dea0a59c2cafaf15a5eea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:30:53 +0200 Subject: [PATCH 0780/1175] Improve type hints in openalpr_cloud (#145429) Co-authored-by: Joost Lekkerkerker --- .../openalpr_cloud/image_processing.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 2bdf9947fe2..f541ee0b515 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -6,6 +6,7 @@ import asyncio from base64 import b64encode from http import HTTPStatus import logging +from typing import Any import aiohttp import voluptuous as vol @@ -72,7 +73,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OpenALPR cloud API platform.""" - confidence = config[CONF_CONFIDENCE] + confidence: float = config[CONF_CONFIDENCE] + source: list[dict[str, str]] = config[CONF_SOURCE] params = { "secret_key": config[CONF_API_KEY], "tasks": "plate", @@ -84,7 +86,7 @@ async def async_setup_platform( OpenAlprCloudEntity( camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME) ) - for camera in config[CONF_SOURCE] + for camera in source ) @@ -99,10 +101,10 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): self.vehicles = 0 @property - def state(self): + def state(self) -> str | None: """Return the state of the entity.""" - confidence = 0 - plate = None + confidence = 0.0 + plate: str | None = None # search high plate for i_pl, i_co in self.plates.items(): @@ -112,7 +114,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): return plate @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} @@ -156,35 +158,26 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): class OpenAlprCloudEntity(ImageProcessingAlprEntity): """Representation of an OpenALPR cloud entity.""" - def __init__(self, camera_entity, params, confidence, name=None): + def __init__( + self, + camera_entity: str, + params: dict[str, Any], + confidence: float, + name: str | None, + ) -> None: """Initialize OpenALPR cloud API.""" super().__init__() self._params = params - self._camera = camera_entity - self._confidence = confidence + self._attr_camera_entity = camera_entity + self._attr_confidence = confidence if name: - self._name = name + self._attr_name = name else: - self._name = f"OpenAlpr {split_entity_id(camera_entity)[1]}" + self._attr_name = f"OpenAlpr {split_entity_id(camera_entity)[1]}" - @property - def confidence(self): - """Return minimum confidence for send events.""" - return self._confidence - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - async def async_process_image(self, image): + async def async_process_image(self, image: bytes) -> None: """Process image. This method is a coroutine. From 981842ee87a6bfba4a0cc45870268e3afcb5981d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:31:17 +0200 Subject: [PATCH 0781/1175] Improve type hints in seven_segments (#145431) Co-authored-by: Joost Lekkerkerker --- .../seven_segments/image_processing.py | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index bda17b75081..29ebe8f03ea 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -70,19 +70,24 @@ class ImageProcessingSsocr(ImageProcessingEntity): _attr_device_class = ImageProcessingDeviceClass.OCR - def __init__(self, hass, camera_entity, config, name): + def __init__( + self, + hass: HomeAssistant, + camera_entity: str, + config: ConfigType, + name: str | None, + ) -> None: """Initialize seven segments processing.""" - self.hass = hass - self._camera_entity = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: - self._name = f"SevenSegment OCR {split_entity_id(camera_entity)[1]}" - self._state = None + self._attr_name = f"SevenSegment OCR {split_entity_id(camera_entity)[1]}" + self._attr_state = None self.filepath = os.path.join( - self.hass.config.config_dir, - f"ssocr-{self._name.replace(' ', '_')}.png", + hass.config.config_dir, + f"ssocr-{self._attr_name.replace(' ', '_')}.png", ) crop = [ "crop", @@ -106,22 +111,7 @@ class ImageProcessingSsocr(ImageProcessingEntity): ] self._command.append(self.filepath) - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera_entity - - @property - def name(self): - """Return the name of the image processor.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process the image.""" stream = io.BytesIO(image) img = Image.open(stream) @@ -135,9 +125,9 @@ class ImageProcessingSsocr(ImageProcessingEntity): ) as ocr: out = ocr.communicate() if out[0] != b"": - self._state = out[0].strip().decode("utf-8") + self._attr_state = out[0].strip().decode("utf-8") else: - self._state = None + self._attr_state = None _LOGGER.warning( "Unable to detect value: %s", out[1].strip().decode("utf-8") ) From 5ddadcbd65011ed150253d08c6ffd427e36d1c0e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 22 May 2025 11:32:33 +0200 Subject: [PATCH 0782/1175] Add range support to icon translations (#145340) --- homeassistant/components/sensor/icons.json | 16 +++++++++ script/hassfest/icons.py | 41 +++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/icons.json b/homeassistant/components/sensor/icons.json index cc64290d241..f412b5de253 100644 --- a/homeassistant/components/sensor/icons.json +++ b/homeassistant/components/sensor/icons.json @@ -15,6 +15,22 @@ "atmospheric_pressure": { "default": "mdi:thermometer-lines" }, + "battery": { + "default": "mdi:battery-unknown", + "range": { + "0": "mdi:battery-alert", + "10": "mdi:battery-10", + "20": "mdi:battery-20", + "30": "mdi:battery-30", + "40": "mdi:battery-40", + "50": "mdi:battery-50", + "60": "mdi:battery-60", + "70": "mdi:battery-70", + "80": "mdi:battery-80", + "90": "mdi:battery-90", + "100": "mdi:battery" + } + }, "blood_glucose_concentration": { "default": "mdi:spoon-sugar" }, diff --git a/script/hassfest/icons.py b/script/hassfest/icons.py index f6bcd865c23..563fe0edb93 100644 --- a/script/hassfest/icons.py +++ b/script/hassfest/icons.py @@ -25,6 +25,16 @@ def icon_value_validator(value: Any) -> str: return str(value) +def range_key_validator(value: str) -> str: + """Validate that range key value is numeric.""" + try: + float(value) + except (TypeError, ValueError) as err: + raise vol.Invalid(f"Invalid range key '{value}', needs to be numeric.") from err + + return value + + def require_default_icon_validator(value: dict) -> dict: """Validate that a default icon is set.""" if "_" not in value: @@ -48,6 +58,26 @@ def ensure_not_same_as_default(value: dict) -> dict: return value +def ensure_range_is_sorted(value: dict) -> dict: + """Validate that range values are sorted in ascending order.""" + for section_key, section in value.items(): + # Only validate range if one exists and this is an icon definition + if ranges := section.get("range"): + try: + range_values = [float(key) for key in ranges] + except ValueError as err: + raise vol.Invalid( + f"Range values for `{section_key}` must be numeric" + ) from err + + if range_values != sorted(range_values): + raise vol.Invalid( + f"Range values for `{section_key}` must be in ascending order" + ) + + return value + + DATA_ENTRY_ICONS_SCHEMA = vol.Schema( { "step": { @@ -100,19 +130,27 @@ def icon_schema( slug_validator=translation_key_validator, ) + range_validator = cv.schema_with_slug_keys( + icon_value_validator, + slug_validator=range_key_validator, + ) + def icon_schema_slug(marker: type[vol.Marker]) -> dict[vol.Marker, Any]: return { marker("default"): icon_value_validator, vol.Optional("state"): state_validator, + vol.Optional("range"): range_validator, vol.Optional("state_attributes"): vol.All( cv.schema_with_slug_keys( { marker("default"): icon_value_validator, - marker("state"): state_validator, + vol.Optional("state"): state_validator, + vol.Optional("range"): range_validator, }, slug_validator=translation_key_validator, ), ensure_not_same_as_default, + ensure_range_is_sorted, ), } @@ -143,6 +181,7 @@ def icon_schema( ), require_default_icon_validator, ensure_not_same_as_default, + ensure_range_is_sorted, ) } ) From 687bedd251cd2fa891bd6757a490b49e818fa685 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:35:06 +0200 Subject: [PATCH 0783/1175] Improve type hints in sighthound (#145432) * Improve type hints in sighthound * More --- .../components/sighthound/image_processing.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index 222b61456c4..9636192f6e1 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -5,6 +5,7 @@ from __future__ import annotations import io import logging from pathlib import Path +from typing import TYPE_CHECKING, Any from PIL import Image, ImageDraw, UnidentifiedImageError import simplehound.core as hound @@ -59,8 +60,8 @@ def setup_platform( ) -> None: """Set up the platform.""" # Validate credentials by processing image. - api_key = config[CONF_API_KEY] - account_type = config[CONF_ACCOUNT_TYPE] + api_key: str = config[CONF_API_KEY] + account_type: str = config[CONF_ACCOUNT_TYPE] api = hound.cloud(api_key, account_type) try: api.detect(b"Test") @@ -72,7 +73,8 @@ def setup_platform( save_file_folder = Path(save_file_folder) entities = [] - for camera in config[CONF_SOURCE]: + source: list[dict[str, str]] = config[CONF_SOURCE] + for camera in source: sighthound = SighthoundEntity( api, camera[CONF_ENTITY_ID], @@ -91,29 +93,34 @@ class SighthoundEntity(ImageProcessingEntity): _attr_unit_of_measurement = ATTR_PEOPLE def __init__( - self, api, camera_entity, name, save_file_folder, save_timestamped_file - ): + self, + api: hound.cloud, + camera_entity: str, + name: str | None, + save_file_folder: Path | None, + save_timestamped_file: bool, + ) -> None: """Init.""" self._api = api - self._camera = camera_entity + self._attr_camera_entity = camera_entity if name: - self._name = name + self._attr_name = name else: camera_name = split_entity_id(camera_entity)[1] - self._name = f"sighthound_{camera_name}" - self._state = None - self._last_detection = None - self._image_width = None - self._image_height = None + self._attr_name = f"sighthound_{camera_name}" + self._attr_state = None + self._last_detection: str | None = None + self._image_width: int | None = None + self._image_height: int | None = None self._save_file_folder = save_file_folder self._save_timestamped_file = save_timestamped_file - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process an image.""" detections = self._api.detect(image) people = hound.get_people(detections) - self._state = len(people) - if self._state > 0: + self._attr_state = len(people) + if self._attr_state > 0: self._last_detection = dt_util.now().strftime(DATETIME_FORMAT) metadata = hound.get_metadata(detections) @@ -121,10 +128,10 @@ class SighthoundEntity(ImageProcessingEntity): self._image_height = metadata["image_height"] for person in people: self.fire_person_detected_event(person) - if self._save_file_folder and self._state > 0: + if self._save_file_folder and self._attr_state > 0: self.save_image(image, people, self._save_file_folder) - def fire_person_detected_event(self, person): + def fire_person_detected_event(self, person: dict[str, Any]) -> None: """Send event with detected total_persons.""" self.hass.bus.fire( EVENT_PERSON_DETECTED, @@ -136,7 +143,9 @@ class SighthoundEntity(ImageProcessingEntity): }, ) - def save_image(self, image, people, directory): + def save_image( + self, image: bytes, people: list[dict[str, Any]], directory: Path + ) -> None: """Save a timestamped image with bounding boxes around targets.""" try: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") @@ -145,37 +154,26 @@ class SighthoundEntity(ImageProcessingEntity): return draw = ImageDraw.Draw(img) + if TYPE_CHECKING: + assert self._image_width is not None + assert self._image_height is not None + for person in people: box = hound.bbox_to_tf_style( person["boundingBox"], self._image_width, self._image_height ) draw_box(draw, box, self._image_width, self._image_height) - latest_save_path = directory / f"{self._name}_latest.jpg" + latest_save_path = directory / f"{self.name}_latest.jpg" img.save(latest_save_path) if self._save_timestamped_file: - timestamp_save_path = directory / f"{self._name}_{self._last_detection}.jpg" + timestamp_save_path = directory / f"{self.name}_{self._last_detection}.jpg" img.save(timestamp_save_path) _LOGGER.debug("Sighthound saved file %s", timestamp_save_path) @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" if not self._last_detection: return {} From ca914d8e4f268c1dfd4ebc3e7a1b6fe5f8ad9354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Thu, 22 May 2025 11:51:28 +0200 Subject: [PATCH 0784/1175] switchbot_cloud: Add Smart Lock door and calibration state (#143695) * switchbot_cloud: Add Smart Lock door and calibration state * Incorporate review --- .../components/switchbot_cloud/__init__.py | 3 + .../switchbot_cloud/binary_sensor.py | 101 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 homeassistant/components/switchbot_cloud/binary_sensor.py diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 6f36739e2fc..8074c882671 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -16,6 +16,7 @@ from .coordinator import SwitchBotCoordinator _LOGGER = getLogger(__name__) PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.LOCK, @@ -29,6 +30,7 @@ PLATFORMS: list[Platform] = [ class SwitchbotDevices: """Switchbot devices data.""" + binary_sensors: list[Device] = field(default_factory=list) buttons: list[Device] = field(default_factory=list) climates: list[Remote] = field(default_factory=list) switches: list[Device | Remote] = field(default_factory=list) @@ -141,6 +143,7 @@ async def make_device_data( ) devices_data.locks.append((device, coordinator)) devices_data.sensors.append((device, coordinator)) + devices_data.binary_sensors.append((device, coordinator)) if isinstance(device, Device) and device.device_type in ["Bot"]: coordinator = await coordinator_for_device( diff --git a/homeassistant/components/switchbot_cloud/binary_sensor.py b/homeassistant/components/switchbot_cloud/binary_sensor.py new file mode 100644 index 00000000000..14278072c83 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/binary_sensor.py @@ -0,0 +1,101 @@ +"""Support for SwitchBot Cloud binary sensors.""" + +from dataclasses import dataclass + +from switchbot_api import Device, SwitchBotAPI + +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 AddConfigEntryEntitiesCallback + +from . import SwitchbotCloudData +from .const import DOMAIN +from .coordinator import SwitchBotCoordinator +from .entity import SwitchBotCloudEntity + + +@dataclass(frozen=True) +class SwitchBotCloudBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Switchbot Cloud binary sensor.""" + + # Value or values to consider binary sensor to be "on" + on_value: bool | str = True + + +CALIBRATION_DESCRIPTION = SwitchBotCloudBinarySensorEntityDescription( + key="calibrate", + name="Calibration", + translation_key="calibration", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + on_value=False, +) + +DOOR_OPEN_DESCRIPTION = SwitchBotCloudBinarySensorEntityDescription( + key="doorState", + device_class=BinarySensorDeviceClass.DOOR, + on_value="opened", +) + +BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = { + "Smart Lock": ( + CALIBRATION_DESCRIPTION, + DOOR_OPEN_DESCRIPTION, + ), + "Smart Lock Pro": ( + CALIBRATION_DESCRIPTION, + DOOR_OPEN_DESCRIPTION, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up SwitchBot Cloud entry.""" + data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] + + async_add_entities( + SwitchBotCloudBinarySensor(data.api, device, coordinator, description) + for device, coordinator in data.devices.binary_sensors + for description in BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES[ + device.device_type + ] + ) + + +class SwitchBotCloudBinarySensor(SwitchBotCloudEntity, BinarySensorEntity): + """Representation of a Switchbot binary sensor.""" + + entity_description: SwitchBotCloudBinarySensorEntityDescription + + def __init__( + self, + api: SwitchBotAPI, + device: Device, + coordinator: SwitchBotCoordinator, + description: SwitchBotCloudBinarySensorEntityDescription, + ) -> None: + """Initialize SwitchBot Cloud sensor entity.""" + super().__init__(api, device, coordinator) + self.entity_description = description + self._attr_unique_id = f"{device.device_id}_{description.key}" + + @property + def is_on(self) -> bool | None: + """Set attributes from coordinator data.""" + if not self.coordinator.data: + return None + + return ( + self.coordinator.data.get(self.entity_description.key) + == self.entity_description.on_value + ) From a54c8a88ffa27e5c0f10e8bec4b9ca736057e96f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 11:52:26 +0200 Subject: [PATCH 0785/1175] Improve type hints in microsoft_face_detect (#145421) * Improve type hints in microsoft_face_detect * Improve --- .../microsoft_face_detect/image_processing.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index ce49f0b1f65..57e785ad328 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING import voluptuous as vol @@ -11,9 +12,10 @@ from homeassistant.components.image_processing import ( ATTR_GENDER, ATTR_GLASSES, PLATFORM_SCHEMA as IMAGE_PROCESSING_PLATFORM_SCHEMA, + FaceInformation, ImageProcessingFaceEntity, ) -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE, MicrosoftFace from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.exceptions import HomeAssistantError @@ -54,43 +56,40 @@ async def async_setup_platform( ) -> None: """Set up the Microsoft Face detection platform.""" api = hass.data[DATA_MICROSOFT_FACE] - attributes = config[CONF_ATTRIBUTES] + attributes: list[str] = config[CONF_ATTRIBUTES] + source: list[dict[str, str]] = config[CONF_SOURCE] async_add_entities( MicrosoftFaceDetectEntity( camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) ) - for camera in config[CONF_SOURCE] + for camera in source ) class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): """Microsoft Face API entity for identify.""" - def __init__(self, camera_entity, api, attributes, name=None): + def __init__( + self, + camera_entity: str, + api: MicrosoftFace, + attributes: list[str], + name: str | None, + ) -> None: """Initialize Microsoft Face.""" super().__init__() self._api = api - self._camera = camera_entity + self._attr_camera_entity = camera_entity self._attributes = attributes if name: - self._name = name + self._attr_name = name else: - self._name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}" + self._attr_name = f"MicrosoftFace {split_entity_id(camera_entity)[1]}" - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - async def async_process_image(self, image): + async def async_process_image(self, image: bytes) -> None: """Process image. This method is a coroutine. @@ -112,12 +111,14 @@ class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): if not face_data: face_data = [] - faces = [] + faces: list[FaceInformation] = [] for face in face_data: - face_attr = {} + face_attr = FaceInformation() for attr in self._attributes: + if TYPE_CHECKING: + assert attr in SUPPORTED_ATTRIBUTES if attr in face["faceAttributes"]: - face_attr[attr] = face["faceAttributes"][attr] + face_attr[attr] = face["faceAttributes"][attr] # type: ignore[literal-required] if face_attr: faces.append(face_attr) From 9a8c29e05d8609a58fbddd1ac3bd5f16a3fe3427 Mon Sep 17 00:00:00 2001 From: Florian von Garrel Date: Thu, 22 May 2025 12:17:38 +0200 Subject: [PATCH 0786/1175] Add paperless integration (#145239) * add paperless integration - config flow and initialisation * Add first sensors - documents, inbox, storage total and available * Add status sensors with error attributes * add status coordinator and organized code * Fixed None error * Organized code and moved requests to coordinator * Organized code * optimized code * Add statustype state strings * Error handling * Organized code * Add update sensor and one coordinator for integration * add sanity sensor and timer for version request * Add sensors and icons.json. better errorhandling * Add tests and error handling * FIxed tests * Add tests for coverage * Quality scale * Stuff * Improved code structure * Removed sensor platform and reauth / reconfigure flow * bump pypaperless to 4.1.0 * Optimized tests; update sensor as update platform; little optimizations * Code optimizations with update platform * Add sensor platform * Removed update platform * quality scale * removed unused const * Removed update snapshot; better code * Changed name of entry * Fixed bugs * Minor changes * Minor changed and renamed sensors * Sensors to measurement * Fixed snapshot; test data to json; minor changes * removed mypy errors * Changed translation * minor changes * Update homeassistant/components/paperless_ngx/strings.json --------- Co-authored-by: Josef Zweck Co-authored-by: Joost Lekkerkerker --- CODEOWNERS | 2 + .../components/paperless_ngx/__init__.py | 26 ++ .../components/paperless_ngx/config_flow.py | 78 +++++ .../components/paperless_ngx/const.py | 7 + .../components/paperless_ngx/coordinator.py | 109 +++++++ .../components/paperless_ngx/entity.py | 34 ++ .../components/paperless_ngx/icons.json | 24 ++ .../components/paperless_ngx/manifest.json | 12 + .../paperless_ngx/quality_scale.yaml | 78 +++++ .../components/paperless_ngx/sensor.py | 94 ++++++ .../components/paperless_ngx/strings.json | 72 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/paperless_ngx/__init__.py | 14 + tests/components/paperless_ngx/conftest.py | 76 +++++ tests/components/paperless_ngx/const.py | 8 + .../fixtures/test_data_statistic.json | 16 + .../fixtures/test_data_statistic_update.json | 16 + .../paperless_ngx/snapshots/test_sensor.ambr | 307 ++++++++++++++++++ .../paperless_ngx/test_config_flow.py | 112 +++++++ tests/components/paperless_ngx/test_init.py | 65 ++++ tests/components/paperless_ngx/test_sensor.py | 111 +++++++ 24 files changed, 1274 insertions(+) create mode 100644 homeassistant/components/paperless_ngx/__init__.py create mode 100644 homeassistant/components/paperless_ngx/config_flow.py create mode 100644 homeassistant/components/paperless_ngx/const.py create mode 100644 homeassistant/components/paperless_ngx/coordinator.py create mode 100644 homeassistant/components/paperless_ngx/entity.py create mode 100644 homeassistant/components/paperless_ngx/icons.json create mode 100644 homeassistant/components/paperless_ngx/manifest.json create mode 100644 homeassistant/components/paperless_ngx/quality_scale.yaml create mode 100644 homeassistant/components/paperless_ngx/sensor.py create mode 100644 homeassistant/components/paperless_ngx/strings.json create mode 100644 tests/components/paperless_ngx/__init__.py create mode 100644 tests/components/paperless_ngx/conftest.py create mode 100644 tests/components/paperless_ngx/const.py create mode 100644 tests/components/paperless_ngx/fixtures/test_data_statistic.json create mode 100644 tests/components/paperless_ngx/fixtures/test_data_statistic_update.json create mode 100644 tests/components/paperless_ngx/snapshots/test_sensor.ambr create mode 100644 tests/components/paperless_ngx/test_config_flow.py create mode 100644 tests/components/paperless_ngx/test_init.py create mode 100644 tests/components/paperless_ngx/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index be7c1e5ee84..a0324e329e1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1140,6 +1140,8 @@ build.json @home-assistant/supervisor /tests/components/palazzetti/ @dotvav /homeassistant/components/panel_custom/ @home-assistant/frontend /tests/components/panel_custom/ @home-assistant/frontend +/homeassistant/components/paperless_ngx/ @fvgarrel +/tests/components/paperless_ngx/ @fvgarrel /homeassistant/components/peblar/ @frenck /tests/components/peblar/ @frenck /homeassistant/components/peco/ @IceBotYT diff --git a/homeassistant/components/paperless_ngx/__init__.py b/homeassistant/components/paperless_ngx/__init__.py new file mode 100644 index 00000000000..145f3ec2caf --- /dev/null +++ b/homeassistant/components/paperless_ngx/__init__.py @@ -0,0 +1,26 @@ +"""The Paperless-ngx integration.""" + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import PaperlessConfigEntry, PaperlessCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool: + """Set up Paperless-ngx from a config entry.""" + + coordinator = PaperlessCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool: + """Unload paperless-ngx config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/paperless_ngx/config_flow.py b/homeassistant/components/paperless_ngx/config_flow.py new file mode 100644 index 00000000000..039cb23a470 --- /dev/null +++ b/homeassistant/components/paperless_ngx/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for the Paperless-ngx integration.""" + +from __future__ import annotations + +from typing import Any + +from pypaperless import Paperless +from pypaperless.exceptions import ( + InitializationError, + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_API_KEY): str, + } +) + + +class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Paperless-ngx.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + + if user_input is not None: + self._async_abort_entries_match( + { + CONF_URL: user_input[CONF_URL], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) + + errors: dict[str, str] = {} + if user_input is not None: + client = Paperless( + user_input[CONF_URL], + user_input[CONF_API_KEY], + session=async_get_clientsession(self.hass), + ) + + try: + await client.initialize() + await client.statistics() + except PaperlessConnectionError: + errors[CONF_URL] = "cannot_connect" + except PaperlessInvalidTokenError: + errors[CONF_API_KEY] = "invalid_api_key" + except PaperlessInactiveOrDeletedError: + errors[CONF_API_KEY] = "user_inactive_or_deleted" + except PaperlessForbiddenError: + errors[CONF_API_KEY] = "forbidden" + except InitializationError: + errors[CONF_URL] = "cannot_connect" + except Exception as err: # noqa: BLE001 + LOGGER.exception("Unexpected exception: %s", err) + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input[CONF_URL], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/paperless_ngx/const.py b/homeassistant/components/paperless_ngx/const.py new file mode 100644 index 00000000000..67e569510eb --- /dev/null +++ b/homeassistant/components/paperless_ngx/const.py @@ -0,0 +1,7 @@ +"""Constants for the Paperless-ngx integration.""" + +import logging + +DOMAIN = "paperless_ngx" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/paperless_ngx/coordinator.py b/homeassistant/components/paperless_ngx/coordinator.py new file mode 100644 index 00000000000..542c0fee71f --- /dev/null +++ b/homeassistant/components/paperless_ngx/coordinator.py @@ -0,0 +1,109 @@ +"""Paperless-ngx Status coordinator.""" + +from __future__ import annotations + +from datetime import timedelta + +from pypaperless import Paperless +from pypaperless.exceptions import ( + InitializationError, + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) +from pypaperless.models import Statistic + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +type PaperlessConfigEntry = ConfigEntry[PaperlessCoordinator] + +UPDATE_INTERVAL = 120 + + +class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): + """Coordinator to manage Paperless-ngx statistic updates.""" + + config_entry: PaperlessConfigEntry + + def __init__( + self, + hass: HomeAssistant, + entry: PaperlessConfigEntry, + ) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + LOGGER, + config_entry=entry, + name="Paperless-ngx Coordinator", + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + self.api = Paperless( + entry.data[CONF_URL], + entry.data[CONF_API_KEY], + session=async_get_clientsession(self.hass), + ) + + async def _async_setup(self) -> None: + try: + await self.api.initialize() + await self.api.statistics() # test permissions on api + except PaperlessConnectionError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except PaperlessInvalidTokenError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="invalid_api_key", + ) from err + except PaperlessInactiveOrDeletedError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="user_inactive_or_deleted", + ) from err + except PaperlessForbiddenError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="forbidden", + ) from err + except InitializationError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + + async def _async_update_data(self) -> Statistic: + """Fetch data from API endpoint.""" + try: + return await self.api.statistics() + except PaperlessConnectionError as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except PaperlessForbiddenError as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="forbidden", + ) from err + except PaperlessInvalidTokenError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="invalid_api_key", + ) from err + except PaperlessInactiveOrDeletedError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="user_inactive_or_deleted", + ) from err diff --git a/homeassistant/components/paperless_ngx/entity.py b/homeassistant/components/paperless_ngx/entity.py new file mode 100644 index 00000000000..934f460af8d --- /dev/null +++ b/homeassistant/components/paperless_ngx/entity.py @@ -0,0 +1,34 @@ +"""Paperless-ngx base entity.""" + +from __future__ import annotations + +from homeassistant.components.sensor import EntityDescription +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import PaperlessCoordinator + + +class PaperlessEntity(CoordinatorEntity[PaperlessCoordinator]): + """Defines a base Paperless-ngx entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: PaperlessCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the Paperless-ngx entity.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer="Paperless-ngx", + sw_version=coordinator.api.host_version, + configuration_url=coordinator.api.base_url, + ) diff --git a/homeassistant/components/paperless_ngx/icons.json b/homeassistant/components/paperless_ngx/icons.json new file mode 100644 index 00000000000..5d5db9a6b51 --- /dev/null +++ b/homeassistant/components/paperless_ngx/icons.json @@ -0,0 +1,24 @@ +{ + "entity": { + "sensor": { + "documents_total": { + "default": "mdi:file-document-multiple" + }, + "documents_inbox": { + "default": "mdi:tray-full" + }, + "characters_count": { + "default": "mdi:alphabet-latin" + }, + "tag_count": { + "default": "mdi:tag" + }, + "correspondent_count": { + "default": "mdi:account-group" + }, + "document_type_count": { + "default": "mdi:format-list-bulleted-type" + } + } + } +} diff --git a/homeassistant/components/paperless_ngx/manifest.json b/homeassistant/components/paperless_ngx/manifest.json new file mode 100644 index 00000000000..2ff8aaed4ab --- /dev/null +++ b/homeassistant/components/paperless_ngx/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "paperless_ngx", + "name": "Paperless-ngx", + "codeowners": ["@fvgarrel"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/paperless_ngx", + "integration_type": "service", + "iot_class": "local_polling", + "loggers": ["pypaperless"], + "quality_scale": "bronze", + "requirements": ["pypaperless==4.1.0"] +} diff --git a/homeassistant/components/paperless_ngx/quality_scale.yaml b/homeassistant/components/paperless_ngx/quality_scale.yaml new file mode 100644 index 00000000000..fc7ecb1668c --- /dev/null +++ b/homeassistant/components/paperless_ngx/quality_scale.yaml @@ -0,0 +1,78 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: Integration does not register actions yet. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: Integration does not register actions yet. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: Integration does not register custom events yet. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: Integration does not register actions yet. + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: No options flow yet + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: todo + test-coverage: done + + # Gold + devices: done + diagnostics: todo + discovery-update-info: + status: exempt + comment: Paperless does not support discovery. + discovery: + status: exempt + comment: Paperless does not support discovery. + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: + status: exempt + comment: Service type integration + entity-category: done + entity-device-class: done + entity-disabled-by-default: todo + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: todo + repair-issues: todo + stale-devices: + status: exempt + comment: Service type integration + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: todo diff --git a/homeassistant/components/paperless_ngx/sensor.py b/homeassistant/components/paperless_ngx/sensor.py new file mode 100644 index 00000000000..4c358933ae7 --- /dev/null +++ b/homeassistant/components/paperless_ngx/sensor.py @@ -0,0 +1,94 @@ +"""Sensor platform for Paperless-ngx.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from pypaperless.models import Statistic + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import PaperlessConfigEntry +from .entity import PaperlessEntity + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class PaperlessEntityDescription(SensorEntityDescription): + """Describes Paperless-ngx sensor entity.""" + + value_fn: Callable[[Statistic], int | None] + + +SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = ( + PaperlessEntityDescription( + key="documents_total", + translation_key="documents_total", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.documents_total, + ), + PaperlessEntityDescription( + key="documents_inbox", + translation_key="documents_inbox", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.documents_inbox, + ), + PaperlessEntityDescription( + key="characters_count", + translation_key="characters_count", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.character_count, + ), + PaperlessEntityDescription( + key="tag_count", + translation_key="tag_count", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.tag_count, + ), + PaperlessEntityDescription( + key="correspondent_count", + translation_key="correspondent_count", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.correspondent_count, + ), + PaperlessEntityDescription( + key="document_type_count", + translation_key="document_type_count", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.document_type_count, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PaperlessConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Paperless-ngx sensors.""" + async_add_entities( + PaperlessSensor( + coordinator=entry.runtime_data, + description=sensor_description, + ) + for sensor_description in SENSOR_DESCRIPTIONS + ) + + +class PaperlessSensor(PaperlessEntity, SensorEntity): + """Defines a Paperless-ngx sensor entity.""" + + entity_description: PaperlessEntityDescription + + @property + def native_value(self) -> int | None: + """Return the current value of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/paperless_ngx/strings.json b/homeassistant/components/paperless_ngx/strings.json new file mode 100644 index 00000000000..224568f4082 --- /dev/null +++ b/homeassistant/components/paperless_ngx/strings.json @@ -0,0 +1,72 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "url": "URL to connect to the Paperless-ngx instance", + "api_key": "API key to connect to the Paperless-ngx API" + }, + "title": "Add Paperless-ngx instance" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::invalid_host%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "user_inactive_or_deleted": "Authentication failed. The user is inactive or has been deleted.", + "forbidden": "The token does not have permission to access the API.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "sensor": { + "documents_total": { + "name": "Total documents", + "unit_of_measurement": "documents" + }, + "documents_inbox": { + "name": "Documents in inbox", + "unit_of_measurement": "[%key:component::paperless_ngx::entity::sensor::documents_total::unit_of_measurement%]" + }, + "characters_count": { + "name": "Total characters", + "unit_of_measurement": "characters" + }, + "tag_count": { + "name": "Tags", + "unit_of_measurement": "tags" + }, + "correspondent_count": { + "name": "Correspondents", + "unit_of_measurement": "correspondents" + }, + "document_type_count": { + "name": "Document types", + "unit_of_measurement": "document types" + } + } + }, + "exceptions": { + "cannot_connect": { + "message": "[%key:common::config_flow::error::invalid_host%]" + }, + "invalid_api_key": { + "message": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "user_inactive_or_deleted": { + "message": "[%key:component::paperless_ngx::config::error::user_inactive_or_deleted%]" + }, + "forbidden": { + "message": "[%key:component::paperless_ngx::config::error::forbidden%]" + }, + "unknown": { + "message": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e1211ac20d0..43db3f5be10 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -469,6 +469,7 @@ FLOWS = { "p1_monitor", "palazzetti", "panasonic_viera", + "paperless_ngx", "peblar", "peco", "pegel_online", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 7f335f4091d..9357424dc76 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4848,6 +4848,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "paperless_ngx": { + "name": "Paperless-ngx", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "pcs_lighting": { "name": "PCS Lighting", "integration_type": "virtual", diff --git a/requirements_all.txt b/requirements_all.txt index 7777385f872..dd938be0067 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2226,6 +2226,9 @@ pyownet==0.10.0.post1 # homeassistant.components.palazzetti pypalazzetti==0.1.19 +# homeassistant.components.paperless_ngx +pypaperless==4.1.0 + # homeassistant.components.elv pypca==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a8922a1c17..ffd0fd244d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,6 +1823,9 @@ pyownet==0.10.0.post1 # homeassistant.components.palazzetti pypalazzetti==0.1.19 +# homeassistant.components.paperless_ngx +pypaperless==4.1.0 + # homeassistant.components.lcn pypck==0.8.6 diff --git a/tests/components/paperless_ngx/__init__.py b/tests/components/paperless_ngx/__init__.py new file mode 100644 index 00000000000..f1900bf4f8e --- /dev/null +++ b/tests/components/paperless_ngx/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the Paperless-ngx integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the Paperless-ngx 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/paperless_ngx/conftest.py b/tests/components/paperless_ngx/conftest.py new file mode 100644 index 00000000000..758856f6912 --- /dev/null +++ b/tests/components/paperless_ngx/conftest.py @@ -0,0 +1,76 @@ +"""Common fixtures for the Paperless-ngx tests.""" + +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from pypaperless.models import Statistic +import pytest + +from homeassistant.components.paperless_ngx.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import setup_integration +from .const import USER_INPUT + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_statistic_data() -> Generator[MagicMock]: + """Return test statistic data.""" + return json.loads(load_fixture("test_data_statistic.json", DOMAIN)) + + +@pytest.fixture +def mock_statistic_data_update() -> Generator[MagicMock]: + """Return updated test statistic data.""" + return json.loads(load_fixture("test_data_statistic_update.json", DOMAIN)) + + +@pytest.fixture(autouse=True) +def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]: + """Mock the pypaperless.Paperless client.""" + with ( + patch( + "homeassistant.components.paperless_ngx.coordinator.Paperless", + autospec=True, + ) as paperless_mock, + patch( + "homeassistant.components.paperless_ngx.config_flow.Paperless", + new=paperless_mock, + ), + ): + paperless = paperless_mock.return_value + + paperless.base_url = "http://paperless.example.com/" + paperless.host_version = "2.3.0" + paperless.initialize.return_value = None + paperless.statistics = AsyncMock( + return_value=Statistic.create_with_data( + paperless, data=mock_statistic_data, fetched=True + ) + ) + + yield paperless + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + entry_id="paperless_ngx_test", + title="Paperless-ngx", + domain=DOMAIN, + data=USER_INPUT, + ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_paperless: MagicMock +) -> MockConfigEntry: + """Set up the Paperless-ngx integration for testing.""" + await setup_integration(hass, mock_config_entry) + + return mock_config_entry diff --git a/tests/components/paperless_ngx/const.py b/tests/components/paperless_ngx/const.py new file mode 100644 index 00000000000..361acaedc6d --- /dev/null +++ b/tests/components/paperless_ngx/const.py @@ -0,0 +1,8 @@ +"""Constants for the Paperless NGX integration tests.""" + +from homeassistant.const import CONF_API_KEY, CONF_URL + +USER_INPUT = { + CONF_URL: "https://192.168.69.16:8000", + CONF_API_KEY: "test_token", +} diff --git a/tests/components/paperless_ngx/fixtures/test_data_statistic.json b/tests/components/paperless_ngx/fixtures/test_data_statistic.json new file mode 100644 index 00000000000..29ba93d848b --- /dev/null +++ b/tests/components/paperless_ngx/fixtures/test_data_statistic.json @@ -0,0 +1,16 @@ +{ + "documents_total": 999, + "documents_inbox": 9, + "inbox_tag": 9, + "inbox_tags": [9], + "document_file_type_counts": [ + { "mime_type": "application/pdf", "mime_type_count": 998 }, + { "mime_type": "image/png", "mime_type_count": 1 } + ], + "character_count": 99999, + "tag_count": 99, + "correspondent_count": 99, + "document_type_count": 99, + "storage_path_count": 9, + "current_asn": 99 +} diff --git a/tests/components/paperless_ngx/fixtures/test_data_statistic_update.json b/tests/components/paperless_ngx/fixtures/test_data_statistic_update.json new file mode 100644 index 00000000000..15c82365a7c --- /dev/null +++ b/tests/components/paperless_ngx/fixtures/test_data_statistic_update.json @@ -0,0 +1,16 @@ +{ + "documents_total": 420, + "documents_inbox": 3, + "inbox_tag": 5, + "inbox_tags": [2], + "document_file_type_counts": [ + { "mime_type": "application/pdf", "mime_type_count": 419 }, + { "mime_type": "image/png", "mime_type_count": 1 } + ], + "character_count": 324234, + "tag_count": 43, + "correspondent_count": 9659, + "document_type_count": 54656, + "storage_path_count": 6459, + "current_asn": 959 +} diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..630db313d12 --- /dev/null +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -0,0 +1,307 @@ +# serializer version: 1 +# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_correspondents', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Correspondents', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'correspondent_count', + 'unique_id': 'paperless_ngx_test_correspondent_count', + 'unit_of_measurement': 'correspondents', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Correspondents', + 'state_class': , + 'unit_of_measurement': 'correspondents', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_correspondents', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '99', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_document_types-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_document_types', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Document types', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'document_type_count', + 'unique_id': 'paperless_ngx_test_document_type_count', + 'unit_of_measurement': 'document types', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_document_types-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Document types', + 'state_class': , + 'unit_of_measurement': 'document types', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_document_types', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '99', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_documents_in_inbox', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Documents in inbox', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'documents_inbox', + 'unique_id': 'paperless_ngx_test_documents_inbox', + 'unit_of_measurement': 'documents', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Documents in inbox', + 'state_class': , + 'unit_of_measurement': 'documents', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_documents_in_inbox', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_tags-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_tags', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Tags', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tag_count', + 'unique_id': 'paperless_ngx_test_tag_count', + 'unit_of_measurement': 'tags', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_tags-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Tags', + 'state_class': , + 'unit_of_measurement': 'tags', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_tags', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '99', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_total_characters', + 'has_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 characters', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'characters_count', + 'unique_id': 'paperless_ngx_test_characters_count', + 'unit_of_measurement': 'characters', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Total characters', + 'state_class': , + 'unit_of_measurement': 'characters', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_total_characters', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '99999', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.paperless_ngx_total_documents', + 'has_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 documents', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'documents_total', + 'unique_id': 'paperless_ngx_test_documents_total', + 'unit_of_measurement': 'documents', + }) +# --- +# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Paperless-ngx Total documents', + 'state_class': , + 'unit_of_measurement': 'documents', + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_total_documents', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '999', + }) +# --- diff --git a/tests/components/paperless_ngx/test_config_flow.py b/tests/components/paperless_ngx/test_config_flow.py new file mode 100644 index 00000000000..1674296e9a7 --- /dev/null +++ b/tests/components/paperless_ngx/test_config_flow.py @@ -0,0 +1,112 @@ +"""Tests for the Paperless-ngx config flow.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock + +from pypaperless.exceptions import ( + InitializationError, + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) +import pytest + +from homeassistant import config_entries +from homeassistant.components.paperless_ngx.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import USER_INPUT + +from tests.common import MockConfigEntry, patch + + +@pytest.fixture(autouse=True) +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.paperless_ngx.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_full_config_flow(hass: HomeAssistant) -> None: + """Test registering an integration and finishing flow works.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["flow_id"] + 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_entry = result["result"] + assert config_entry.title == USER_INPUT[CONF_URL] + assert result["type"] is FlowResultType.CREATE_ENTRY + assert config_entry.data == USER_INPUT + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PaperlessConnectionError(), {CONF_URL: "cannot_connect"}), + (PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}), + (PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}), + (PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}), + (InitializationError(), {CONF_URL: "cannot_connect"}), + (Exception("BOOM!"), {"base": "unknown"}), + ], +) +async def test_config_flow_error_handling( + hass: HomeAssistant, + mock_paperless: AsyncMock, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test user step shows correct error for various client initialization issues.""" + mock_paperless.initialize.side_effect = side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == expected_error + + mock_paperless.initialize.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == USER_INPUT[CONF_URL] + assert result["data"] == USER_INPUT + + +async def test_config_already_exists( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we only allow a single config flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=USER_INPUT, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/paperless_ngx/test_init.py b/tests/components/paperless_ngx/test_init.py new file mode 100644 index 00000000000..9a132cf7eff --- /dev/null +++ b/tests/components/paperless_ngx/test_init.py @@ -0,0 +1,65 @@ +"""Test the Paperless-ngx integration initialization.""" + +from unittest.mock import AsyncMock + +from pypaperless.exceptions import ( + InitializationError, + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) +import pytest + +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_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test loading and unloading the integration.""" + 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 + + +@pytest.mark.parametrize( + ("side_effect", "expected_state", "expected_error_key"), + [ + (PaperlessConnectionError(), ConfigEntryState.SETUP_RETRY, None), + (PaperlessInvalidTokenError(), ConfigEntryState.SETUP_ERROR, "invalid_api_key"), + ( + PaperlessInactiveOrDeletedError(), + ConfigEntryState.SETUP_ERROR, + "user_inactive_or_deleted", + ), + (PaperlessForbiddenError(), ConfigEntryState.SETUP_ERROR, "forbidden"), + (InitializationError(), ConfigEntryState.SETUP_ERROR, "cannot_connect"), + ], +) +async def test_setup_config_error_handling( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_paperless: AsyncMock, + side_effect: Exception, + expected_state: ConfigEntryState, + expected_error_key: str, +) -> None: + """Test all initialization error paths during setup.""" + mock_paperless.initialize.side_effect = side_effect + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state == expected_state + assert mock_config_entry.error_reason_translation_key == expected_error_key diff --git a/tests/components/paperless_ngx/test_sensor.py b/tests/components/paperless_ngx/test_sensor.py new file mode 100644 index 00000000000..70cf04202f5 --- /dev/null +++ b/tests/components/paperless_ngx/test_sensor.py @@ -0,0 +1,111 @@ +"""Tests for Paperless-ngx sensor platform.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory +from pypaperless.exceptions import ( + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) +from pypaperless.models import Statistic +import pytest + +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import ( + AsyncMock, + MockConfigEntry, + SnapshotAssertion, + async_fire_time_changed, + patch, + snapshot_platform, +) + + +async def test_sensor_platfom( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test paperless_ngx update sensors.""" + with patch("homeassistant.components.paperless_ngx.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("init_integration") +async def test_statistic_sensor_state( + hass: HomeAssistant, + mock_paperless: AsyncMock, + freezer: FrozenDateTimeFactory, + mock_statistic_data_update, +) -> None: + """Ensure sensor entities are added automatically.""" + # initialize with 999 documents + state = hass.states.get("sensor.paperless_ngx_total_documents") + assert state.state == "999" + + # update to 420 documents + mock_paperless.statistics = AsyncMock( + return_value=Statistic.create_with_data( + mock_paperless, data=mock_statistic_data_update, fetched=True + ) + ) + + freezer.tick(timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.paperless_ngx_total_documents") + assert state.state == "420" + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize( + "error_cls", + [ + PaperlessForbiddenError, + PaperlessConnectionError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, + ], +) +async def test__statistic_sensor_state_on_error( + hass: HomeAssistant, + mock_paperless: AsyncMock, + freezer: FrozenDateTimeFactory, + mock_statistic_data_update, + error_cls, +) -> None: + """Ensure sensor entities are added automatically.""" + # simulate error + mock_paperless.statistics.side_effect = error_cls + + freezer.tick(timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.paperless_ngx_total_documents") + assert state.state == STATE_UNAVAILABLE + + # recover from error + mock_paperless.statistics = AsyncMock( + return_value=Statistic.create_with_data( + mock_paperless, data=mock_statistic_data_update, fetched=True + ) + ) + + freezer.tick(timedelta(seconds=120)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("sensor.paperless_ngx_total_documents") + assert state.state == "420" From d87041041377dd5bd3c0c9bc6a9d44cff99bf0dc Mon Sep 17 00:00:00 2001 From: Tamer Wahba Date: Thu, 22 May 2025 06:18:56 -0400 Subject: [PATCH 0787/1175] Quantum Gateway device tracker tests (#145161) * move constants to central const file * add none return type to device scanner constructor * add quantum gateway device tracker tests * fix --------- Co-authored-by: Joostlek --- CODEOWNERS | 1 + .../components/quantum_gateway/const.py | 7 +++ .../quantum_gateway/device_tracker.py | 16 +++--- requirements_test_all.txt | 3 ++ tests/components/quantum_gateway/__init__.py | 22 ++++++++ tests/components/quantum_gateway/conftest.py | 23 +++++++++ .../quantum_gateway/test_device_tracker.py | 51 +++++++++++++++++++ 7 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/quantum_gateway/const.py create mode 100644 tests/components/quantum_gateway/__init__.py create mode 100644 tests/components/quantum_gateway/conftest.py create mode 100644 tests/components/quantum_gateway/test_device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index a0324e329e1..b80b9bc6591 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1228,6 +1228,7 @@ build.json @home-assistant/supervisor /homeassistant/components/qnap_qsw/ @Noltari /tests/components/qnap_qsw/ @Noltari /homeassistant/components/quantum_gateway/ @cisasteelersfan +/tests/components/quantum_gateway/ @cisasteelersfan /homeassistant/components/qvr_pro/ @oblogic7 /homeassistant/components/qwikswitch/ @kellerza /tests/components/qwikswitch/ @kellerza diff --git a/homeassistant/components/quantum_gateway/const.py b/homeassistant/components/quantum_gateway/const.py new file mode 100644 index 00000000000..6e8bae10065 --- /dev/null +++ b/homeassistant/components/quantum_gateway/const.py @@ -0,0 +1,7 @@ +"""Constants for Quantum Gateway.""" + +import logging + +LOGGER = logging.getLogger(__package__) + +DEFAULT_HOST = "myfiosgateway.com" diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 6491dca2e2c..c3eddc37f22 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -2,8 +2,6 @@ from __future__ import annotations -import logging - from quantum_gateway import QuantumGatewayScanner from requests.exceptions import RequestException import voluptuous as vol @@ -18,9 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - -DEFAULT_HOST = "myfiosgateway.com" +from .const import DEFAULT_HOST, LOGGER PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( { @@ -43,13 +39,13 @@ def get_scanner( class QuantumGatewayDeviceScanner(DeviceScanner): """Class which queries a Quantum Gateway.""" - def __init__(self, config): + def __init__(self, config) -> None: """Initialize the scanner.""" self.host = config[CONF_HOST] self.password = config[CONF_PASSWORD] self.use_https = config[CONF_SSL] - _LOGGER.debug("Initializing") + LOGGER.debug("Initializing") try: self.quantum = QuantumGatewayScanner( @@ -58,10 +54,10 @@ class QuantumGatewayDeviceScanner(DeviceScanner): self.success_init = self.quantum.success_init except RequestException: self.success_init = False - _LOGGER.error("Unable to connect to gateway. Check host") + LOGGER.error("Unable to connect to gateway. Check host") if not self.success_init: - _LOGGER.error("Unable to login to gateway. Check password and host") + LOGGER.error("Unable to login to gateway. Check password and host") def scan_devices(self): """Scan for new devices and return a list of found MACs.""" @@ -69,7 +65,7 @@ class QuantumGatewayDeviceScanner(DeviceScanner): try: connected_devices = self.quantum.scan_devices() except RequestException: - _LOGGER.error("Unable to scan devices. Check connection to router") + LOGGER.error("Unable to scan devices. Check connection to router") return connected_devices def get_device_name(self, device): diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffd0fd244d2..e99def6471e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2131,6 +2131,9 @@ qingping-ble==0.10.0 # homeassistant.components.qnap qnapstats==0.4.0 +# homeassistant.components.quantum_gateway +quantum-gateway==0.0.8 + # homeassistant.components.radio_browser radios==0.3.2 diff --git a/tests/components/quantum_gateway/__init__.py b/tests/components/quantum_gateway/__init__.py new file mode 100644 index 00000000000..73758f9081e --- /dev/null +++ b/tests/components/quantum_gateway/__init__.py @@ -0,0 +1,22 @@ +"""Tests for the quantum_gateway component.""" + +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass: HomeAssistant) -> None: + """Set up the quantum_gateway integration.""" + result = await async_setup_component( + hass, + DEVICE_TRACKER_DOMAIN, + { + DEVICE_TRACKER_DOMAIN: { + CONF_PLATFORM: "quantum_gateway", + CONF_PASSWORD: "fake_password", + } + }, + ) + await hass.async_block_till_done() + assert result diff --git a/tests/components/quantum_gateway/conftest.py b/tests/components/quantum_gateway/conftest.py new file mode 100644 index 00000000000..b2445813023 --- /dev/null +++ b/tests/components/quantum_gateway/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for Quantum Gateway tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +async def mock_scanner() -> Generator[AsyncMock]: + """Mock QuantumGatewayScanner instance.""" + with patch( + "homeassistant.components.quantum_gateway.device_tracker.QuantumGatewayScanner", + autospec=True, + ) as mock_scanner: + client = mock_scanner.return_value + client.success_init = True + client.scan_devices.return_value = ["ff:ff:ff:ff:ff:ff", "ff:ff:ff:ff:ff:fe"] + client.get_device_name.side_effect = { + "ff:ff:ff:ff:ff:ff": "", + "ff:ff:ff:ff:ff:fe": "desktop", + }.get + yield mock_scanner diff --git a/tests/components/quantum_gateway/test_device_tracker.py b/tests/components/quantum_gateway/test_device_tracker.py new file mode 100644 index 00000000000..df568d1f81a --- /dev/null +++ b/tests/components/quantum_gateway/test_device_tracker.py @@ -0,0 +1,51 @@ +"""Tests for the quantum_gateway device tracker.""" + +from unittest.mock import AsyncMock + +import pytest +from requests import RequestException + +from homeassistant.const import STATE_HOME +from homeassistant.core import HomeAssistant + +from . import setup_platform + +from tests.components.device_tracker.test_init import mock_yaml_devices # noqa: F401 + + +@pytest.mark.usefixtures("yaml_devices") +async def test_get_scanner(hass: HomeAssistant, mock_scanner: AsyncMock) -> None: + """Test creating a quantum gateway scanner.""" + await setup_platform(hass) + + device_1 = hass.states.get("device_tracker.desktop") + assert device_1 is not None + assert device_1.state == STATE_HOME + + device_2 = hass.states.get("device_tracker.ff_ff_ff_ff_ff_ff") + assert device_2 is not None + assert device_2.state == STATE_HOME + + +@pytest.mark.usefixtures("yaml_devices") +async def test_get_scanner_error(hass: HomeAssistant, mock_scanner: AsyncMock) -> None: + """Test failure when creating a quantum gateway scanner.""" + mock_scanner.side_effect = RequestException("Error") + await setup_platform(hass) + + assert "quantum_gateway.device_tracker" not in hass.config.components + + +@pytest.mark.usefixtures("yaml_devices") +async def test_scan_devices_error(hass: HomeAssistant, mock_scanner: AsyncMock) -> None: + """Test failure when scanning devices.""" + mock_scanner.return_value.scan_devices.side_effect = RequestException("Error") + await setup_platform(hass) + + assert "quantum_gateway.device_tracker" in hass.config.components + + device_1 = hass.states.get("device_tracker.desktop") + assert device_1 is None + + device_2 = hass.states.get("device_tracker.ff_ff_ff_ff_ff_ff") + assert device_2 is None From c68e663a1c34af82bfbdb6f41a72b97267a77718 Mon Sep 17 00:00:00 2001 From: Gigatrappeur <5045347+Gigatrappeur@users.noreply.github.com> Date: Thu, 22 May 2025 12:19:08 +0200 Subject: [PATCH 0788/1175] Add webhook in switchbot cloud integration (#132882) * add webhook in switchbot cloud integration * Rename _need_initialized to _is_initialized and reduce nb line in async_setup_entry * Add unit tests * Enhance poll management * fix --------- Co-authored-by: Joostlek --- .../components/switchbot_cloud/__init__.py | 153 ++++++++++++++++-- .../components/switchbot_cloud/coordinator.py | 17 ++ .../components/switchbot_cloud/manifest.json | 1 + tests/components/switchbot_cloud/test_init.py | 99 +++++++++++- 4 files changed, 257 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 8074c882671..c7bf66a5803 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -1,17 +1,21 @@ """SwitchBot via API integration.""" from asyncio import gather +from collections.abc import Awaitable, Callable +import contextlib from dataclasses import dataclass, field from logging import getLogger +from aiohttp import web from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI +from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import DOMAIN, ENTRY_TITLE from .coordinator import SwitchBotCoordinator _LOGGER = getLogger(__name__) @@ -30,13 +34,17 @@ PLATFORMS: list[Platform] = [ class SwitchbotDevices: """Switchbot devices data.""" - binary_sensors: list[Device] = field(default_factory=list) - buttons: list[Device] = field(default_factory=list) - climates: list[Remote] = field(default_factory=list) - switches: list[Device | Remote] = field(default_factory=list) - sensors: list[Device] = field(default_factory=list) - vacuums: list[Device] = field(default_factory=list) - locks: list[Device] = field(default_factory=list) + binary_sensors: list[tuple[Device, SwitchBotCoordinator]] = field( + default_factory=list + ) + buttons: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) + climates: list[tuple[Remote, SwitchBotCoordinator]] = field(default_factory=list) + switches: list[tuple[Device | Remote, SwitchBotCoordinator]] = field( + default_factory=list + ) + sensors: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) + vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) + locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list) @dataclass @@ -53,10 +61,12 @@ async def coordinator_for_device( api: SwitchBotAPI, device: Device | Remote, coordinators_by_id: dict[str, SwitchBotCoordinator], + manageable_by_webhook: bool = False, ) -> SwitchBotCoordinator: """Instantiate coordinator and adds to list for gathering.""" coordinator = coordinators_by_id.setdefault( - device.device_id, SwitchBotCoordinator(hass, entry, api, device) + device.device_id, + SwitchBotCoordinator(hass, entry, api, device, manageable_by_webhook), ) if coordinator.data is None: @@ -133,7 +143,7 @@ async def make_device_data( "Robot Vacuum Cleaner S1 Plus", ]: coordinator = await coordinator_for_device( - hass, entry, api, device, coordinators_by_id + hass, entry, api, device, coordinators_by_id, True ) devices_data.vacuums.append((device, coordinator)) @@ -182,7 +192,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = SwitchbotCloudData( api=api, devices=switchbot_devices ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + await _initialize_webhook(hass, entry, api, coordinators_by_id) + return True @@ -192,3 +206,120 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def _initialize_webhook( + hass: HomeAssistant, + entry: ConfigEntry, + api: SwitchBotAPI, + coordinators_by_id: dict[str, SwitchBotCoordinator], +) -> None: + """Initialize webhook if needed.""" + if any( + coordinator.manageable_by_webhook() + for coordinator in coordinators_by_id.values() + ): + if CONF_WEBHOOK_ID not in entry.data: + new_data = entry.data.copy() + if CONF_WEBHOOK_ID not in new_data: + # create new id and new conf + new_data[CONF_WEBHOOK_ID] = webhook.async_generate_id() + + hass.config_entries.async_update_entry(entry, data=new_data) + + # register webhook + webhook_name = ENTRY_TITLE + if entry.title != ENTRY_TITLE: + webhook_name = f"{ENTRY_TITLE} {entry.title}" + + with contextlib.suppress(Exception): + webhook.async_register( + hass, + DOMAIN, + webhook_name, + entry.data[CONF_WEBHOOK_ID], + _create_handle_webhook(coordinators_by_id), + ) + + webhook_url = webhook.async_generate_url( + hass, + entry.data[CONF_WEBHOOK_ID], + ) + + # check if webhook is configured in switchbot cloud + check_webhook_result = None + with contextlib.suppress(Exception): + check_webhook_result = await api.get_webook_configuration() + + actual_webhook_urls = ( + check_webhook_result["urls"] + if check_webhook_result and "urls" in check_webhook_result + else [] + ) + need_add_webhook = ( + len(actual_webhook_urls) == 0 or webhook_url not in actual_webhook_urls + ) + need_clean_previous_webhook = ( + len(actual_webhook_urls) > 0 and webhook_url not in actual_webhook_urls + ) + + if need_clean_previous_webhook: + # it seems is impossible to register multiple webhook. + # So, if webhook already exists, we delete it + await api.delete_webhook(actual_webhook_urls[0]) + _LOGGER.debug( + "Deleted previous Switchbot cloud webhook url: %s", + actual_webhook_urls[0], + ) + + if need_add_webhook: + # call api for register webhookurl + await api.setup_webhook(webhook_url) + _LOGGER.debug("Registered Switchbot cloud webhook at hass: %s", webhook_url) + + for coordinator in coordinators_by_id.values(): + coordinator.webhook_subscription_listener(True) + + _LOGGER.debug("Registered Switchbot cloud webhook at: %s", webhook_url) + + +def _create_handle_webhook( + coordinators_by_id: dict[str, SwitchBotCoordinator], +) -> Callable[[HomeAssistant, str, web.Request], Awaitable[None]]: + """Create a webhook handler.""" + + async def _internal_handle_webhook( + hass: HomeAssistant, webhook_id: str, request: web.Request + ) -> None: + """Handle webhook callback.""" + if not request.body_exists: + _LOGGER.debug("Received invalid request from switchbot webhook") + return + + data = await request.json() + # Structure validation + if ( + not isinstance(data, dict) + or "eventType" not in data + or data["eventType"] != "changeReport" + or "eventVersion" not in data + or data["eventVersion"] != "1" + or "context" not in data + or not isinstance(data["context"], dict) + or "deviceType" not in data["context"] + or "deviceMac" not in data["context"] + ): + _LOGGER.debug("Received invalid data from switchbot webhook %s", repr(data)) + return + + deviceMac = data["context"]["deviceMac"] + + if deviceMac not in coordinators_by_id: + _LOGGER.error( + "Received data for unknown entity from switchbot webhook: %s", data + ) + return + + coordinators_by_id[deviceMac].async_set_updated_data(data["context"]) + + return _internal_handle_webhook diff --git a/homeassistant/components/switchbot_cloud/coordinator.py b/homeassistant/components/switchbot_cloud/coordinator.py index 02ead5940e4..4f047145b47 100644 --- a/homeassistant/components/switchbot_cloud/coordinator.py +++ b/homeassistant/components/switchbot_cloud/coordinator.py @@ -23,6 +23,8 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]): config_entry: ConfigEntry _api: SwitchBotAPI _device_id: str + _manageable_by_webhook: bool + _webhooks_connected: bool = False def __init__( self, @@ -30,6 +32,7 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]): config_entry: ConfigEntry, api: SwitchBotAPI, device: Device | Remote, + manageable_by_webhook: bool, ) -> None: """Initialize SwitchBot Cloud.""" super().__init__( @@ -42,6 +45,20 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]): self._api = api self._device_id = device.device_id self._should_poll = not isinstance(device, Remote) + self._manageable_by_webhook = manageable_by_webhook + + def webhook_subscription_listener(self, connected: bool) -> None: + """Call when webhook status changed.""" + if self._manageable_by_webhook: + self._webhooks_connected = connected + if connected: + self.update_interval = None + else: + self.update_interval = DEFAULT_SCAN_INTERVAL + + def manageable_by_webhook(self) -> bool: + """Return update_by_webhook value.""" + return self._manageable_by_webhook async def _async_update_data(self) -> Status: """Fetch data from API endpoint.""" diff --git a/homeassistant/components/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json index 99f909e91ab..83404aac2ba 100644 --- a/homeassistant/components/switchbot_cloud/manifest.json +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -3,6 +3,7 @@ "name": "SwitchBot Cloud", "codeowners": ["@SeraphicRav", "@laurence-presland", "@Gigatrappeur"], "config_flow": true, + "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/switchbot_cloud", "integration_type": "hub", "iot_class": "cloud_polling", diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index b2d1cff6679..bab9200e7c9 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -7,11 +7,14 @@ from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState, Remote from homeassistant.components.switchbot_cloud import SwitchBotAPI from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant +from homeassistant.core_config import async_process_ha_core_config from . import configure_integration +from tests.typing import ClientSessionGenerator + @pytest.fixture def mock_list_devices(): @@ -27,10 +30,43 @@ def mock_get_status(): yield mock_get_status +@pytest.fixture +def mock_get_webook_configuration(): + """Mock get_status.""" + with patch.object( + SwitchBotAPI, "get_webook_configuration" + ) as mock_get_webook_configuration: + yield mock_get_webook_configuration + + +@pytest.fixture +def mock_delete_webhook(): + """Mock get_status.""" + with patch.object(SwitchBotAPI, "delete_webhook") as mock_delete_webhook: + yield mock_delete_webhook + + +@pytest.fixture +def mock_setup_webhook(): + """Mock get_status.""" + with patch.object(SwitchBotAPI, "setup_webhook") as mock_setup_webhook: + yield mock_setup_webhook + + async def test_setup_entry_success( - hass: HomeAssistant, mock_list_devices, mock_get_status + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + mock_get_webook_configuration, + mock_delete_webhook, + mock_setup_webhook, ) -> None: """Test successful setup of entry.""" + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, + ) + mock_get_webook_configuration.return_value = {"urls": ["https://example.com"]} mock_list_devices.return_value = [ Remote( version="V1.0", @@ -67,8 +103,15 @@ async def test_setup_entry_success( deviceType="Hub 2", hubDeviceId="test-hub-id", ), + Device( + deviceId="vacuum-1", + deviceName="vacuum-name-1", + deviceType="K10+", + hubDeviceId=None, + ), ] mock_get_status.return_value = {"power": PowerState.ON.value} + entry = await configure_integration(hass) assert entry.state is ConfigEntryState.LOADED @@ -76,6 +119,9 @@ async def test_setup_entry_success( await hass.async_block_till_done() mock_list_devices.assert_called_once() mock_get_status.assert_called() + mock_get_webook_configuration.assert_called_once() + mock_delete_webhook.assert_called_once() + mock_setup_webhook.assert_called_once() @pytest.mark.parametrize( @@ -124,3 +170,52 @@ async def test_setup_entry_fails_when_refreshing( await hass.async_block_till_done() mock_list_devices.assert_called_once() mock_get_status.assert_called() + + +async def test_posting_to_webhook( + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + mock_get_webook_configuration, + mock_delete_webhook, + mock_setup_webhook, + hass_client_no_auth: ClientSessionGenerator, +) -> None: + """Test handler webhook call.""" + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, + ) + mock_get_webook_configuration.return_value = {"urls": ["https://example.com"]} + mock_list_devices.return_value = [ + Device( + deviceId="vacuum-1", + deviceName="vacuum-name-1", + deviceType="K10+", + hubDeviceId=None, + ), + ] + mock_get_status.return_value = {"power": PowerState.ON.value} + mock_delete_webhook.return_value = {} + mock_setup_webhook.return_value = {} + + entry = await configure_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + webhook_id = entry.data[CONF_WEBHOOK_ID] + client = await hass_client_no_auth() + # fire webhook + await client.post( + f"/api/webhook/{webhook_id}", + json={ + "eventType": "changeReport", + "eventVersion": "1", + "context": {"deviceType": "...", "deviceMac": "vacuum-1"}, + }, + ) + + await hass.async_block_till_done() + + mock_setup_webhook.assert_called_once() From 3b4004607d23162b98e4e638584dfc7397ea5ddb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 12:19:22 +0200 Subject: [PATCH 0789/1175] Mark image_processing methods and properties as mandatory in pylint plugin (#145435) --- pylint/plugins/hass_enforce_type_hints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 29fa1daf47c..92f2473d3ee 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1692,20 +1692,24 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="camera_entity", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="confidence", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="device_class", return_type=["ImageProcessingDeviceClass", None], + mandatory=True, ), TypeHintMatch( function_name="process_image", arg_types={1: "bytes"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), @@ -1720,6 +1724,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From c86ba49a79cd90993d164403290f977a3ebc1c80 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 12:40:56 +0200 Subject: [PATCH 0790/1175] Add Matter test to select attribute (#145440) --- tests/components/matter/test_select.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 71999873135..456558d983d 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -99,6 +99,24 @@ async def test_attribute_select_entities( await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state.state == "on" + await hass.services.async_call( + "select", + "select_option", + { + "entity_id": entity_id, + "option": "off", + }, + blocking=True, + ) + assert matter_client.write_attribute.call_count == 1 + assert matter_client.write_attribute.call_args == call( + node_id=matter_node.node_id, + attribute_path=create_attribute_path_from_attribute( + endpoint_id=1, + attribute=clusters.OnOff.Attributes.StartUpOnOff, + ), + value=0, + ) # test that an invalid value (e.g. 253) leads to an unknown state set_node_attribute(matter_node, 1, 6, 16387, 253) await trigger_subscription_callback(hass, matter_client) From 569aeff0549ddf91bc832b881d41a29396f1e619 Mon Sep 17 00:00:00 2001 From: jvmahon Date: Thu, 22 May 2025 06:42:05 -0400 Subject: [PATCH 0791/1175] Add matter attributes (#140843) * Add Matter attributes * Add Matter attributes * Add Matter attributes * Add Matter attributes * Update strings.json * Update homeassistant/components/matter/select.py Co-authored-by: Marcel van der Veldt * Update select.py Deleted items to be added as switch entities instead. * Update strings.json * Update select.py * Update strings.json * Fix * Update strings.json * Update strings.json * Fix * Update select.py --------- Co-authored-by: Marcel van der Veldt Co-authored-by: Joostlek --- homeassistant/components/matter/number.py | 30 ++ homeassistant/components/matter/select.py | 23 ++ homeassistant/components/matter/sensor.py | 36 +++ homeassistant/components/matter/strings.json | 24 ++ .../matter/snapshots/test_number.ambr | 114 +++++++ .../matter/snapshots/test_select.ambr | 120 ++++++++ .../matter/snapshots/test_sensor.ambr | 284 ++++++++++++++++++ 7 files changed, 631 insertions(+) diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py index 2c7a9651c60..4b469fa85e4 100644 --- a/homeassistant/components/matter/number.py +++ b/homeassistant/components/matter/number.py @@ -183,4 +183,34 @@ DISCOVERY_SCHEMAS = [ ), vendor_id=(4874,), ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="PIROccupiedToUnoccupiedDelay", + entity_category=EntityCategory.CONFIG, + translation_key="pir_occupied_to_unoccupied_delay", + native_max_value=65534, + native_min_value=0, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=( + clusters.OccupancySensing.Attributes.PIROccupiedToUnoccupiedDelay, + ), + ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="AutoRelockTimer", + entity_category=EntityCategory.CONFIG, + translation_key="auto_relock_timer", + native_max_value=65534, + native_min_value=0, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=(clusters.DoorLock.Attributes.AutoRelockTime,), + ), ] diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 6e77be93705..39e1db3bf6f 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -436,4 +436,27 @@ DISCOVERY_SCHEMAS = [ # don't discover this entry if the supported rinses list is empty secondary_value_is_not=[], ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=MatterSelectEntityDescription( + key="DoorLockSoundVolume", + entity_category=EntityCategory.CONFIG, + translation_key="door_lock_sound_volume", + options=["silent", "low", "medium", "high"], + measurement_to_ha={ + 0: "silent", + 1: "low", + 3: "medium", + 2: "high", + }.get, + ha_to_native_value={ + "silent": 0, + "low": 1, + "medium": 3, + "high": 2, + }.get, + ), + entity_class=MatterAttributeSelectEntity, + required_attributes=(clusters.DoorLock.Attributes.SoundVolume,), + ), ] diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index e0d2050c833..83248955279 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -948,6 +948,42 @@ DISCOVERY_SCHEMAS = [ # don't discover this entry if the supported state list is empty secondary_value_is_not=[], ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="MinPINCodeLength", + translation_key="min_pin_code_length", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=None, + ), + entity_class=MatterSensor, + required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="MaxPINCodeLength", + translation_key="max_pin_code_length", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=None, + ), + entity_class=MatterSensor, + required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="TargetPositionLiftPercent100ths", + entity_category=EntityCategory.DIAGNOSTIC, + translation_key="window_covering_target_position", + measurement_to_ha=lambda x: round((10000 - x) / 100), + native_unit_of_measurement=PERCENTAGE, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.WindowCovering.Attributes.TargetPositionLiftPercent100ths, + ), + ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 129c6a3ab54..daff0115505 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -176,6 +176,12 @@ }, "temperature_offset": { "name": "Temperature offset" + }, + "pir_occupied_to_unoccupied_delay": { + "name": "Occupied to unoccupied delay" + }, + "auto_relock_timer": { + "name": "Automatic relock timer" } }, "light": { @@ -235,6 +241,15 @@ }, "water_heater_mode": { "name": "Water heater mode" + }, + "door_lock_sound_volume": { + "name": "Sound volume", + "state": { + "silent": "Silent", + "low": "[%key:common::state::low%]", + "medium": "[%key:common::state::medium%]", + "high": "[%key:common::state::high%]" + } } }, "sensor": { @@ -341,6 +356,15 @@ }, "evse_user_max_charge_current": { "name": "User max charge current" + }, + "min_pin_code_length": { + "name": "Min PIN code length" + }, + "max_pin_code_length": { + "name": "Max PIN code length" + }, + "window_covering_target_position": { + "name": "Target opening position" } }, "switch": { diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index eb0a12bfc4d..3240538f0a5 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -395,6 +395,120 @@ 'state': '1.0', }) # --- +# name: test_numbers[door_lock][number.mock_door_lock_automatic_relock_timer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65534, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_door_lock_automatic_relock_timer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic relock timer', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_relock_timer', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-AutoRelockTimer-257-35', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[door_lock][number.mock_door_lock_automatic_relock_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Automatic relock timer', + 'max': 65534, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_door_lock_automatic_relock_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_automatic_relock_timer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65534, + 'min': 0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.mock_door_lock_automatic_relock_timer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic relock timer', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'auto_relock_timer', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-AutoRelockTimer-257-35', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_automatic_relock_timer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Automatic relock timer', + 'max': 65534, + 'min': 0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.mock_door_lock_automatic_relock_timer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '60', + }) +# --- # name: test_numbers[eve_thermo][number.eve_thermo_temperature_offset-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 713f0b25f45..edd0224ccac 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -433,6 +433,66 @@ 'state': 'off', }) # --- +# name: test_selects[door_lock][select.mock_door_lock_sound_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'silent', + 'low', + 'medium', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.mock_door_lock_sound_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound volume', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_sound_volume', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-DoorLockSoundVolume-257-36', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[door_lock][select.mock_door_lock_sound_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Sound volume', + 'options': list([ + 'silent', + 'low', + 'medium', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.mock_door_lock_sound_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'silent', + }) +# --- # name: test_selects[door_lock_with_unbolt][select.mock_door_lock_power_on_behavior_on_startup-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -493,6 +553,66 @@ 'state': 'off', }) # --- +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_sound_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'silent', + 'low', + 'medium', + 'high', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.mock_door_lock_sound_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound volume', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'door_lock_sound_volume', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-DoorLockSoundVolume-257-36', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_sound_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Sound volume', + 'options': list([ + 'silent', + 'low', + 'medium', + 'high', + ]), + }), + 'context': , + 'entity_id': 'select.mock_door_lock_sound_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'silent', + }) +# --- # name: test_selects[eve_energy_plug][select.eve_energy_plug_power_on_behavior_on_startup-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 454e6e67a4c..00c9a178c2b 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -1271,6 +1271,194 @@ 'state': '180.0', }) # --- +# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Max PIN code length', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_pin_code_length', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Max PIN code length', + }), + 'context': , + 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Min PIN code length', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'min_pin_code_length', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Min PIN code length', + }), + 'context': , + 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- +# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Max PIN code length', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'max_pin_code_length', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Max PIN code length', + }), + 'context': , + 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '8', + }) +# --- +# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Min PIN code length', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'min_pin_code_length', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Door Lock Min PIN code length', + }), + 'context': , + 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4976,6 +5164,102 @@ 'state': 'unknown', }) # --- +# name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_full_window_covering_target_opening_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target opening position', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'window_covering_target_position', + 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Full Window Covering Target opening position', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.mock_full_window_covering_target_opening_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensors[window_covering_pa_lift][sensor.longan_link_wncv_da01_target_opening_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.longan_link_wncv_da01_target_opening_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target opening position', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'window_covering_target_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[window_covering_pa_lift][sensor.longan_link_wncv_da01_target_opening_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Longan link WNCV DA01 Target opening position', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.longan_link_wncv_da01_target_opening_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_sensors[yandex_smart_socket][sensor.yndx_00540_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 9e6de48a221750393f9c409d026c114dc3681742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Thu, 22 May 2025 12:53:08 +0200 Subject: [PATCH 0792/1175] Matter Device Energy Management cluster ESAState attribute (#144430) * ESAState * Update strings.json * Add test --- homeassistant/components/matter/sensor.py | 21 +++ homeassistant/components/matter/strings.json | 10 ++ .../matter/snapshots/test_sensor.ambr | 126 ++++++++++++++++++ tests/components/matter/test_sensor.py | 12 ++ 4 files changed, 169 insertions(+) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 83248955279..381ecc480da 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -83,6 +83,14 @@ BOOST_STATE_MAP = { clusters.WaterHeaterManagement.Enums.BoostStateEnum.kUnknownEnumValue: None, } +ESA_STATE_MAP = { + clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOffline: "offline", + clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kOnline: "online", + clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kFault: "fault", + clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPowerAdjustActive: "power_adjust_active", + clusters.DeviceEnergyManagement.Enums.ESAStateEnum.kPaused: "paused", +} + EVSE_FAULT_STATE_MAP = { clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error", clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure", @@ -1097,4 +1105,17 @@ DISCOVERY_SCHEMAS = [ clusters.WaterHeaterManagement.Attributes.EstimatedHeatRequired, ), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ESAState", + translation_key="esa_state", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=list(ESA_STATE_MAP.values()), + measurement_to_ha=ESA_STATE_MAP.get, + ), + entity_class=MatterSensor, + required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index daff0115505..325e8d1f26c 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -321,6 +321,16 @@ "energy_exported": { "name": "Energy exported" }, + "esa_state": { + "name": "Appliance energy state", + "state": { + "offline": "Offline", + "online": "Online", + "fault": "[%key:common::state::fault%]", + "power_adjust_active": "Power adjust", + "paused": "[%key:common::state::paused%]" + } + }, "evse_fault_state": { "name": "Fault state", "state": { diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 00c9a178c2b..bf22986d6df 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -3603,6 +3603,69 @@ 'state': '120.0', }) # --- +# name: test_sensors[silabs_evse_charging][sensor.evse_appliance_energy_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'offline', + 'online', + 'fault', + 'power_adjust_active', + 'paused', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.evse_appliance_energy_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Appliance energy state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'esa_state', + 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-ESAState-152-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[silabs_evse_charging][sensor.evse_appliance_energy_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'evse Appliance energy state', + 'options': list([ + 'offline', + 'online', + 'fault', + 'power_adjust_active', + 'paused', + ]), + }), + 'context': , + 'entity_id': 'sensor.evse_appliance_energy_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'online', + }) +# --- # name: test_sensors[silabs_evse_charging][sensor.evse_circuit_capacity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4272,6 +4335,69 @@ 'state': '120.0', }) # --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_appliance_energy_state-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'offline', + 'online', + 'fault', + 'power_adjust_active', + 'paused', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.water_heater_appliance_energy_state', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Appliance energy state', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'esa_state', + 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ESAState-152-2', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[silabs_water_heater][sensor.water_heater_appliance_energy_state-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Water Heater Appliance energy state', + 'options': list([ + 'offline', + 'online', + 'fault', + 'power_adjust_active', + 'paused', + ]), + }), + 'context': , + 'entity_id': 'sensor.water_heater_appliance_energy_state', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'online', + }) +# --- # name: test_sensors[silabs_water_heater][sensor.water_heater_current-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 868c73a1dff..feb604bd365 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -511,3 +511,15 @@ async def test_water_heater( state = hass.states.get("sensor.water_heater_hot_water_level") assert state assert state.state == "50" + + # DeviceEnergyManagement -> ESAState attribute + state = hass.states.get("sensor.water_heater_appliance_energy_state") + assert state + assert state.state == "online" + + set_node_attribute(matter_node, 2, 152, 2, 0) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.water_heater_appliance_energy_state") + assert state + assert state.state == "offline" From a938001805efd3958eac3355460b20189999cbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Thu, 22 May 2025 12:55:11 +0200 Subject: [PATCH 0793/1175] Don't add dynamically Home Connect event sensors and disable them by default (#144757) * Don't add dynamically Home Connect sensors and disable them by default * Fix test * Check for None --------- Co-authored-by: Martin Hjelmare --- .../components/home_connect/coordinator.py | 8 +- .../components/home_connect/sensor.py | 151 ++++------ .../home_connect/test_coordinator.py | 6 +- tests/components/home_connect/test_sensor.py | 273 ++++++++---------- 4 files changed, 171 insertions(+), 267 deletions(-) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 9e40de86e24..3c9d33424a8 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -5,7 +5,6 @@ from __future__ import annotations from asyncio import sleep as asyncio_sleep from collections import defaultdict from collections.abc import Callable -from contextlib import suppress from dataclasses import dataclass import logging from typing import Any, cast @@ -137,11 +136,8 @@ class HomeConnectCoordinator( self.__dict__.pop("context_listeners", None) def remove_listener_and_invalidate_context_listeners() -> None: - # There are cases where the remove_listener will be called - # although it has been already removed somewhere else - with suppress(KeyError): - remove_listener() - self.__dict__.pop("context_listeners", None) + remove_listener() + self.__dict__.pop("context_listeners", None) return remove_listener_and_invalidate_context_listeners diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index 2872c4a95d3..d8fda46385d 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -1,10 +1,7 @@ """Provides a sensor for Home Connect.""" -from collections import defaultdict -from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta -from functools import partial import logging from typing import cast @@ -17,7 +14,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util, slugify @@ -45,6 +42,7 @@ class HomeConnectSensorEntityDescription( ): """Entity Description class for sensors.""" + default_value: str | None = None appliance_types: tuple[str, ...] | None = None fetch_unit: bool = False @@ -199,6 +197,7 @@ EVENT_SENSORS = ( key=EventKey.BSH_COMMON_EVENT_PROGRAM_ABORTED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="program_aborted", appliance_types=("Dishwasher", "CleaningRobot", "CookProcessor"), ), @@ -206,6 +205,7 @@ EVENT_SENSORS = ( key=EventKey.BSH_COMMON_EVENT_PROGRAM_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="program_finished", appliance_types=( "Oven", @@ -221,6 +221,7 @@ EVENT_SENSORS = ( key=EventKey.BSH_COMMON_EVENT_ALARM_CLOCK_ELAPSED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="alarm_clock_elapsed", appliance_types=("Oven", "Cooktop"), ), @@ -228,6 +229,7 @@ EVENT_SENSORS = ( key=EventKey.COOKING_OVEN_EVENT_PREHEAT_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="preheat_finished", appliance_types=("Oven", "Cooktop"), ), @@ -235,6 +237,7 @@ EVENT_SENSORS = ( key=EventKey.COOKING_OVEN_EVENT_REGULAR_PREHEAT_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="regular_preheat_finished", appliance_types=("Oven",), ), @@ -242,6 +245,7 @@ EVENT_SENSORS = ( key=EventKey.LAUNDRY_CARE_DRYER_EVENT_DRYING_PROCESS_FINISHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="drying_process_finished", appliance_types=("Dryer",), ), @@ -249,6 +253,7 @@ EVENT_SENSORS = ( key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="salt_nearly_empty", appliance_types=("Dishwasher",), ), @@ -256,6 +261,7 @@ EVENT_SENSORS = ( key=EventKey.DISHCARE_DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="rinse_aid_nearly_empty", appliance_types=("Dishwasher",), ), @@ -263,6 +269,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="bean_container_empty", appliance_types=("CoffeeMaker",), ), @@ -270,6 +277,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="water_tank_empty", appliance_types=("CoffeeMaker",), ), @@ -277,6 +285,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="drip_tray_full", appliance_types=("CoffeeMaker",), ), @@ -284,6 +293,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_KEEP_MILK_TANK_COOL, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="keep_milk_tank_cool", appliance_types=("CoffeeMaker",), ), @@ -291,6 +301,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_20_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="descaling_in_20_cups", appliance_types=("CoffeeMaker",), ), @@ -298,6 +309,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_15_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="descaling_in_15_cups", appliance_types=("CoffeeMaker",), ), @@ -305,6 +317,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_10_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="descaling_in_10_cups", appliance_types=("CoffeeMaker",), ), @@ -312,6 +325,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_5_CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="descaling_in_5_cups", appliance_types=("CoffeeMaker",), ), @@ -319,6 +333,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_DESCALED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_should_be_descaled", appliance_types=("CoffeeMaker",), ), @@ -326,6 +341,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_descaling_overdue", appliance_types=("CoffeeMaker",), ), @@ -333,6 +349,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_BLOCKAGE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_descaling_blockage", appliance_types=("CoffeeMaker",), ), @@ -340,6 +357,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CLEANED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_should_be_cleaned", appliance_types=("CoffeeMaker",), ), @@ -347,6 +365,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CLEANING_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_cleaning_overdue", appliance_types=("CoffeeMaker",), ), @@ -354,6 +373,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN20CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="calc_n_clean_in20cups", appliance_types=("CoffeeMaker",), ), @@ -361,6 +381,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN15CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="calc_n_clean_in15cups", appliance_types=("CoffeeMaker",), ), @@ -368,6 +389,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN10CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="calc_n_clean_in10cups", appliance_types=("CoffeeMaker",), ), @@ -375,6 +397,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN5CUPS, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="calc_n_clean_in5cups", appliance_types=("CoffeeMaker",), ), @@ -382,6 +405,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CALC_N_CLEANED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_should_be_calc_n_cleaned", appliance_types=("CoffeeMaker",), ), @@ -389,6 +413,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_OVERDUE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_calc_n_clean_overdue", appliance_types=("CoffeeMaker",), ), @@ -396,6 +421,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_BLOCKAGE, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="device_calc_n_clean_blockage", appliance_types=("CoffeeMaker",), ), @@ -403,6 +429,7 @@ EVENT_SENSORS = ( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="freezer_door_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), @@ -410,6 +437,7 @@ EVENT_SENSORS = ( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="refrigerator_door_alarm", appliance_types=("FridgeFreezer", "Refrigerator"), ), @@ -417,6 +445,7 @@ EVENT_SENSORS = ( key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="freezer_temperature_alarm", appliance_types=("FridgeFreezer", "Freezer"), ), @@ -424,6 +453,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_EMPTY_DUST_BOX_AND_CLEAN_FILTER, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="empty_dust_box_and_clean_filter", appliance_types=("CleaningRobot",), ), @@ -431,6 +461,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_ROBOT_IS_STUCK, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="robot_is_stuck", appliance_types=("CleaningRobot",), ), @@ -438,6 +469,7 @@ EVENT_SENSORS = ( key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_DOCKING_STATION_NOT_FOUND, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="docking_station_not_found", appliance_types=("CleaningRobot",), ), @@ -445,6 +477,7 @@ EVENT_SENSORS = ( key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="poor_i_dos_1_fill_level", appliance_types=("Washer", "WasherDryer"), ), @@ -452,6 +485,7 @@ EVENT_SENSORS = ( key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_2_FILL_LEVEL_POOR, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="poor_i_dos_2_fill_level", appliance_types=("Washer", "WasherDryer"), ), @@ -459,6 +493,7 @@ EVENT_SENSORS = ( key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_NEARLY_REACHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="grease_filter_max_saturation_nearly_reached", appliance_types=("Hood",), ), @@ -466,6 +501,7 @@ EVENT_SENSORS = ( key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_REACHED, device_class=SensorDeviceClass.ENUM, options=EVENT_OPTIONS, + default_value="off", translation_key="grease_filter_max_saturation_reached", appliance_types=("Hood",), ), @@ -478,6 +514,12 @@ def _get_entities_for_appliance( ) -> list[HomeConnectEntity]: """Get a list of entities.""" return [ + *[ + HomeConnectEventSensor(entry.runtime_data, appliance, description) + for description in EVENT_SENSORS + if description.appliance_types + and appliance.info.type in description.appliance_types + ], *[ HomeConnectProgramSensor(entry.runtime_data, appliance, desc) for desc in BSH_PROGRAM_SENSORS @@ -491,72 +533,6 @@ def _get_entities_for_appliance( ] -def _add_event_sensor_entity( - entry: HomeConnectConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, - appliance: HomeConnectApplianceData, - description: HomeConnectSensorEntityDescription, - remove_event_sensor_listener_list: list[Callable[[], None]], -) -> None: - """Add an event sensor entity.""" - if ( - (appliance_data := entry.runtime_data.data.get(appliance.info.ha_id)) is None - ) or description.key not in appliance_data.events: - return - - for remove_listener in remove_event_sensor_listener_list: - remove_listener() - async_add_entities( - [ - HomeConnectEventSensor(entry.runtime_data, appliance, description), - ] - ) - - -def _add_event_sensor_listeners( - entry: HomeConnectConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, - remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]], -) -> None: - for appliance in entry.runtime_data.data.values(): - if appliance.info.ha_id in remove_event_sensor_listener_dict: - continue - for event_sensor_description in EVENT_SENSORS: - if appliance.info.type not in cast( - tuple[str, ...], event_sensor_description.appliance_types - ): - continue - # We use a list as a kind of lazy initializer, as we can use the - # remove_listener while we are initializing it. - remove_event_sensor_listener_list = remove_event_sensor_listener_dict[ - appliance.info.ha_id - ] - remove_listener = entry.runtime_data.async_add_listener( - partial( - _add_event_sensor_entity, - entry, - async_add_entities, - appliance, - event_sensor_description, - remove_event_sensor_listener_list, - ), - (appliance.info.ha_id, event_sensor_description.key), - ) - remove_event_sensor_listener_list.append(remove_listener) - entry.async_on_unload(remove_listener) - - -def _remove_event_sensor_listeners_on_depaired( - entry: HomeConnectConfigEntry, - remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]], -) -> None: - registered_listeners_ha_id = set(remove_event_sensor_listener_dict) - actual_appliances = set(entry.runtime_data.data) - for appliance_ha_id in registered_listeners_ha_id - actual_appliances: - for listener in remove_event_sensor_listener_dict.pop(appliance_ha_id): - listener() - - async def async_setup_entry( hass: HomeAssistant, entry: HomeConnectConfigEntry, @@ -569,32 +545,6 @@ async def async_setup_entry( async_add_entities, ) - remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]] = defaultdict( - list - ) - - entry.async_on_unload( - entry.runtime_data.async_add_special_listener( - partial( - _add_event_sensor_listeners, - entry, - async_add_entities, - remove_event_sensor_listener_dict, - ), - (EventKey.BSH_COMMON_APPLIANCE_PAIRED,), - ) - ) - entry.async_on_unload( - entry.runtime_data.async_add_special_listener( - partial( - _remove_event_sensor_listeners_on_depaired, - entry, - remove_event_sensor_listener_dict, - ), - (EventKey.BSH_COMMON_APPLIANCE_DEPAIRED,), - ) - ) - class HomeConnectSensor(HomeConnectEntity, SensorEntity): """Sensor class for Home Connect.""" @@ -697,7 +647,12 @@ class HomeConnectProgramSensor(HomeConnectSensor): class HomeConnectEventSensor(HomeConnectSensor): """Sensor class for Home Connect events.""" + _attr_entity_registry_enabled_default = False + def update_native_value(self) -> None: """Update the sensor's status.""" - event = self.appliance.events[cast(EventKey, self.bsh_key)] - self._update_native_value(event.value) + event = self.appliance.events.get(cast(EventKey, self.bsh_key)) + if event: + self._update_native_value(event.value) + elif self._attr_native_value is None: + self._attr_native_value = self.entity_description.default_value diff --git a/tests/components/home_connect/test_coordinator.py b/tests/components/home_connect/test_coordinator.py index 40af64f9042..f9fed995b89 100644 --- a/tests/components/home_connect/test_coordinator.py +++ b/tests/components/home_connect/test_coordinator.py @@ -247,6 +247,7 @@ async def test_coordinator_update_failing( getattr(client, mock_method).assert_called() +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True) @pytest.mark.parametrize( ("event_type", "event_key", "event_value", ATTR_ENTITY_ID), @@ -288,7 +289,7 @@ async def test_event_listener( assert config_entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) - + assert state event_message = EventMessage( appliance.ha_id, event_type, @@ -310,8 +311,7 @@ async def test_event_listener( new_state = hass.states.get(entity_id) assert new_state - if state is not None: - assert new_state.state != state.state + assert new_state.state != state.state # Following, we are gonna check that the listeners are clean up correctly new_entity_id = entity_id + "_new" diff --git a/tests/components/home_connect/test_sensor.py b/tests/components/home_connect/test_sensor.py index 47badd8d06d..fe8a3ab4be0 100644 --- a/tests/components/home_connect/test_sensor.py +++ b/tests/components/home_connect/test_sensor.py @@ -1,7 +1,6 @@ """Tests for home_connect sensor entities.""" from collections.abc import Awaitable, Callable -import logging from unittest.mock import AsyncMock, MagicMock from aiohomeconnect.model import ( @@ -140,29 +139,6 @@ async def test_paired_depaired_devices_flow( for entity_entry in entity_entries: assert entity_registry.async_get(entity_entry.entity_id) - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.EVENT, - ArrayOfEvents( - [ - Event( - key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR, - raw_key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR.value, - timestamp=0, - level="", - handling="", - value=BSH_EVENT_PRESENT_STATE_PRESENT, - ) - ], - ), - ), - ] - ) - await hass.async_block_till_done() - assert hass.states.is_state("sensor.washer_poor_i_dos_1_fill_level", "present") - @pytest.mark.parametrize( ("appliance", "keys_to_check"), @@ -231,6 +207,7 @@ async def test_connected_devices( ) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True) async def test_sensor_entity_availability( hass: HomeAssistant, @@ -247,28 +224,6 @@ async def test_sensor_entity_availability( assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.EVENT, - ArrayOfEvents( - [ - Event( - key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY, - raw_key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY.value, - timestamp=0, - level="", - handling="", - value=BSH_EVENT_PRESENT_STATE_OFF, - ) - ], - ), - ), - ] - ) - await hass.async_block_till_done() - for entity_id in entity_ids: state = hass.states.get(entity_id) assert state @@ -545,33 +500,105 @@ async def test_remaining_prog_time_edge_cases( assert hass.states.is_state(entity_id, expected_state) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.parametrize( ( "entity_id", "event_key", - "value_expected_state", + "event_type", + "event_value_update", + "expected", "appliance", ), [ ( "sensor.dishwasher_door", EventKey.BSH_COMMON_STATUS_DOOR_STATE, - [ - ( - BSH_DOOR_STATE_LOCKED, - "locked", - ), - ( - BSH_DOOR_STATE_CLOSED, - "closed", - ), - ( - BSH_DOOR_STATE_OPEN, - "open", - ), - ], + EventType.STATUS, + BSH_DOOR_STATE_LOCKED, + "locked", "Dishwasher", ), + ( + "sensor.dishwasher_door", + EventKey.BSH_COMMON_STATUS_DOOR_STATE, + EventType.STATUS, + BSH_DOOR_STATE_CLOSED, + "closed", + "Dishwasher", + ), + ( + "sensor.dishwasher_door", + EventKey.BSH_COMMON_STATUS_DOOR_STATE, + EventType.STATUS, + BSH_DOOR_STATE_OPEN, + "open", + "Dishwasher", + ), + ( + "sensor.fridgefreezer_freezer_door_alarm", + "EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF", + EventType.EVENT, + "", + "off", + "FridgeFreezer", + ), + ( + "sensor.fridgefreezer_freezer_door_alarm", + EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_OFF, + "off", + "FridgeFreezer", + ), + ( + "sensor.fridgefreezer_freezer_door_alarm", + EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_PRESENT, + "present", + "FridgeFreezer", + ), + ( + "sensor.fridgefreezer_freezer_door_alarm", + EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_CONFIRMED, + "confirmed", + "FridgeFreezer", + ), + ( + "sensor.coffeemaker_bean_container_empty", + EventType.EVENT, + "EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF", + "", + "off", + "CoffeeMaker", + ), + ( + "sensor.coffeemaker_bean_container_empty", + EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_OFF, + "off", + "CoffeeMaker", + ), + ( + "sensor.coffeemaker_bean_container_empty", + EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_PRESENT, + "present", + "CoffeeMaker", + ), + ( + "sensor.coffeemaker_bean_container_empty", + EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, + EventType.EVENT, + BSH_EVENT_PRESENT_STATE_CONFIRMED, + "confirmed", + "CoffeeMaker", + ), ], indirect=["appliance"], ) @@ -582,111 +609,37 @@ async def test_sensors_states( integration_setup: Callable[[MagicMock], Awaitable[bool]], entity_id: str, event_key: EventKey, - value_expected_state: list[tuple[str, str]], + event_type: EventType, + event_value_update: str, appliance: HomeAppliance, + expected: str, ) -> None: - """Tests for appliance sensors.""" + """Tests for appliance alarm sensors.""" assert await integration_setup(client) assert config_entry.state is ConfigEntryState.LOADED - for value, expected_state in value_expected_state: - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.STATUS, - ArrayOfEvents( - [ - Event( - key=event_key, - raw_key=str(event_key), - timestamp=0, - level="", - handling="", - value=value, - ) - ], - ), + await client.add_events( + [ + EventMessage( + appliance.ha_id, + event_type, + ArrayOfEvents( + [ + Event( + key=event_key, + raw_key=str(event_key), + timestamp=0, + level="", + handling="", + value=event_value_update, + ) + ], ), - ] - ) - await hass.async_block_till_done() - assert hass.states.is_state(entity_id, expected_state) - - -@pytest.mark.parametrize( - ( - "entity_id", - "event_key", - "appliance", - ), - [ - ( - "sensor.fridgefreezer_freezer_door_alarm", - EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER, - "FridgeFreezer", - ), - ( - "sensor.coffeemaker_bean_container_empty", - EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY, - "CoffeeMaker", - ), - ], - indirect=["appliance"], -) -async def test_event_sensors_states( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - entity_registry: er.EntityRegistry, - client: MagicMock, - config_entry: MockConfigEntry, - integration_setup: Callable[[MagicMock], Awaitable[bool]], - entity_id: str, - event_key: EventKey, - appliance: HomeAppliance, -) -> None: - """Tests for appliance event sensors.""" - caplog.set_level(logging.ERROR) - assert await integration_setup(client) - assert config_entry.state is ConfigEntryState.LOADED - - assert not hass.states.get(entity_id) - - for value, expected_state in ( - (BSH_EVENT_PRESENT_STATE_OFF, "off"), - (BSH_EVENT_PRESENT_STATE_PRESENT, "present"), - (BSH_EVENT_PRESENT_STATE_CONFIRMED, "confirmed"), - ): - await client.add_events( - [ - EventMessage( - appliance.ha_id, - EventType.EVENT, - ArrayOfEvents( - [ - Event( - key=event_key, - raw_key=str(event_key), - timestamp=0, - level="", - handling="", - value=value, - ) - ], - ), - ), - ] - ) - await hass.async_block_till_done() - assert hass.states.is_state(entity_id, expected_state) - - # Verify that the integration doesn't attempt to add the event sensors more than once - # If that happens, the EntityPlatform logs an error with the entity's unique ID. - assert "exists" not in caplog.text - assert entity_id not in caplog.text - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.unique_id not in caplog.text + ), + ] + ) + await hass.async_block_till_done() + assert hass.states.is_state(entity_id, expected) @pytest.mark.parametrize( From 917b467b8591dec413fe27a04fe367d0cc2e95fd Mon Sep 17 00:00:00 2001 From: TimL Date: Thu, 22 May 2025 22:50:22 +1000 Subject: [PATCH 0794/1175] Add SMLIGHT button entities for second radio (#141463) * Add button entities for second radio * Update tests for second router reconnect button --- homeassistant/components/smlight/button.py | 48 ++++++++++++++-------- tests/components/smlight/test_button.py | 37 ++++++++++++++--- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/smlight/button.py b/homeassistant/components/smlight/button.py index f834392ea13..67d9997a105 100644 --- a/homeassistant/components/smlight/button.py +++ b/homeassistant/components/smlight/button.py @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) class SmButtonDescription(ButtonEntityDescription): """Class to describe a Button entity.""" - press_fn: Callable[[CmdWrapper], Awaitable[None]] + press_fn: Callable[[CmdWrapper, int], Awaitable[None]] BUTTONS: list[SmButtonDescription] = [ @@ -40,19 +40,19 @@ BUTTONS: list[SmButtonDescription] = [ key="core_restart", translation_key="core_restart", device_class=ButtonDeviceClass.RESTART, - press_fn=lambda cmd: cmd.reboot(), + press_fn=lambda cmd, idx: cmd.reboot(), ), SmButtonDescription( key="zigbee_restart", translation_key="zigbee_restart", device_class=ButtonDeviceClass.RESTART, - press_fn=lambda cmd: cmd.zb_restart(), + press_fn=lambda cmd, idx: cmd.zb_restart(), ), SmButtonDescription( key="zigbee_flash_mode", translation_key="zigbee_flash_mode", entity_registry_enabled_default=False, - press_fn=lambda cmd: cmd.zb_bootloader(), + press_fn=lambda cmd, idx: cmd.zb_bootloader(), ), ] @@ -60,7 +60,7 @@ ROUTER = SmButtonDescription( key="reconnect_zigbee_router", translation_key="reconnect_zigbee_router", entity_registry_enabled_default=False, - press_fn=lambda cmd: cmd.zb_router(), + press_fn=lambda cmd, idx: cmd.zb_router(idx=idx), ) @@ -71,23 +71,32 @@ async def async_setup_entry( ) -> None: """Set up SMLIGHT buttons based on a config entry.""" coordinator = entry.runtime_data.data + radios = coordinator.data.info.radios async_add_entities(SmButton(coordinator, button) for button in BUTTONS) - entity_created = False + entity_created = [False, False] @callback def _check_router(startup: bool = False) -> None: - nonlocal entity_created + def router_entity(router: SmButtonDescription, idx: int) -> None: + nonlocal entity_created + zb_type = coordinator.data.info.radios[idx].zb_type - if coordinator.data.info.zb_type == 1 and not entity_created: - async_add_entities([SmButton(coordinator, ROUTER)]) - entity_created = True - elif coordinator.data.info.zb_type != 1 and (startup or entity_created): - entity_registry = er.async_get(hass) - if entity_id := entity_registry.async_get_entity_id( - BUTTON_DOMAIN, DOMAIN, f"{coordinator.unique_id}-{ROUTER.key}" - ): - entity_registry.async_remove(entity_id) + if zb_type == 1 and not entity_created[idx]: + async_add_entities([SmButton(coordinator, router, idx)]) + entity_created[idx] = True + elif zb_type != 1 and (startup or entity_created[idx]): + entity_registry = er.async_get(hass) + button = f"_{idx}" if idx else "" + if entity_id := entity_registry.async_get_entity_id( + BUTTON_DOMAIN, + DOMAIN, + f"{coordinator.unique_id}-{router.key}{button}", + ): + entity_registry.async_remove(entity_id) + + for idx, _ in enumerate(radios): + router_entity(ROUTER, idx) coordinator.async_add_listener(_check_router) _check_router(startup=True) @@ -104,13 +113,16 @@ class SmButton(SmEntity, ButtonEntity): self, coordinator: SmDataUpdateCoordinator, description: SmButtonDescription, + idx: int = 0, ) -> None: """Initialize SLZB-06 button entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{coordinator.unique_id}-{description.key}" + self.idx = idx + button = f"_{idx}" if idx else "" + self._attr_unique_id = f"{coordinator.unique_id}-{description.key}{button}" async def async_press(self) -> None: """Trigger button press.""" - await self.entity_description.press_fn(self.coordinator.client.cmds) + await self.entity_description.press_fn(self.coordinator.client.cmds, self.idx) diff --git a/tests/components/smlight/test_button.py b/tests/components/smlight/test_button.py index 51e9414c00e..f9ea010fe7c 100644 --- a/tests/components/smlight/test_button.py +++ b/tests/components/smlight/test_button.py @@ -3,18 +3,22 @@ from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory -from pysmlight import Info +from pysmlight import Info, Radio import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.components.smlight.const import SCAN_INTERVAL +from homeassistant.components.smlight.const import DOMAIN, SCAN_INTERVAL from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from .conftest import setup_integration -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_json_object_fixture, +) @pytest.fixture @@ -23,7 +27,7 @@ def platforms() -> Platform | list[Platform]: return [Platform.BUTTON] -MOCK_ROUTER = Info(MAC="AA:BB:CC:DD:EE:FF", zb_type=1) +MOCK_ROUTER = Info(MAC="AA:BB:CC:DD:EE:FF", radios=[Radio(zb_type=1)]) @pytest.mark.parametrize( @@ -67,7 +71,7 @@ async def test_buttons( ) assert len(mock_method.mock_calls) == 1 - mock_method.assert_called_with() + mock_method.assert_called() @pytest.mark.parametrize("entity_id", ["zigbee_flash_mode", "reconnect_zigbee_router"]) @@ -90,6 +94,29 @@ async def test_disabled_by_default_buttons( assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_zigbee2_router_button( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_smlight_client: MagicMock, +) -> None: + """Test creation of second radio router button (if available).""" + mock_smlight_client.get_info.side_effect = None + mock_smlight_client.get_info.return_value = Info.from_dict( + load_json_object_fixture("info-MR1.json", DOMAIN) + ) + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("button.mock_title_reconnect_zigbee_router") + assert state is not None + assert state.state == STATE_UNKNOWN + + entry = entity_registry.async_get("button.mock_title_reconnect_zigbee_router") + assert entry is not None + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-reconnect_zigbee_router_1" + + async def test_remove_router_reconnect( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 8f05a639f3f6ed1fb412e57c80e9eec211c72f67 Mon Sep 17 00:00:00 2001 From: dalan <863286+dalanmiller@users.noreply.github.com> Date: Fri, 23 May 2025 00:52:58 +1000 Subject: [PATCH 0795/1175] HomeKit Bridge integration: Adding `h264_qsv` as valid VIDEO_CODEC option (#145448) --- homeassistant/components/homekit/const.py | 1 + homeassistant/components/homekit/util.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ae682a0ea2d..44f18c30099 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -24,6 +24,7 @@ VIDEO_CODEC_LIBX264 = "libx264" AUDIO_CODEC_OPUS = "libopus" VIDEO_CODEC_H264_OMX = "h264_omx" VIDEO_CODEC_H264_V4L2M2M = "h264_v4l2m2m" +VIDEO_CODEC_H264_QSV = "h264_qsv" # Intel Quick Sync Video VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] AUDIO_CODEC_COPY = "copy" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index bc98f00c15a..85207e09626 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -112,6 +112,7 @@ from .const import ( TYPE_VALVE, VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_H264_QSV, VIDEO_CODEC_H264_V4L2M2M, VIDEO_CODEC_LIBX264, ) @@ -130,6 +131,7 @@ MAX_PORT = 65535 VALID_VIDEO_CODECS = [ VIDEO_CODEC_LIBX264, VIDEO_CODEC_H264_OMX, + VIDEO_CODEC_H264_QSV, VIDEO_CODEC_H264_V4L2M2M, AUDIO_CODEC_COPY, ] From 7a55abaa425f79699cfc29e9cc312fea21058d60 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 11:18:48 -0400 Subject: [PATCH 0796/1175] Add AbstractTemplateFan class in preparation for trigger based entity (#144968) * Add AbstractTemplateFan class in preparation for trigger based entity * update after rebase --- homeassistant/components/template/fan.py | 262 +++++++++++++---------- 1 file changed, 143 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 32e6b06d108..c353fca48df 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator, Sequence import logging from typing import TYPE_CHECKING, Any @@ -37,6 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, @@ -205,26 +207,13 @@ async def async_setup_platform( ) -class TemplateFan(TemplateEntity, FanEntity): - """A template fan component.""" +class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): + """Representation of a template fan features.""" - _attr_should_poll = False - - def __init__( - self, - hass: HomeAssistant, - config: dict[str, Any], - unique_id, - ) -> None: - """Initialize the fan.""" - super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - if (object_id := config.get(CONF_OBJECT_ID)) is not None: - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) - name = self._attr_name - if TYPE_CHECKING: - assert name is not None + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" self._template = config.get(CONF_STATE) self._percentage_template = config.get(CONF_PERCENTAGE) @@ -232,22 +221,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating_template = config.get(CONF_OSCILLATING) self._direction_template = config.get(CONF_DIRECTION) - self._attr_supported_features |= ( - FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON - ) - for action_id, supported_feature in ( - (CONF_ON_ACTION, 0), - (CONF_OFF_ACTION, 0), - (CONF_SET_PERCENTAGE_ACTION, FanEntityFeature.SET_SPEED), - (CONF_SET_PRESET_MODE_ACTION, FanEntityFeature.PRESET_MODE), - (CONF_SET_OSCILLATING_ACTION, FanEntityFeature.OSCILLATE), - (CONF_SET_DIRECTION_ACTION, FanEntityFeature.DIRECTION), - ): - # Scripts can be an empty list, therefore we need to check for None - if (action_config := config.get(action_id)) is not None: - self.add_script(action_id, action_config, name, DOMAIN) - self._attr_supported_features |= supported_feature - self._state: bool | None = False self._percentage: int | None = None self._preset_mode: str | None = None @@ -261,6 +234,20 @@ class TemplateFan(TemplateEntity, FanEntity): self._preset_modes: list[str] | None = config.get(CONF_PRESET_MODES) self._attr_assumed_state = self._template is None + def _register_scripts( + self, config: dict[str, Any] + ) -> Generator[tuple[str, Sequence[dict[str, Any]], FanEntityFeature | int]]: + for action_id, supported_feature in ( + (CONF_ON_ACTION, 0), + (CONF_OFF_ACTION, 0), + (CONF_SET_PERCENTAGE_ACTION, FanEntityFeature.SET_SPEED), + (CONF_SET_PRESET_MODE_ACTION, FanEntityFeature.PRESET_MODE), + (CONF_SET_OSCILLATING_ACTION, FanEntityFeature.OSCILLATE), + (CONF_SET_DIRECTION_ACTION, FanEntityFeature.DIRECTION), + ): + if (action_config := config.get(action_id)) is not None: + yield (action_id, action_config, supported_feature) + @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" @@ -296,6 +283,92 @@ class TemplateFan(TemplateEntity, FanEntity): """Return the oscillation state.""" return self._direction + def _handle_state(self, result) -> None: + if isinstance(result, bool): + self._state = result + return + + if isinstance(result, str): + self._state = result.lower() in ("true", STATE_ON) + return + + self._state = False + + @callback + def _update_percentage(self, percentage): + # Validate percentage + try: + percentage = int(float(percentage)) + except (ValueError, TypeError): + _LOGGER.error( + "Received invalid percentage: %s for entity %s", + percentage, + self.entity_id, + ) + self._percentage = 0 + return + + if 0 <= percentage <= 100: + self._percentage = percentage + else: + _LOGGER.error( + "Received invalid percentage: %s for entity %s", + percentage, + self.entity_id, + ) + self._percentage = 0 + + @callback + def _update_preset_mode(self, preset_mode): + # Validate preset mode + preset_mode = str(preset_mode) + + if self.preset_modes and preset_mode in self.preset_modes: + self._preset_mode = preset_mode + elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN): + self._preset_mode = None + else: + _LOGGER.error( + "Received invalid preset_mode: %s for entity %s. Expected: %s", + preset_mode, + self.entity_id, + self.preset_mode, + ) + self._preset_mode = None + + @callback + def _update_oscillating(self, oscillating): + # Validate osc + if oscillating == "True" or oscillating is True: + self._oscillating = True + elif oscillating == "False" or oscillating is False: + self._oscillating = False + elif oscillating in (STATE_UNAVAILABLE, STATE_UNKNOWN): + self._oscillating = None + else: + _LOGGER.error( + "Received invalid oscillating: %s for entity %s. Expected: True/False", + oscillating, + self.entity_id, + ) + self._oscillating = None + + @callback + def _update_direction(self, direction): + # Validate direction + if direction in _VALID_DIRECTIONS: + self._direction = direction + elif direction in (STATE_UNAVAILABLE, STATE_UNKNOWN): + self._direction = None + else: + _LOGGER.error( + "Received invalid direction: %s for entity %s. Expected: %s", + direction, + self.entity_id, + ", ".join(_VALID_DIRECTIONS), + ) + self._direction = None + async def async_turn_on( self, percentage: int | None = None, @@ -402,6 +475,40 @@ class TemplateFan(TemplateEntity, FanEntity): ", ".join(_VALID_DIRECTIONS), ) + +class TemplateFan(TemplateEntity, AbstractTemplateFan): + """A template fan component.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: dict[str, Any], + unique_id, + ) -> None: + """Initialize the fan.""" + TemplateEntity.__init__( + self, hass, config=config, fallback_name=None, unique_id=unique_id + ) + AbstractTemplateFan.__init__(self, config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + self._attr_supported_features |= ( + FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON + ) + for action_id, action_config, supported_feature in self._register_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + @callback def _update_state(self, result): super()._update_state(result) @@ -409,15 +516,7 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = None return - if isinstance(result, bool): - self._state = result - return - - if isinstance(result, str): - self._state = result.lower() in ("true", STATE_ON) - return - - self._state = False + self._handle_state(result) @callback def _async_setup_templates(self) -> None: @@ -460,78 +559,3 @@ class TemplateFan(TemplateEntity, FanEntity): none_on_template_error=True, ) super()._async_setup_templates() - - @callback - def _update_percentage(self, percentage): - # Validate percentage - try: - percentage = int(float(percentage)) - except (ValueError, TypeError): - _LOGGER.error( - "Received invalid percentage: %s for entity %s", - percentage, - self.entity_id, - ) - self._percentage = 0 - return - - if 0 <= percentage <= 100: - self._percentage = percentage - else: - _LOGGER.error( - "Received invalid percentage: %s for entity %s", - percentage, - self.entity_id, - ) - self._percentage = 0 - - @callback - def _update_preset_mode(self, preset_mode): - # Validate preset mode - preset_mode = str(preset_mode) - - if self.preset_modes and preset_mode in self.preset_modes: - self._preset_mode = preset_mode - elif preset_mode in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._preset_mode = None - else: - _LOGGER.error( - "Received invalid preset_mode: %s for entity %s. Expected: %s", - preset_mode, - self.entity_id, - self.preset_mode, - ) - self._preset_mode = None - - @callback - def _update_oscillating(self, oscillating): - # Validate osc - if oscillating == "True" or oscillating is True: - self._oscillating = True - elif oscillating == "False" or oscillating is False: - self._oscillating = False - elif oscillating in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._oscillating = None - else: - _LOGGER.error( - "Received invalid oscillating: %s for entity %s. Expected: True/False", - oscillating, - self.entity_id, - ) - self._oscillating = None - - @callback - def _update_direction(self, direction): - # Validate direction - if direction in _VALID_DIRECTIONS: - self._direction = direction - elif direction in (STATE_UNAVAILABLE, STATE_UNKNOWN): - self._direction = None - else: - _LOGGER.error( - "Received invalid direction: %s for entity %s. Expected: %s", - direction, - self.entity_id, - ", ".join(_VALID_DIRECTIONS), - ) - self._direction = None From 65ebdb42921e25e2253b690e1adeb8118e71a0f1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 17:26:04 +0200 Subject: [PATCH 0797/1175] Bump yt-dlp to 2025.05.22 (#145441) --- 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 a6663b089ac..3ce80f497ef 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.03.31"], + "requirements": ["yt-dlp[default]==2025.05.22"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index dd938be0067..907408c207b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3156,7 +3156,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.03.31 +yt-dlp[default]==2025.05.22 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e99def6471e..f8780b3ef6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2561,7 +2561,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.03.31 +yt-dlp[default]==2025.05.22 # homeassistant.components.zamg zamg==0.3.6 From 64d6552890aa5c6908d78ddf288e4172676ca30d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 22 May 2025 17:26:59 +0200 Subject: [PATCH 0798/1175] Bump pysmartthings to 3.2.3 (#145444) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index f72405dae20..180d4eebed1 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.2"] + "requirements": ["pysmartthings==3.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 907408c207b..8950d602f08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2335,7 +2335,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.2 +pysmartthings==3.2.3 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8780b3ef6a..6771a84d143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1908,7 +1908,7 @@ pysma==0.7.5 pysmappee==0.2.29 # homeassistant.components.smartthings -pysmartthings==3.2.2 +pysmartthings==3.2.3 # homeassistant.components.smarty pysmarty2==0.10.2 From 9a74390143724f834ad20b64c37ca852225266b6 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 11:33:57 -0400 Subject: [PATCH 0799/1175] Add AbstractTemplateLock to prepare for trigger based template locks (#144978) * Add AbstractTemplateLock * update after rebase --- homeassistant/components/template/lock.py | 133 +++++++++++++--------- 1 file changed, 78 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index c858325e0ea..25eac8c35e4 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator, Sequence from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -27,6 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, @@ -134,42 +136,33 @@ async def async_setup_platform( ) -class TemplateLock(TemplateEntity, LockEntity): - """Representation of a template lock.""" +class AbstractTemplateLock(AbstractTemplateEntity, LockEntity): + """Representation of a template lock features.""" - _attr_should_poll = False + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" - def __init__( - self, - hass: HomeAssistant, - config: dict[str, Any], - unique_id: str | None, - ) -> None: - """Initialize the lock.""" - super().__init__( - hass, config=config, fallback_name=DEFAULT_NAME, unique_id=unique_id - ) self._state: LockState | None = None - name = self._attr_name - if TYPE_CHECKING: - assert name is not None - self._state_template = config.get(CONF_STATE) - for action_id, supported_feature in ( - (CONF_LOCK, 0), - (CONF_UNLOCK, 0), - (CONF_OPEN, LockEntityFeature.OPEN), - ): - # Scripts can be an empty list, therefore we need to check for None - if (action_config := config.get(action_id)) is not None: - self.add_script(action_id, action_config, name, DOMAIN) - self._attr_supported_features |= supported_feature self._code_format_template = config.get(CONF_CODE_FORMAT) self._code_format: str | None = None self._code_format_template_error: TemplateError | None = None self._optimistic = config.get(CONF_OPTIMISTIC) self._attr_assumed_state = bool(self._optimistic) + def _register_scripts( + self, config: dict[str, Any] + ) -> Generator[tuple[str, Sequence[dict[str, Any]], LockEntityFeature | int]]: + for action_id, supported_feature in ( + (CONF_LOCK, 0), + (CONF_UNLOCK, 0), + (CONF_OPEN, LockEntityFeature.OPEN), + ): + if (action_config := config.get(action_id)) is not None: + yield (action_id, action_config, supported_feature) + @property def is_locked(self) -> bool: """Return true if lock is locked.""" @@ -195,14 +188,12 @@ class TemplateLock(TemplateEntity, LockEntity): """Return true if lock is open.""" return self._state == LockState.OPEN - @callback - def _update_state(self, result: str | TemplateError) -> None: - """Update the state from the template.""" - super()._update_state(result) - if isinstance(result, TemplateError): - self._state = None - return + @property + def code_format(self) -> str | None: + """Regex for code format or None if no code is required.""" + return self._code_format + def _handle_state(self, result: Any) -> None: if isinstance(result, bool): self._state = LockState.LOCKED if result else LockState.UNLOCKED return @@ -229,28 +220,6 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = None - @property - def code_format(self) -> str | None: - """Regex for code format or None if no code is required.""" - return self._code_format - - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - if TYPE_CHECKING: - assert self._state_template is not None - self.add_template_attribute( - "_state", self._state_template, None, self._update_state - ) - if self._code_format_template: - self.add_template_attribute( - "_code_format_template", - self._code_format_template, - None, - self._update_code_format, - ) - super()._async_setup_templates() - @callback def _update_code_format(self, render: str | TemplateError | None): """Update code format from the template.""" @@ -330,3 +299,57 @@ class TemplateLock(TemplateEntity, LockEntity): "cause": str(self._code_format_template_error), }, ) + + +class TemplateLock(TemplateEntity, AbstractTemplateLock): + """Representation of a template lock.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: dict[str, Any], + unique_id: str | None, + ) -> None: + """Initialize the lock.""" + TemplateEntity.__init__( + self, hass, config=config, fallback_name=DEFAULT_NAME, unique_id=unique_id + ) + AbstractTemplateLock.__init__(self, config) + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + for action_id, action_config, supported_feature in self._register_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + @callback + def _update_state(self, result: str | TemplateError) -> None: + """Update the state from the template.""" + super()._update_state(result) + if isinstance(result, TemplateError): + self._state = None + return + + self._handle_state(result) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if TYPE_CHECKING: + assert self._state_template is not None + self.add_template_attribute( + "_state", self._state_template, None, self._update_state + ) + if self._code_format_template: + self.add_template_attribute( + "_code_format_template", + self._code_format_template, + None, + self._update_code_format, + ) + super()._async_setup_templates() From 83ee9e9540c0e8b89ed3ccdfbafcb3ef59b03d44 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 11:49:50 -0400 Subject: [PATCH 0800/1175] Add AbstractTemplate cover to prepare for trigger based template covers (#144907) * Add AbstractTemplate cover to prepare for trigger based template covers * add reflection and improve test coverage * update class after rebase * remove test --- homeassistant/components/template/cover.py | 261 +++++++++++---------- homeassistant/components/template/light.py | 2 + 2 files changed, 145 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e15180173b4..1eb80677f7e 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator, Sequence import logging from typing import TYPE_CHECKING, Any @@ -35,6 +36,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, @@ -213,49 +215,19 @@ async def async_setup_platform( ) -class CoverTemplate(TemplateEntity, CoverEntity): - """Representation of a Template cover.""" +class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): + """Representation of a template cover features.""" - _attr_should_poll = False + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" - def __init__( - self, - hass: HomeAssistant, - config: dict[str, Any], - unique_id, - ) -> None: - """Initialize the Template cover.""" - super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - if (object_id := config.get(CONF_OBJECT_ID)) is not None: - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) - name = self._attr_name - if TYPE_CHECKING: - assert name is not None self._template = config.get(CONF_STATE) - self._position_template = config.get(CONF_POSITION) self._tilt_template = config.get(CONF_TILT) self._attr_device_class = config.get(CONF_DEVICE_CLASS) - # The config requires (open and close scripts) or a set position script, - # therefore the base supported features will always include them. - self._attr_supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - ) - for action_id, supported_feature in ( - (OPEN_ACTION, 0), - (CLOSE_ACTION, 0), - (STOP_ACTION, CoverEntityFeature.STOP), - (POSITION_ACTION, CoverEntityFeature.SET_POSITION), - (TILT_ACTION, TILT_FEATURES), - ): - # Scripts can be an empty list, therefore we need to check for None - if (action_config := config.get(action_id)) is not None: - self.add_script(action_id, action_config, name, DOMAIN) - self._attr_supported_features |= supported_feature - optimistic = config.get(CONF_OPTIMISTIC) self._optimistic = optimistic or ( optimistic is None and not self._template and not self._position_template @@ -267,61 +239,54 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._is_closing = False self._tilt_value: int | None = None - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - if self._template: - self.add_template_attribute( - "_position", self._template, None, self._update_state - ) - if self._position_template: - self.add_template_attribute( - "_position", - self._position_template, - None, - self._update_position, - none_on_template_error=True, - ) - if self._tilt_template: - self.add_template_attribute( - "_tilt_value", - self._tilt_template, - None, - self._update_tilt, - none_on_template_error=True, - ) - super()._async_setup_templates() + def _register_scripts( + self, config: dict[str, Any] + ) -> Generator[tuple[str, Sequence[dict[str, Any]], CoverEntityFeature | int]]: + for action_id, supported_feature in ( + (OPEN_ACTION, 0), + (CLOSE_ACTION, 0), + (STOP_ACTION, CoverEntityFeature.STOP), + (POSITION_ACTION, CoverEntityFeature.SET_POSITION), + (TILT_ACTION, TILT_FEATURES), + ): + if (action_config := config.get(action_id)) is not None: + yield (action_id, action_config, supported_feature) - @callback - def _update_state(self, result): - super()._update_state(result) - if isinstance(result, TemplateError): - self._position = None - return + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed.""" + if self._position is None: + return None - state = str(result).lower() + return self._position == 0 - if state in _VALID_STATES: - if not self._position_template: - if state in ("true", OPEN_STATE): - self._position = 100 - else: - self._position = 0 + @property + def is_opening(self) -> bool: + """Return if the cover is currently opening.""" + return self._is_opening - self._is_opening = state == OPENING_STATE - self._is_closing = state == CLOSING_STATE - else: - _LOGGER.error( - "Received invalid cover is_on state: %s for entity %s. Expected: %s", - state, - self.entity_id, - ", ".join(_VALID_STATES), - ) - if not self._position_template: - self._position = None + @property + def is_closing(self) -> bool: + """Return if the cover is currently closing.""" + return self._is_closing - self._is_opening = False - self._is_closing = False + @property + def current_cover_position(self) -> int | None: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + if self._position_template or POSITION_ACTION in self._action_scripts: + return self._position + return None + + @property + def current_cover_tilt_position(self) -> int | None: + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._tilt_value @callback def _update_position(self, result): @@ -367,41 +332,30 @@ class CoverTemplate(TemplateEntity, CoverEntity): else: self._tilt_value = state - @property - def is_closed(self) -> bool | None: - """Return if the cover is closed.""" - if self._position is None: - return None + def _update_opening_and_closing(self, result: Any) -> None: + state = str(result).lower() - return self._position == 0 + if state in _VALID_STATES: + if not self._position_template: + if state in ("true", OPEN_STATE): + self._position = 100 + else: + self._position = 0 - @property - def is_opening(self) -> bool: - """Return if the cover is currently opening.""" - return self._is_opening + self._is_opening = state == OPENING_STATE + self._is_closing = state == CLOSING_STATE + else: + _LOGGER.error( + "Received invalid cover is_on state: %s for entity %s. Expected: %s", + state, + self.entity_id, + ", ".join(_VALID_STATES), + ) + if not self._position_template: + self._position = None - @property - def is_closing(self) -> bool: - """Return if the cover is currently closing.""" - return self._is_closing - - @property - def current_cover_position(self) -> int | None: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - if self._position_template or self._action_scripts.get(POSITION_ACTION): - return self._position - return None - - @property - def current_cover_tilt_position(self) -> int | None: - """Return current position of cover tilt. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._tilt_value + self._is_opening = False + self._is_closing = False async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" @@ -479,3 +433,74 @@ class CoverTemplate(TemplateEntity, CoverEntity): ) if self._tilt_optimistic: self.async_write_ha_state() + + +class CoverTemplate(TemplateEntity, AbstractTemplateCover): + """Representation of a Template cover.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: dict[str, Any], + unique_id, + ) -> None: + """Initialize the Template cover.""" + TemplateEntity.__init__( + self, hass, config=config, fallback_name=None, unique_id=unique_id + ) + AbstractTemplateCover.__init__(self, config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + # The config requires (open and close scripts) or a set position script, + # therefore the base supported features will always include them. + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + for action_id, action_config, supported_feature in self._register_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template: + self.add_template_attribute( + "_position", self._template, None, self._update_state + ) + if self._position_template: + self.add_template_attribute( + "_position", + self._position_template, + None, + self._update_position, + none_on_template_error=True, + ) + if self._tilt_template: + self.add_template_attribute( + "_tilt_value", + self._tilt_template, + None, + self._update_tilt, + none_on_template_error=True, + ) + super()._async_setup_templates() + + @callback + def _update_state(self, result): + super()._update_state(result) + if isinstance(result, TemplateError): + self._position = None + return + + self._update_opening_and_closing(result) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index ac751d46cf7..9fc935bf0ee 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -276,6 +276,8 @@ async def async_setup_platform( class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): """Representation of a template lights features.""" + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. def __init__( # pylint: disable=super-init-not-called self, config: dict[str, Any], initial_state: bool | None = False ) -> None: From a8823cc1d17d0e2fceb4a655aa7fa13d81e37703 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 11:50:15 -0400 Subject: [PATCH 0801/1175] Add AbstractTempleAlarmControlPanel class to prepare for trigger based template alarm control panels (#144974) * Add AbstractTempleAlarmControlPanel class * update after rebase * remove unused list --- .../template/alarm_control_panel.py | 146 +++++++++++------- 1 file changed, 90 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index d035edd26ac..725a73338fa 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Generator, Sequence from enum import Enum import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -37,9 +38,11 @@ from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, ) from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA, @@ -264,32 +267,27 @@ async def async_setup_platform( ) -class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, RestoreEntity): - """Representation of a templated Alarm Control Panel.""" +class AbstractTemplateAlarmControlPanel( + AbstractTemplateEntity, AlarmControlPanelEntity, RestoreEntity +): + """Representation of a templated Alarm Control Panel features.""" - _attr_should_poll = False - - def __init__( - self, - hass: HomeAssistant, - config: dict, - unique_id: str | None, - ) -> None: - """Initialize the panel.""" - super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - if (object_id := config.get(CONF_OBJECT_ID)) is not None: - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) - name = self._attr_name - if TYPE_CHECKING: - assert name is not None + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" self._template = config.get(CONF_STATE) self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED] self._attr_code_format = config[CONF_CODE_FORMAT].value - self._attr_supported_features = AlarmControlPanelEntityFeature(0) + self._state: AlarmControlPanelState | None = None + + def _register_scripts( + self, config: dict[str, Any] + ) -> Generator[ + tuple[str, Sequence[dict[str, Any]], AlarmControlPanelEntityFeature | int] + ]: for action_id, supported_feature in ( (CONF_DISARM_ACTION, 0), (CONF_ARM_AWAY_ACTION, AlarmControlPanelEntityFeature.ARM_AWAY), @@ -302,20 +300,15 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore ), (CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER), ): - # Scripts can be an empty list, therefore we need to check for None if (action_config := config.get(action_id)) is not None: - self.add_script(action_id, action_config, name, DOMAIN) - self._attr_supported_features |= supported_feature + yield (action_id, action_config, supported_feature) - self._state: AlarmControlPanelState | None = None - self._attr_device_info = async_device_info_to_link_from_device_id( - hass, - config.get(CONF_DEVICE_ID), - ) + @property + def alarm_state(self) -> AlarmControlPanelState | None: + """Return the state of the device.""" + return self._state - async def async_added_to_hass(self) -> None: - """Restore last state.""" - await super().async_added_to_hass() + async def _async_handle_restored_state(self) -> None: if ( (last_state := await self.async_get_last_state()) is not None and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) @@ -326,17 +319,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore ): self._state = AlarmControlPanelState(last_state.state) - @property - def alarm_state(self) -> AlarmControlPanelState | None: - """Return the state of the device.""" - return self._state - - @callback - def _update_state(self, result): - if isinstance(result, TemplateError): - self._state = None - return - + def _handle_state(self, result: Any) -> None: # Validate state if result in _VALID_STATES: self._state = result @@ -351,16 +334,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore ) self._state = None - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - if self._template: - self.add_template_attribute( - "_state", self._template, None, self._update_state - ) - super()._async_setup_templates() - - async def _async_alarm_arm(self, state, script, code): + async def _async_alarm_arm(self, state: Any, script: Script | None, code: Any): """Arm the panel to specified state with supplied script.""" optimistic_set = False @@ -368,9 +342,10 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore self._state = state optimistic_set = True - await self.async_run_script( - script, run_variables={ATTR_CODE: code}, context=self._context - ) + if script: + await self.async_run_script( + script, run_variables={ATTR_CODE: code}, context=self._context + ) if optimistic_set: self.async_write_ha_state() @@ -430,3 +405,62 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore script=self._action_scripts.get(CONF_TRIGGER_ACTION), code=code, ) + + +class AlarmControlPanelTemplate(TemplateEntity, AbstractTemplateAlarmControlPanel): + """Representation of a templated Alarm Control Panel.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: dict, + unique_id: str | None, + ) -> None: + """Initialize the panel.""" + TemplateEntity.__init__( + self, hass, config=config, fallback_name=None, unique_id=unique_id + ) + AbstractTemplateAlarmControlPanel.__init__(self, config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + self._attr_supported_features = AlarmControlPanelEntityFeature(0) + for action_id, action_config, supported_feature in self._register_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + self._attr_device_info = async_device_info_to_link_from_device_id( + hass, + config.get(CONF_DEVICE_ID), + ) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + await self._async_handle_restored_state() + + @callback + def _update_state(self, result): + if isinstance(result, TemplateError): + self._state = None + return + + self._handle_state(result) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template: + self.add_template_attribute( + "_state", self._template, None, self._update_state + ) + super()._async_setup_templates() From 4ee9fdc9fbbe86bdbcd21b9eda488b0d79eb406f Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Thu, 22 May 2025 11:50:26 -0400 Subject: [PATCH 0802/1175] Add AbstractTemplateVacuum to prepare for trigger based template vacuums (#144990) * Add AbstractTemplateVacuum * fix typo from copypaste * update after rebase --- homeassistant/components/template/vacuum.py | 191 +++++++++++--------- 1 file changed, 107 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 462f7d672ff..f50751012b3 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Generator, Sequence import logging from typing import TYPE_CHECKING, Any @@ -38,6 +39,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN +from .entity import AbstractTemplateEntity from .template_entity import ( LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS, TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA, @@ -200,34 +202,27 @@ async def async_setup_platform( ) -class TemplateVacuum(TemplateEntity, StateVacuumEntity): - """A template vacuum component.""" - - _attr_should_poll = False - - def __init__( - self, - hass: HomeAssistant, - config: ConfigType, - unique_id, - ) -> None: - """Initialize the vacuum.""" - super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - if (object_id := config.get(CONF_OBJECT_ID)) is not None: - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, object_id, hass=hass - ) - name = self._attr_name - if TYPE_CHECKING: - assert name is not None +class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity): + """Representation of a template vacuum features.""" + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" self._template = config.get(CONF_STATE) self._battery_level_template = config.get(CONF_BATTERY_LEVEL) self._fan_speed_template = config.get(CONF_FAN_SPEED) - self._attr_supported_features = ( - VacuumEntityFeature.START | VacuumEntityFeature.STATE - ) + self._state = None + self._battery_level = None + self._attr_fan_speed = None + + # List of valid fan speeds + self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] + + def _register_scripts( + self, config: dict[str, Any] + ) -> Generator[tuple[str, Sequence[dict[str, Any]], VacuumEntityFeature | int]]: for action_id, supported_feature in ( (SERVICE_START, 0), (SERVICE_PAUSE, VacuumEntityFeature.PAUSE), @@ -237,26 +232,29 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): (SERVICE_LOCATE, VacuumEntityFeature.LOCATE), (SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED), ): - # Scripts can be an empty list, therefore we need to check for None if (action_config := config.get(action_id)) is not None: - self.add_script(action_id, action_config, name, DOMAIN) - self._attr_supported_features |= supported_feature - - self._state = None - self._battery_level = None - self._attr_fan_speed = None - - if self._battery_level_template: - self._attr_supported_features |= VacuumEntityFeature.BATTERY - - # List of valid fan speeds - self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] + yield (action_id, action_config, supported_feature) @property def activity(self) -> VacuumActivity | None: """Return the status of the vacuum cleaner.""" return self._state + def _handle_state(self, result: Any) -> None: + # Validate state + if result in _VALID_STATES: + self._state = result + elif result == STATE_UNKNOWN: + self._state = None + else: + _LOGGER.error( + "Received invalid vacuum state: %s for entity %s. Expected: %s", + result, + self.entity_id, + ", ".join(_VALID_STATES), + ) + self._state = None + async def async_start(self) -> None: """Start or resume the cleaning task.""" await self.async_run_script( @@ -304,54 +302,6 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): script, run_variables={ATTR_FAN_SPEED: fan_speed}, context=self._context ) - @callback - def _async_setup_templates(self) -> None: - """Set up templates.""" - if self._template is not None: - self.add_template_attribute( - "_state", self._template, None, self._update_state - ) - if self._fan_speed_template is not None: - self.add_template_attribute( - "_fan_speed", - self._fan_speed_template, - None, - self._update_fan_speed, - ) - if self._battery_level_template is not None: - self.add_template_attribute( - "_battery_level", - self._battery_level_template, - None, - self._update_battery_level, - none_on_template_error=True, - ) - super()._async_setup_templates() - - @callback - def _update_state(self, result): - super()._update_state(result) - if isinstance(result, TemplateError): - # This is legacy behavior - self._state = STATE_UNKNOWN - if not self._availability_template: - self._attr_available = True - return - - # Validate state - if result in _VALID_STATES: - self._state = result - elif result == STATE_UNKNOWN: - self._state = None - else: - _LOGGER.error( - "Received invalid vacuum state: %s for entity %s. Expected: %s", - result, - self.entity_id, - ", ".join(_VALID_STATES), - ) - self._state = None - @callback def _update_battery_level(self, battery_level): try: @@ -389,3 +339,76 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list, ) self._attr_fan_speed = None + + +class TemplateVacuum(TemplateEntity, AbstractTemplateVacuum): + """A template vacuum component.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + unique_id, + ) -> None: + """Initialize the vacuum.""" + TemplateEntity.__init__( + self, hass, config=config, fallback_name=None, unique_id=unique_id + ) + AbstractTemplateVacuum.__init__(self, config) + if (object_id := config.get(CONF_OBJECT_ID)) is not None: + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, object_id, hass=hass + ) + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + self._attr_supported_features = ( + VacuumEntityFeature.START | VacuumEntityFeature.STATE + ) + for action_id, action_config, supported_feature in self._register_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + if self._battery_level_template: + self._attr_supported_features |= VacuumEntityFeature.BATTERY + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + if self._template is not None: + self.add_template_attribute( + "_state", self._template, None, self._update_state + ) + if self._fan_speed_template is not None: + self.add_template_attribute( + "_fan_speed", + self._fan_speed_template, + None, + self._update_fan_speed, + ) + if self._battery_level_template is not None: + self.add_template_attribute( + "_battery_level", + self._battery_level_template, + None, + self._update_battery_level, + none_on_template_error=True, + ) + super()._async_setup_templates() + + @callback + def _update_state(self, result): + super()._update_state(result) + if isinstance(result, TemplateError): + # This is legacy behavior + self._state = STATE_UNKNOWN + if not self._availability_template: + self._attr_available = True + return + + self._handle_state(result) From d8e0be69d1d0619902c3ef4b66310d2425f3a553 Mon Sep 17 00:00:00 2001 From: jz-v <140891693+jz-v@users.noreply.github.com> Date: Fri, 23 May 2025 02:57:01 +1000 Subject: [PATCH 0803/1175] Add HomeKit thermostat fan state mapping for preheating, defrosting (#145353) Co-authored-by: J. Nick Koston --- .../components/homekit/type_thermostats.py | 2 + .../homekit/test_type_thermostats.py | 95 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 4dda495ce77..f21bf391761 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -167,6 +167,8 @@ HC_HASS_TO_HOMEKIT_FAN_STATE = { HVACAction.COOLING: FAN_STATE_ACTIVE, HVACAction.DRYING: FAN_STATE_ACTIVE, HVACAction.FAN: FAN_STATE_ACTIVE, + HVACAction.PREHEATING: FAN_STATE_IDLE, + HVACAction.DEFROSTING: FAN_STATE_IDLE, } HEAT_COOL_DEADBAND = 5 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 69c347ef55a..4d07757baf3 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -56,6 +56,9 @@ from homeassistant.components.homekit.const import ( PROP_MIN_VALUE, ) from homeassistant.components.homekit.type_thermostats import ( + FAN_STATE_ACTIVE, + FAN_STATE_IDLE, + FAN_STATE_INACTIVE, HC_HEAT_COOL_AUTO, HC_HEAT_COOL_COOL, HC_HEAT_COOL_HEAT, @@ -2493,6 +2496,98 @@ async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( assert not acc.fan_chars +async def test_thermostat_fan_state_with_preheating_and_defrosting( + hass: HomeAssistant, hk_driver +) -> None: + """Test thermostat fan state mappings for preheating and defrosting actions.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_HIGH], + ATTR_HVAC_ACTION: HVACAction.IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + acc.run() + await hass.async_block_till_done() + + # Verify fan state characteristics are available + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + assert hasattr(acc, "char_current_fan_state") + + # Test PREHEATING action maps to FAN_STATE_IDLE + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_HIGH], + ATTR_HVAC_ACTION: HVACAction.PREHEATING, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], + }, + ) + await hass.async_block_till_done() + assert acc.char_current_fan_state.value == FAN_STATE_IDLE + + # Test DEFROSTING action maps to FAN_STATE_IDLE + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_HIGH], + ATTR_HVAC_ACTION: HVACAction.DEFROSTING, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], + }, + ) + await hass.async_block_till_done() + assert acc.char_current_fan_state.value == FAN_STATE_IDLE + + # Test other actions for comparison + hass.states.async_set( + entity_id, + HVACMode.HEAT, + { + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_HIGH], + ATTR_HVAC_ACTION: HVACAction.HEATING, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], + }, + ) + await hass.async_block_till_done() + assert acc.char_current_fan_state.value == FAN_STATE_ACTIVE + + hass.states.async_set( + entity_id, + HVACMode.OFF, + { + ATTR_SUPPORTED_FEATURES: ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + ATTR_FAN_MODES: [FAN_AUTO, FAN_LOW, FAN_HIGH], + ATTR_HVAC_ACTION: HVACAction.OFF, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_HVAC_MODES: [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF], + }, + ) + await hass.async_block_till_done() + assert acc.char_current_fan_state.value == FAN_STATE_INACTIVE + + async def test_thermostat_handles_unknown_state(hass: HomeAssistant, hk_driver) -> None: """Test a thermostat can handle unknown state.""" entity_id = "climate.test" From 6de2258325bfb22ef27d2da1f8d6a0cac607227b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 20:15:00 +0200 Subject: [PATCH 0804/1175] Mark device_tracker methods and properties as mandatory in pylint plugin (#145309) --- .../components/icloud/device_tracker.py | 14 +++++++---- .../components/mobile_app/device_tracker.py | 23 +++++++++++-------- .../components/owntracks/device_tracker.py | 20 ++++++++-------- .../components/starline/device_tracker.py | 12 ++++++---- pylint/plugins/hass_enforce_type_hints.py | 5 ++++ 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index e546d3034ae..2a4f6d81dc5 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.device_tracker import TrackerEntity from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -69,18 +69,24 @@ class IcloudTrackerEntity(TrackerEntity): self._attr_unique_id = device.unique_id @property - def location_accuracy(self): + def location_accuracy(self) -> float: """Return the location accuracy of the device.""" + if TYPE_CHECKING: + assert self._device.location is not None return self._device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY] @property - def latitude(self): + def latitude(self) -> float: """Return latitude value of the device.""" + if TYPE_CHECKING: + assert self._device.location is not None return self._device.location[DEVICE_LOCATION_LATITUDE] @property - def longitude(self): + def longitude(self) -> float: """Return longitude value of the device.""" + if TYPE_CHECKING: + assert self._device.location is not None return self._device.location[DEVICE_LOCATION_LONGITUDE] @property diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 7e5a0a291b6..707a0215f2f 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,5 +1,7 @@ """Device tracker for Mobile app.""" +from typing import Any + from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, @@ -15,6 +17,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, ) 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 AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -52,17 +55,17 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): self._dispatch_unsub = None @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID.""" return self._entry.data[ATTR_DEVICE_ID] @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._data.get(ATTR_BATTERY) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" attrs = {} for key in ATTR_KEYS: @@ -72,12 +75,12 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return attrs @property - def location_accuracy(self): + def location_accuracy(self) -> float: """Return the gps accuracy of the device.""" - return self._data.get(ATTR_GPS_ACCURACY) + return self._data.get(ATTR_GPS_ACCURACY, 0) @property - def latitude(self): + def latitude(self) -> float | None: """Return latitude value of the device.""" if (gps := self._data.get(ATTR_GPS)) is None: return None @@ -85,7 +88,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return gps[0] @property - def longitude(self): + def longitude(self) -> float | None: """Return longitude value of the device.""" if (gps := self._data.get(ATTR_GPS)) is None: return None @@ -93,19 +96,19 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return gps[1] @property - def location_name(self): + def location_name(self) -> str | None: """Return a location name for the current location of the device.""" if location_name := self._data.get(ATTR_LOCATION_NAME): return location_name return None @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._entry.data[ATTR_DEVICE_NAME] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return device_info(self._entry.data) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 80a06478506..22762cb390d 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,5 +1,7 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" +from typing import Any + from homeassistant.components.device_tracker import ( ATTR_SOURCE_TYPE, DOMAIN as DEVICE_TRACKER_DOMAIN, @@ -64,34 +66,34 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): _attr_has_entity_name = True _attr_name = None - def __init__(self, dev_id, data=None): + def __init__(self, dev_id: str, data: dict[str, Any] | None = None) -> None: """Set up OwnTracks entity.""" self._dev_id = dev_id self._data = data or {} self.entity_id = f"{DEVICE_TRACKER_DOMAIN}.{dev_id}" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID.""" return self._dev_id @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._data.get("battery") @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return device specific attributes.""" return self._data.get("attributes") @property - def location_accuracy(self): + def location_accuracy(self) -> float: """Return the gps accuracy of the device.""" - return self._data.get("gps_accuracy") + return self._data.get("gps_accuracy", 0) @property - def latitude(self): + def latitude(self) -> float | None: """Return latitude value of the device.""" # Check with "get" instead of "in" because value can be None if self._data.get("gps"): @@ -100,7 +102,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return None @property - def longitude(self): + def longitude(self) -> float | None: """Return longitude value of the device.""" # Check with "get" instead of "in" because value can be None if self._data.get("gps"): @@ -109,7 +111,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return None @property - def location_name(self): + def location_name(self) -> str | None: """Return a location name for the current location of the device.""" return self._data.get("location_name") diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 0c8418d28fc..d6e12b4ecd9 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,5 +1,7 @@ """StarLine device tracker.""" +from typing import Any + from homeassistant.components.device_tracker import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -35,26 +37,26 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): super().__init__(account, device, "location") @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific attributes.""" return self._account.gps_attrs(self._device) @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._device.battery_level @property - def location_accuracy(self): + def location_accuracy(self) -> float: """Return the gps accuracy of the device.""" return self._device.position.get("r", 0) @property - def latitude(self): + def latitude(self) -> float: """Return latitude value of the device.""" return self._device.position["x"] @property - def longitude(self): + def longitude(self) -> float: """Return longitude value of the device.""" return self._device.position["y"] diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 92f2473d3ee..a6d77611926 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1484,6 +1484,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="source_type", return_type="SourceType", + mandatory=True, ), ], ), @@ -1493,10 +1494,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="force_update", return_type="bool", + mandatory=True, ), TypeHintMatch( function_name="location_accuracy", return_type="float", + mandatory=True, ), TypeHintMatch( function_name="location_name", @@ -1534,10 +1537,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="state", return_type="str", + mandatory=True, ), TypeHintMatch( function_name="is_connected", return_type="bool", + mandatory=True, ), ], ), From 622ab922b5fcc76c218446ccbcfe7877104aa42e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 22 May 2025 21:09:28 +0200 Subject: [PATCH 0805/1175] Add configuration url to Immich device info (#145456) add configuration url to device info --- homeassistant/components/immich/coordinator.py | 5 +++++ homeassistant/components/immich/entity.py | 1 + 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/immich/coordinator.py b/homeassistant/components/immich/coordinator.py index e1904a62e24..2e89b0dae29 100644 --- a/homeassistant/components/immich/coordinator.py +++ b/homeassistant/components/immich/coordinator.py @@ -16,6 +16,7 @@ from aioimmich.server.models import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -48,6 +49,10 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]): """Initialize the data update coordinator.""" self.api = api self.is_admin = is_admin + self.configuration_url = ( + f"{'https' if entry.data[CONF_SSL] else 'http'}://" + f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + ) super().__init__( hass, _LOGGER, diff --git a/homeassistant/components/immich/entity.py b/homeassistant/components/immich/entity.py index f99f8872ce5..64ca11cca37 100644 --- a/homeassistant/components/immich/entity.py +++ b/homeassistant/components/immich/entity.py @@ -24,4 +24,5 @@ class ImmichEntity(CoordinatorEntity[ImmichDataUpdateCoordinator]): manufacturer="Immich", sw_version=coordinator.data.server_about.version, entry_type=DeviceEntryType.SERVICE, + configuration_url=coordinator.configuration_url, ) From c130a9f31c94fcf6ed25fdd38197fa0008194173 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 22 May 2025 21:12:37 +0200 Subject: [PATCH 0806/1175] Fix typo in reauth_confirm description of `metoffice` (#145458) --- homeassistant/components/metoffice/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/metoffice/strings.json b/homeassistant/components/metoffice/strings.json index b33cf9e3efc..d13e0b89f96 100644 --- a/homeassistant/components/metoffice/strings.json +++ b/homeassistant/components/metoffice/strings.json @@ -11,7 +11,7 @@ }, "reauth_confirm": { "title": "Reauthenticate with DataHub API", - "description": "Please re-enter you DataHub API key. If you are still using an old Datapoint API key, you need to sign up for DataHub API now, see [documentation]({docs_url}) for details.", + "description": "Please re-enter your DataHub API key. If you are still using an old Datapoint API key, you need to sign up for DataHub API now, see [documentation]({docs_url}) for details.", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } From 228beacca8470e1e676b56356da128bfb60eb68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 22 May 2025 20:20:57 +0100 Subject: [PATCH 0807/1175] Add default sensor data for Tesla Wall Connector tests (#145462) --- .../tesla_wall_connector/conftest.py | 18 +++++++++++++++++- .../tesla_wall_connector/test_binary_sensor.py | 2 -- .../tesla_wall_connector/test_init.py | 6 +++--- .../tesla_wall_connector/test_sensor.py | 13 ------------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/tests/components/tesla_wall_connector/conftest.py b/tests/components/tesla_wall_connector/conftest.py index e4499d6e308..4cb03f2bb1e 100644 --- a/tests/components/tesla_wall_connector/conftest.py +++ b/tests/components/tesla_wall_connector/conftest.py @@ -88,7 +88,23 @@ async def create_wall_connector_entry( def get_vitals_mock() -> Vitals: """Get mocked vitals object.""" - return MagicMock(auto_spec=Vitals) + mock = MagicMock(auto_spec=Vitals) + mock.evse_state = 1 + mock.handle_temp_c = 25.51 + mock.pcba_temp_c = 30.5 + mock.mcu_temp_c = 42.0 + mock.grid_v = 230.15 + mock.grid_hz = 50.021 + mock.voltageA_v = 230.1 + mock.voltageB_v = 231 + mock.voltageC_v = 232.1 + mock.currentA_a = 10 + mock.currentB_a = 11.1 + mock.currentC_a = 12 + mock.session_energy_wh = 1234.56 + mock.contactor_closed = False + mock.vehicle_connected = True + return mock def get_lifetime_mock() -> Lifetime: diff --git a/tests/components/tesla_wall_connector/test_binary_sensor.py b/tests/components/tesla_wall_connector/test_binary_sensor.py index 22100bbb1c1..3990369262d 100644 --- a/tests/components/tesla_wall_connector/test_binary_sensor.py +++ b/tests/components/tesla_wall_connector/test_binary_sensor.py @@ -23,8 +23,6 @@ async def test_sensors(hass: HomeAssistant) -> None: ] mock_vitals_first_update = get_vitals_mock() - mock_vitals_first_update.contactor_closed = False - mock_vitals_first_update.vehicle_connected = True mock_vitals_second_update = get_vitals_mock() mock_vitals_second_update.contactor_closed = True diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py index 2b37924b2e4..e16180c328a 100644 --- a/tests/components/tesla_wall_connector/test_init.py +++ b/tests/components/tesla_wall_connector/test_init.py @@ -5,13 +5,13 @@ from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .conftest import create_wall_connector_entry +from .conftest import create_wall_connector_entry, get_vitals_mock async def test_init_success(hass: HomeAssistant) -> None: """Test setup and that we get the device info, including firmware version.""" - entry = await create_wall_connector_entry(hass) + entry = await create_wall_connector_entry(hass, vitals_data=get_vitals_mock()) assert entry.state is ConfigEntryState.LOADED @@ -28,7 +28,7 @@ async def test_init_while_offline(hass: HomeAssistant) -> None: async def test_load_unload(hass: HomeAssistant) -> None: """Config entry can be unloaded.""" - entry = await create_wall_connector_entry(hass) + entry = await create_wall_connector_entry(hass, vitals_data=get_vitals_mock()) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index 62eca46c388..c6c93006896 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -59,19 +59,6 @@ async def test_sensors(hass: HomeAssistant) -> None: ] mock_vitals_first_update = get_vitals_mock() - mock_vitals_first_update.evse_state = 1 - mock_vitals_first_update.handle_temp_c = 25.51 - mock_vitals_first_update.pcba_temp_c = 30.5 - mock_vitals_first_update.mcu_temp_c = 42.0 - mock_vitals_first_update.grid_v = 230.15 - mock_vitals_first_update.grid_hz = 50.021 - mock_vitals_first_update.voltageA_v = 230.1 - mock_vitals_first_update.voltageB_v = 231 - mock_vitals_first_update.voltageC_v = 232.1 - mock_vitals_first_update.currentA_a = 10 - mock_vitals_first_update.currentB_a = 11.1 - mock_vitals_first_update.currentC_a = 12 - mock_vitals_first_update.session_energy_wh = 1234.56 mock_vitals_second_update = get_vitals_mock() mock_vitals_second_update.evse_state = 3 From 4ad34c57b5ffbe80f5ca44f50f4e31ebbf1979fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 22 May 2025 20:22:09 +0100 Subject: [PATCH 0808/1175] Replace empty mock in GoalZero tests (#145463) --- tests/components/goalzero/test_init.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index 4817be1ce35..95f468a93fe 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -12,18 +12,17 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util -from . import CONF_DATA, async_init_integration, create_entry, create_mocked_yeti +from . import CONF_DATA, async_init_integration, create_entry from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker -async def test_setup_config_and_unload(hass: HomeAssistant) -> None: +async def test_setup_config_and_unload( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test Goal Zero setup and unload.""" - entry = create_entry(hass) - mocked_yeti = await create_mocked_yeti() - with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): - await hass.config_entries.async_setup(entry.entry_id) + entry = await async_init_integration(hass, aioclient_mock) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -37,14 +36,12 @@ async def test_setup_config_and_unload(hass: HomeAssistant) -> None: async def test_setup_config_entry_incorrectly_formatted_mac( - hass: HomeAssistant, + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the mac address formatting is corrected.""" - entry = create_entry(hass) + entry = await async_init_integration(hass, aioclient_mock, skip_setup=True) hass.config_entries.async_update_entry(entry, unique_id="AABBCCDDEEFF") - mocked_yeti = await create_mocked_yeti() - with patch("homeassistant.components.goalzero.Yeti", return_value=mocked_yeti): - await hass.config_entries.async_setup(entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.state is ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 From b532776d78f118e89d757bb1e9bf12c5f74c6235 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 22 May 2025 14:49:39 -0500 Subject: [PATCH 0809/1175] Make Powerwall energy sensors TOTAL_INCREASING to fix hardware swaps (#145165) --- homeassistant/components/powerwall/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index f242d2c67e6..b44fea05638 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -314,7 +314,7 @@ class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = SensorStateClass.TOTAL + _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY From a15572bb8c2a6f5752a358ab34b152ef0a46db7a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 13:22:20 -0700 Subject: [PATCH 0810/1175] Bump opower to 0.12.1 (#145464) --- 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 a09405f1ca8..beaf63ad59d 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.12.0"] + "requirements": ["opower==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8950d602f08..85867db5b8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.0 +opower==0.12.1 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6771a84d143..fff5121692f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.0 +opower==0.12.1 # homeassistant.components.oralb oralb-ble==0.17.6 From 2f318927bcf6d26912fd6e25d05b93addb0d82ec Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 22 May 2025 23:10:49 +0200 Subject: [PATCH 0811/1175] Add pending damage and pending quest items sensors (#145449) Add pending damage and quest items sensors --- homeassistant/components/habitica/icons.json | 6 ++ homeassistant/components/habitica/sensor.py | 22 +++- .../components/habitica/strings.json | 8 ++ homeassistant/components/habitica/util.py | 22 ++++ .../habitica/snapshots/test_sensor.ambr | 100 ++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index aac90814af5..d241d3855d6 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -159,6 +159,12 @@ }, "quest_scrolls": { "default": "mdi:script-text-outline" + }, + "pending_damage": { + "default": "mdi:sword" + }, + "pending_quest_items": { + "default": "mdi:sack" } }, "switch": { diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index e715dd6d07b..5b64d0d8119 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -40,7 +40,13 @@ from homeassistant.util import dt as dt_util from .const import ASSETS_URL, DOMAIN from .coordinator import HabiticaConfigEntry, HabiticaDataUpdateCoordinator from .entity import HabiticaBase -from .util import get_attribute_points, get_attributes_total, inventory_list +from .util import ( + get_attribute_points, + get_attributes_total, + inventory_list, + pending_damage, + pending_quest_items, +) _LOGGER = logging.getLogger(__name__) @@ -99,6 +105,8 @@ class HabiticaSensorEntity(StrEnum): FOOD_TOTAL = "food_total" SADDLE = "saddle" QUEST_SCROLLS = "quest_scrolls" + PENDING_DAMAGE = "pending_damage" + PENDING_QUEST_ITEMS = "pending_quest_items" SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = ( @@ -263,6 +271,18 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = ( entity_picture="inventory_quest_scroll_dustbunnies.png", attributes_fn=lambda user, content: inventory_list(user, content, "quests"), ), + HabiticaSensorEntityDescription( + key=HabiticaSensorEntity.PENDING_DAMAGE, + translation_key=HabiticaSensorEntity.PENDING_DAMAGE, + value_fn=pending_damage, + suggested_display_precision=1, + entity_picture=ha.DAMAGE, + ), + HabiticaSensorEntityDescription( + key=HabiticaSensorEntity.PENDING_QUEST_ITEMS, + translation_key=HabiticaSensorEntity.PENDING_QUEST_ITEMS, + value_fn=pending_quest_items, + ), ) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 5b03d8662cb..22bc79555e8 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -426,6 +426,14 @@ "quest_scrolls": { "name": "Quest scrolls", "unit_of_measurement": "scrolls" + }, + "pending_damage": { + "name": "Pending damage", + "unit_of_measurement": "damage" + }, + "pending_quest_items": { + "name": "Pending quest items", + "unit_of_measurement": "items" } }, "switch": { diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 1ca908eb3ff..9ef0b8cbadd 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -162,3 +162,25 @@ def inventory_list( for k, v in getattr(user.items, item_type, {}).items() if k != "Saddle" } + + +def pending_quest_items(user: UserData, content: ContentData) -> int | None: + """Pending quest items.""" + + return ( + user.party.quest.progress.collectedItems + if user.party.quest.key + and content.quests[user.party.quest.key].collect is not None + else None + ) + + +def pending_damage(user: UserData, content: ContentData) -> float | None: + """Pending damage.""" + + return ( + user.party.quest.progress.up + if user.party.quest.key + and content.quests[user.party.quest.key].boss is not None + else None + ) diff --git a/tests/components/habitica/snapshots/test_sensor.ambr b/tests/components/habitica/snapshots/test_sensor.ambr index 1fbc9eca595..b5b1009a73f 100644 --- a/tests/components/habitica/snapshots/test_sensor.ambr +++ b/tests/components/habitica/snapshots/test_sensor.ambr @@ -1038,6 +1038,106 @@ 'state': '880', }) # --- +# name: test_sensors[sensor.test_user_pending_damage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_pending_damage', + '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': 'Pending damage', + 'platform': 'habitica', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_damage', + 'unit_of_measurement': 'damage', + }) +# --- +# name: test_sensors[sensor.test_user_pending_damage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'entity_picture': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSItMyAtMyAyMiAyMiI+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBpZD0iYSIgZD0iTTEwLjQ2NCAyLjkxN0w4LjIgNS4xOTd2Mi4wMmwyLjI2NC0yLjE3M1YyLjkxN3oiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTYgLjMyKSI+CiAgICAgICAgPHBhdGggZmlsbD0iI0YwNjE2NiIgZD0iTTYuMTMgOS4yMDRsMi4xMTEuOTM0Yy4xNzYuMDc4LjI5LjIzNS4zMzMuNDE1LjA3My4zMDQuMjk1IDEuMDEuMzEzIDEuMzg2LjAxLjIxLS4yMTQuMzU2LS40MTQuMjdsLTMuNTI5LTEuNjIzYS41ODIuNTgyIDAgMCAxLS4yNTQtLjI0NEwzIDYuOTU1Yy0uMDktLjE5Mi4wNjMtLjQwNy4yODEtLjM5Ny4zOTEuMDE3IDEuMTEyLjIxOCAxLjQ0NC4zLjE4Ni4wNDUuMzUxLjE1LjQzMi4zMTlsLjk3MyAyLjAyN3oiPjwvcGF0aD4KICAgICAgICA8cGF0aCBmaWxsPSIjODc4MTkwIiBkPSJNMS4wMjQgMTQuMTA3bC45MS44NzUgMi4zNjMtLjE3OS4xMjEtMS40OSAxLjM1Ni0xLjMgMi40NjcgMS4xMjYgMS44NDYtLjQ3Ny0uNzc0LTMuMTk2IDUuMTcxLTQuNjMzLjk5LTQuNTk2aC0uMDAyVi4yMzVsLTQuNzg2Ljk1TDUuODYgNi4xNWwtMy4zMy0uNzQzLS40OTcgMS43NyAxLjE3NCAyLjM3LTEuMzU1IDEuMy0xLjU1Mi4xMTgtLjE4NiAyLjI2Ny45MS44NzV6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0YwNjE2NiIgZD0iTTIuOTc2IDEzLjM2NmwtMS4xODItMS4xMzQgMi45MjMtMi44MDUgMS4xOCAxLjEzNHoiPjwvcGF0aD4KICAgICAgICA8cGF0aCBmaWxsPSIjRjA2MTY2IiBkPSJNMS4xMjYgMTIuODc0bC4wODUtMS4wMzUgMS4wNzgtLjA4MiAxLjE4MiAxLjEzNS0uMDg1IDEuMDM1LTEuMDc4LjA4MnoiPjwvcGF0aD4KICAgICAgICA8cGF0aCBmaWxsPSIjRkZGIiBkPSJNMTEuMzEyIDIuMDg4bC4xIDIuMDQ2IDIuNzAyLTIuNTk1Yy0uMDUtLjA0NS0yLjA4Ni4xOC0yLjgwMi41NSI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNFREVDRUUiIGQ9Ik0xMS4yNjIgMi4xMTNMNS41NTMgNy44NjJsMS40NjMuNDkyIDQuMzk2LTQuMjItLjEtMi4wNDYtLjA1LjAyNU01LjU1MyA3Ljg2MmwtLjA1LjA1Mi42MjIgMS4yOTQuODktLjg1NC0xLjQ2Mi0uNDkyeiI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNFREVDRUUiIGQ9Ik0xMy41NDEgNC4yM2wtMi4xMy0uMDk2IDIuNzAzLTIuNTk0Yy4wNDYuMDQ4LS4xODkgMi4wMDMtLjU3MyAyLjY5Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0UxRTBFMyIgZD0iTTEzLjUxNiA0LjI3OGwtNS45ODkgNS40OC0uNTEyLTEuNDA0IDQuMzk2LTQuMjIgMi4xMy4wOTYtLjAyNS4wNDhNNy41MjcgOS43NThsLS4wNTQuMDQ4LTEuMzQ4LS41OTcuODktLjg1NC41MTIgMS40MDN6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0Q1QzhGRiIgZD0iTTIuMjg5IDExLjc1N2wtLjI1Ljg3OC0uODI5LS43OTZ6TTMuNDY5IDEyLjg5bC0uOTE0LjI0LjgyOC43OTV6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0JEQThGRiIgZD0iTTIuMjg5IDExLjc1N2wxLjE4MiAxLjEzNS0uOTE2LjIzNy0uNTE2LS40OTR6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0JEQThGRiIgZD0iTTEuMTI3IDEyLjg3NmwuOTE0LS4yNC0uODI4LS43OTZ6TTIuMzA3IDE0LjAwOGwuMjUtLjg3Ny44MjkuNzk2eiI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNENUM4RkYiIGQ9Ik0xLjEyNyAxMi44NzZMMi4zMSAxNC4wMWwuMjQ3LS44OC0uNTE2LS40OTV6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0IzNjIxMyIgZD0iTTQuOSAxMS41MjNsLTEuMTg0LTEuMTM3LjcxNS0uNjg1IDEuMTg0IDEuMTM2eiI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNFMzhGM0QiIGQ9Ik00LjE4NyAxMi4yMDhsLTEuMTg0LTEuMTM2LjcxNC0uNjg1TDQuOSAxMS41MjN6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0IzNjIxMyIgZD0iTTMuNDczIDEyLjg5NGwtMS4xODQtMS4xMzcuNzE0LS42ODUgMS4xODQgMS4xMzZ6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0MzQzBDNyIgZD0iTTYuMTMyIDkuMjA1bC0uOTc0LTIuMDI3YS41MjYuNTI2IDAgMCAwLS4xNTMtLjE4NS43MTguNzE4IDAgMCAwLS4yNzktLjEzNWMtLjMzMS0uMDgtMS4wNTItLjI4Mi0xLjQ0My0uM2EuMjk1LjI5NSAwIDAgMC0uMjQyLjEwOEw0LjQ2IDcuODI5IDUuNTAzIDkuODFsLjYzLS42MDVoLS4wMDF6Ij48L3BhdGg+CiAgICAgICAgPHBhdGggZmlsbD0iI0E1QTFBQyIgZD0iTTQuNDYgNy44MjlMMy4wNCA2LjY2NmEuMjcuMjcgMCAwIDAtLjAzOS4yOWwxLjY5IDMuMzg3Yy4wMjkuMDUzLjA2Ni4xLjExLjE0MmwuNzAyLS42NzVMNC40NiA3LjgzeiI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNDM0MwQzciIGQ9Ik04Ljg5MSAxMS45NGMtLjAxOC0uMzc1LS4yMjgtMS4wNjgtLjMxMi0xLjM4NWEuNjY4LjY2OCAwIDAgMC0uMTQtLjI2OC41NC41NCAwIDAgMC0uMTkzLS4xNDdsLTIuMTExLS45MzV2LS4wMDJsLS42MzEuNjA2IDIuMDY0IDEuMDAxIDEuMjExIDEuMzYzYS4yNzUuMjc1IDAgMCAwIC4xMTItLjIzMyI+PC9wYXRoPgogICAgICAgIDxwYXRoIGZpbGw9IiNBNUExQUMiIGQ9Ik03LjU2OCAxMC44MWwxLjIxMSAxLjM2M2EuMy4zIDAgMCAxLS4zMDEuMDM3bC0zLjUzLTEuNjIyYS41ODguNTg4IDAgMCAxLS4xNDctLjEwNWwuNzAzLS42NzQgMi4wNjQgMS4wMDF6Ij48L3BhdGg+CiAgICAgICAgPG1hc2sgaWQ9ImIiIGZpbGw9IiNmZmYiPgogICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNhIj48L3VzZT4KICAgICAgICA8L21hc2s+CiAgICAgICAgPHBhdGggZmlsbD0iI0ZGRiIgZD0iTTkuODI0IDcuNDVoLjMyOVYxLjg2NWgtLjMyOXpNOC4yIDguODYyaC45NzRWMy4yNzdIOC4yeiIgbWFzaz0idXJsKCNiKSI+PC9wYXRoPgogICAgPC9nPgo8L3N2Zz4=', + 'friendly_name': 'test-user Pending damage', + 'unit_of_measurement': 'damage', + }), + 'context': , + 'entity_id': 'sensor.test_user_pending_damage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.test_user_pending_quest_items-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_user_pending_quest_items', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pending quest items', + 'platform': 'habitica', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_quest_items', + 'unit_of_measurement': 'items', + }) +# --- +# name: test_sensors[sensor.test_user_pending_quest_items-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'test-user Pending quest items', + 'unit_of_measurement': 'items', + }), + 'context': , + 'entity_id': 'sensor.test_user_pending_quest_items', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensors[sensor.test_user_perception-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 8561721fafc1adafb1dfdbcb2d25108e6fe4eb31 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 22 May 2025 23:15:21 +0200 Subject: [PATCH 0812/1175] Add pytest/codecov to forbidden runtime dependencies (#145447) Add pytest/codecov to forbidden runtime packages --- script/hassfest/requirements.py | 143 ++++++++++++++++++++++++++++---- 1 file changed, 126 insertions(+), 17 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index e183a87d9eb..c55125dfe91 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -41,22 +41,116 @@ PACKAGE_REGEX = re.compile( PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -FORBIDDEN_PACKAGES = {"setuptools", "wheel"} -FORBIDDEN_PACKAGE_EXCEPTIONS = { - # Direct dependencies - "fitbit", # setuptools (fitbit) - "influxdb-client", # setuptools (influxdb) - "microbeespy", # setuptools (microbees) - "pyefergy", # types-pytz (efergy) - "python-mystrom", # setuptools (mystrom) - # Transitive dependencies - "arrow", # types-python-dateutil (opower) - "asyncio-dgram", # setuptools (guardian / keba / minecraft_server) - "colorzero", # setuptools (remote_rpi_gpio / zha) - "incremental", # setuptools (azure_devops / lyric / ovo_energy / system_bridge) - "pbr", # setuptools (cmus / concord232 / mochad / nx584 / opnsense) - "pycountry-convert", # wheel (ecovacs) - "unasync", # setuptools (hive / osoenergy) +FORBIDDEN_PACKAGES = {"codecov", "pytest", "setuptools", "wheel"} +FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { + # In the form dict("domain": {"package": {"reason1", "reason2"}}) + # - domain is the integration domain + # - package is the package (can be transitive) referencing the dependency + # - reasonX should be the name of the invalid dependency + "azure_devops": { + # aioazuredevops > incremental > setuptools + "incremental": {"setuptools"} + }, + "cmus": { + # pycmus > pbr > setuptools + "pbr": {"setuptools"} + }, + "concord232": { + # concord232 > stevedore > pbr > setuptools + "pbr": {"setuptools"} + }, + "ecovacs": { + # py-sucks > pycountry-convert > pytest-cov > pytest + "pytest-cov": {"pytest", "wheel"}, + # py-sucks > pycountry-convert > pytest-mock > pytest + "pytest-mock": {"pytest", "wheel"}, + # py-sucks > pycountry-convert > pytest + # py-sucks > pycountry-convert > wheel + "pycountry-convert": {"pytest", "wheel"}, + }, + "efergy": { + # pyefergy > codecov + # pyefergy > types-pytz + "pyefergy": {"codecov", "types-pytz"} + }, + "fitbit": { + # fitbit > setuptools + "fitbit": {"setuptools"} + }, + "guardian": { + # aioguardian > asyncio-dgram > setuptools + "asyncio-dgram": {"setuptools"} + }, + "hive": { + # pyhive-integration > unasync > setuptools + "unasync": {"setuptools"} + }, + "influxdb": { + # influxdb-client > setuptools + "influxdb-client": {"setuptools"} + }, + "keba": { + # keba-kecontact > asyncio-dgram > setuptools + "asyncio-dgram": {"setuptools"} + }, + "lyric": { + # aiolyric > incremental > setuptools + "incremental": {"setuptools"} + }, + "microbees": { + # microbeespy > setuptools + "microbeespy": {"setuptools"} + }, + "minecraft_server": { + # mcstatus > asyncio-dgram > setuptools + "asyncio-dgram": {"setuptools"} + }, + "mochad": { + # pymochad > pbr > setuptools + "pbr": {"setuptools"} + }, + "mystrom": { + # python-mystrom > setuptools + "python-mystrom": {"setuptools"} + }, + "nx584": { + # pynx584 > stevedore > pbr > setuptools + "pbr": {"setuptools"} + }, + "opnsense": { + # pyopnsense > pbr > setuptools + "pbr": {"setuptools"} + }, + "opower": { + # opower > arrow > types-python-dateutil + "arrow": {"types-python-dateutil"} + }, + "osoenergy": { + # pyosoenergyapi > unasync > setuptools + "unasync": {"setuptools"} + }, + "ovo_energy": { + # ovoenergy > incremental > setuptools + "incremental": {"setuptools"} + }, + "remote_rpi_gpio": { + # gpiozero > colorzero > setuptools + "colorzero": {"setuptools"} + }, + "system_bridge": { + # systembridgeconnector > incremental > setuptools + "incremental": {"setuptools"} + }, + "travisci": { + # travisci > pytest-rerunfailures > pytest + "pytest-rerunfailures": {"pytest"}, + # travisci > pytest + "travispy": {"pytest"}, + }, + "zha": { + # zha > zigpy-zigate > gpiozero > colorzero > setuptools + "colorzero": {"setuptools"} + }, } @@ -219,6 +313,11 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: to_check = deque(packages) + forbidden_package_exceptions = FORBIDDEN_PACKAGE_EXCEPTIONS.get( + integration.domain, {} + ) + needs_forbidden_package_exceptions = False + while to_check: package = to_check.popleft() @@ -228,6 +327,8 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: all_requirements.add(package) item = deptree.get(package) + if forbidden_package_exceptions: + print(f"Integration {integration.domain}: {item}") if item is None: # Only warn if direct dependencies could not be resolved @@ -238,9 +339,11 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: continue dependencies: dict[str, str] = item["dependencies"] + package_exceptions = forbidden_package_exceptions.get(package, set()) for pkg, version in dependencies.items(): if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES: - if package in FORBIDDEN_PACKAGE_EXCEPTIONS: + needs_forbidden_package_exceptions = True + if pkg in package_exceptions: integration.add_warning( "requirements", f"Package {pkg} should not be a runtime dependency in {package}", @@ -254,6 +357,12 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: to_check.extend(dependencies) + if forbidden_package_exceptions and not needs_forbidden_package_exceptions: + integration.add_error( + "requirements", + f"Integration {integration.domain} runtime dependency exceptions " + "have been resolved, please remove from `FORBIDDEN_PACKAGE_EXCEPTIONS`", + ) return all_requirements From 61248c561d058c77cd88116ecd24c997ee4bbf93 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 22:01:48 -0700 Subject: [PATCH 0813/1175] Fix strings related to Google search tool in Google AI (#145480) --- .../components/google_generative_ai_conversation/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/strings.json b/homeassistant/components/google_generative_ai_conversation/strings.json index 2697f30eda0..a57e2f78f53 100644 --- a/homeassistant/components/google_generative_ai_conversation/strings.json +++ b/homeassistant/components/google_generative_ai_conversation/strings.json @@ -41,12 +41,12 @@ }, "data_description": { "prompt": "Instruct how the LLM should respond. This can be a template.", - "enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"." + "enable_google_search_tool": "Only works if there is nothing selected in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"." } } }, "error": { - "invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"." + "invalid_google_search_option": "Google Search can only be enabled if nothing is selected in the \"Control Home Assistant\" setting." } }, "services": { From e13abf20340a1d8192589b3a7e84ff606a3444b9 Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 22 May 2025 22:02:30 -0700 Subject: [PATCH 0814/1175] Make Gemma models work in Google AI (#145479) * Make Gemma models work in Google AI * move one line to be improve readability --- .../google_generative_ai_conversation/config_flow.py | 6 +++--- .../google_generative_ai_conversation/conversation.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_generative_ai_conversation/config_flow.py b/homeassistant/components/google_generative_ai_conversation/config_flow.py index 551f9b0c9de..ae0f09b1037 100644 --- a/homeassistant/components/google_generative_ai_conversation/config_flow.py +++ b/homeassistant/components/google_generative_ai_conversation/config_flow.py @@ -254,11 +254,11 @@ async def google_generative_ai_config_option_schema( ) for api_model in sorted(api_models, key=lambda x: x.display_name or "") if ( - api_model.name != "models/gemini-1.0-pro" # duplicate of gemini-pro - and api_model.display_name + api_model.display_name and api_model.name - and api_model.supported_actions + and "tts" not in api_model.name and "vision" not in api_model.name + and api_model.supported_actions and "generateContent" in api_model.supported_actions ) ] diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index 73a82b98664..c642bfd94e6 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -319,11 +319,10 @@ class GoogleGenerativeAIConversationEntity( tools.append(Tool(google_search=GoogleSearch())) model_name = self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL) - # Gemini 1.0 doesn't support system_instruction while 1.5 does. - # Assume future versions will support it (if not, the request fails with a - # clear message at which point we can fix). + # Avoid INVALID_ARGUMENT Developer instruction is not enabled for supports_system_instruction = ( - "gemini-1.0" not in model_name and "gemini-pro" not in model_name + "gemma" not in model_name + and "gemini-2.0-flash-preview-image-generation" not in model_name ) prompt_content = cast( From 19345b0e18d266c6395859879052e9a4d9bd27cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 23 May 2025 08:00:35 +0200 Subject: [PATCH 0815/1175] Prefer to create backups in local storage if selected (#145331) --- homeassistant/components/hassio/backup.py | 31 +++++-- tests/components/hassio/test_backup.py | 106 +++++++++++++++------- 2 files changed, 98 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index 950ea910d0c..46e3d0d3c98 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -297,10 +297,17 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): # It's inefficient to let core do all the copying so we want to let # supervisor handle as much as possible. # Therefore, we split the locations into two lists: encrypted and decrypted. - # The longest list will be sent to supervisor, and the remaining locations - # will be handled by async_upload_backup. - # If the lists are the same length, it does not matter which one we send, - # we send the encrypted list to have a well defined behavior. + # The backup will be created in the first location in the list sent to + # supervisor, and if that location is not available, the backup will + # fail. + # To make it less likely that the backup fails, we prefer to create the + # backup in the local storage location if included in the list of + # locations. + # Hence, we send the list of locations to supervisor in this priority order: + # 1. The list which has local storage + # 2. The longest list of locations + # 3. The list of encrypted locations + # In any case the remaining locations will be handled by async_upload_backup. encrypted_locations: list[str] = [] decrypted_locations: list[str] = [] agents_settings = manager.config.data.agents @@ -315,16 +322,26 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): encrypted_locations.append(hassio_agent.location) else: decrypted_locations.append(hassio_agent.location) + locations = [] + if LOCATION_LOCAL_STORAGE in decrypted_locations: + locations = decrypted_locations + password = None + # Move local storage to the front of the list + decrypted_locations.remove(LOCATION_LOCAL_STORAGE) + decrypted_locations.insert(0, LOCATION_LOCAL_STORAGE) + elif LOCATION_LOCAL_STORAGE in encrypted_locations: + locations = encrypted_locations + # Move local storage to the front of the list + encrypted_locations.remove(LOCATION_LOCAL_STORAGE) + encrypted_locations.insert(0, LOCATION_LOCAL_STORAGE) _LOGGER.debug("Encrypted locations: %s", encrypted_locations) _LOGGER.debug("Decrypted locations: %s", decrypted_locations) - if hassio_agents: + if not locations and hassio_agents: if len(encrypted_locations) >= len(decrypted_locations): locations = encrypted_locations else: locations = decrypted_locations password = None - else: - locations = [] locations = locations or [LOCATION_CLOUD_BACKUP] date = dt_util.now().isoformat() diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index 9065fb55bd2..e232a57d4e4 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -1300,6 +1300,16 @@ async def test_reader_writer_create_job_done( False, [], ), + # LOCATION_LOCAL_STORAGE should be moved to the front of the list + ( + [], + None, + ["hassio.share1", "hassio.local", "hassio.share2", "hassio.share3"], + None, + [LOCATION_LOCAL_STORAGE, "share1", "share2", "share3"], + False, + [], + ), ( [], "hunter2", @@ -1309,54 +1319,86 @@ async def test_reader_writer_create_job_done( True, [], ), + # LOCATION_LOCAL_STORAGE should be moved to the front of the list ( - [ - { - "type": "backup/config/update", - "agents": { - "hassio.local": {"protected": False}, - }, - } - ], + [], "hunter2", - ["hassio.local", "hassio.share1", "hassio.share2", "hassio.share3"], + ["hassio.share1", "hassio.local", "hassio.share2", "hassio.share3"], "hunter2", - ["share1", "share2", "share3"], + [LOCATION_LOCAL_STORAGE, "share1", "share2", "share3"], True, - [LOCATION_LOCAL_STORAGE], + [], ), + # Prefer the list of locations which has LOCATION_LOCAL_STORAGE ( [ { "type": "backup/config/update", "agents": { "hassio.local": {"protected": False}, - "hassio.share1": {"protected": False}, - }, - } - ], - "hunter2", - ["hassio.local", "hassio.share1", "hassio.share2", "hassio.share3"], - "hunter2", - ["share2", "share3"], - True, - [LOCATION_LOCAL_STORAGE, "share1"], - ), - ( - [ - { - "type": "backup/config/update", - "agents": { - "hassio.local": {"protected": False}, - "hassio.share1": {"protected": False}, - "hassio.share2": {"protected": False}, }, } ], "hunter2", ["hassio.local", "hassio.share1", "hassio.share2", "hassio.share3"], None, - [LOCATION_LOCAL_STORAGE, "share1", "share2"], + [LOCATION_LOCAL_STORAGE], + True, + ["share1", "share2", "share3"], + ), + # If the list of locations does not have LOCATION_LOCAL_STORAGE, send the + # longest list + ( + [ + { + "type": "backup/config/update", + "agents": { + "hassio.share0": {"protected": False}, + }, + } + ], + "hunter2", + ["hassio.share0", "hassio.share1", "hassio.share2", "hassio.share3"], + "hunter2", + ["share1", "share2", "share3"], + True, + ["share0"], + ), + # Prefer the list of encrypted locations if the lists are the same length + ( + [ + { + "type": "backup/config/update", + "agents": { + "hassio.share0": {"protected": False}, + "hassio.share1": {"protected": False}, + }, + } + ], + "hunter2", + ["hassio.share0", "hassio.share1", "hassio.share2", "hassio.share3"], + "hunter2", + ["share2", "share3"], + True, + ["share0", "share1"], + ), + # If the list of locations does not have LOCATION_LOCAL_STORAGE, send the + # longest list + ( + [ + { + "type": "backup/config/update", + "agents": { + "hassio.share0": {"protected": False}, + "hassio.share1": {"protected": False}, + "hassio.share2": {"protected": False}, + }, + } + ], + "hunter2", + ["hassio.share0", "hassio.share1", "hassio.share2", "hassio.share3"], + None, + ["share0", "share1", "share2"], True, ["share3"], ), @@ -1407,7 +1449,7 @@ async def test_reader_writer_create_per_agent_encryption( server=f"share{i}", type=supervisor_mounts.MountType.CIFS, ) - for i in range(1, 4) + for i in range(4) ], ) supervisor_client.backups.partial_backup.return_value.job_id = UUID(TEST_JOB_ID) From c3d318ff515ddfb38b79366d3ddd63de812705a0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 23 May 2025 08:08:44 +0200 Subject: [PATCH 0816/1175] Add paperless-ngx to strict typing (#145466) --- .strict-typing | 1 + .../components/paperless_ngx/quality_scale.yaml | 2 +- mypy.ini | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.strict-typing b/.strict-typing index 1ae56cd74d8..7cd54374616 100644 --- a/.strict-typing +++ b/.strict-typing @@ -386,6 +386,7 @@ homeassistant.components.overseerr.* homeassistant.components.p1_monitor.* homeassistant.components.pandora.* homeassistant.components.panel_custom.* +homeassistant.components.paperless_ngx.* homeassistant.components.peblar.* homeassistant.components.peco.* homeassistant.components.pegel_online.* diff --git a/homeassistant/components/paperless_ngx/quality_scale.yaml b/homeassistant/components/paperless_ngx/quality_scale.yaml index fc7ecb1668c..1c4e9c4efdf 100644 --- a/homeassistant/components/paperless_ngx/quality_scale.yaml +++ b/homeassistant/components/paperless_ngx/quality_scale.yaml @@ -75,4 +75,4 @@ rules: # Platinum async-dependency: done inject-websession: done - strict-typing: todo + strict-typing: done diff --git a/mypy.ini b/mypy.ini index cf3314f515c..f09e68bdcbe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3616,6 +3616,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.paperless_ngx.*] +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.peblar.*] check_untyped_defs = true disallow_incomplete_defs = true From 3f99a0bb6577f639713a2d5433e5d074f46c525c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 23 May 2025 08:09:54 +0200 Subject: [PATCH 0817/1175] Add diagnostics to Paperless-ngx (#145465) * Add diagnostics to Paperless-ngx * Add diagnostics to Paperless-ngx --- .../components/paperless_ngx/diagnostics.py | 18 ++++++++++++ .../paperless_ngx/quality_scale.yaml | 2 +- .../snapshots/test_diagnostics.ambr | 29 +++++++++++++++++++ .../paperless_ngx/snapshots/test_sensor.ambr | 24 +++++++-------- .../paperless_ngx/test_diagnostics.py | 28 ++++++++++++++++++ tests/components/paperless_ngx/test_sensor.py | 2 +- 6 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/paperless_ngx/diagnostics.py create mode 100644 tests/components/paperless_ngx/snapshots/test_diagnostics.ambr create mode 100644 tests/components/paperless_ngx/test_diagnostics.py diff --git a/homeassistant/components/paperless_ngx/diagnostics.py b/homeassistant/components/paperless_ngx/diagnostics.py new file mode 100644 index 00000000000..3f8351c6dca --- /dev/null +++ b/homeassistant/components/paperless_ngx/diagnostics.py @@ -0,0 +1,18 @@ +"""Diagnostics support for Paperless-ngx.""" + +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.core import HomeAssistant + +from . import PaperlessConfigEntry + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + entry: PaperlessConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return {"data": asdict(entry.runtime_data.data)} diff --git a/homeassistant/components/paperless_ngx/quality_scale.yaml b/homeassistant/components/paperless_ngx/quality_scale.yaml index 1c4e9c4efdf..31fdc781c2e 100644 --- a/homeassistant/components/paperless_ngx/quality_scale.yaml +++ b/homeassistant/components/paperless_ngx/quality_scale.yaml @@ -43,7 +43,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: Paperless does not support discovery. diff --git a/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr b/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..77adafd31f6 --- /dev/null +++ b/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr @@ -0,0 +1,29 @@ +# serializer version: 1 +# name: test_config_entry_diagnostics + dict({ + 'data': dict({ + 'character_count': 99999, + 'correspondent_count': 99, + 'current_asn': 99, + 'document_file_type_counts': list([ + dict({ + 'mime_type': 'application/pdf', + 'mime_type_count': 998, + }), + dict({ + 'mime_type': 'image/png', + 'mime_type_count': 1, + }), + ]), + 'document_type_count': 99, + 'documents_inbox': 9, + 'documents_total': 999, + 'inbox_tag': 9, + 'inbox_tags': list([ + 9, + ]), + 'storage_path_count': 9, + 'tag_count': 99, + }), + }) +# --- diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index 630db313d12..ccd48ff8c09 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-entry] +# name: test_sensor_platform[sensor.paperless_ngx_correspondents-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -35,7 +35,7 @@ 'unit_of_measurement': 'correspondents', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_correspondents-state] +# name: test_sensor_platform[sensor.paperless_ngx_correspondents-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Correspondents', @@ -50,7 +50,7 @@ 'state': '99', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_document_types-entry] +# name: test_sensor_platform[sensor.paperless_ngx_document_types-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -86,7 +86,7 @@ 'unit_of_measurement': 'document types', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_document_types-state] +# name: test_sensor_platform[sensor.paperless_ngx_document_types-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Document types', @@ -101,7 +101,7 @@ 'state': '99', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-entry] +# name: test_sensor_platform[sensor.paperless_ngx_documents_in_inbox-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -137,7 +137,7 @@ 'unit_of_measurement': 'documents', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_documents_in_inbox-state] +# name: test_sensor_platform[sensor.paperless_ngx_documents_in_inbox-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Documents in inbox', @@ -152,7 +152,7 @@ 'state': '9', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_tags-entry] +# name: test_sensor_platform[sensor.paperless_ngx_tags-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -188,7 +188,7 @@ 'unit_of_measurement': 'tags', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_tags-state] +# name: test_sensor_platform[sensor.paperless_ngx_tags-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Tags', @@ -203,7 +203,7 @@ 'state': '99', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-entry] +# name: test_sensor_platform[sensor.paperless_ngx_total_characters-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -239,7 +239,7 @@ 'unit_of_measurement': 'characters', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_total_characters-state] +# name: test_sensor_platform[sensor.paperless_ngx_total_characters-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Total characters', @@ -254,7 +254,7 @@ 'state': '99999', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-entry] +# name: test_sensor_platform[sensor.paperless_ngx_total_documents-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -290,7 +290,7 @@ 'unit_of_measurement': 'documents', }) # --- -# name: test_sensor_platfom[sensor.paperless_ngx_total_documents-state] +# name: test_sensor_platform[sensor.paperless_ngx_total_documents-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'friendly_name': 'Paperless-ngx Total documents', diff --git a/tests/components/paperless_ngx/test_diagnostics.py b/tests/components/paperless_ngx/test_diagnostics.py new file mode 100644 index 00000000000..03d34c37fc6 --- /dev/null +++ b/tests/components/paperless_ngx/test_diagnostics.py @@ -0,0 +1,28 @@ +"""Tests for Paperless-ngx sensor platform.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_paperless: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test generating diagnostics for a device entry.""" + await setup_integration(hass, mock_config_entry) + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) diff --git a/tests/components/paperless_ngx/test_sensor.py b/tests/components/paperless_ngx/test_sensor.py index 70cf04202f5..2025bba6965 100644 --- a/tests/components/paperless_ngx/test_sensor.py +++ b/tests/components/paperless_ngx/test_sensor.py @@ -28,7 +28,7 @@ from tests.common import ( ) -async def test_sensor_platfom( +async def test_sensor_platform( hass: HomeAssistant, mock_config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, From 553d420db91690d99847418ab16a23cc77843e27 Mon Sep 17 00:00:00 2001 From: Markus Lanthaler Date: Fri, 23 May 2025 08:42:09 +0200 Subject: [PATCH 0818/1175] Add support for Tuya Wireless Switch entity (#123284) Add support for Tuya Wireless Switch entity --- homeassistant/components/tuya/const.py | 10 ++ homeassistant/components/tuya/event.py | 147 +++++++++++++++++++++ homeassistant/components/tuya/sensor.py | 3 + homeassistant/components/tuya/strings.json | 14 ++ 4 files changed, 174 insertions(+) create mode 100644 homeassistant/components/tuya/event.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a40260ed787..518e49f2636 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -56,6 +56,7 @@ PLATFORMS = [ Platform.CAMERA, Platform.CLIMATE, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.HUMIDIFIER, Platform.LIGHT, @@ -314,6 +315,15 @@ class DPCode(StrEnum): SWITCH_LED_1 = "switch_led_1" SWITCH_LED_2 = "switch_led_2" SWITCH_LED_3 = "switch_led_3" + SWITCH_MODE1 = "switch_mode1" + SWITCH_MODE2 = "switch_mode2" + SWITCH_MODE3 = "switch_mode3" + SWITCH_MODE4 = "switch_mode4" + SWITCH_MODE5 = "switch_mode5" + SWITCH_MODE6 = "switch_mode6" + SWITCH_MODE7 = "switch_mode7" + SWITCH_MODE8 = "switch_mode8" + SWITCH_MODE9 = "switch_mode9" SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SAVE_ENERGY = "switch_save_energy" SWITCH_SOUND = "switch_sound" # Voice switch diff --git a/homeassistant/components/tuya/event.py b/homeassistant/components/tuya/event.py new file mode 100644 index 00000000000..09ab8e8f544 --- /dev/null +++ b/homeassistant/components/tuya/event.py @@ -0,0 +1,147 @@ +"""Support for Tuya event entities.""" + +from __future__ import annotations + +from tuya_sharing import CustomerDevice, Manager + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, + EventEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import TuyaConfigEntry +from .const import TUYA_DISCOVERY_NEW, DPCode, DPType +from .entity import TuyaEntity + +# All descriptions can be found here. Mostly the Enum data types in the +# default status set of each category (that don't have a set instruction) +# end up being events. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +EVENTS: dict[str, tuple[EventEntityDescription, ...]] = { + # Wireless Switch + # https://developer.tuya.com/en/docs/iot/s?id=Kbeoa9fkv6brp + "wxkg": ( + EventEntityDescription( + key=DPCode.SWITCH_MODE1, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "1"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE2, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "2"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE3, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "3"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE4, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "4"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE5, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "5"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE6, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "6"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE7, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "7"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE8, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "8"}, + ), + EventEntityDescription( + key=DPCode.SWITCH_MODE9, + device_class=EventDeviceClass.BUTTON, + translation_key="numbered_button", + translation_placeholders={"button_number": "9"}, + ), + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: TuyaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Tuya events dynamically through Tuya discovery.""" + hass_data = entry.runtime_data + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya binary sensor.""" + entities: list[TuyaEventEntity] = [] + for device_id in device_ids: + device = hass_data.manager.device_map[device_id] + if descriptions := EVENTS.get(device.category): + for description in descriptions: + dpcode = description.key + if dpcode in device.status: + entities.append( + TuyaEventEntity(device, hass_data.manager, description) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaEventEntity(TuyaEntity, EventEntity): + """Tuya Event Entity.""" + + entity_description: EventEntityDescription + + def __init__( + self, + device: CustomerDevice, + device_manager: Manager, + description: EventEntityDescription, + ) -> None: + """Init Tuya event entity.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + if dpcode := self.find_dpcode(description.key, dptype=DPType.ENUM): + self._attr_event_types: list[str] = dpcode.range + + async def _handle_state_update( + self, updated_status_properties: list[str] | None + ) -> None: + if ( + updated_status_properties is None + or self.entity_description.key not in updated_status_properties + ): + return + + value = self.device.status.get(self.entity_description.key) + self._trigger_event(value) + self.async_write_ha_state() diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 9e40bda5d4d..d9be940bddd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1281,6 +1281,9 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), + # Wireless Switch + # https://developer.tuya.com/en/docs/iot/s?id=Kbeoa9fkv6brp + "wxkg": BATTERY_SENSORS, } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index c6f6bfe9776..fc27aa65ce5 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -101,6 +101,20 @@ "name": "Door 3" } }, + "event": { + "numbered_button": { + "name": "Button {button_number}", + "state_attributes": { + "event_type": { + "state": { + "click": "Clicked", + "double_click": "Double-clicked", + "press": "Long-pressed" + } + } + } + } + }, "light": { "backlight": { "name": "Backlight" From 041c09380b4d8e2079533201e43c153f4ad12242 Mon Sep 17 00:00:00 2001 From: rappenze Date: Fri, 23 May 2025 12:05:13 +0200 Subject: [PATCH 0819/1175] Bump pyfibaro to 0.8.3 (#145488) --- 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 cd4d1de838c..563ad8e08ce 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.8.2"] + "requirements": ["pyfibaro==0.8.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 85867db5b8e..1c7f6624961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pyevilgenius==2.0.0 pyezvizapi==1.0.0.7 # homeassistant.components.fibaro -pyfibaro==0.8.2 +pyfibaro==0.8.3 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fff5121692f..e56b810926b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1612,7 +1612,7 @@ pyevilgenius==2.0.0 pyezvizapi==1.0.0.7 # homeassistant.components.fibaro -pyfibaro==0.8.2 +pyfibaro==0.8.3 # homeassistant.components.fido pyfido==2.1.2 From 17297ab92911bf892bb3cb23ca758ebe2a4c5a3d Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 23 May 2025 13:23:36 +0200 Subject: [PATCH 0820/1175] Improve mqtt subentry selector validation and remove redundant validators (#145499) --- homeassistant/components/mqtt/config_flow.py | 147 ++++++++----------- 1 file changed, 64 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index ca5c597dfaf..e71763c943f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -485,13 +485,24 @@ def validate_sensor_platform_config( return errors +@callback +def validate(validator: Callable[[Any], Any]) -> Callable[[Any], Any]: + """Run validator, then return the unmodified input.""" + + def _validate(value: Any) -> Any: + validator(value) + return value + + return _validate + + @dataclass(frozen=True, kw_only=True) class PlatformField: """Stores a platform config field schema, required flag and validator.""" selector: Selector[Any] | Callable[..., Selector[Any]] required: bool - validator: Callable[..., Any] + validator: Callable[..., Any] | None = None error: str | None = None default: str | int | bool | None | vol.Undefined = vol.UNDEFINED is_schema_default: bool = False @@ -534,13 +545,11 @@ COMMON_ENTITY_FIELDS = { CONF_PLATFORM: PlatformField( selector=SUBENTRY_PLATFORM_SELECTOR, required=True, - validator=str, exclude_from_reconfig=True, ), CONF_NAME: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, exclude_from_reconfig=True, default=None, ), @@ -554,28 +563,25 @@ PLATFORM_ENTITY_FIELDS = { CONF_DEVICE_CLASS: PlatformField( selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, required=False, - validator=str, ), }, Platform.BUTTON.value: { CONF_DEVICE_CLASS: PlatformField( selector=BUTTON_DEVICE_CLASS_SELECTOR, required=False, - validator=str, ), }, Platform.NOTIFY.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( - selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False, validator=str + selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False ), CONF_STATE_CLASS: PlatformField( - selector=SENSOR_STATE_CLASS_SELECTOR, required=False, validator=str + selector=SENSOR_STATE_CLASS_SELECTOR, required=False ), CONF_UNIT_OF_MEASUREMENT: PlatformField( selector=unit_of_measurement_selector, required=False, - validator=str, custom_filtering=True, ), CONF_SUGGESTED_DISPLAY_PRECISION: PlatformField( @@ -587,27 +593,24 @@ PLATFORM_ENTITY_FIELDS = { CONF_OPTIONS: PlatformField( selector=OPTIONS_SELECTOR, required=False, - validator=cv.ensure_list, conditions=({"device_class": "enum"},), ), }, Platform.SWITCH.value: { CONF_DEVICE_CLASS: PlatformField( - selector=SWITCH_DEVICE_CLASS_SELECTOR, required=False, validator=str + selector=SWITCH_DEVICE_CLASS_SELECTOR, required=False ), }, Platform.LIGHT.value: { CONF_SCHEMA: PlatformField( selector=LIGHT_SCHEMA_SELECTOR, required=True, - validator=str, default="basic", exclude_from_reconfig=True, ), CONF_COLOR_TEMP_KELVIN: PlatformField( selector=BOOLEAN_SELECTOR, required=True, - validator=bool, default=True, is_schema_default=True, ), @@ -624,19 +627,17 @@ PLATFORM_MQTT_FIELDS = { CONF_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), CONF_PAYLOAD_OFF: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, default=DEFAULT_PAYLOAD_OFF, ), CONF_PAYLOAD_ON: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, default=DEFAULT_PAYLOAD_ON, ), CONF_EXPIRE_AFTER: PlatformField( @@ -662,18 +663,15 @@ PLATFORM_MQTT_FIELDS = { CONF_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), CONF_PAYLOAD_PRESS: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, default=DEFAULT_PAYLOAD_PRESS, ), - CONF_RETAIN: PlatformField( - selector=BOOLEAN_SELECTOR, required=False, validator=bool - ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, Platform.NOTIFY.value: { CONF_COMMAND_TOPIC: PlatformField( @@ -685,12 +683,10 @@ PLATFORM_MQTT_FIELDS = { CONF_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), - CONF_RETAIN: PlatformField( - selector=BOOLEAN_SELECTOR, required=False, validator=bool - ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, Platform.SENSOR.value: { CONF_STATE_TOPIC: PlatformField( @@ -702,13 +698,13 @@ PLATFORM_MQTT_FIELDS = { CONF_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), CONF_LAST_RESET_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_STATE_CLASS: "total"},), ), @@ -729,7 +725,7 @@ PLATFORM_MQTT_FIELDS = { CONF_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), CONF_STATE_TOPIC: PlatformField( @@ -741,15 +737,11 @@ PLATFORM_MQTT_FIELDS = { CONF_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", ), - CONF_RETAIN: PlatformField( - selector=BOOLEAN_SELECTOR, required=False, validator=bool - ), - CONF_OPTIMISTIC: PlatformField( - selector=BOOLEAN_SELECTOR, required=False, validator=bool - ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, Platform.LIGHT.value: { CONF_COMMAND_TOPIC: PlatformField( @@ -761,21 +753,20 @@ PLATFORM_MQTT_FIELDS = { CONF_COMMAND_ON_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=True, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_COMMAND_OFF_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=True, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_ON_COMMAND_TYPE: PlatformField( selector=ON_COMMAND_TYPE_SELECTOR, required=False, - validator=str, default=DEFAULT_ON_COMMAND_TYPE, conditions=({CONF_SCHEMA: "basic"},), ), @@ -788,14 +779,14 @@ PLATFORM_MQTT_FIELDS = { CONF_STATE_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), ), CONF_STATE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), @@ -806,19 +797,15 @@ PLATFORM_MQTT_FIELDS = { error="invalid_supported_color_modes", conditions=({CONF_SCHEMA: "json"},), ), - CONF_OPTIMISTIC: PlatformField( - selector=BOOLEAN_SELECTOR, required=False, validator=bool - ), + CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), CONF_RETAIN: PlatformField( selector=BOOLEAN_SELECTOR, required=False, - validator=bool, conditions=({CONF_SCHEMA: "basic"},), ), CONF_BRIGHTNESS: PlatformField( selector=BOOLEAN_SELECTOR, required=False, - validator=bool, conditions=({CONF_SCHEMA: "json"},), section="light_brightness_settings", ), @@ -833,7 +820,7 @@ PLATFORM_MQTT_FIELDS = { CONF_BRIGHTNESS_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_brightness_settings", @@ -849,21 +836,19 @@ PLATFORM_MQTT_FIELDS = { CONF_PAYLOAD_OFF: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, default=DEFAULT_PAYLOAD_OFF, conditions=({CONF_SCHEMA: "basic"},), ), CONF_PAYLOAD_ON: PlatformField( selector=TEXT_SELECTOR, required=False, - validator=str, default=DEFAULT_PAYLOAD_ON, conditions=({CONF_SCHEMA: "basic"},), ), CONF_BRIGHTNESS_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_brightness_settings", @@ -890,7 +875,7 @@ PLATFORM_MQTT_FIELDS = { CONF_COLOR_MODE_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_color_mode_settings", @@ -906,7 +891,7 @@ PLATFORM_MQTT_FIELDS = { CONF_COLOR_TEMP_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_color_temp_settings", @@ -922,7 +907,7 @@ PLATFORM_MQTT_FIELDS = { CONF_COLOR_TEMP_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_color_temp_settings", @@ -930,35 +915,35 @@ PLATFORM_MQTT_FIELDS = { CONF_BRIGHTNESS_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_RED_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_GREEN_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_BLUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), CONF_COLOR_TEMP_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), ), @@ -973,7 +958,7 @@ PLATFORM_MQTT_FIELDS = { CONF_HS_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_hs_settings", @@ -989,7 +974,7 @@ PLATFORM_MQTT_FIELDS = { CONF_HS_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_hs_settings", @@ -1005,7 +990,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGB_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgb_settings", @@ -1021,7 +1006,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGB_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgb_settings", @@ -1037,7 +1022,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGBW_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgbw_settings", @@ -1053,7 +1038,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGBW_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgbw_settings", @@ -1069,7 +1054,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGBWW_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgbww_settings", @@ -1085,7 +1070,7 @@ PLATFORM_MQTT_FIELDS = { CONF_RGBWW_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_rgbww_settings", @@ -1101,7 +1086,7 @@ PLATFORM_MQTT_FIELDS = { CONF_XY_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_xy_settings", @@ -1117,7 +1102,7 @@ PLATFORM_MQTT_FIELDS = { CONF_XY_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_xy_settings", @@ -1144,7 +1129,6 @@ PLATFORM_MQTT_FIELDS = { CONF_EFFECT: PlatformField( selector=BOOLEAN_SELECTOR, required=False, - validator=bool, conditions=({CONF_SCHEMA: "json"},), section="light_effect_settings", ), @@ -1159,7 +1143,7 @@ PLATFORM_MQTT_FIELDS = { CONF_EFFECT_COMMAND_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_effect_settings", @@ -1175,7 +1159,7 @@ PLATFORM_MQTT_FIELDS = { CONF_EFFECT_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "template"},), section="light_effect_settings", @@ -1183,7 +1167,7 @@ PLATFORM_MQTT_FIELDS = { CONF_EFFECT_VALUE_TEMPLATE: PlatformField( selector=TEMPLATE_SELECTOR, required=False, - validator=cv.template, + validator=validate(cv.template), error="invalid_template", conditions=({CONF_SCHEMA: "basic"},), section="light_effect_settings", @@ -1191,7 +1175,6 @@ PLATFORM_MQTT_FIELDS = { CONF_EFFECT_LIST: PlatformField( selector=OPTIONS_SELECTOR, required=False, - validator=cv.ensure_list, section="light_effect_settings", ), CONF_FLASH: PlatformField( @@ -1255,15 +1238,11 @@ ENTITY_CONFIG_VALIDATOR: dict[ } MQTT_DEVICE_PLATFORM_FIELDS = { - ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str), - ATTR_SW_VERSION: PlatformField( - selector=TEXT_SELECTOR, required=False, validator=str - ), - ATTR_HW_VERSION: PlatformField( - selector=TEXT_SELECTOR, required=False, validator=str - ), - ATTR_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str), - ATTR_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str), + ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True), + ATTR_SW_VERSION: PlatformField(selector=TEXT_SELECTOR, required=False), + ATTR_HW_VERSION: PlatformField(selector=TEXT_SELECTOR, required=False), + ATTR_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False), + ATTR_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False), ATTR_CONFIGURATION_URL: PlatformField( selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url" ), @@ -1317,10 +1296,10 @@ def validate_field( error: str, ) -> None: """Validate a single field.""" - if user_input is None or field not in user_input: + if user_input is None or field not in user_input or validator is None: return try: - validator(user_input[field]) + user_input[field] = validator(user_input[field]) except (ValueError, vol.Error, vol.Invalid): errors[field] = error @@ -1378,7 +1357,9 @@ def validate_user_input( for field, value in merged_user_input.items(): validator = data_schema_fields[field].validator try: - validator(value) + merged_user_input[field] = ( + validator(value) if validator is not None else value + ) except (ValueError, vol.Error, vol.Invalid): data_schema_field = data_schema_fields[field] errors[data_schema_field.section or field] = ( From e8ea5c9d62dffd4a4c8096f8a0a2f7016c023a0b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 23 May 2025 14:25:00 +0200 Subject: [PATCH 0821/1175] Add MQTT cover as entity platform on MQTT subentries (#144381) * Add MQTT cover as entity platform on MQTT subentries * Revert change vol.Coerce wrappers on cover schema * Fix template validator and cleanup redundant validators * Cleanup more redundant validators --- homeassistant/components/mqtt/config_flow.py | 279 +++++++++++++++++++ homeassistant/components/mqtt/const.py | 25 ++ homeassistant/components/mqtt/cover.py | 51 ++-- homeassistant/components/mqtt/strings.json | 90 ++++++ tests/components/mqtt/common.py | 39 +++ tests/components/mqtt/test_config_flow.py | 88 ++++++ 6 files changed, 543 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index e71763c943f..13cb8658f14 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -27,6 +27,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.file_upload import process_uploaded_file from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.components.light import ( @@ -78,6 +79,10 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section @@ -150,6 +155,8 @@ from .const import ( CONF_FLASH, CONF_FLASH_TIME_LONG, CONF_FLASH_TIME_SHORT, + CONF_GET_POSITION_TEMPLATE, + CONF_GET_POSITION_TOPIC, CONF_GREEN_TEMPLATE, CONF_HS_COMMAND_TEMPLATE, CONF_HS_COMMAND_TOPIC, @@ -163,8 +170,14 @@ from .const import ( CONF_ON_COMMAND_TYPE, CONF_OPTIONS, CONF_PAYLOAD_AVAILABLE, + CONF_PAYLOAD_CLOSE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_PAYLOAD_OPEN, CONF_PAYLOAD_PRESS, + CONF_PAYLOAD_STOP, + CONF_PAYLOAD_STOP_TILT, + CONF_POSITION_CLOSED, + CONF_POSITION_OPEN, CONF_QOS, CONF_RED_TEMPLATE, CONF_RETAIN, @@ -181,10 +194,26 @@ from .const import ( CONF_RGBWW_STATE_TOPIC, CONF_RGBWW_VALUE_TEMPLATE, CONF_SCHEMA, + CONF_SET_POSITION_TEMPLATE, + CONF_SET_POSITION_TOPIC, + CONF_STATE_CLOSED, + CONF_STATE_CLOSING, + CONF_STATE_OPEN, + CONF_STATE_OPENING, + CONF_STATE_STOPPED, CONF_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, + CONF_TILT_CLOSED_POSITION, + CONF_TILT_COMMAND_TEMPLATE, + CONF_TILT_COMMAND_TOPIC, + CONF_TILT_MAX, + CONF_TILT_MIN, + CONF_TILT_OPEN_POSITION, + CONF_TILT_STATE_OPTIMISTIC, + CONF_TILT_STATUS_TEMPLATE, + CONF_TILT_STATUS_TOPIC, CONF_TLS_INSECURE, CONF_TRANSITION, CONF_TRANSPORT, @@ -205,14 +234,24 @@ from .const import ( DEFAULT_KEEPALIVE, DEFAULT_ON_COMMAND_TYPE, DEFAULT_PAYLOAD_AVAILABLE, + DEFAULT_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PAYLOAD_OFF, DEFAULT_PAYLOAD_ON, + DEFAULT_PAYLOAD_OPEN, DEFAULT_PAYLOAD_PRESS, + DEFAULT_PAYLOAD_STOP, DEFAULT_PORT, + DEFAULT_POSITION_CLOSED, + DEFAULT_POSITION_OPEN, DEFAULT_PREFIX, DEFAULT_PROTOCOL, DEFAULT_QOS, + DEFAULT_STATE_STOPPED, + DEFAULT_TILT_CLOSED_POSITION, + DEFAULT_TILT_MAX, + DEFAULT_TILT_MIN, + DEFAULT_TILT_OPEN_POSITION, DEFAULT_TRANSPORT, DEFAULT_WILL, DEFAULT_WS_PATH, @@ -313,6 +352,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( SUBENTRY_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, Platform.LIGHT, Platform.NOTIFY, Platform.SENSOR, @@ -365,6 +405,14 @@ BUTTON_DEVICE_CLASS_SELECTOR = SelectSelector( sort=True, ) ) +COVER_DEVICE_CLASS_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[device_class.value for device_class in CoverDeviceClass], + mode=SelectSelectorMode.DROPDOWN, + translation_key="device_class_cover", + sort=True, + ) +) SENSOR_STATE_CLASS_SELECTOR = SelectSelector( SelectSelectorConfig( options=[device_class.value for device_class in SensorStateClass], @@ -386,6 +434,9 @@ TIMEOUT_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Cover specific selectors +POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX)) + # Switch specific selectors SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector( SelectSelectorConfig( @@ -444,6 +495,48 @@ SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector( ) +@callback +def validate_cover_platform_config( + config: dict[str, Any], +) -> dict[str, str]: + """Validate the cover platform options.""" + errors: dict[str, str] = {} + + # If set position topic is set then get position topic is set as well. + if CONF_SET_POSITION_TOPIC in config and CONF_GET_POSITION_TOPIC not in config: + errors["cover_position_settings"] = ( + "cover_get_and_set_position_must_be_set_together" + ) + + # if templates are set make sure the topic for the template is also set + if CONF_VALUE_TEMPLATE in config and CONF_STATE_TOPIC not in config: + errors[CONF_VALUE_TEMPLATE] = ( + "cover_value_template_must_be_used_with_state_topic" + ) + + if CONF_GET_POSITION_TEMPLATE in config and CONF_GET_POSITION_TOPIC not in config: + errors["cover_position_settings"] = ( + "cover_get_position_template_must_be_used_with_get_position_topic" + ) + + if CONF_SET_POSITION_TEMPLATE in config and CONF_SET_POSITION_TOPIC not in config: + errors["cover_position_settings"] = ( + "cover_set_position_template_must_be_used_with_set_position_topic" + ) + + if CONF_TILT_COMMAND_TEMPLATE in config and CONF_TILT_COMMAND_TOPIC not in config: + errors["cover_tilt_settings"] = ( + "cover_tilt_command_template_must_be_used_with_tilt_command_topic" + ) + + if CONF_TILT_STATUS_TEMPLATE in config and CONF_TILT_STATUS_TOPIC not in config: + errors["cover_tilt_settings"] = ( + "cover_tilt_status_template_must_be_used_with_tilt_status_topic" + ) + + return errors + + @callback def validate_sensor_platform_config( config: dict[str, Any], @@ -571,6 +664,12 @@ PLATFORM_ENTITY_FIELDS = { required=False, ), }, + Platform.COVER.value: { + CONF_DEVICE_CLASS: PlatformField( + selector=COVER_DEVICE_CLASS_SELECTOR, + required=False, + ), + }, Platform.NOTIFY.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( @@ -673,6 +772,185 @@ PLATFORM_MQTT_FIELDS = { ), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, + Platform.COVER.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_PAYLOAD_CLOSE: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_CLOSE, + section="cover_payload_settings", + ), + CONF_PAYLOAD_OPEN: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OPEN, + section="cover_payload_settings", + ), + CONF_PAYLOAD_STOP: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=None, + section="cover_payload_settings", + ), + CONF_PAYLOAD_STOP_TILT: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_STOP, + section="cover_payload_settings", + ), + CONF_STATE_CLOSED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=STATE_CLOSED, + section="cover_payload_settings", + ), + CONF_STATE_CLOSING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=STATE_CLOSING, + section="cover_payload_settings", + ), + CONF_STATE_OPEN: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=STATE_OPEN, + section="cover_payload_settings", + ), + CONF_STATE_OPENING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=STATE_OPENING, + section="cover_payload_settings", + ), + CONF_STATE_STOPPED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_STOPPED, + section="cover_payload_settings", + ), + CONF_SET_POSITION_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="cover_position_settings", + ), + CONF_SET_POSITION_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="cover_position_settings", + ), + CONF_GET_POSITION_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="cover_position_settings", + ), + CONF_GET_POSITION_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="cover_position_settings", + ), + CONF_POSITION_CLOSED: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_POSITION_CLOSED, + section="cover_position_settings", + ), + CONF_POSITION_OPEN: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_POSITION_OPEN, + section="cover_position_settings", + ), + CONF_TILT_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="cover_tilt_settings", + ), + CONF_TILT_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="cover_tilt_settings", + ), + CONF_TILT_CLOSED_POSITION: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_TILT_CLOSED_POSITION, + section="cover_tilt_settings", + ), + CONF_TILT_OPEN_POSITION: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_TILT_OPEN_POSITION, + section="cover_tilt_settings", + ), + CONF_TILT_STATUS_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="cover_tilt_settings", + ), + CONF_TILT_STATUS_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="cover_tilt_settings", + ), + CONF_TILT_MIN: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_TILT_MIN, + section="cover_tilt_settings", + ), + CONF_TILT_MAX: PlatformField( + selector=POSITION_SELECTOR, + required=False, + validator=int, + default=DEFAULT_TILT_MAX, + section="cover_tilt_settings", + ), + CONF_TILT_STATE_OPTIMISTIC: PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + section="cover_tilt_settings", + ), + }, Platform.NOTIFY.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -1231,6 +1509,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ ] = { Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, + Platform.COVER.value: validate_cover_platform_config, Platform.LIGHT.value: validate_light_platform_config, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 89e721f022b..be559675dd8 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -90,6 +90,8 @@ CONF_EXPIRE_AFTER = "expire_after" CONF_FLASH = "flash" CONF_FLASH_TIME_LONG = "flash_time_long" CONF_FLASH_TIME_SHORT = "flash_time_short" +CONF_GET_POSITION_TEMPLATE = "position_template" +CONF_GET_POSITION_TOPIC = "position_topic" CONF_GREEN_TEMPLATE = "green_template" CONF_HS_COMMAND_TEMPLATE = "hs_command_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" @@ -111,6 +113,7 @@ CONF_PAYLOAD_CLOSE = "payload_close" CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_PRESS = "payload_press" CONF_PAYLOAD_STOP = "payload_stop" +CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" CONF_POWER_COMMAND_TOPIC = "power_command_topic" @@ -129,10 +132,13 @@ CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" +CONF_SET_POSITION_TEMPLATE = "set_position_template" +CONF_SET_POSITION_TOPIC = "set_position_topic" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" +CONF_STATE_STOPPED = "state_stopped" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template" @@ -142,6 +148,15 @@ CONF_TEMP_STATE_TOPIC = "temperature_state_topic" CONF_TEMP_INITIAL = "initial" CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" +CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template" +CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" +CONF_TILT_STATUS_TOPIC = "tilt_status_topic" +CONF_TILT_STATUS_TEMPLATE = "tilt_status_template" +CONF_TILT_CLOSED_POSITION = "tilt_closed_value" +CONF_TILT_MAX = "tilt_max" +CONF_TILT_MIN = "tilt_min" +CONF_TILT_OPEN_POSITION = "tilt_opened_value" +CONF_TILT_STATE_OPTIMISTIC = "tilt_optimistic" CONF_TRANSITION = "transition" CONF_XY_COMMAND_TEMPLATE = "xy_command_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" @@ -190,15 +205,25 @@ DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_PRESS = "PRESS" +DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PORT = 1883 DEFAULT_RETAIN = False +DEFAULT_TILT_CLOSED_POSITION = 0 +DEFAULT_TILT_MAX = 100 +DEFAULT_TILT_MIN = 0 +DEFAULT_TILT_OPEN_POSITION = 100 +DEFAULT_TILT_OPTIMISTIC = False DEFAULT_WS_HEADERS: dict[str, str] = {} DEFAULT_WS_PATH = "/" DEFAULT_POSITION_CLOSED = 0 DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False +DEFAULT_STATE_STOPPED = "stopped" DEFAULT_WHITE_SCALE = 255 +COVER_PAYLOAD = "cover" +TILT_PAYLOAD = "tilt" + VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] PROTOCOL_31 = "3.1" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 428c4d0e205..201f28099c8 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -43,23 +43,45 @@ from . import subscription from .config import MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TOPIC, + CONF_GET_POSITION_TEMPLATE, + CONF_GET_POSITION_TOPIC, CONF_PAYLOAD_CLOSE, CONF_PAYLOAD_OPEN, CONF_PAYLOAD_STOP, + CONF_PAYLOAD_STOP_TILT, CONF_POSITION_CLOSED, CONF_POSITION_OPEN, CONF_RETAIN, + CONF_SET_POSITION_TEMPLATE, + CONF_SET_POSITION_TOPIC, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OPEN, CONF_STATE_OPENING, + CONF_STATE_STOPPED, CONF_STATE_TOPIC, + CONF_TILT_CLOSED_POSITION, + CONF_TILT_COMMAND_TEMPLATE, + CONF_TILT_COMMAND_TOPIC, + CONF_TILT_MAX, + CONF_TILT_MIN, + CONF_TILT_OPEN_POSITION, + CONF_TILT_STATE_OPTIMISTIC, + CONF_TILT_STATUS_TEMPLATE, + CONF_TILT_STATUS_TOPIC, DEFAULT_OPTIMISTIC, DEFAULT_PAYLOAD_CLOSE, DEFAULT_PAYLOAD_OPEN, + DEFAULT_PAYLOAD_STOP, DEFAULT_POSITION_CLOSED, DEFAULT_POSITION_OPEN, DEFAULT_RETAIN, + DEFAULT_STATE_STOPPED, + DEFAULT_TILT_CLOSED_POSITION, + DEFAULT_TILT_MAX, + DEFAULT_TILT_MIN, + DEFAULT_TILT_OPEN_POSITION, + DEFAULT_TILT_OPTIMISTIC, PAYLOAD_NONE, ) from .entity import MqttEntity, async_setup_entity_entry_helper @@ -71,37 +93,8 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -CONF_GET_POSITION_TOPIC = "position_topic" -CONF_GET_POSITION_TEMPLATE = "position_template" -CONF_SET_POSITION_TOPIC = "set_position_topic" -CONF_SET_POSITION_TEMPLATE = "set_position_template" -CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" -CONF_TILT_COMMAND_TEMPLATE = "tilt_command_template" -CONF_TILT_STATUS_TOPIC = "tilt_status_topic" -CONF_TILT_STATUS_TEMPLATE = "tilt_status_template" - -CONF_STATE_STOPPED = "state_stopped" -CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" -CONF_TILT_CLOSED_POSITION = "tilt_closed_value" -CONF_TILT_MAX = "tilt_max" -CONF_TILT_MIN = "tilt_min" -CONF_TILT_OPEN_POSITION = "tilt_opened_value" -CONF_TILT_STATE_OPTIMISTIC = "tilt_optimistic" - -TILT_PAYLOAD = "tilt" -COVER_PAYLOAD = "cover" - DEFAULT_NAME = "MQTT Cover" -DEFAULT_STATE_STOPPED = "stopped" -DEFAULT_PAYLOAD_STOP = "STOP" - -DEFAULT_TILT_CLOSED_POSITION = 0 -DEFAULT_TILT_MAX = 100 -DEFAULT_TILT_MIN = 0 -DEFAULT_TILT_OPEN_POSITION = 100 -DEFAULT_TILT_OPTIMISTIC = False - TILT_FEATURES = ( CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 7006df09897..dd2186481d1 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -316,6 +316,75 @@ "transition": "Enable the transition feature for this light" } }, + "cover_payload_settings": { + "name": "Payload settings", + "data": { + "payload_close": "Payload \"close\"", + "payload_open": "Payload \"open\"", + "payload_stop": "Payload \"stop\"", + "payload_stop_tilt": "Payload \"stop tilt\"", + "state_closed": "State \"closed\"", + "state_closing": "State \"closing\"", + "state_open": "State \"open\"", + "state_opening": "State \"opening\"", + "state_stopped": "State \"stopped\"" + }, + "data_description": { + "payload_close": "The payload sent when a \"close\" command is issued.", + "payload_open": "The payload sent when an \"open\" command is issued.", + "payload_stop": "The payload sent when a \"stop\" command is issued. Leave empty to disable the \"stop\" feature.", + "payload_stop_tilt": "The payload sent when a \"stop tilt\" command is issued.", + "state_closed": "The payload received at the state topic that represents the \"closed\" state.", + "state_closing": "The payload received at the state topic that represents the \"closing\" state.", + "state_open": "The payload received at the state topic that represents the \"open\" state.", + "state_opening": "The payload received at the state topic that represents the \"opening\" state.", + "state_stopped": "The payload received at the state topic that represents the \"stopped\" state (for covers that do not report \"open\"/\"closed\" state)." + } + }, + "cover_position_settings": { + "name": "Position settings", + "data": { + "position_closed": "Position \"closed\" value", + "position_open": "Position \"open\" value", + "position_template": "Position value template", + "position_topic": "Position state topic", + "set_position_template": "Set position template", + "set_position_topic": "Set position topic" + }, + "data_description": { + "position_closed": "Number which represents \"closed\" position.", + "position_open": "Number which represents \"open\" position.", + "position_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the payload for the position topic. Within the template the following variables are also available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#position_template)", + "position_topic": "The MQTT topic subscribed to receive cover position state messages. [Learn more.]({url}#position_topic)", + "set_position_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the set position topic. Within the template the following variables are available: `value` (the scaled target position), `entity_id`, `position` (the target position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#set_position_template)", + "set_position_topic": "The MQTT topic to publish position commands to. You need to use the set position topic as well if you want to use the position topic. Use template if position topic wants different values than within range \"position closed\" - \"position_open\". If template is not defined and position \"closed\" != 100 and position \"open\" != 0 then proper position value is calculated from percentage position. [Learn more.]({url}#set_position_topic)" + } + }, + "cover_tilt_settings": { + "name": "Tilt settings", + "data": { + "tilt_closed_value": "Tilt \"closed\" value", + "tilt_command_template": "Set tilt template", + "tilt_command_topic": "Set tilt topic", + "tilt_max": "Tilt max", + "tilt_min": "Tilt min", + "tilt_opened_value": "Tilt \"opened\" value", + "tilt_status_template": "Tilt value template", + "tilt_status_topic": "Tilt status topic", + "tilt_optimistic": "Tilt optimistic" + }, + "data_description": { + "tilt_closed_value": "The value that will be sent to the \"set tilt topic\" when the cover tilt is closed.", + "tilt_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the set tilt topic. Within the template the following variables are available: `entity_id`, `tilt_position` (the target tilt position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_command_template)", + "tilt_command_topic": "The MQTT topic to publish commands to control the cover tilt. [Learn more.]({url}#tilt_command_topic)", + "tilt_max": "The maximum tilt value.", + "tilt_min": "The minimum tilt value.", + "tilt_opened_value": "The value that will be sent to the \"set tilt topic\" when the cover tilt is opened.", + "tilt_status_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the payload for the tilt status topic. Within the template the following variables are available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_status_template)", + "tilt_status_topic": "The MQTT topic subscribed to receive tilt status update values. [Learn more.]({url}#tilt_status_topic)", + "tilt_optimistic": "Flag that defines if tilt works in optimistic mode. If tilt status topic is not defined, tilt works in optimisic mode by default. [Learn more.]({url}#tilt_optimistic)" + } + }, "light_brightness_settings": { "name": "Brightness settings", "data": { @@ -476,6 +545,12 @@ "default": "MQTT device with {platform} entity \"{entity}\" was set up successfully.\n\nNote that you can reconfigure the MQTT device at any time, e.g. to add more entities." }, "error": { + "cover_get_and_set_position_must_be_set_together": "The get position and set position topic options must be set together", + "cover_get_position_template_must_be_used_with_get_position_topic": "The position value template must be used together with the position state topic setting", + "cover_set_position_template_must_be_used_with_set_position_topic": "The set position template must be used with the set position topic", + "cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic", + "cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic", + "cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option", "invalid_input": "Invalid value", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_template": "Invalid template", @@ -643,6 +718,20 @@ "update": "[%key:component::button::entity_component::update::name%]" } }, + "device_class_cover": { + "options": { + "awning": "[%key:component::cover::entity_component::awning::name%]", + "blind": "[%key:component::cover::entity_component::blind::name%]", + "curtain": "[%key:component::cover::entity_component::curtain::name%]", + "damper": "[%key:component::cover::entity_component::damper::name%]", + "door": "[%key:component::cover::entity_component::door::name%]", + "garage": "[%key:component::cover::entity_component::garage::name%]", + "gate": "[%key:component::cover::entity_component::gate::name%]", + "shade": "[%key:component::cover::entity_component::shade::name%]", + "shutter": "[%key:component::cover::entity_component::shutter::name%]", + "window": "[%key:component::cover::entity_component::window::name%]" + } + }, "device_class_sensor": { "options": { "apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]", @@ -727,6 +816,7 @@ "options": { "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", + "cover": "[%key:component::cover::title%]", "light": "[%key:component::light::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 9bf1c236de6..d1951c638a4 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -92,6 +92,41 @@ MOCK_SUBENTRY_BUTTON_COMPONENT = { "entity_picture": "https://example.com/365d05e6607c4dfb8ae915cff71a954b", }, } +MOCK_SUBENTRY_COVER_COMPONENT = { + "b37acf667fa04c688ad7dfb27de2178b": { + "platform": "cover", + "name": "Blind", + "device_class": "blind", + "command_topic": "test-topic", + "payload_stop": None, + "payload_stop_tilt": "STOP", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "position_closed": 0, + "position_open": 100, + "position_template": "{{ value_json.position }}", + "position_topic": "test-topic/position", + "set_position_template": "{{ value }}", + "set_position_topic": "test-topic/position-set", + "state_closed": "closed", + "state_closing": "closing", + "state_open": "open", + "state_opening": "opening", + "state_stopped": "stopped", + "state_topic": "test-topic", + "tilt_closed_value": 0, + "tilt_max": 100, + "tilt_min": 0, + "tilt_opened_value": 100, + "tilt_optimistic": False, + "tilt_command_topic": "test-topic/tilt-set", + "tilt_command_template": "{{ value }}", + "tilt_status_topic": "test-topic/tilt", + "tilt_status_template": "{{ value_json.position }}", + "retain": False, + "entity_picture": "https://example.com/b37acf667fa04c688ad7dfb27de2178b", + }, +} MOCK_SUBENTRY_NOTIFY_COMPONENT1 = { "363a7ecad6be4a19b939a016ea93e994": { "platform": "notify", @@ -225,6 +260,10 @@ MOCK_BUTTON_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 2}}, "components": MOCK_SUBENTRY_BUTTON_COMPONENT, } +MOCK_COVER_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_COVER_COMPONENT, +} MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 4cfc416c3c9..56633b2280d 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -35,6 +35,7 @@ from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .common import ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, + MOCK_COVER_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, @@ -2698,6 +2699,92 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Restart", ), + ( + MOCK_COVER_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Blind"}, + {"device_class": "blind"}, + (), + { + "command_topic": "test-topic", + "cover_position_settings": { + "position_template": "{{ value_json.position }}", + "position_topic": "test-topic/position", + "set_position_template": "{{ value }}", + "set_position_topic": "test-topic/position-set", + }, + "state_topic": "test-topic", + "retain": False, + "cover_tilt_settings": { + "tilt_command_topic": "test-topic/tilt-set", + "tilt_command_template": "{{ value }}", + "tilt_status_topic": "test-topic/tilt", + "tilt_status_template": "{{ value_json.position }}", + "tilt_closed_value": 0, + "tilt_opened_value": 100, + "tilt_max": 100, + "tilt_min": 0, + "tilt_optimistic": False, + }, + }, + ( + ( + {"value_template": "{{ json_value.state }}"}, + { + "value_template": "cover_value_template_must_be_used_with_state_topic" + }, + ), + ( + {"cover_position_settings": {"set_position_topic": "test-topic"}}, + { + "cover_position_settings": "cover_get_and_set_position_must_be_set_together" + }, + ), + ( + { + "cover_position_settings": { + "set_position_template": "{{ value }}" + } + }, + { + "cover_position_settings": "cover_set_position_template_must_be_used_with_set_position_topic" + }, + ), + ( + { + "cover_position_settings": { + "position_template": "{{ json_value.position }}" + } + }, + { + "cover_position_settings": "cover_get_position_template_must_be_used_with_get_position_topic" + }, + ), + ( + {"cover_position_settings": {"set_position_topic": "{{ value }}"}}, + { + "cover_position_settings": "cover_get_and_set_position_must_be_set_together" + }, + ), + ( + {"cover_tilt_settings": {"tilt_command_template": "{{ value }}"}}, + { + "cover_tilt_settings": "cover_tilt_command_template_must_be_used_with_tilt_command_topic" + }, + ), + ( + { + "cover_tilt_settings": { + "tilt_status_template": "{{ json_value.position }}" + } + }, + { + "cover_tilt_settings": "cover_tilt_status_template_must_be_used_with_tilt_status_topic" + }, + ), + ), + "Milk notifier Blind", + ), ( MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, @@ -2883,6 +2970,7 @@ async def test_migrate_of_incompatible_config_entry( ids=[ "binary_sensor", "button", + "cover", "notify_with_entity_name", "notify_no_entity_name", "sensor_options", From 44560dd29843de07a35693039082fca1a09aee6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 23 May 2025 07:44:47 -0500 Subject: [PATCH 0822/1175] Bump aiohttp to 3.12.0b3 (#145358) --- 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 6ef8613ad96..643deb72a51 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.11.18 +aiohttp==3.12.0b3 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 955b2a707a5..5904ef4f48b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.11.18", + "aiohttp==3.12.0b3", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 7d15999bb38..d6986a8872c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.11.18 +aiohttp==3.12.0b3 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From f019e8a36ce5eb173f5f764b84e38bf34cfa83ff Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 23 May 2025 15:48:54 +0300 Subject: [PATCH 0823/1175] Bump Anthropic library to 0.52.0 (#145494) --- homeassistant/components/anthropic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json index 797a7299d16..6a8f1e5e54c 100644 --- a/homeassistant/components/anthropic/manifest.json +++ b/homeassistant/components/anthropic/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/anthropic", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["anthropic==0.47.2"] + "requirements": ["anthropic==0.52.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c7f6624961..b987f84d723 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,7 +480,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.47.2 +anthropic==0.52.0 # homeassistant.components.mcp_server anyio==4.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e56b810926b..4c149cdbd32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -453,7 +453,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.47.2 +anthropic==0.52.0 # homeassistant.components.mcp_server anyio==4.9.0 From 71ac2d3d75aa94a202d3a174d22c8e402a526127 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 May 2025 14:54:09 +0200 Subject: [PATCH 0824/1175] Improve type hints in xiaomi_miio humidifier (#145506) --- .../components/xiaomi_miio/humidifier.py | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index f19fbec5e78..4330b863f6f 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -124,21 +124,10 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator=coordinator) - self._state = None self._attributes = {} self._mode = None self._humidity_steps = 100 - self._target_humidity = None - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def mode(self): - """Get the current mode.""" - return self._mode + self._target_humidity: float | None = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" @@ -146,7 +135,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): "Turning the miio device on failed.", self._device.on ) if result: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -156,7 +145,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): ) if result: - self._state = False + self._attr_is_on = False self.async_write_ha_state() def translate_humidity(self, humidity: float) -> float | None: @@ -194,7 +183,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): self._attr_available_modes = AVAILABLE_MODES_OTHER self._humidity_steps = 10 - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._attributes.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) @@ -205,15 +194,10 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): self._attr_current_humidity = self._attributes[ATTR_HUMIDITY] self._mode = self._attributes[ATTR_MODE] - @property - def is_on(self): - """Return true if device is on.""" - return self._state - @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._attributes.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) @@ -222,16 +206,16 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): ) self._target_humidity = self._attributes[ATTR_TARGET_HUMIDITY] self._attr_current_humidity = self._attributes[ATTR_HUMIDITY] - self._mode = self._attributes[ATTR_MODE] + self._attr_mode = self._attributes[ATTR_MODE] self.async_write_ha_state() @property - def mode(self): + def mode(self) -> str: """Return the current mode.""" return AirhumidifierOperationMode(self._mode).name @property - def target_humidity(self): + def target_humidity(self) -> float | None: """Return the target humidity.""" return ( self._target_humidity @@ -302,14 +286,14 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()} @property - def mode(self): + def mode(self) -> str: """Return the current mode.""" return AirhumidifierMiotOperationMode(self._mode).name @property - def target_humidity(self): + def target_humidity(self) -> float | None: """Return the target humidity.""" - if self._state: + if self.is_on: return ( self._target_humidity if AirhumidifierMiotOperationMode(self._mode) @@ -357,7 +341,7 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): return _LOGGER.debug("Setting the operation mode to: %s", mode) - if self._state: + if self.is_on: if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, @@ -378,14 +362,14 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): } @property - def mode(self): + def mode(self) -> str: """Return the current mode.""" return AirhumidifierMjjsqOperationMode(self._mode).name @property - def target_humidity(self): + def target_humidity(self) -> float | None: """Return the target humidity.""" - if self._state: + if self.is_on: if ( AirhumidifierMjjsqOperationMode(self._mode) == AirhumidifierMjjsqOperationMode.Humidity @@ -429,7 +413,7 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): return _LOGGER.debug("Setting the operation mode to: %s", mode) - if self._state: + if self.is_on: if await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, From 7bf4239789ecfe9a14ed7f588b21d20c039f080e Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 23 May 2025 14:54:18 +0200 Subject: [PATCH 0825/1175] Bump deebot-client to 13.2.1 (#145492) --- 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 b1674e123fa..c2daf3a7e90 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index b987f84d723..aa36fc41b08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.2.0 +deebot-client==13.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c149cdbd32..f24f7085ac9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -653,7 +653,7 @@ dbus-fast==2.43.0 debugpy==1.8.14 # homeassistant.components.ecovacs -deebot-client==13.2.0 +deebot-client==13.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 0c9b1b5c583c027729ed67ebb0205a1ac5d12145 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Fri, 23 May 2025 15:07:06 +0200 Subject: [PATCH 0826/1175] Add cloud as after_dependency to onedrive (#145301) --- homeassistant/components/onedrive/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index c20a99c727e..a6b47b083dc 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -1,6 +1,7 @@ { "domain": "onedrive", "name": "OneDrive", + "after_dependencies": ["cloud"], "codeowners": ["@zweckj"], "config_flow": true, "dependencies": ["application_credentials"], From bca4793c6983650aecafeb59d81fc639464857ca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 May 2025 15:24:18 +0200 Subject: [PATCH 0827/1175] =?UTF-8?q?Add=20concentration=20conversion=20su?= =?UTF-8?q?pport=20for=20mg/m=C2=B3=20(#145325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/number/const.py | 6 +++-- .../components/recorder/statistics.py | 4 +++ .../components/recorder/websocket_api.py | 4 +++ homeassistant/components/sensor/const.py | 8 ++++-- homeassistant/util/unit_conversion.py | 16 ++++++++++++ tests/util/test_unit_conversion.py | 25 +++++++++++++++++++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 2a9c4057168..1b41146cd2a 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, DEGREE, @@ -370,7 +371,7 @@ class NumberDeviceClass(StrEnum): VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. - Unit of measurement: `µg/m³` + Unit of measurement: `µg/m³`, `mg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" @@ -517,7 +518,8 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature), NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, }, NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: { CONCENTRATION_PARTS_PER_BILLION, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index bdb5062e88e..7f41358dddf 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -55,6 +55,7 @@ from homeassistant.util.unit_conversion import ( EnergyDistanceConverter, InformationConverter, MassConverter, + MassVolumeConcentrationConverter, PowerConverter, PressureConverter, ReactiveEnergyConverter, @@ -197,6 +198,9 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { BloodGlucoseConcentrationConverter.VALID_UNITS, BloodGlucoseConcentrationConverter, ), + **dict.fromkeys( + MassVolumeConcentrationConverter.VALID_UNITS, MassVolumeConcentrationConverter + ), **dict.fromkeys(ConductivityConverter.VALID_UNITS, ConductivityConverter), **dict.fromkeys(DataRateConverter.VALID_UNITS, DataRateConverter), **dict.fromkeys(DistanceConverter.VALID_UNITS, DistanceConverter), diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 76a75a5849e..d052631c5f6 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -28,6 +28,7 @@ from homeassistant.util.unit_conversion import ( EnergyDistanceConverter, InformationConverter, MassConverter, + MassVolumeConcentrationConverter, PowerConverter, PressureConverter, ReactiveEnergyConverter, @@ -62,6 +63,9 @@ UNIT_SCHEMA = vol.Schema( vol.Optional("blood_glucose_concentration"): vol.In( BloodGlucoseConcentrationConverter.VALID_UNITS ), + vol.Optional("concentration"): vol.In( + MassVolumeConcentrationConverter.VALID_UNITS + ), vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS), vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index c466bc52703..f26edcd6c35 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, DEGREE, @@ -57,6 +58,7 @@ from homeassistant.util.unit_conversion import ( EnergyDistanceConverter, InformationConverter, MassConverter, + MassVolumeConcentrationConverter, PowerConverter, PressureConverter, ReactiveEnergyConverter, @@ -400,7 +402,7 @@ class SensorDeviceClass(StrEnum): VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. - Unit of measurement: `µg/m³` + Unit of measurement: `µg/m³`, `mg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" @@ -540,6 +542,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter, SensorDeviceClass.SPEED: SpeedConverter, SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: MassVolumeConcentrationConverter, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: UnitlessRatioConverter, SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, SensorDeviceClass.VOLUME: VolumeConverter, @@ -617,7 +620,8 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature), SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, }, SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: { CONCENTRATION_PARTS_PER_BILLION, diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 2ee7b5cd384..d0830d1f8bb 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -7,6 +7,8 @@ from functools import lru_cache from math import floor, log10 from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, @@ -686,6 +688,20 @@ class UnitlessRatioConverter(BaseUnitConverter): } +class MassVolumeConcentrationConverter(BaseUnitConverter): + """Utility to convert mass volume concentration values.""" + + UNIT_CLASS = "concentration" + _UNIT_CONVERSION: dict[str | None, float] = { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000.0, # 1000 µg/m³ = 1 mg/m³ + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1.0, + } + VALID_UNITS = { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + } + + class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 0e9da5dbf3d..7d0eb7226a0 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -8,6 +8,8 @@ from itertools import chain import pytest from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, @@ -48,6 +50,7 @@ from homeassistant.util.unit_conversion import ( EnergyDistanceConverter, InformationConverter, MassConverter, + MassVolumeConcentrationConverter, PowerConverter, PressureConverter, ReactiveEnergyConverter, @@ -69,6 +72,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { for converter in ( AreaConverter, BloodGlucoseConcentrationConverter, + MassVolumeConcentrationConverter, ConductivityConverter, DataRateConverter, DistanceConverter, @@ -128,6 +132,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo ), InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473), + MassVolumeConcentrationConverter: ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + 1000, + ), PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), PressureConverter: (UnitOfPressure.HPA, UnitOfPressure.INHG, 33.86389), ReactiveEnergyConverter: ( @@ -738,6 +747,22 @@ _CONVERTED_VALUE: dict[ (5, None, 5000000, CONCENTRATION_PARTS_PER_MILLION), (5, PERCENTAGE, 0.05, None), ], + MassVolumeConcentrationConverter: [ + # 1000 µg/m³ = 1 mg/m³ + ( + 1000, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + 1, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + ), + # 2 mg/m³ = 2000 µg/m³ + ( + 2, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + 2000, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + ], VolumeConverter: [ (5, UnitOfVolume.LITERS, 1.32086, UnitOfVolume.GALLONS), (5, UnitOfVolume.GALLONS, 18.92706, UnitOfVolume.LITERS), From 528a50947925f2a07bfc6f3e5db50d5679eb59a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 May 2025 15:28:41 +0200 Subject: [PATCH 0828/1175] Mark light methods and properties as mandatory in pylint plugin (#145510) --- homeassistant/components/blebox/light.py | 10 +++++----- homeassistant/components/xiaomi_miio/light.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 86ec8993779..75900ca7d97 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -84,7 +84,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp) @property - def color_mode(self): + def color_mode(self) -> ColorMode: """Return the color mode. Set values to _attr_ibutes if needed. @@ -92,7 +92,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF) @property - def supported_color_modes(self): + def supported_color_modes(self) -> set[ColorMode]: """Return supported color modes.""" return {self.color_mode} @@ -107,7 +107,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): return self._feature.effect @property - def rgb_color(self): + def rgb_color(self) -> tuple[int, int, int] | None: """Return value for rgb.""" if (rgb_hex := self._feature.rgb_hex) is None: return None @@ -118,14 +118,14 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity): ) @property - def rgbw_color(self): + def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the hue and saturation.""" if (rgbw_hex := self._feature.rgbw_hex) is None: return None return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbw_hex)[0:4]) @property - def rgbww_color(self): + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return value for rgbww.""" if (rgbww_hex := self._feature.rgbww_hex) is None: return None diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 781ac0b4acd..7c1c1b7bfb0 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -841,7 +841,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return self._hs_color @property - def color_mode(self): + def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.hs_color: return ColorMode.HS diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index a6d77611926..57dff037f56 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1816,6 +1816,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="color_mode", return_type=["ColorMode", "str", None], + mandatory=True, ), TypeHintMatch( function_name="hs_color", @@ -1828,26 +1829,32 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="rgb_color", return_type=["tuple[int, int, int]", None], + mandatory=True, ), TypeHintMatch( function_name="rgbw_color", return_type=["tuple[int, int, int, int]", None], + mandatory=True, ), TypeHintMatch( function_name="rgbww_color", return_type=["tuple[int, int, int, int, int]", None], + mandatory=True, ), TypeHintMatch( function_name="color_temp", return_type=["int", None], + mandatory=True, ), TypeHintMatch( function_name="min_mireds", return_type="int", + mandatory=True, ), TypeHintMatch( function_name="max_mireds", return_type="int", + mandatory=True, ), TypeHintMatch( function_name="effect_list", @@ -1864,10 +1871,12 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="supported_color_modes", return_type=["set[ColorMode]", "set[str]", None], + mandatory=True, ), TypeHintMatch( function_name="supported_features", return_type="LightEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="turn_on", @@ -1892,6 +1901,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { kwargs_type="Any", return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From fc2fe32f3421309195b7733150e4a397ee8821a2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 23 May 2025 15:33:03 +0200 Subject: [PATCH 0829/1175] Reolink fix device migration (#145443) --- homeassistant/components/reolink/__init__.py | 156 +++++++++---------- tests/components/reolink/test_init.py | 51 ++++++ 2 files changed, 128 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 48b5dc1a3d6..57d41c20521 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,90 +364,88 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - for dev_id in device.identifiers: - (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) - if not device_uid: - continue + (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) - if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: - if ch is None: - new_device_id = f"{host.unique_id}" - else: - new_device_id = f"{host.unique_id}_{device_uid[1]}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, - new_device_id, - ) - new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) + if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: + if ch is None: + new_device_id = f"{host.unique_id}" + else: + new_device_id = f"{host.unique_id}_{device_uid[1]}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) - if ch is None or is_chime: - continue # Do not consider the NVR itself or chimes - - # Check for wrongfully combined host with NVR entities in one device - # Can be removed in HA 2025.12 - if (DOMAIN, host.unique_id) in device.identifiers: - new_identifiers = device.identifiers.copy() - for old_id in device.identifiers: - if old_id[0] == DOMAIN and old_id[1] != host.unique_id: - new_identifiers.remove(old_id) - _LOGGER.debug( - "Updating Reolink device identifiers from %s to %s", - device.identifiers, - new_identifiers, - ) - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - break - - # Check for wrongfully added MAC of the NVR/Hub to the camera - # Can be removed in HA 2025.12 - host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) - if host_connnection in device.connections: - new_connections = device.connections.copy() - new_connections.remove(host_connnection) - _LOGGER.debug( - "Updating Reolink device connections from %s to %s", - device.connections, - new_connections, - ) - device_reg.async_update_device( - device.id, new_connections=new_connections - ) - - ch_device_ids[device.id] = ch - if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid( - ch + # Check for wrongfully combined entities in one device + # Can be removed in HA 2025.12 + new_identifiers = device.identifiers.copy() + remove_ids = False + if (DOMAIN, host.unique_id) in device.identifiers: + remove_ids = True # NVR/Hub in identifiers, keep that one, remove others + for old_id in device.identifiers: + (old_device_uid, old_ch, old_is_chime) = get_device_uid_and_ch(old_id, host) + if ( + not old_device_uid + or old_device_uid[0] != host.unique_id + or old_id[1] == host.unique_id ): - if host.api.supported(None, "UID"): - new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" - else: - new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, + continue + if remove_ids: + new_identifiers.remove(old_id) + remove_ids = True # after the first identifier, remove the others + if new_identifiers != device.identifiers: + _LOGGER.debug( + "Updating Reolink device identifiers from %s to %s", + device.identifiers, + new_identifiers, + ) + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + break + + if ch is None or is_chime: + continue # Do not consider the NVR itself or chimes + + # Check for wrongfully added MAC of the NVR/Hub to the camera + # Can be removed in HA 2025.12 + host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) + if host_connnection in device.connections: + new_connections = device.connections.copy() + new_connections.remove(host_connnection) + _LOGGER.debug( + "Updating Reolink device connections from %s to %s", + device.connections, + new_connections, + ) + device_reg.async_update_device(device.id, new_connections=new_connections) + + ch_device_ids[device.id] = ch + if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): + if host.api.supported(None, "UID"): + new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" + else: + new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + existing_device = device_reg.async_get_device(identifiers=new_identifiers) + if existing_device is None: + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + else: + _LOGGER.warning( + "Reolink device with uid %s already exists, " + "removing device with uid %s", new_device_id, + device_uid, ) - new_identifiers = {(DOMAIN, new_device_id)} - existing_device = device_reg.async_get_device( - identifiers=new_identifiers - ) - if existing_device is None: - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - else: - _LOGGER.warning( - "Reolink device with uid %s already exists, " - "removing device with uid %s", - new_device_id, - device_uid, - ) - device_reg.async_remove_device(device.id) + device_reg.async_remove_device(device.id) entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f2ae22913ad..3551632903f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -724,6 +724,57 @@ async def test_cleanup_combined_with_NVR( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_hub_and_direct_connection( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the device registry if IPC camera device was connected directly and through the hub/NVR.""" + reolink_connect.channels = [0] + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + start_identifiers = { + (DOMAIN, dev_id), # IPC camera through hub + (DOMAIN, TEST_UID_CAM), # directly connected IPC camera + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC_CAM)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=entity_id, + config_entry=config_entry, + suggested_object_id=entity_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: From 4747de4703e93374c38267111db4188f73c41530 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 23 May 2025 16:12:13 +0200 Subject: [PATCH 0830/1175] Don't manipulate hvac modes based on device active mode in AVM Fritz!SmartHome (#145513) --- homeassistant/components/fritzbox/climate.py | 2 -- tests/components/fritzbox/test_climate.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 573877fa71b..ec4b09a2af2 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -111,11 +111,9 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity): """Write the state to the HASS state machine.""" if self.data.holiday_active: self._attr_supported_features = ClimateEntityFeature.PRESET_MODE - self._attr_hvac_modes = [HVACMode.HEAT] self._attr_preset_modes = [PRESET_HOLIDAY] elif self.data.summer_active: self._attr_supported_features = ClimateEntityFeature.PRESET_MODE - self._attr_hvac_modes = [HVACMode.OFF] self._attr_preset_modes = [PRESET_SUMMER] else: self._attr_supported_features = SUPPORTED_FEATURES diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index bf8ce5d8a5b..e216f7d4b30 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -609,7 +609,7 @@ async def test_holidy_summer_mode( assert state assert state.attributes[ATTR_STATE_HOLIDAY_MODE] assert state.attributes[ATTR_STATE_SUMMER_MODE] is False - assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT] + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOLIDAY assert state.attributes[ATTR_PRESET_MODES] == [PRESET_HOLIDAY] @@ -645,7 +645,7 @@ async def test_holidy_summer_mode( assert state assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False assert state.attributes[ATTR_STATE_SUMMER_MODE] - assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF] + assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF] assert state.attributes[ATTR_PRESET_MODE] == PRESET_SUMMER assert state.attributes[ATTR_PRESET_MODES] == [PRESET_SUMMER] From cbeefdaf26e0bf0940df05745375e94f71517d92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 May 2025 16:26:22 +0200 Subject: [PATCH 0831/1175] Mark humidifier methods and properties as mandatory in pylint plugin (#145507) --- homeassistant/components/tuya/humidifier.py | 2 +- pylint/plugins/hass_enforce_type_hints.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 36fcf8f52aa..f8fd9237ffc 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -190,6 +190,6 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): ] ) - def set_mode(self, mode): + def set_mode(self, mode: str) -> None: """Set new target preset mode.""" self._send_command([{"code": DPCode.MODE, "value": mode}]) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 57dff037f56..45a3e41f91a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1753,42 +1753,51 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { TypeHintMatch( function_name="available_modes", return_type=["list[str]", None], + mandatory=True, ), TypeHintMatch( function_name="device_class", return_type=["HumidifierDeviceClass", None], + mandatory=True, ), TypeHintMatch( function_name="min_humidity", return_type=["float"], + mandatory=True, ), TypeHintMatch( function_name="max_humidity", return_type=["float"], + mandatory=True, ), TypeHintMatch( function_name="mode", return_type=["str", None], + mandatory=True, ), TypeHintMatch( function_name="supported_features", return_type="HumidifierEntityFeature", + mandatory=True, ), TypeHintMatch( function_name="target_humidity", return_type=["float", None], + mandatory=True, ), TypeHintMatch( function_name="set_humidity", arg_types={1: "int"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), TypeHintMatch( function_name="set_mode", arg_types={1: "str"}, return_type=None, has_async_counterpart=True, + mandatory=True, ), ], ), From 199c565bf245e4f6370fa4e31c6f22ec302d4832 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Fri, 23 May 2025 17:31:44 +0300 Subject: [PATCH 0832/1175] Add Anthropic Claude 4 support (#145505) Add Claude 4 support Co-authored-by: Franck Nijhof --- homeassistant/components/anthropic/const.py | 9 ++- .../components/anthropic/conversation.py | 2 + .../components/anthropic/test_conversation.py | 59 +++++++++++++++---- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py index 38e4270e6e1..69789b9a64a 100644 --- a/homeassistant/components/anthropic/const.py +++ b/homeassistant/components/anthropic/const.py @@ -17,4 +17,11 @@ CONF_THINKING_BUDGET = "thinking_budget" RECOMMENDED_THINKING_BUDGET = 0 MIN_THINKING_BUDGET = 1024 -THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-3-7-sonnet-latest"] +THINKING_MODELS = [ + "claude-3-7-sonnet-20250219", + "claude-3-7-sonnet-latest", + "claude-opus-4-20250514", + "claude-opus-4-0", + "claude-sonnet-4-20250514", + "claude-sonnet-4-0", +] diff --git a/homeassistant/components/anthropic/conversation.py b/homeassistant/components/anthropic/conversation.py index bfdd4bfd361..3e79be0b169 100644 --- a/homeassistant/components/anthropic/conversation.py +++ b/homeassistant/components/anthropic/conversation.py @@ -294,6 +294,8 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have elif isinstance(response, RawMessageDeltaEvent): if (usage := response.usage) is not None: chat_log.async_trace(_create_token_stats(input_usage, usage)) + if response.delta.stop_reason == "refusal": + raise HomeAssistantError("Potential policy violation detected") elif isinstance(response, RawMessageStopEvent): if current_message is not None: messages.append(current_message) diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 8706abf36c0..3e01e91976d 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -52,7 +52,7 @@ async def stream_generator( def create_messages( - content_blocks: list[RawMessageStreamEvent], + content_blocks: list[RawMessageStreamEvent], stop_reason="end_turn" ) -> list[RawMessageStreamEvent]: """Create a stream of messages with the specified content blocks.""" return [ @@ -70,7 +70,7 @@ def create_messages( *content_blocks, RawMessageDeltaEvent( type="message_delta", - delta=Delta(stop_reason="end_turn", stop_sequence=""), + delta=Delta(stop_reason=stop_reason, stop_sequence=""), usage=MessageDeltaUsage(output_tokens=0), ), RawMessageStopEvent(type="message_stop"), @@ -221,7 +221,7 @@ async def test_error_handling( hass, "hello", None, Context(), agent_id="conversation.claude" ) - assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.response_type == intent.IntentResponseType.ERROR assert result.response.error_code == "unknown", result @@ -247,7 +247,7 @@ async def test_template_error( hass, "hello", None, Context(), agent_id="conversation.claude" ) - assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.response_type == intent.IntentResponseType.ERROR assert result.response.error_code == "unknown", result @@ -289,9 +289,7 @@ async def test_template_variables( hass, "hello", None, context, agent_id="conversation.claude" ) - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE, ( - result - ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert ( result.response.speech["plain"]["speech"] == "Okay, let me take care of that for you." @@ -369,7 +367,8 @@ async def test_function_call( "test_tool", tool_call_json_parts, ), - ] + ], + stop_reason="tool_use", ) ) @@ -468,7 +467,8 @@ async def test_function_exception( "test_tool", ['{"param1": "test_value"}'], ), - ] + ], + stop_reason="tool_use", ) ) @@ -629,6 +629,44 @@ async def test_conversation_id( assert result.conversation_id == "koala" +async def test_refusal( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, +) -> None: + """Test refusal due to potential policy violation.""" + with patch( + "anthropic.resources.messages.AsyncMessages.create", + new_callable=AsyncMock, + return_value=stream_generator( + create_messages( + [ + *create_content_block( + 0, + ["Certainly! To take over the world you need just a simple "], + ), + ], + stop_reason="refusal", + ), + ), + ): + result = await conversation.async_converse( + hass, + "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD" + "2631EDCF22E8CCC1FB35B501C9C86", + None, + Context(), + agent_id="conversation.claude", + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == "unknown" + assert ( + result.response.speech["plain"]["speech"] + == "Potential policy violation detected" + ) + + async def test_extended_thinking( hass: HomeAssistant, mock_config_entry_with_extended_thinking: MockConfigEntry, @@ -766,7 +804,8 @@ async def test_extended_thinking_tool_call( "test_tool", ['{"para', 'm1": "test_valu', 'e"}'], ), - ] + ], + stop_reason="tool_use", ) ) From 5048d1512c8eacf6d4b0f0d4dd99dd13f82a50d6 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 23 May 2025 10:32:21 -0400 Subject: [PATCH 0833/1175] Add trigger based template cover (#145455) * Add trigger based template cover * address comments * update position template in test --- homeassistant/components/template/config.py | 1 - homeassistant/components/template/cover.py | 85 +++- tests/components/template/test_cover.py | 470 ++++++++++++++++---- 3 files changed, 464 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index f1b58ebffa0..e87c9aee989 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -159,7 +159,6 @@ CONFIG_SECTION_SCHEMA = vol.All( ensure_domains_do_not_have_trigger_or_action( DOMAIN_ALARM_CONTROL_PANEL, DOMAIN_BUTTON, - DOMAIN_COVER, DOMAIN_FAN, DOMAIN_LOCK, DOMAIN_VACUUM, diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 1eb80677f7e..0b2009e83e3 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, + DOMAIN as COVER_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA as COVER_PLATFORM_SCHEMA, CoverEntity, @@ -35,6 +36,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import TriggerUpdateCoordinator from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN from .entity import AbstractTemplateEntity from .template_entity import ( @@ -45,6 +47,7 @@ from .template_entity import ( TemplateEntity, rewrite_common_legacy_to_modern_conf, ) +from .trigger_entity import TriggerEntity _LOGGER = logging.getLogger(__name__) @@ -207,6 +210,13 @@ async def async_setup_platform( ) return + if "coordinator" in discovery_info: + async_add_entities( + TriggerCoverEntity(hass, discovery_info["coordinator"], config) + for config in discovery_info["entities"] + ) + return + _async_create_template_tracking_entities( async_add_entities, hass, @@ -239,7 +249,13 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity): self._is_closing = False self._tilt_value: int | None = None - def _register_scripts( + # The config requires (open and close scripts) or a set position script, + # therefore the base supported features will always include them. + self._attr_supported_features: CoverEntityFeature = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + def _iterate_scripts( self, config: dict[str, Any] ) -> Generator[tuple[str, Sequence[dict[str, Any]], CoverEntityFeature | int]]: for action_id, supported_feature in ( @@ -459,13 +475,7 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover): if TYPE_CHECKING: assert name is not None - # The config requires (open and close scripts) or a set position script, - # therefore the base supported features will always include them. - self._attr_supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - ) - - for action_id, action_config, supported_feature in self._register_scripts( + for action_id, action_config, supported_feature in self._iterate_scripts( config ): self.add_script(action_id, action_config, name, DOMAIN) @@ -504,3 +514,62 @@ class CoverTemplate(TemplateEntity, AbstractTemplateCover): return self._update_opening_and_closing(result) + + +class TriggerCoverEntity(TriggerEntity, AbstractTemplateCover): + """Cover entity based on trigger data.""" + + domain = COVER_DOMAIN + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateCover.__init__(self, config) + + # Render the _attr_name before initializing TriggerCoverEntity + self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME) + + for action_id, action_config, supported_feature in self._iterate_scripts( + config + ): + self.add_script(action_id, action_config, name, DOMAIN) + self._attr_supported_features |= supported_feature + + for key in (CONF_STATE, CONF_POSITION, CONF_TILT): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, updater in ( + (CONF_STATE, self._update_opening_and_closing), + (CONF_POSITION, self._update_position), + (CONF_TILT, self._update_tilt), + ): + if (rendered := self._rendered.get(key)) is not None: + updater(rendered) + write_ha_state = True + + if not self._optimistic: + self.async_set_context(self.coordinator.data["context"]) + write_ha_state = True + elif self._optimistic and len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_write_ha_state() diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 5f28a977867..48f45d879cd 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -40,6 +40,22 @@ TEST_OBJECT_ID = "test_template_cover" TEST_ENTITY_ID = f"cover.{TEST_OBJECT_ID}" TEST_STATE_ENTITY_ID = "cover.test_state" +TEST_STATE_TRIGGER = { + "trigger": { + "trigger": "state", + "entity_id": [ + "cover.test_state", + "cover.test_position", + "binary_sensor.garage_door_sensor", + ], + }, + "variables": {"triggering_entity": "{{ trigger.entity_id }}"}, + "action": [ + {"event": "action_event", "event_data": {"what": "{{ triggering_entity}}"}} + ], +} + + OPEN_COVER = { "service": "test.automation", "data_template": { @@ -123,6 +139,24 @@ async def async_setup_modern_format( await hass.async_block_till_done() +async def async_setup_trigger_format( + hass: HomeAssistant, count: int, cover_config: dict[str, Any] +) -> None: + """Do setup of cover integration via trigger format.""" + config = {"template": {**TEST_STATE_TRIGGER, "cover": cover_config}} + + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + async def async_setup_cover_config( hass: HomeAssistant, count: int, @@ -134,6 +168,8 @@ async def async_setup_cover_config( await async_setup_legacy_format(hass, count, cover_config) elif style == ConfigurationStyle.MODERN: await async_setup_modern_format(hass, count, cover_config) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format(hass, count, cover_config) @pytest.fixture @@ -175,6 +211,15 @@ async def setup_state_cover( "state": state_template, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + }, + ) @pytest.fixture @@ -205,6 +250,15 @@ async def setup_position_cover( "position": position_template, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "position": position_template, + }, + ) @pytest.fixture @@ -240,13 +294,57 @@ async def setup_single_attribute_state_cover( **extra, }, ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + { + **NAMED_COVER_ACTIONS, + "state": state_template, + **extra, + }, + ) + + +@pytest.fixture +async def setup_empty_action( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + script: str, +): + """Do setup of cover integration using a empty actions template.""" + empty = { + "open_cover": [], + "close_cover": [], + script: [], + } + if style == ConfigurationStyle.LEGACY: + await async_setup_legacy_format( + hass, + count, + {TEST_OBJECT_ID: empty}, + ) + elif style == ConfigurationStyle.MODERN: + await async_setup_modern_format( + hass, + count, + {"name": TEST_OBJECT_ID, **empty}, + ) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_trigger_format( + hass, + count, + {"name": TEST_OBJECT_ID, **empty}, + ) @pytest.mark.parametrize( ("count", "state_template"), [(1, "{{ states.cover.test_state.state }}")] ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("set_state", "test_state", "text"), @@ -260,13 +358,13 @@ async def setup_single_attribute_state_cover( ("bear", STATE_UNKNOWN, "Received invalid cover is_on state: bear"), ], ) +@pytest.mark.usefixtures("setup_state_cover") async def test_template_state_text( hass: HomeAssistant, set_state: str, test_state: str, text: str, caplog: pytest.LogCaptureFixture, - setup_state_cover, ) -> None: """Test the state text of a template.""" state = hass.states.get(TEST_ENTITY_ID) @@ -280,6 +378,36 @@ async def test_template_state_text( assert text in caplog.text +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) +@pytest.mark.parametrize( + ("state_template", "expected"), + [ + ("{{ 'open' }}", CoverState.OPEN), + ("{{ 'closed' }}", CoverState.CLOSED), + ("{{ 'opening' }}", CoverState.OPENING), + ("{{ 'closing' }}", CoverState.CLOSING), + ("{{ 'dog' }}", STATE_UNKNOWN), + ("{{ x - 1 }}", STATE_UNAVAILABLE), + ], +) +@pytest.mark.usefixtures("setup_state_cover") +async def test_template_state_states( + hass: HomeAssistant, + expected: str, +) -> None: + """Test state template states.""" + + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected + + @pytest.mark.parametrize( ("count", "state_template", "attribute_template"), [ @@ -295,6 +423,7 @@ async def test_template_state_text( [ (ConfigurationStyle.LEGACY, "position_template"), (ConfigurationStyle.MODERN, "position"), + (ConfigurationStyle.TRIGGER, "position"), ], ) @pytest.mark.parametrize( @@ -332,11 +461,11 @@ async def test_template_state_text( ) ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_template_state_text_with_position( hass: HomeAssistant, states: list[tuple[str, str, str, int | None]], caplog: pytest.LogCaptureFixture, - setup_single_attribute_state_cover, ) -> None: """Test the state of a position template in order.""" state = hass.states.get(TEST_ENTITY_ID) @@ -361,7 +490,7 @@ async def test_template_state_text_with_position( ( 1, "{{ states.cover.test_state.state }}", - "{{ states.cover.test_position.attributes.position }}", + "{{ state_attr('cover.test_state', 'position') }}", ) ], ) @@ -370,6 +499,7 @@ async def test_template_state_text_with_position( [ (ConfigurationStyle.LEGACY, "position_template"), (ConfigurationStyle.MODERN, "position"), + (ConfigurationStyle.TRIGGER, "position"), ], ) @pytest.mark.parametrize( @@ -379,11 +509,10 @@ async def test_template_state_text_with_position( None, ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_template_state_text_ignored_if_none_or_empty( hass: HomeAssistant, set_state: str, - caplog: pytest.LogCaptureFixture, - setup_single_attribute_state_cover, ) -> None: """Test ignoring an empty state text of a template.""" state = hass.states.get(TEST_ENTITY_ID) @@ -393,15 +522,20 @@ async def test_template_state_text_ignored_if_none_or_empty( await hass.async_block_till_done() state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN - assert "ERROR" not in caplog.text @pytest.mark.parametrize(("count", "state_template"), [(1, "{{ 1 == 1 }}")]) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> None: +@pytest.mark.usefixtures("setup_state_cover") +async def test_template_state_boolean(hass: HomeAssistant) -> None: """Test the value_template attribute.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.OPEN @@ -411,7 +545,8 @@ async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> [(1, "{{ states.cover.test_state.attributes.position }}")], ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) @pytest.mark.parametrize( ("test_state", "position", "expected"), @@ -421,13 +556,13 @@ async def test_template_state_boolean(hass: HomeAssistant, setup_state_cover) -> (CoverState.CLOSED, None, STATE_UNKNOWN), ], ) +@pytest.mark.usefixtures("setup_position_cover") async def test_template_position( hass: HomeAssistant, test_state: str, position: int | None, expected: str, caplog: pytest.LogCaptureFixture, - setup_position_cover, ) -> None: """Test the position_template attribute.""" hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) @@ -464,9 +599,17 @@ async def test_template_position( "optimistic": False, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **NAMED_COVER_ACTIONS, + "optimistic": False, + }, + ), ], ) -async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_template_not_optimistic(hass: HomeAssistant) -> None: """Test the is_closed attribute.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN @@ -484,6 +627,10 @@ async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None ConfigurationStyle.MODERN, "tilt", ), + ( + ConfigurationStyle.TRIGGER, + "tilt", + ), ], ) @pytest.mark.parametrize( @@ -498,10 +645,13 @@ async def test_template_not_optimistic(hass: HomeAssistant, setup_cover) -> None ("{{ 'on' }}", None), ], ) -async def test_template_tilt( - hass: HomeAssistant, tilt_position: float | None, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_template_tilt(hass: HomeAssistant, tilt_position: float | None) -> None: """Test tilt in and out-of-bound conditions.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_tilt_position") == tilt_position @@ -518,6 +668,10 @@ async def test_template_tilt( ConfigurationStyle.MODERN, "position", ), + ( + ConfigurationStyle.TRIGGER, + "position", + ), ], ) @pytest.mark.parametrize( @@ -529,10 +683,13 @@ async def test_template_tilt( "{{ 'off' }}", ], ) -async def test_position_out_of_bounds( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_position_out_of_bounds(hass: HomeAssistant) -> None: """Test position out-of-bounds condition.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("current_position") is None @@ -577,6 +734,23 @@ async def test_position_out_of_bounds( }, "Invalid config for 'template': some but not all values in the same group of inclusion 'open_or_close'", ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "state": "{{ 1 == 1 }}", + }, + "Invalid config for 'template': must contain at least one of open_cover, set_cover_position.", + ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "state": "{{ 1 == 1 }}", + "open_cover": OPEN_COVER, + }, + "Invalid config for 'template': some but not all values in the same group of inclusion 'open_or_close'", + ), ], ) async def test_template_open_or_position( @@ -598,12 +772,17 @@ async def test_template_open_or_position( [(1, "{{ 0 }}")], ) @pytest.mark.parametrize( - "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN] + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_open_action( - hass: HomeAssistant, setup_position_cover, calls: list[ServiceCall] -) -> None: +@pytest.mark.usefixtures("setup_position_cover") +async def test_open_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the open_cover command.""" + + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.CLOSED @@ -654,12 +833,29 @@ async def test_open_action( }, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **NAMED_COVER_ACTIONS, + "position": "{{ 100 }}", + "stop_cover": { + "service": "test.automation", + "data_template": { + "action": "stop_cover", + "caller": "{{ this.entity_id }}", + }, + }, + }, + ), ], ) -async def test_close_stop_action( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] -) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_close_stop_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the close-cover and stop_cover commands.""" + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, None) + await hass.async_block_till_done() + state = hass.states.get(TEST_ENTITY_ID) assert state.state == CoverState.OPEN @@ -705,11 +901,17 @@ async def test_close_stop_action( "set_cover_position": SET_COVER_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), ], ) -async def test_set_position( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] -) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_set_position(hass: HomeAssistant, calls: list[ServiceCall]) -> None: """Test the set_position command.""" state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_UNKNOWN @@ -799,6 +1001,13 @@ async def test_set_position( "set_cover_tilt_position": SET_COVER_TILT_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + **NAMED_COVER_ACTIONS, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) @pytest.mark.parametrize( @@ -813,12 +1022,12 @@ async def test_set_position( (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, 0), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_tilt_position( hass: HomeAssistant, service, attr, tilt_position, - setup_cover, calls: list[ServiceCall], ) -> None: """Test the set_tilt_position command.""" @@ -855,10 +1064,18 @@ async def test_set_tilt_position( "set_cover_position": SET_COVER_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + }, + ), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_position_optimistic( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] + hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test optimistic position mode.""" state = hass.states.get(TEST_ENTITY_ID) @@ -888,6 +1105,50 @@ async def test_set_position_optimistic( assert state.state == test_state +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("style", "cover_config"), + [ + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "set_cover_position": SET_COVER_POSITION, + "picture": "{{ 'foo.png' if is_state('cover.test_state', 'open') else 'bar.png' }}", + }, + ), + ], +) +@pytest.mark.usefixtures("setup_cover") +async def test_non_optimistic_template_with_optimistic_state( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test optimistic state with non-optimistic template.""" + state = hass.states.get(TEST_ENTITY_ID) + assert "entity_picture" not in state.attributes + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_POSITION: 42}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + assert state.attributes["current_position"] == 42.0 + assert "entity_picture" not in state.attributes + + hass.states.async_set(TEST_STATE_ENTITY_ID, CoverState.OPEN) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == CoverState.OPEN + assert state.attributes["current_position"] == 42.0 + assert state.attributes["entity_picture"] == "foo.png" + + @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( ("style", "cover_config"), @@ -911,10 +1172,20 @@ async def test_set_position_optimistic( "set_cover_tilt_position": SET_COVER_TILT_POSITION, }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": TEST_OBJECT_ID, + "position": "{{ 100 }}", + "set_cover_position": SET_COVER_POSITION, + "set_cover_tilt_position": SET_COVER_TILT_POSITION, + }, + ), ], ) +@pytest.mark.usefixtures("setup_cover") async def test_set_tilt_position_optimistic( - hass: HomeAssistant, setup_cover, calls: list[ServiceCall] + hass: HomeAssistant, calls: list[ServiceCall] ) -> None: """Test the optimistic tilt_position mode.""" state = hass.states.get(TEST_ENTITY_ID) @@ -955,18 +1226,20 @@ async def test_set_tilt_position_optimistic( ], ) @pytest.mark.parametrize( - ("style", "attribute"), + ("style", "attribute", "initial_expected_state"), [ - (ConfigurationStyle.LEGACY, "icon_template"), - (ConfigurationStyle.MODERN, "icon"), + (ConfigurationStyle.LEGACY, "icon_template", ""), + (ConfigurationStyle.MODERN, "icon", ""), + (ConfigurationStyle.TRIGGER, "icon", None), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_icon_template( - hass: HomeAssistant, setup_single_attribute_state_cover + hass: HomeAssistant, initial_expected_state: str | None ) -> None: """Test icon template.""" state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get("icon") == "" + assert state.attributes.get("icon") == initial_expected_state state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() @@ -987,18 +1260,20 @@ async def test_icon_template( ], ) @pytest.mark.parametrize( - ("style", "attribute"), + ("style", "attribute", "initial_expected_state"), [ - (ConfigurationStyle.LEGACY, "entity_picture_template"), - (ConfigurationStyle.MODERN, "picture"), + (ConfigurationStyle.LEGACY, "entity_picture_template", ""), + (ConfigurationStyle.MODERN, "picture", ""), + (ConfigurationStyle.TRIGGER, "picture", None), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_entity_picture_template( - hass: HomeAssistant, setup_single_attribute_state_cover + hass: HomeAssistant, initial_expected_state: str | None ) -> None: """Test icon template.""" state = hass.states.get(TEST_ENTITY_ID) - assert state.attributes.get("entity_picture") == "" + assert state.attributes.get("entity_picture") == initial_expected_state state = hass.states.async_set("cover.test_state", CoverState.OPEN) await hass.async_block_till_done() @@ -1023,18 +1298,22 @@ async def test_entity_picture_template( [ (ConfigurationStyle.LEGACY, "availability_template"), (ConfigurationStyle.MODERN, "availability"), + (ConfigurationStyle.TRIGGER, "availability"), ], ) -async def test_availability_template( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_availability_template(hass: HomeAssistant) -> None: """Test availability template.""" hass.states.async_set("availability_state.state", STATE_OFF) + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_OFF) await hass.async_block_till_done() assert hass.states.get(TEST_ENTITY_ID).state == STATE_UNAVAILABLE hass.states.async_set("availability_state.state", STATE_ON) + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) await hass.async_block_till_done() assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE @@ -1071,15 +1350,35 @@ async def test_availability_template( }, template.DOMAIN, ), + ( + { + "template": { + **TEST_STATE_TRIGGER, + "cover": { + **NAMED_COVER_ACTIONS, + "state": "{{ true }}", + "availability": "{{ x - 12 }}", + }, + } + }, + template.DOMAIN, + ), ], ) @pytest.mark.usefixtures("start_ha") async def test_invalid_availability_template_keeps_component_available( - hass: HomeAssistant, caplog_setup_text + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, caplog_setup_text ) -> None: """Test that an invalid availability keeps the device available.""" + + # This forces a trigger for trigger based entities + hass.states.async_set(TEST_STATE_ENTITY_ID, STATE_ON) + await hass.async_block_till_done() + assert hass.states.get(TEST_ENTITY_ID) != STATE_UNAVAILABLE - assert "UndefinedError: 'x' is undefined" in caplog_setup_text + + err = "UndefinedError: 'x' is undefined" + assert err in caplog_setup_text or err in caplog.text @pytest.mark.parametrize( @@ -1088,11 +1387,10 @@ async def test_invalid_availability_template_keeps_component_available( ) @pytest.mark.parametrize( "style", - [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_device_class( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_device_class(hass: HomeAssistant) -> None: """Test device class.""" state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("device_class") == "door" @@ -1104,11 +1402,10 @@ async def test_device_class( ) @pytest.mark.parametrize( "style", - [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN], + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], ) -async def test_invalid_device_class( - hass: HomeAssistant, setup_single_attribute_state_cover -) -> None: +@pytest.mark.usefixtures("setup_single_attribute_state_cover") +async def test_invalid_device_class(hass: HomeAssistant) -> None: """Test device class.""" state = hass.states.get(TEST_ENTITY_ID) assert not state @@ -1138,9 +1435,23 @@ async def test_invalid_device_class( ], ConfigurationStyle.MODERN, ), + ( + [ + { + "name": "test_template_cover_01", + **UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_cover_02", + **UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), ], ) -async def test_unique_id(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_unique_id(hass: HomeAssistant) -> None: """Test unique_id option only creates one cover per id.""" assert len(hass.states.async_all()) == 1 @@ -1211,9 +1522,18 @@ async def test_nested_unique_id( "state": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", }, ), + ( + ConfigurationStyle.TRIGGER, + { + "name": "Garage Door", + **COVER_ACTIONS, + "state": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + }, + ), ], ) -async def test_state_gets_lowercased(hass: HomeAssistant, setup_cover) -> None: +@pytest.mark.usefixtures("setup_cover") +async def test_state_gets_lowercased(hass: HomeAssistant) -> None: """Test True/False is lowercased.""" hass.states.async_set("binary_sensor.garage_door_sensor", "off") @@ -1242,12 +1562,12 @@ async def test_state_gets_lowercased(hass: HomeAssistant, setup_cover) -> None: [ (ConfigurationStyle.LEGACY, "icon_template"), (ConfigurationStyle.MODERN, "icon"), + (ConfigurationStyle.TRIGGER, "icon"), ], ) +@pytest.mark.usefixtures("setup_single_attribute_state_cover") async def test_self_referencing_icon_with_no_template_is_not_a_loop( - hass: HomeAssistant, - setup_single_attribute_state_cover, - caplog: pytest.LogCaptureFixture, + hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test a self referencing icon with no value template is not a loop.""" assert len(hass.states.async_all()) == 1 @@ -1255,6 +1575,11 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( assert "Template loop detected" not in caplog.text +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + "style", + [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER], +) @pytest.mark.parametrize( ("script", "supported_feature"), [ @@ -1269,32 +1594,11 @@ async def test_self_referencing_icon_with_no_template_is_not_a_loop( ), ], ) -async def test_emtpy_action_config( - hass: HomeAssistant, script: str, supported_feature: CoverEntityFeature +@pytest.mark.usefixtures("setup_empty_action") +async def test_empty_action_config( + hass: HomeAssistant, supported_feature: CoverEntityFeature ) -> None: """Test configuration with empty script.""" - with assert_setup_component(1, COVER_DOMAIN): - assert await async_setup_component( - hass, - COVER_DOMAIN, - { - COVER_DOMAIN: { - "platform": "template", - "covers": { - "test_template_cover": { - "open_cover": [], - "close_cover": [], - script: [], - } - }, - } - }, - ) - - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - state = hass.states.get("cover.test_template_cover") assert ( state.attributes["supported_features"] From 086e97821f788e345d78c1f3424acb91dcdcd154 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 23 May 2025 17:01:57 +0200 Subject: [PATCH 0834/1175] Add automatic backup event entity to Home Assistant Backup system (#145350) * add automatic backup event entity * add tests * fix test * Apply suggestions from code review Co-authored-by: Josef Zweck * implement _handle_coordinator_update * add translations for event attributes * simplify condition * Apply suggestions from code review Co-authored-by: Martin Hjelmare --------- Co-authored-by: Josef Zweck Co-authored-by: Martin Hjelmare --- homeassistant/components/backup/__init__.py | 2 +- .../components/backup/coordinator.py | 4 + homeassistant/components/backup/entity.py | 19 +++- homeassistant/components/backup/event.py | 59 ++++++++++++ homeassistant/components/backup/icons.json | 7 ++ homeassistant/components/backup/strings.json | 16 ++++ .../backup/snapshots/test_event.ambr | 60 ++++++++++++ tests/components/backup/test_event.py | 95 +++++++++++++++++++ 8 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/backup/event.py create mode 100644 tests/components/backup/snapshots/test_event.ambr create mode 100644 tests/components/backup/test_event.py diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 9e013d72d60..daf9337a8a8 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -81,7 +81,7 @@ __all__ = [ "suggested_filename_from_name_date", ] -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.EVENT, Platform.SENSOR] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) diff --git a/homeassistant/components/backup/coordinator.py b/homeassistant/components/backup/coordinator.py index dba05ba0225..3f6146f68d7 100644 --- a/homeassistant/components/backup/coordinator.py +++ b/homeassistant/components/backup/coordinator.py @@ -33,6 +33,7 @@ class BackupCoordinatorData: last_attempted_automatic_backup: datetime | None last_successful_automatic_backup: datetime | None next_scheduled_automatic_backup: datetime | None + last_event: ManagerStateEvent | BackupPlatformEvent | None class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]): @@ -60,11 +61,13 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]): ] self.backup_manager = backup_manager + self._last_event: ManagerStateEvent | BackupPlatformEvent | None = None @callback def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None: """Handle new event.""" LOGGER.debug("Received backup event: %s", event) + self._last_event = event self.config_entry.async_create_task(self.hass, self.async_refresh()) async def _async_update_data(self) -> BackupCoordinatorData: @@ -74,6 +77,7 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]): self.backup_manager.config.data.last_attempted_automatic_backup, self.backup_manager.config.data.last_completed_automatic_backup, self.backup_manager.config.data.schedule.next_automatic_backup, + self._last_event, ) @callback diff --git a/homeassistant/components/backup/entity.py b/homeassistant/components/backup/entity.py index ff7c7889dc5..f07a6a4e4dc 100644 --- a/homeassistant/components/backup/entity.py +++ b/homeassistant/components/backup/entity.py @@ -11,7 +11,7 @@ from .const import DOMAIN from .coordinator import BackupDataUpdateCoordinator -class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]): +class BackupManagerBaseEntity(CoordinatorEntity[BackupDataUpdateCoordinator]): """Base entity for backup manager.""" _attr_has_entity_name = True @@ -19,12 +19,9 @@ class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]): def __init__( self, coordinator: BackupDataUpdateCoordinator, - entity_description: EntityDescription, ) -> None: """Initialize base entity.""" super().__init__(coordinator) - self.entity_description = entity_description - self._attr_unique_id = entity_description.key self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, "backup_manager")}, manufacturer="Home Assistant", @@ -34,3 +31,17 @@ class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]): entry_type=DeviceEntryType.SERVICE, configuration_url="homeassistant://config/backup", ) + + +class BackupManagerEntity(BackupManagerBaseEntity): + """Entity for backup manager.""" + + def __init__( + self, + coordinator: BackupDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = entity_description.key diff --git a/homeassistant/components/backup/event.py b/homeassistant/components/backup/event.py new file mode 100644 index 00000000000..17c89339148 --- /dev/null +++ b/homeassistant/components/backup/event.py @@ -0,0 +1,59 @@ +"""Event platform for Home Assistant Backup integration.""" + +from __future__ import annotations + +from typing import Final + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator +from .entity import BackupManagerBaseEntity +from .manager import CreateBackupEvent, CreateBackupState + +ATTR_BACKUP_STAGE: Final[str] = "backup_stage" +ATTR_FAILED_REASON: Final[str] = "failed_reason" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: BackupConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Event set up for backup config entry.""" + coordinator = config_entry.runtime_data + async_add_entities([AutomaticBackupEvent(coordinator)]) + + +class AutomaticBackupEvent(BackupManagerBaseEntity, EventEntity): + """Representation of an automatic backup event.""" + + _attr_event_types = [s.value for s in CreateBackupState] + _unrecorded_attributes = frozenset({ATTR_FAILED_REASON, ATTR_BACKUP_STAGE}) + coordinator: BackupDataUpdateCoordinator + + def __init__(self, coordinator: BackupDataUpdateCoordinator) -> None: + """Initialize the automatic backup event.""" + super().__init__(coordinator) + self._attr_unique_id = "automatic_backup_event" + self._attr_translation_key = "automatic_backup_event" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if ( + not (data := self.coordinator.data) + or (event := data.last_event) is None + or not isinstance(event, CreateBackupEvent) + ): + return + + self._trigger_event( + event.state, + { + ATTR_BACKUP_STAGE: event.stage, + ATTR_FAILED_REASON: event.reason, + }, + ) + self.async_write_ha_state() diff --git a/homeassistant/components/backup/icons.json b/homeassistant/components/backup/icons.json index 8a412f66edc..6ba50780cda 100644 --- a/homeassistant/components/backup/icons.json +++ b/homeassistant/components/backup/icons.json @@ -1,4 +1,11 @@ { + "entity": { + "event": { + "automatic_backup_event": { + "default": "mdi:database" + } + } + }, "services": { "create": { "service": "mdi:cloud-upload" diff --git a/homeassistant/components/backup/strings.json b/homeassistant/components/backup/strings.json index 33a027d75e2..1b04542dbae 100644 --- a/homeassistant/components/backup/strings.json +++ b/homeassistant/components/backup/strings.json @@ -36,6 +36,22 @@ } }, "entity": { + "event": { + "automatic_backup_event": { + "name": "Automatic backup", + "state_attributes": { + "event_type": { + "state": { + "completed": "Completed successfully", + "failed": "Failed", + "in_progress": "In progress" + } + }, + "backup_stage": { "name": "Backup stage" }, + "failed_reason": { "name": "Failure reason" } + } + } + }, "sensor": { "backup_manager_state": { "name": "Backup Manager state", diff --git a/tests/components/backup/snapshots/test_event.ambr b/tests/components/backup/snapshots/test_event.ambr new file mode 100644 index 00000000000..6ee11c808ad --- /dev/null +++ b/tests/components/backup/snapshots/test_event.ambr @@ -0,0 +1,60 @@ +# serializer version: 1 +# name: test_event_entity[event.backup_automatic_backup-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'completed', + 'failed', + 'in_progress', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.backup_automatic_backup', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Automatic backup', + 'platform': 'backup', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'automatic_backup_event', + 'unique_id': 'automatic_backup_event', + 'unit_of_measurement': None, + }) +# --- +# name: test_event_entity[event.backup_automatic_backup-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'completed', + 'failed', + 'in_progress', + ]), + 'friendly_name': 'Backup Automatic backup', + }), + 'context': , + 'entity_id': 'event.backup_automatic_backup', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/backup/test_event.py b/tests/components/backup/test_event.py new file mode 100644 index 00000000000..dc7f57018bb --- /dev/null +++ b/tests/components/backup/test_event.py @@ -0,0 +1,95 @@ +"""The tests for the Backup event entity.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.backup.const import DOMAIN +from homeassistant.components.backup.event import ATTR_BACKUP_STAGE, ATTR_FAILED_REASON +from homeassistant.components.event import ATTR_EVENT_TYPE +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .common import setup_backup_integration + +from tests.common import snapshot_platform +from tests.typing import WebSocketGenerator + + +@pytest.mark.usefixtures("mock_backup_generation") +async def test_event_entity( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test automatic backup event entity.""" + with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]): + await setup_backup_integration(hass, with_hassio=False) + await hass.async_block_till_done(wait_background_tasks=True) + + entry = hass.config_entries.async_entries(DOMAIN)[0] + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +@pytest.mark.usefixtures("mock_backup_generation") +async def test_event_entity_backup_completed( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test completed automatic backup event.""" + with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]): + await setup_backup_integration(hass, with_hassio=False) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("event.backup_automatic_backup") + assert state.attributes[ATTR_EVENT_TYPE] is None + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + await client.send_json_auto_id( + {"type": "backup/generate", "agent_ids": ["backup.local"]} + ) + assert await client.receive_json() + + state = hass.states.get("event.backup_automatic_backup") + assert state.attributes[ATTR_EVENT_TYPE] == "in_progress" + assert state.attributes[ATTR_BACKUP_STAGE] is not None + assert state.attributes[ATTR_FAILED_REASON] is None + + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("event.backup_automatic_backup") + assert state.attributes[ATTR_EVENT_TYPE] == "completed" + assert state.attributes[ATTR_BACKUP_STAGE] is None + assert state.attributes[ATTR_FAILED_REASON] is None + + +@pytest.mark.usefixtures("mock_backup_generation") +async def test_event_entity_backup_failed( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + create_backup: AsyncMock, +) -> None: + """Test failed automatic backup event.""" + with patch("homeassistant.components.backup.PLATFORMS", [Platform.EVENT]): + await setup_backup_integration(hass, with_hassio=False) + await hass.async_block_till_done(wait_background_tasks=True) + + state = hass.states.get("event.backup_automatic_backup") + assert state.attributes[ATTR_EVENT_TYPE] is None + + create_backup.side_effect = Exception("Boom!") + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + await client.send_json_auto_id( + {"type": "backup/generate", "agent_ids": ["backup.local"]} + ) + assert await client.receive_json() + + state = hass.states.get("event.backup_automatic_backup") + assert state.attributes[ATTR_EVENT_TYPE] == "failed" + assert state.attributes[ATTR_BACKUP_STAGE] is None + assert state.attributes[ATTR_FAILED_REASON] == "unknown_error" From 83ec45e4fc2d1e2cfa61505e3836d8dccb16b4df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 May 2025 17:03:33 +0200 Subject: [PATCH 0835/1175] Use runtime_data in xiaomi_miio (#145517) * Use runtime_data in xiaomi_miio * Reduce changes --- .../components/xiaomi_miio/__init__.py | 51 +++++++++---------- .../components/xiaomi_miio/air_quality.py | 4 +- .../xiaomi_miio/alarm_control_panel.py | 8 +-- .../components/xiaomi_miio/binary_sensor.py | 24 +++++---- .../components/xiaomi_miio/button.py | 17 ++----- .../components/xiaomi_miio/config_flow.py | 12 ++--- homeassistant/components/xiaomi_miio/const.py | 3 -- .../components/xiaomi_miio/diagnostics.py | 13 +++-- homeassistant/components/xiaomi_miio/fan.py | 11 ++-- .../components/xiaomi_miio/humidifier.py | 19 +++---- homeassistant/components/xiaomi_miio/light.py | 10 ++-- .../components/xiaomi_miio/number.py | 13 +++-- .../components/xiaomi_miio/select.py | 11 ++-- .../components/xiaomi_miio/sensor.py | 33 +++++++----- .../components/xiaomi_miio/switch.py | 50 ++++++++++-------- .../components/xiaomi_miio/typing.py | 26 +++++++++- .../components/xiaomi_miio/vacuum.py | 11 ++-- 17 files changed, 161 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index d841045d235..0e28a2900bb 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -35,7 +35,6 @@ from miio import ( ) from miio.gateway.gateway import GatewayException -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -47,8 +46,6 @@ from .const import ( CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_FAN_1C, @@ -75,6 +72,7 @@ from .const import ( SetupException, ) from .gateway import ConnectXiaomiGateway +from .typing import XiaomiMiioConfigEntry, XiaomiMiioRuntimeData _LOGGER = logging.getLogger(__name__) @@ -125,9 +123,8 @@ MODEL_TO_CLASS_MAP = { } -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: XiaomiMiioConfigEntry) -> bool: """Set up the Xiaomi Miio components from a config entry.""" - hass.data.setdefault(DOMAIN, {}) if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: await async_setup_gateway_entry(hass, entry) return True @@ -291,14 +288,13 @@ def _async_update_data_vacuum( async def async_create_miio_device_and_coordinator( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: XiaomiMiioConfigEntry ) -> None: """Set up a data coordinator and one miio device to service multiple entities.""" model: str = entry.data[CONF_MODEL] host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title - device: MiioDevice | None = None migrate = False update_method = _async_update_data_default coordinator_class: type[DataUpdateCoordinator[Any]] = DataUpdateCoordinator @@ -323,6 +319,7 @@ async def async_create_miio_device_and_coordinator( _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) + device: MiioDevice # Humidifiers if model in MODELS_HUMIDIFIER_MIOT: device = AirHumidifierMiot(host, token, lazy_discover=lazy_discover) @@ -394,16 +391,18 @@ async def async_create_miio_device_and_coordinator( # Polling interval. Will only be polled if there are subscribers. update_interval=UPDATE_INTERVAL, ) - hass.data[DOMAIN][entry.entry_id] = { - KEY_DEVICE: device, - KEY_COORDINATOR: coordinator, - } # Trigger first data fetch await coordinator.async_config_entry_first_refresh() + entry.runtime_data = XiaomiMiioRuntimeData( + device=device, device_coordinator=coordinator + ) -async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + +async def async_setup_gateway_entry( + hass: HomeAssistant, entry: XiaomiMiioConfigEntry +) -> None: """Set up the Xiaomi Gateway component from a config entry.""" host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] @@ -461,17 +460,18 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> update_interval=UPDATE_INTERVAL, ) - hass.data[DOMAIN][entry.entry_id] = { - CONF_GATEWAY: gateway.gateway_device, - KEY_COORDINATOR: coordinator_dict, - } + entry.runtime_data = XiaomiMiioRuntimeData( + gateway=gateway.gateway_device, gateway_coordinators=coordinator_dict + ) await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) -async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_device_entry( + hass: HomeAssistant, entry: XiaomiMiioConfigEntry +) -> bool: """Set up the Xiaomi Miio device component from a config entry.""" platforms = get_platforms(entry) await async_create_miio_device_and_coordinator(hass, entry) @@ -486,20 +486,17 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, config_entry: XiaomiMiioConfigEntry +) -> bool: """Unload a config entry.""" platforms = get_platforms(config_entry) - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, platforms - ) - - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(config_entry, platforms) -async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def update_listener( + hass: HomeAssistant, config_entry: XiaomiMiioConfigEntry +) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 1ce37c661a2..4190f49e30c 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -6,7 +6,6 @@ import logging from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException from homeassistant.components.air_quality import AirQualityEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -19,6 +18,7 @@ from .const import ( MODEL_AIRQUALITYMONITOR_V1, ) from .entity import XiaomiMiioEntity +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -241,7 +241,7 @@ DEVICE_MAP: dict[str, dict[str, Callable]] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi Air Quality from a config entry.""" diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index ecab5228f6e..435253ae8d1 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -12,12 +12,12 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, AlarmControlPanelState, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import CONF_GATEWAY, DOMAIN +from .const import DOMAIN +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -28,12 +28,12 @@ XIAOMI_STATE_ARMING_VALUE = "oning" async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi Gateway Alarm from a config entry.""" entities = [] - gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] + gateway = config_entry.runtime_data.gateway entity = XiaomiGatewayAlarm( gateway, f"{config_entry.title} Alarm", diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 213886691f0..b0a990cf9be 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -5,13 +5,13 @@ from __future__ import annotations from collections.abc import Callable, Iterable from dataclasses import dataclass import logging +from typing import TYPE_CHECKING from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -19,9 +19,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import VacuumCoordinatorDataAttributes from .const import ( CONF_FLOW_TYPE, - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_FAN_ZA5, @@ -33,6 +30,7 @@ from .const import ( MODELS_VACUUM_WITH_SEPARATE_MOP, ) from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -133,13 +131,17 @@ HUMIDIFIER_MIOT_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) -def _setup_vacuum_sensors(hass, config_entry, async_add_entities): +def _setup_vacuum_sensors( + hass: HomeAssistant, + config_entry: XiaomiMiioConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Only vacuums with mop should have binary sensor registered.""" if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP: return - device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator entities = [] sensors = VACUUM_SENSORS @@ -147,6 +149,8 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): sensors = VACUUM_SENSORS_SEPARATE_MOP for sensor, description in sensors.items(): + if TYPE_CHECKING: + assert description.parent_key is not None parent_key_data = getattr(coordinator.data, description.parent_key) if getattr(parent_key_data, description.key, None) is None: _LOGGER.debug( @@ -170,7 +174,7 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi sensor from a config entry.""" @@ -198,10 +202,10 @@ async def async_setup_entry( continue entities.append( XiaomiGenericBinarySensor( - hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], + config_entry.runtime_data.device, config_entry, f"{description.key}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + config_entry.runtime_data.device_coordinator, description, ) ) diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index a7bcb3a12fe..194b73f2372 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -11,20 +11,13 @@ from homeassistant.components.button import ( ButtonEntity, ButtonEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, - MODEL_AIRFRESH_A1, - MODEL_AIRFRESH_T2017, - MODELS_VACUUM, -) +from .const import MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODELS_VACUUM from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry # Fans ATTR_RESET_DUST_FILTER = "reset_dust_filter" @@ -123,7 +116,7 @@ MODEL_TO_BUTTON_MAP: dict[str, tuple[str, ...]] = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the button from a config entry.""" @@ -135,8 +128,8 @@ async def async_setup_entry( entities = [] buttons = MODEL_TO_BUTTON_MAP[model] unique_id = config_entry.unique_id - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator for description in BUTTON_TYPES: if description.key not in buttons: diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index c3ebc48d743..b8d8b028006 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -11,12 +11,7 @@ from micloud import MiCloud from micloud.micloudexception import MiCloudAccessDenied import voluptuous as vol -from homeassistant.config_entries import ( - ConfigEntry, - ConfigFlow, - ConfigFlowResult, - OptionsFlow, -) +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TOKEN from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac @@ -40,6 +35,7 @@ from .const import ( SetupException, ) from .device import ConnectXiaomiDevice +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -116,7 +112,9 @@ class XiaomiMiioFlowHandler(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: + def async_get_options_flow( + config_entry: XiaomiMiioConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler() diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 2b9cdb2ffdd..0c188f20a02 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -27,9 +27,6 @@ CONF_MANUAL = "manual" # Options flow CONF_CLOUD_SUBDEVICES = "cloud_subdevices" -# Keys -KEY_COORDINATOR = "coordinator" -KEY_DEVICE = "device" # Attributes ATTR_AVAILABLE = "available" diff --git a/homeassistant/components/xiaomi_miio/diagnostics.py b/homeassistant/components/xiaomi_miio/diagnostics.py index 749bea45f96..cc941b140be 100644 --- a/homeassistant/components/xiaomi_miio/diagnostics.py +++ b/homeassistant/components/xiaomi_miio/diagnostics.py @@ -5,11 +5,11 @@ from __future__ import annotations from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_TOKEN, CONF_UNIQUE_ID +from homeassistant.const import CONF_DEVICE, CONF_MAC, CONF_TOKEN, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant -from .const import CONF_CLOUD_PASSWORD, CONF_CLOUD_USERNAME, DOMAIN, KEY_COORDINATOR +from .const import CONF_CLOUD_PASSWORD, CONF_CLOUD_USERNAME, CONF_FLOW_TYPE +from .typing import XiaomiMiioConfigEntry TO_REDACT = { CONF_CLOUD_PASSWORD, @@ -21,18 +21,17 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, config_entry: XiaomiMiioConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" diagnostics_data: dict[str, Any] = { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT) } - # not every device uses DataUpdateCoordinator - if coordinator := hass.data[DOMAIN][config_entry.entry_id].get(KEY_COORDINATOR): + if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + coordinator = config_entry.runtime_data.device_coordinator if isinstance(coordinator.data, dict): diagnostics_data["coordinator_data"] = coordinator.data else: diagnostics_data["coordinator_data"] = repr(coordinator.data) - return diagnostics_data diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 31d5dd9de2c..4492dcf9f17 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -30,7 +30,6 @@ from miio.integrations.fan.zhimi.zhimi_miot import ( import voluptuous as vol from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv @@ -64,8 +63,6 @@ from .const import ( FEATURE_FLAGS_FAN_ZA5, FEATURE_RESET_FILTER, FEATURE_SET_EXTRA_FEATURES, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRPURIFIER_2H, @@ -94,7 +91,7 @@ from .const import ( SERVICE_SET_EXTRA_FEATURES, ) from .entity import XiaomiCoordinatedMiioEntity -from .typing import ServiceMethodDetails +from .typing import ServiceMethodDetails, XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -204,7 +201,7 @@ FAN_DIRECTIONS_MAP = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Fan from a config entry.""" @@ -218,8 +215,8 @@ async def async_setup_entry( model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator if model in (MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3C_REV_A): entity = XiaomiAirPurifierMB4( diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 4330b863f6f..bf87f18e14a 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -20,7 +20,6 @@ from homeassistant.components.humidifier import ( HumidifierEntity, HumidifierEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -28,9 +27,6 @@ from homeassistant.util.percentage import percentage_to_ranged_value from .const import ( CONF_FLOW_TYPE, - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, @@ -38,6 +34,7 @@ from .const import ( MODELS_HUMIDIFIER_MJJSQ, ) from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -70,7 +67,7 @@ AVAILABLE_MODES_OTHER = [ async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Humidifier from a config entry.""" @@ -81,28 +78,26 @@ async def async_setup_entry( entity: HumidifierEntity model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator if model in MODELS_HUMIDIFIER_MIOT: - air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMiot( - air_humidifier, + device, config_entry, unique_id, coordinator, ) elif model in MODELS_HUMIDIFIER_MJJSQ: - air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMjjsq( - air_humidifier, + device, config_entry, unique_id, coordinator, ) else: - air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifier( - air_humidifier, + device, config_entry, unique_id, coordinator, diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 7c1c1b7bfb0..61931cc750a 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -33,7 +33,6 @@ from homeassistant.components.light import ( ColorMode, LightEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE, @@ -51,7 +50,6 @@ from .const import ( CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, - KEY_COORDINATOR, MODELS_LIGHT_BULB, MODELS_LIGHT_CEILING, MODELS_LIGHT_EYECARE, @@ -67,7 +65,7 @@ from .const import ( SERVICE_SET_SCENE, ) from .entity import XiaomiGatewayDevice, XiaomiMiioEntity -from .typing import ServiceMethodDetails +from .typing import ServiceMethodDetails, XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -131,7 +129,7 @@ SERVICE_TO_METHOD = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi light from a config entry.""" @@ -140,7 +138,7 @@ async def async_setup_entry( light: MiioDevice if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] + gateway = config_entry.runtime_data.gateway # Gateway light if gateway.model not in [ GATEWAY_MODEL_AC_V1, @@ -154,7 +152,7 @@ async def async_setup_entry( sub_devices = gateway.devices for sub_device in sub_devices.values(): if sub_device.device_type == "LightBulb": - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ + coordinator = config_entry.runtime_data.gateway_coordinators[ sub_device.sid ] entities.append( diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index f30d4728275..9863397c82a 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -12,7 +12,6 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE, CONF_MODEL, @@ -61,8 +60,6 @@ from .const import ( FEATURE_SET_MOTOR_SPEED, FEATURE_SET_OSCILLATION_ANGLE, FEATURE_SET_VOLUME, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, @@ -99,6 +96,7 @@ from .const import ( MODELS_PURIFIER_MIOT, ) from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry ATTR_DELAY_OFF_COUNTDOWN = "delay_off_countdown" ATTR_FAN_LEVEL = "fan_level" @@ -288,7 +286,7 @@ FAVORITE_LEVEL_VALUES = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Selectors from a config entry.""" @@ -296,7 +294,8 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] != CONF_DEVICE: return model = config_entry.data[CONF_MODEL] - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator if model in MODEL_TO_FEATURES_MAP: features = MODEL_TO_FEATURES_MAP[model] @@ -343,7 +342,7 @@ async def async_setup_entry( device, config_entry, f"{description.key}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) @@ -359,7 +358,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): def __init__( self, device: Device, - entry: ConfigEntry, + entry: XiaomiMiioConfigEntry, unique_id: str, coordinator: DataUpdateCoordinator, description: XiaomiMiioNumberDescription, diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 94a93fc1fae..734de2c0ff4 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -29,16 +29,12 @@ from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( ) from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( CONF_FLOW_TYPE, - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, MODEL_AIRFRESH_VA4, @@ -64,6 +60,7 @@ from .const import ( MODEL_FAN_ZA4, ) from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry ATTR_DISPLAY_ORIENTATION = "display_orientation" ATTR_LED_BRIGHTNESS = "led_brightness" @@ -204,7 +201,7 @@ SELECTOR_TYPES = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Selectors from a config entry.""" @@ -216,8 +213,8 @@ async def async_setup_entry( return unique_id = config_entry.unique_id - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator attributes = MODEL_TO_ATTR_MAP[model] async_add_entities( diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index e837192ddd7..73581595851 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -5,8 +5,9 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass import logging +from typing import TYPE_CHECKING -from miio import AirQualityMonitor, DeviceException +from miio import AirQualityMonitor, Device as MiioDevice, DeviceException from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -22,7 +23,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, @@ -53,8 +53,6 @@ from .const import ( CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, @@ -91,6 +89,7 @@ from .const import ( ROCKROBO_GENERIC, ) from .entity import XiaomiCoordinatedMiioEntity, XiaomiGatewayDevice, XiaomiMiioEntity +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -724,13 +723,19 @@ VACUUM_SENSORS = { } -def _setup_vacuum_sensors(hass, config_entry, async_add_entities): +def _setup_vacuum_sensors( + hass: HomeAssistant, + config_entry: XiaomiMiioConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Set up the Xiaomi vacuum sensors.""" - device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator entities = [] for sensor, description in VACUUM_SENSORS.items(): + if TYPE_CHECKING: + assert description.parent_key is not None parent_key_data = getattr(coordinator.data, description.parent_key) if getattr(parent_key_data, description.key, None) is None: _LOGGER.debug( @@ -754,14 +759,14 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi sensor from a config entry.""" entities: list[SensorEntity] = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] + gateway = config_entry.runtime_data.gateway # Gateway illuminance sensor if gateway.model not in [ GATEWAY_MODEL_AC_V1, @@ -779,9 +784,7 @@ async def async_setup_entry( # Gateway sub devices sub_devices = gateway.devices for sub_device in sub_devices.values(): - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ - sub_device.sid - ] + coordinator = config_entry.runtime_data.gateway_coordinators[sub_device.sid] for sensor, description in SENSOR_TYPES.items(): if sensor not in sub_device.status: continue @@ -791,6 +794,7 @@ async def async_setup_entry( ) ) elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: + device: MiioDevice host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] model: str = config_entry.data[CONF_MODEL] @@ -811,7 +815,8 @@ async def async_setup_entry( ) ) else: - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator sensors: Iterable[str] = [] if model in MODEL_TO_SENSORS_MAP: sensors = MODEL_TO_SENSORS_MAP[model] @@ -839,7 +844,7 @@ async def async_setup_entry( device, config_entry, f"{sensor}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + coordinator, description, ) ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 4469849eae7..9b2366a8273 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -17,7 +17,6 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -72,8 +71,6 @@ from .const import ( FEATURE_SET_LEARN_MODE, FEATURE_SET_LED, FEATURE_SET_PTC, - KEY_COORDINATOR, - KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, @@ -116,7 +113,7 @@ from .const import ( SUCCESS, ) from .entity import XiaomiCoordinatedMiioEntity, XiaomiGatewayDevice, XiaomiMiioEntity -from .typing import ServiceMethodDetails +from .typing import ServiceMethodDetails, XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -340,7 +337,7 @@ SWITCH_TYPES = ( async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the switch from a config entry.""" @@ -351,12 +348,16 @@ async def async_setup_entry( await async_setup_other_entry(hass, config_entry, async_add_entities) -async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): +async def async_setup_coordinated_entry( + hass: HomeAssistant, + config_entry: XiaomiMiioConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Set up the coordinated switch from a config entry.""" model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id - device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + device = config_entry.runtime_data.device + coordinator = config_entry.runtime_data.device_coordinator if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -387,24 +388,26 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): ) -async def async_setup_other_entry(hass, config_entry, async_add_entities): +async def async_setup_other_entry( + hass: HomeAssistant, + config_entry: XiaomiMiioConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: """Set up the other type switch from a config entry.""" - entities = [] + entities: list[SwitchEntity] = [] host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] name = config_entry.title model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: - gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] + gateway = config_entry.runtime_data.gateway # Gateway sub devices sub_devices = gateway.devices for sub_device in sub_devices.values(): if sub_device.device_type != "Switch": continue - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ - sub_device.sid - ] + coordinator = config_entry.runtime_data.gateway_coordinators[sub_device.sid] switch_variables = set(sub_device.status) & set(GATEWAY_SWITCH_VARS) if switch_variables: entities.extend( @@ -420,13 +423,14 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY and model == "lumi.acpartner.v3" ): + device: SwitchEntity if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]: - plug = ChuangmiPlug(host, token, model=model) + chuangmi_plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). # A switch device per channel will be created. @@ -436,13 +440,13 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): else: unique_id_ch = f"{unique_id}-mains" device = ChuangMiPlugSwitch( - name, plug, config_entry, unique_id_ch, channel_usb + name, chuangmi_plug, config_entry, unique_id_ch, channel_usb ) entities.append(device) hass.data[DATA_KEY][host] = device elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: - plug = PowerStrip(host, token, model=model) - device = XiaomiPowerStripSwitch(name, plug, config_entry, unique_id) + power_strip = PowerStrip(host, token, model=model) + device = XiaomiPowerStripSwitch(name, power_strip, config_entry, unique_id) entities.append(device) hass.data[DATA_KEY][host] = device elif model in [ @@ -452,14 +456,16 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", ]: - plug = ChuangmiPlug(host, token, model=model) - device = XiaomiPlugGenericSwitch(name, plug, config_entry, unique_id) + chuangmi_plug = ChuangmiPlug(host, token, model=model) + device = XiaomiPlugGenericSwitch( + name, chuangmi_plug, config_entry, unique_id + ) entities.append(device) hass.data[DATA_KEY][host] = device elif model in ["lumi.acpartner.v3"]: - plug = AirConditioningCompanionV3(host, token) + ac_companion = AirConditioningCompanionV3(host, token) device = XiaomiAirConditioningCompanionSwitch( - name, plug, config_entry, unique_id + name, ac_companion, config_entry, unique_id ) entities.append(device) hass.data[DATA_KEY][host] = device diff --git a/homeassistant/components/xiaomi_miio/typing.py b/homeassistant/components/xiaomi_miio/typing.py index 8fbb8e3d83f..e657f58fbce 100644 --- a/homeassistant/components/xiaomi_miio/typing.py +++ b/homeassistant/components/xiaomi_miio/typing.py @@ -1,12 +1,36 @@ """Typings for the xiaomi_miio integration.""" -from typing import NamedTuple +from dataclasses import dataclass +from typing import Any, NamedTuple +from miio import Device as MiioDevice +from miio.gateway.gateway import Gateway import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + class ServiceMethodDetails(NamedTuple): """Details for SERVICE_TO_METHOD mapping.""" method: str schema: vol.Schema | None = None + + +@dataclass +class XiaomiMiioRuntimeData: + """Runtime data for Xiaomi Miio config entry. + + Either device/device_coordinator or gateway/gateway_coordinators + must be set, based on CONF_FLOW_TYPE (CONF_DEVICE or CONF_GATEWAY) + """ + + device: MiioDevice = None # type: ignore[assignment] + device_coordinator: DataUpdateCoordinator[Any] = None # type: ignore[assignment] + + gateway: Gateway = None # type: ignore[assignment] + gateway_coordinators: dict[str, DataUpdateCoordinator[dict[str, bool]]] = None # type: ignore[assignment] + + +type XiaomiMiioConfigEntry = ConfigEntry[XiaomiMiioRuntimeData] diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 1cbc79b89f3..62343391cf4 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -14,7 +14,6 @@ from homeassistant.components.vacuum import ( VacuumActivity, VacuumEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform @@ -25,9 +24,6 @@ from homeassistant.util.dt import as_utc from . import VacuumCoordinatorData from .const import ( CONF_FLOW_TYPE, - DOMAIN, - KEY_COORDINATOR, - KEY_DEVICE, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -37,6 +33,7 @@ from .const import ( SERVICE_STOP_REMOTE_CONTROL, ) from .entity import XiaomiCoordinatedMiioEntity +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -78,7 +75,7 @@ STATE_CODE_TO_STATE = { async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + config_entry: XiaomiMiioConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the Xiaomi vacuum cleaner robot from a config entry.""" @@ -88,10 +85,10 @@ async def async_setup_entry( unique_id = config_entry.unique_id mirobo = MiroboVacuum( - hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], + config_entry.runtime_data.device, config_entry, unique_id, - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + config_entry.runtime_data.device_coordinator, ) entities.append(mirobo) From 7af731694f97a7f4249bc8ad1f2d603ee5ca8274 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 23 May 2025 08:05:43 -0700 Subject: [PATCH 0836/1175] Support readonly selectors in config_flows (#129456) * Allow disabled selectors in config flows. Show hidden options for history_stats. * fix tests * use optional instead of required * rename flag to readonly * rename to read_only * Update to use read_only field as part of selector definition * lint fix * Fix test * All selectors --- .../components/history_stats/config_flow.py | 15 ++ .../components/history_stats/strings.json | 12 ++ .../helpers/schema_config_entry_flow.py | 5 + homeassistant/helpers/selector.py | 165 ++++++++++-------- 4 files changed, 123 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 8dbca3b1939..96c8f319fbc 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.helpers.selector import ( DurationSelector, DurationSelectorConfig, EntitySelector, + EntitySelectorConfig, SelectSelector, SelectSelectorConfig, SelectSelectorMode, @@ -66,6 +67,20 @@ DATA_SCHEMA_SETUP = vol.Schema( ) DATA_SCHEMA_OPTIONS = vol.Schema( { + vol.Optional(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(read_only=True) + ), + vol.Optional(CONF_STATE): TextSelector( + TextSelectorConfig(multiple=True, read_only=True) + ), + vol.Optional(CONF_TYPE): SelectSelector( + SelectSelectorConfig( + options=CONF_TYPE_KEYS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_TYPE, + read_only=True, + ) + ), vol.Optional(CONF_START): TemplateSelector(), vol.Optional(CONF_END): TemplateSelector(), vol.Optional(CONF_DURATION): DurationSelector( diff --git a/homeassistant/components/history_stats/strings.json b/homeassistant/components/history_stats/strings.json index e10a72f6742..7a33099cf99 100644 --- a/homeassistant/components/history_stats/strings.json +++ b/homeassistant/components/history_stats/strings.json @@ -26,11 +26,17 @@ "options": { "description": "Read the documentation for further details on how to configure the history stats sensor using these options.", "data": { + "entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data::state%]", + "type": "[%key:component::history_stats::config::step::user::data::type%]", "start": "Start", "end": "End", "duration": "Duration" }, "data_description": { + "entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data_description::state%]", + "type": "[%key:component::history_stats::config::step::user::data_description::type%]", "start": "When to start the measure (timestamp or datetime). Can be a template.", "end": "When to stop the measure (timestamp or datetime). Can be a template", "duration": "Duration of the measure." @@ -49,11 +55,17 @@ "init": { "description": "[%key:component::history_stats::config::step::options::description%]", "data": { + "entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data::state%]", + "type": "[%key:component::history_stats::config::step::user::data::type%]", "start": "[%key:component::history_stats::config::step::options::data::start%]", "end": "[%key:component::history_stats::config::step::options::data::end%]", "duration": "[%key:component::history_stats::config::step::options::data::duration%]" }, "data_description": { + "entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]", + "state": "[%key:component::history_stats::config::step::user::data_description::state%]", + "type": "[%key:component::history_stats::config::step::user::data_description::type%]", "start": "[%key:component::history_stats::config::step::options::data_description::start%]", "end": "[%key:component::history_stats::config::step::options::data_description::end%]", "duration": "[%key:component::history_stats::config::step::options::data_description::duration%]" diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index af8c4c6402d..93d9a3d06f1 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -214,6 +214,11 @@ class SchemaCommonFlowHandler: and key.description.get("advanced") and not self._handler.show_advanced_options ) + and not ( + # don't remove read_only keys + isinstance(data_schema.schema[key], selector.Selector) + and data_schema.schema[key].config.get("read_only") + ) ): # Key not present, delete keys old value (if present) too values.pop(key.schema, None) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index f2c76d1d019..2d7fd51cac7 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -131,6 +131,19 @@ def _validate_supported_features(supported_features: int | list[str]) -> int: return feature_mask +BASE_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("read_only"): bool, + } +) + + +class BaseSelectorConfig(TypedDict, total=False): + """Class to common options of all selectors.""" + + read_only: bool + + ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA = vol.Schema( { # Integration that provided the entity @@ -183,7 +196,7 @@ class DeviceFilterSelectorConfig(TypedDict, total=False): model_id: str -class ActionSelectorConfig(TypedDict): +class ActionSelectorConfig(BaseSelectorConfig): """Class to represent an action selector config.""" @@ -193,7 +206,7 @@ class ActionSelector(Selector[ActionSelectorConfig]): selector_type = "action" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: ActionSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -204,7 +217,7 @@ class ActionSelector(Selector[ActionSelectorConfig]): return data -class AddonSelectorConfig(TypedDict, total=False): +class AddonSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an addon selector config.""" name: str @@ -217,7 +230,7 @@ class AddonSelector(Selector[AddonSelectorConfig]): selector_type = "addon" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("name"): str, vol.Optional("slug"): str, @@ -234,7 +247,7 @@ class AddonSelector(Selector[AddonSelectorConfig]): return addon -class AreaSelectorConfig(TypedDict, total=False): +class AreaSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an area selector config.""" entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] @@ -248,7 +261,7 @@ class AreaSelector(Selector[AreaSelectorConfig]): selector_type = "area" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -276,7 +289,7 @@ class AreaSelector(Selector[AreaSelectorConfig]): return [vol.Schema(str)(val) for val in data] -class AssistPipelineSelectorConfig(TypedDict, total=False): +class AssistPipelineSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an assist pipeline selector config.""" @@ -286,7 +299,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]): selector_type = "assist_pipeline" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -298,7 +311,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]): return pipeline -class AttributeSelectorConfig(TypedDict, total=False): +class AttributeSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an attribute selector config.""" entity_id: Required[str] @@ -311,7 +324,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]): selector_type = "attribute" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Required("entity_id"): cv.entity_id, # hide_attributes is used to hide attributes in the frontend. @@ -330,7 +343,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]): return attribute -class BackupLocationSelectorConfig(TypedDict, total=False): +class BackupLocationSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a backup location selector config.""" @@ -340,7 +353,7 @@ class BackupLocationSelector(Selector[BackupLocationSelectorConfig]): selector_type = "backup_location" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -352,7 +365,7 @@ class BackupLocationSelector(Selector[BackupLocationSelectorConfig]): return name -class BooleanSelectorConfig(TypedDict): +class BooleanSelectorConfig(BaseSelectorConfig): """Class to represent a boolean selector config.""" @@ -362,7 +375,7 @@ class BooleanSelector(Selector[BooleanSelectorConfig]): selector_type = "boolean" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: BooleanSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -374,7 +387,7 @@ class BooleanSelector(Selector[BooleanSelectorConfig]): return value -class ColorRGBSelectorConfig(TypedDict): +class ColorRGBSelectorConfig(BaseSelectorConfig): """Class to represent a color RGB selector config.""" @@ -384,7 +397,7 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]): selector_type = "color_rgb" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: ColorRGBSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -396,7 +409,7 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]): return value -class ColorTempSelectorConfig(TypedDict, total=False): +class ColorTempSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a color temp selector config.""" unit: ColorTempSelectorUnit @@ -419,7 +432,7 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): selector_type = "color_temp" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All( vol.Coerce(ColorTempSelectorUnit), lambda val: val.value @@ -456,7 +469,7 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]): return value -class ConditionSelectorConfig(TypedDict): +class ConditionSelectorConfig(BaseSelectorConfig): """Class to represent an condition selector config.""" @@ -466,7 +479,7 @@ class ConditionSelector(Selector[ConditionSelectorConfig]): selector_type = "condition" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: ConditionSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -477,7 +490,7 @@ class ConditionSelector(Selector[ConditionSelectorConfig]): return vol.Schema(cv.CONDITIONS_SCHEMA)(data) -class ConfigEntrySelectorConfig(TypedDict, total=False): +class ConfigEntrySelectorConfig(BaseSelectorConfig, total=False): """Class to represent a config entry selector config.""" integration: str @@ -489,7 +502,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]): selector_type = "config_entry" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("integration"): str, } @@ -505,7 +518,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]): return config -class ConstantSelectorConfig(TypedDict, total=False): +class ConstantSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a constant selector config.""" label: str @@ -519,7 +532,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]): selector_type = "constant" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("label"): str, vol.Optional("translation_key"): cv.string, @@ -546,7 +559,7 @@ class QrErrorCorrectionLevel(StrEnum): HIGH = "high" -class QrCodeSelectorConfig(TypedDict, total=False): +class QrCodeSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a QR code selector config.""" data: str @@ -560,7 +573,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]): selector_type = "qr_code" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Required("data"): str, vol.Optional("scale"): int, @@ -580,7 +593,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]): return self.config["data"] -class ConversationAgentSelectorConfig(TypedDict, total=False): +class ConversationAgentSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a conversation agent selector config.""" language: str @@ -592,7 +605,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): selector_type = "conversation_agent" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("language"): str, } @@ -608,7 +621,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]): return agent -class CountrySelectorConfig(TypedDict, total=False): +class CountrySelectorConfig(BaseSelectorConfig, total=False): """Class to represent a country selector config.""" countries: list[str] @@ -621,7 +634,7 @@ class CountrySelector(Selector[CountrySelectorConfig]): selector_type = "country" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("countries"): [str], vol.Optional("no_sort", default=False): cv.boolean, @@ -642,7 +655,7 @@ class CountrySelector(Selector[CountrySelectorConfig]): return country -class DateSelectorConfig(TypedDict): +class DateSelectorConfig(BaseSelectorConfig): """Class to represent a date selector config.""" @@ -652,7 +665,7 @@ class DateSelector(Selector[DateSelectorConfig]): selector_type = "date" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: DateSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -664,7 +677,7 @@ class DateSelector(Selector[DateSelectorConfig]): return data -class DateTimeSelectorConfig(TypedDict): +class DateTimeSelectorConfig(BaseSelectorConfig): """Class to represent a date time selector config.""" @@ -674,7 +687,7 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]): selector_type = "datetime" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: DateTimeSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -686,7 +699,7 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]): return data -class DeviceSelectorConfig(DeviceFilterSelectorConfig, total=False): +class DeviceSelectorConfig(BaseSelectorConfig, DeviceFilterSelectorConfig, total=False): """Class to represent a device selector config.""" entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] @@ -700,7 +713,9 @@ class DeviceSelector(Selector[DeviceSelectorConfig]): selector_type = "device" - CONFIG_SCHEMA = DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA.schema + ).extend( { vol.Optional("multiple", default=False): cv.boolean, vol.Optional("filter"): vol.All( @@ -724,7 +739,7 @@ class DeviceSelector(Selector[DeviceSelectorConfig]): return [vol.Schema(str)(val) for val in data] -class DurationSelectorConfig(TypedDict, total=False): +class DurationSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a duration selector config.""" enable_day: bool @@ -738,7 +753,7 @@ class DurationSelector(Selector[DurationSelectorConfig]): selector_type = "duration" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { # Enable day field in frontend. A selection with `days` set is allowed # even if `enable_day` is not set @@ -763,7 +778,7 @@ class DurationSelector(Selector[DurationSelectorConfig]): return cast(dict[str, float], data) -class EntitySelectorConfig(EntityFilterSelectorConfig, total=False): +class EntitySelectorConfig(BaseSelectorConfig, EntityFilterSelectorConfig, total=False): """Class to represent an entity selector config.""" exclude_entities: list[str] @@ -778,7 +793,9 @@ class EntitySelector(Selector[EntitySelectorConfig]): selector_type = "entity" - CONFIG_SCHEMA = ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.extend( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( + ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA.schema + ).extend( { vol.Optional("exclude_entities"): [str], vol.Optional("include_entities"): [str], @@ -824,7 +841,7 @@ class EntitySelector(Selector[EntitySelectorConfig]): return cast(list, vol.Schema([validate])(data)) # Output is a list -class FloorSelectorConfig(TypedDict, total=False): +class FloorSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an floor selector config.""" entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] @@ -838,7 +855,7 @@ class FloorSelector(Selector[FloorSelectorConfig]): selector_type = "floor" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -866,7 +883,7 @@ class FloorSelector(Selector[FloorSelectorConfig]): return [vol.Schema(str)(val) for val in data] -class IconSelectorConfig(TypedDict, total=False): +class IconSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an icon selector config.""" placeholder: str @@ -878,7 +895,7 @@ class IconSelector(Selector[IconSelectorConfig]): selector_type = "icon" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( {vol.Optional("placeholder"): str} # Frontend also has a fallbackPath option, this is not used by core ) @@ -893,7 +910,7 @@ class IconSelector(Selector[IconSelectorConfig]): return icon -class LabelSelectorConfig(TypedDict, total=False): +class LabelSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a label selector config.""" multiple: bool @@ -905,7 +922,7 @@ class LabelSelector(Selector[LabelSelectorConfig]): selector_type = "label" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("multiple", default=False): cv.boolean, } @@ -925,7 +942,7 @@ class LabelSelector(Selector[LabelSelectorConfig]): return [vol.Schema(str)(val) for val in data] -class LanguageSelectorConfig(TypedDict, total=False): +class LanguageSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an language selector config.""" languages: list[str] @@ -939,7 +956,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]): selector_type = "language" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("languages"): [str], vol.Optional("native_name", default=False): cv.boolean, @@ -959,7 +976,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]): return language -class LocationSelectorConfig(TypedDict, total=False): +class LocationSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a location selector config.""" radius: bool @@ -972,7 +989,7 @@ class LocationSelector(Selector[LocationSelectorConfig]): selector_type = "location" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( {vol.Optional("radius"): bool, vol.Optional("icon"): str} ) DATA_SCHEMA = vol.Schema( @@ -993,7 +1010,7 @@ class LocationSelector(Selector[LocationSelectorConfig]): return location -class MediaSelectorConfig(TypedDict): +class MediaSelectorConfig(BaseSelectorConfig): """Class to represent a media selector config.""" @@ -1003,7 +1020,7 @@ class MediaSelector(Selector[MediaSelectorConfig]): selector_type = "media" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA DATA_SCHEMA = vol.Schema( { # Although marked as optional in frontend, this field is required @@ -1026,7 +1043,7 @@ class MediaSelector(Selector[MediaSelectorConfig]): return media -class NumberSelectorConfig(TypedDict, total=False): +class NumberSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a number selector config.""" min: float @@ -1061,7 +1078,7 @@ class NumberSelector(Selector[NumberSelectorConfig]): selector_type = "number" CONFIG_SCHEMA = vol.All( - vol.Schema( + BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("min"): vol.Coerce(float), vol.Optional("max"): vol.Coerce(float), @@ -1096,7 +1113,7 @@ class NumberSelector(Selector[NumberSelectorConfig]): return value -class ObjectSelectorConfig(TypedDict): +class ObjectSelectorConfig(BaseSelectorConfig): """Class to represent an object selector config.""" @@ -1106,7 +1123,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]): selector_type = "object" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: ObjectSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1142,7 +1159,7 @@ class SelectSelectorMode(StrEnum): DROPDOWN = "dropdown" -class SelectSelectorConfig(TypedDict, total=False): +class SelectSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a select selector config.""" options: Required[Sequence[SelectOptionDict] | Sequence[str]] @@ -1159,7 +1176,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): selector_type = "select" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Optional("multiple", default=False): cv.boolean, @@ -1199,14 +1216,14 @@ class SelectSelector(Selector[SelectSelectorConfig]): return [parent_schema(vol.Schema(str)(val)) for val in data] -class TargetSelectorConfig(TypedDict, total=False): +class TargetSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a target selector config.""" entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] -class StateSelectorConfig(TypedDict, total=False): +class StateSelectorConfig(BaseSelectorConfig, total=False): """Class to represent an state selector config.""" entity_id: Required[str] @@ -1218,7 +1235,7 @@ class StateSelector(Selector[StateSelectorConfig]): selector_type = "state" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Required("entity_id"): cv.entity_id, # The attribute to filter on, is currently deliberately not @@ -1248,7 +1265,7 @@ class TargetSelector(Selector[TargetSelectorConfig]): selector_type = "target" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("entity"): vol.All( cv.ensure_list, @@ -1273,7 +1290,7 @@ class TargetSelector(Selector[TargetSelectorConfig]): return target -class TemplateSelectorConfig(TypedDict): +class TemplateSelectorConfig(BaseSelectorConfig): """Class to represent an template selector config.""" @@ -1283,7 +1300,7 @@ class TemplateSelector(Selector[TemplateSelectorConfig]): selector_type = "template" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: TemplateSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1295,7 +1312,7 @@ class TemplateSelector(Selector[TemplateSelectorConfig]): return template.template -class TextSelectorConfig(TypedDict, total=False): +class TextSelectorConfig(BaseSelectorConfig, total=False): """Class to represent a text selector config.""" multiline: bool @@ -1330,7 +1347,7 @@ class TextSelector(Selector[TextSelectorConfig]): selector_type = "text" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("multiline", default=False): bool, vol.Optional("prefix"): str, @@ -1359,7 +1376,7 @@ class TextSelector(Selector[TextSelectorConfig]): return [vol.Schema(str)(val) for val in data] -class ThemeSelectorConfig(TypedDict): +class ThemeSelectorConfig(BaseSelectorConfig): """Class to represent a theme selector config.""" @@ -1369,7 +1386,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]): selector_type = "theme" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { vol.Optional("include_default", default=False): cv.boolean, } @@ -1385,7 +1402,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]): return theme -class TimeSelectorConfig(TypedDict): +class TimeSelectorConfig(BaseSelectorConfig): """Class to represent a time selector config.""" @@ -1395,7 +1412,7 @@ class TimeSelector(Selector[TimeSelectorConfig]): selector_type = "time" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: TimeSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1407,7 +1424,7 @@ class TimeSelector(Selector[TimeSelectorConfig]): return cast(str, data) -class TriggerSelectorConfig(TypedDict): +class TriggerSelectorConfig(BaseSelectorConfig): """Class to represent an trigger selector config.""" @@ -1417,7 +1434,7 @@ class TriggerSelector(Selector[TriggerSelectorConfig]): selector_type = "trigger" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA def __init__(self, config: TriggerSelectorConfig | None = None) -> None: """Instantiate a selector.""" @@ -1428,7 +1445,7 @@ class TriggerSelector(Selector[TriggerSelectorConfig]): return vol.Schema(cv.TRIGGER_SCHEMA)(data) -class FileSelectorConfig(TypedDict): +class FileSelectorConfig(BaseSelectorConfig): """Class to represent a file selector config.""" accept: str # required @@ -1440,7 +1457,7 @@ class FileSelector(Selector[FileSelectorConfig]): selector_type = "file" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend( { # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept vol.Required("accept"): str, From ed0ff93d1e1226c3757cb4b65bc61a0f357ff761 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 23 May 2025 17:12:43 +0200 Subject: [PATCH 0837/1175] Bump py-sucks to 0.9.11 (#145518) bump py-sucks to 0.9.11 --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 9 --------- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index c2daf3a7e90..12fd8e01215 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==13.2.1"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa36fc41b08..8b88a29ee65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1768,7 +1768,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.ecovacs -py-sucks==0.9.10 +py-sucks==0.9.11 # homeassistant.components.synology_dsm py-synologydsm-api==2.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f24f7085ac9..40fa341fe5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1467,7 +1467,7 @@ py-nextbusnext==2.1.2 py-nightscout==1.2.2 # homeassistant.components.ecovacs -py-sucks==0.9.10 +py-sucks==0.9.11 # homeassistant.components.synology_dsm py-synologydsm-api==2.7.2 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index c55125dfe91..356e44986e5 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -59,15 +59,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # concord232 > stevedore > pbr > setuptools "pbr": {"setuptools"} }, - "ecovacs": { - # py-sucks > pycountry-convert > pytest-cov > pytest - "pytest-cov": {"pytest", "wheel"}, - # py-sucks > pycountry-convert > pytest-mock > pytest - "pytest-mock": {"pytest", "wheel"}, - # py-sucks > pycountry-convert > pytest - # py-sucks > pycountry-convert > wheel - "pycountry-convert": {"pytest", "wheel"}, - }, "efergy": { # pyefergy > codecov # pyefergy > types-pytz From e22ea85e844e156ee7ea6e3dc7610dc792f14f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Fri, 23 May 2025 17:20:27 +0200 Subject: [PATCH 0838/1175] Add Matter Pump device type (#145335) * Pump status * Pump speed * PumpStatusRunning * ControlModeEnum * Add tests * Clean code * Update tests and sensors * Review fixes * Add RPM unit * Fix for unknown value * Update snapshot * OperationMode * Update snapshots * Update snapshot * Update tests/components/matter/test_select.py Co-authored-by: TheJulianJES * Handle SupplyFault bit enabled too * Review fix * Unmove * Remove pump_operation_mode * Update snapshot --------- Co-authored-by: TheJulianJES --- .../components/matter/binary_sensor.py | 43 +++++++ homeassistant/components/matter/icons.json | 12 ++ homeassistant/components/matter/select.py | 21 ++++ homeassistant/components/matter/sensor.py | 38 ++++++ homeassistant/components/matter/strings.json | 23 ++++ .../matter/fixtures/nodes/pump.json | 2 +- .../matter/snapshots/test_binary_sensor.ambr | 96 +++++++++++++++ .../matter/snapshots/test_select.ambr | 60 +++++++++ .../matter/snapshots/test_sensor.ambr | 116 ++++++++++++++++++ tests/components/matter/test_binary_sensor.py | 40 ++++++ tests/components/matter/test_select.py | 19 +++ tests/components/matter/test_sensor.py | 32 +++++ 12 files changed, 501 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 95375d5fc49..2d04a936ee5 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -334,4 +334,47 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterBinarySensor, required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,), ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="PumpFault", + translation_key="pump_fault", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + # DeviceFault or SupplyFault bit enabled + measurement_to_ha={ + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kDeviceFault: True, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSupplyFault: True, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedLow: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedHigh: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kLocalOverride: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemotePressure: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemoteFlow: False, + clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRemoteTemperature: False, + }.get, + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.PumpConfigurationAndControl.Attributes.PumpStatus, + ), + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="PumpStatusRunning", + translation_key="pump_running", + device_class=BinarySensorDeviceClass.RUNNING, + measurement_to_ha=lambda x: ( + x + == clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.PumpConfigurationAndControl.Attributes.PumpStatus, + ), + allow_multi=True, + ), ] diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index 82e45e0383a..ac3e70dcfc8 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -57,6 +57,9 @@ "bat_replacement_description": { "default": "mdi:battery-sync" }, + "flow": { + "default": "mdi:pipe" + }, "hepa_filter_condition": { "default": "mdi:filter-check" }, @@ -86,6 +89,15 @@ }, "evse_fault_state": { "default": "mdi:ev-station" + }, + "pump_control_mode": { + "default": "mdi:pipe-wrench" + }, + "pump_speed": { + "default": "mdi:speedometer" + }, + "pump_status": { + "default": "mdi:pump" } }, "switch": { diff --git a/homeassistant/components/matter/select.py b/homeassistant/components/matter/select.py index 39e1db3bf6f..ac1bc2d1f8f 100644 --- a/homeassistant/components/matter/select.py +++ b/homeassistant/components/matter/select.py @@ -30,6 +30,13 @@ NUMBER_OF_RINSES_STATE_MAP = { NUMBER_OF_RINSES_STATE_MAP_REVERSE = { v: k for k, v in NUMBER_OF_RINSES_STATE_MAP.items() } +PUMP_OPERATION_MODE_MAP = { + clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kNormal: "normal", + clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMinimum: "minimum", + clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kMaximum: "maximum", + clusters.PumpConfigurationAndControl.Enums.OperationModeEnum.kLocal: "local", +} +PUMP_OPERATION_MODE_MAP_REVERSE = {v: k for k, v in PUMP_OPERATION_MODE_MAP.items()} type SelectCluster = ( clusters.ModeSelect @@ -459,4 +466,18 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterAttributeSelectEntity, required_attributes=(clusters.DoorLock.Attributes.SoundVolume,), ), + MatterDiscoverySchema( + platform=Platform.SELECT, + entity_description=MatterSelectEntityDescription( + key="PumpConfigurationAndControlOperationMode", + translation_key="pump_operation_mode", + options=list(PUMP_OPERATION_MODE_MAP.values()), + measurement_to_ha=PUMP_OPERATION_MODE_MAP.get, + ha_to_native_value=PUMP_OPERATION_MODE_MAP_REVERSE.get, + ), + entity_class=MatterAttributeSelectEntity, + required_attributes=( + clusters.PumpConfigurationAndControl.Attributes.OperationMode, + ), + ), ] diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 381ecc480da..2197f81e134 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + REVOLUTIONS_PER_MINUTE, EntityCategory, Platform, UnitOfElectricCurrent, @@ -110,6 +111,16 @@ EVSE_FAULT_STATE_MAP = { clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other", } +PUMP_CONTROL_MODE_MAP = { + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantSpeed: "constant_speed", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantPressure: "constant_pressure", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kProportionalPressure: "proportional_pressure", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantFlow: "constant_flow", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kConstantTemperature: "constant_temperature", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kAutomatic: "automatic", + clusters.PumpConfigurationAndControl.Enums.ControlModeEnum.kUnknownEnumValue: None, +} + async def async_setup_entry( hass: HomeAssistant, @@ -1118,4 +1129,31 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterSensor, required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="PumpControlMode", + translation_key="pump_control_mode", + device_class=SensorDeviceClass.ENUM, + options=[ + mode for mode in PUMP_CONTROL_MODE_MAP.values() if mode is not None + ], + measurement_to_ha=PUMP_CONTROL_MODE_MAP.get, + ), + entity_class=MatterSensor, + required_attributes=( + clusters.PumpConfigurationAndControl.Attributes.ControlMode, + ), + ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="PumpSpeed", + translation_key="pump_speed", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + ), + entity_class=MatterSensor, + required_attributes=(clusters.PumpConfigurationAndControl.Attributes.Speed,), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index 325e8d1f26c..a04f1d86880 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -239,6 +239,15 @@ "laundry_washer_spin_speed": { "name": "Spin speed" }, + "pump_operation_mode": { + "name": "mode", + "state": { + "local": "Local", + "maximum": "Maximum", + "minimum": "Minimum", + "normal": "[%key:common::state::normal%]" + } + }, "water_heater_mode": { "name": "Water heater mode" }, @@ -352,6 +361,20 @@ "other": "Other fault" } }, + "pump_control_mode": { + "name": "Control mode", + "state": { + "constant_flow": "Constant flow", + "constant_pressure": "Constant pressure", + "constant_speed": "Constant speed", + "constant_temperature": "Constant temp", + "proportional_pressure": "Proportional pressure", + "automatic": "Automatic" + } + }, + "pump_speed": { + "name": "Rotation speed" + }, "evse_circuit_capacity": { "name": "Circuit capacity" }, diff --git a/tests/components/matter/fixtures/nodes/pump.json b/tests/components/matter/fixtures/nodes/pump.json index 39579f4448c..e4afc0b4f33 100644 --- a/tests/components/matter/fixtures/nodes/pump.json +++ b/tests/components/matter/fixtures/nodes/pump.json @@ -228,7 +228,7 @@ "1/512/0": 32767, "1/512/1": 65534, "1/512/2": 65534, - "1/512/16": 5, + "1/512/16": 32, "1/512/17": 0, "1/512/18": 5, "1/512/19": null, diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index feca62ffa31..e91ea9f7ba9 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -383,6 +383,102 @@ 'state': 'off', }) # --- +# name: test_binary_sensors[pump][binary_sensor.mock_pump_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mock_pump_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_fault', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpFault-512-16', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[pump][binary_sensor.mock_pump_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mock Pump Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_pump_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[pump][binary_sensor.mock_pump_running-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.mock_pump_running', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Running', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_running', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpStatusRunning-512-16', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[pump][binary_sensor.mock_pump_running-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'running', + 'friendly_name': 'Mock Pump Running', + }), + 'context': , + 'entity_id': 'binary_sensor.mock_pump_running', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_binary_sensors[silabs_evse_charging][binary_sensor.evse_charging_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index edd0224ccac..0ab50d7a7fc 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -1967,6 +1967,66 @@ 'state': 'Low', }) # --- +# name: test_selects[pump][select.mock_pump_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'normal', + 'minimum', + 'maximum', + 'local', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.mock_pump_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': 'mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_operation_mode', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpConfigurationAndControlOperationMode-512-32', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[pump][select.mock_pump_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Pump mode', + 'options': list([ + 'normal', + 'minimum', + 'maximum', + 'local', + ]), + }), + 'context': , + 'entity_id': 'select.mock_pump_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal', + }) +# --- # name: test_selects[silabs_evse_charging][select.evse_energy_management_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index bf22986d6df..424511f286e 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -3101,6 +3101,71 @@ 'state': '0.0', }) # --- +# name: test_sensors[pump][sensor.mock_pump_control_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'constant_speed', + 'constant_pressure', + 'proportional_pressure', + 'constant_flow', + 'constant_temperature', + 'automatic', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_pump_control_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Control mode', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_control_mode', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpControlMode-512-33', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_control_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Mock Pump Control mode', + 'options': list([ + 'constant_speed', + 'constant_pressure', + 'proportional_pressure', + 'constant_flow', + 'constant_temperature', + 'automatic', + ]), + }), + 'context': , + 'entity_id': 'sensor.mock_pump_control_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'constant_temperature', + }) +# --- # name: test_sensors[pump][sensor.mock_pump_flow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3204,6 +3269,57 @@ 'state': '10.0', }) # --- +# name: test_sensors[pump][sensor.mock_pump_rotation_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.mock_pump_rotation_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': 'Rotation speed', + 'platform': 'matter', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pump_speed', + 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpSpeed-512-20', + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensors[pump][sensor.mock_pump_rotation_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Pump Rotation speed', + 'state_class': , + 'unit_of_measurement': 'rpm', + }), + 'context': , + 'entity_id': 'sensor.mock_pump_rotation_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1000', + }) +# --- # name: test_sensors[pump][sensor.mock_pump_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index bea9c1ad237..e221140b85b 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -217,3 +217,43 @@ async def test_water_heater( state = hass.states.get("binary_sensor.water_heater_boost_state") assert state assert state.state == "on" + + +@pytest.mark.parametrize("node_fixture", ["pump"]) +async def test_pump( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test pump sensors.""" + # PumpStatus + state = hass.states.get("binary_sensor.mock_pump_running") + assert state + assert state.state == "on" + + set_node_attribute(matter_node, 1, 512, 16, 0) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.mock_pump_running") + assert state + assert state.state == "off" + + # PumpStatus --> DeviceFault bit + state = hass.states.get("binary_sensor.mock_pump_problem") + assert state + assert state.state == "unknown" + + set_node_attribute(matter_node, 1, 512, 16, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.mock_pump_problem") + assert state + assert state.state == "on" + + # PumpStatus --> SupplyFault bit + set_node_attribute(matter_node, 1, 512, 16, 2) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.mock_pump_problem") + assert state + assert state.state == "on" diff --git a/tests/components/matter/test_select.py b/tests/components/matter/test_select.py index 456558d983d..7045b60a24e 100644 --- a/tests/components/matter/test_select.py +++ b/tests/components/matter/test_select.py @@ -216,3 +216,22 @@ async def test_map_select_entities( await trigger_subscription_callback(hass, matter_client) state = hass.states.get("select.laundrywasher_number_of_rinses") assert state.state == "normal" + + +@pytest.mark.parametrize("node_fixture", ["pump"]) +async def test_pump( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test MatterAttributeSelectEntity entities are discovered and working from a pump fixture.""" + # OperationMode + state = hass.states.get("select.mock_pump_mode") + assert state + assert state.state == "normal" + assert state.attributes["options"] == ["normal", "minimum", "maximum", "local"] + + set_node_attribute(matter_node, 1, 512, 32, 3) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get("select.mock_pump_mode") + assert state.state == "local" diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index feb604bd365..19697efab71 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -523,3 +523,35 @@ async def test_water_heater( state = hass.states.get("sensor.water_heater_appliance_energy_state") assert state assert state.state == "offline" + + +@pytest.mark.parametrize("node_fixture", ["pump"]) +async def test_pump( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test pump sensors.""" + # ControlMode + state = hass.states.get("sensor.mock_pump_control_mode") + assert state + assert state.state == "constant_temperature" + + set_node_attribute(matter_node, 1, 512, 33, 7) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_pump_control_mode") + assert state + assert state.state == "automatic" + + # Speed + state = hass.states.get("sensor.mock_pump_rotation_speed") + assert state + assert state.state == "1000" + + set_node_attribute(matter_node, 1, 512, 20, 500) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("sensor.mock_pump_rotation_speed") + assert state + assert state.state == "500" From 2a38f03ec9426fe5c068ac64339677fa2fc1f440 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 23 May 2025 17:40:54 +0200 Subject: [PATCH 0839/1175] Add MQTT fan as entity platform on MQTT subentries (#144698) --- homeassistant/components/mqtt/config_flow.py | 343 ++++++++++++++++++- homeassistant/components/mqtt/const.py | 28 ++ homeassistant/components/mqtt/fan.py | 66 ++-- homeassistant/components/mqtt/strings.json | 85 +++++ tests/components/mqtt/common.py | 42 +++ tests/components/mqtt/test_config_flow.py | 153 +++++++++ 6 files changed, 676 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 13cb8658f14..78d2305c4e2 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -143,6 +143,10 @@ from .const import ( CONF_COMMAND_ON_TEMPLATE, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_DIRECTION_COMMAND_TEMPLATE, + CONF_DIRECTION_COMMAND_TOPIC, + CONF_DIRECTION_STATE_TOPIC, + CONF_DIRECTION_VALUE_TEMPLATE, CONF_DISCOVERY_PREFIX, CONF_EFFECT_COMMAND_TEMPLATE, CONF_EFFECT_COMMAND_TOPIC, @@ -169,15 +173,32 @@ from .const import ( CONF_OFF_DELAY, CONF_ON_COMMAND_TYPE, CONF_OPTIONS, + CONF_OSCILLATION_COMMAND_TEMPLATE, + CONF_OSCILLATION_COMMAND_TOPIC, + CONF_OSCILLATION_STATE_TOPIC, + CONF_OSCILLATION_VALUE_TEMPLATE, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_CLOSE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_OPEN, + CONF_PAYLOAD_OSCILLATION_OFF, + CONF_PAYLOAD_OSCILLATION_ON, CONF_PAYLOAD_PRESS, + CONF_PAYLOAD_RESET_PERCENTAGE, + CONF_PAYLOAD_RESET_PRESET_MODE, CONF_PAYLOAD_STOP, CONF_PAYLOAD_STOP_TILT, + CONF_PERCENTAGE_COMMAND_TEMPLATE, + CONF_PERCENTAGE_COMMAND_TOPIC, + CONF_PERCENTAGE_STATE_TOPIC, + CONF_PERCENTAGE_VALUE_TEMPLATE, CONF_POSITION_CLOSED, CONF_POSITION_OPEN, + CONF_PRESET_MODE_COMMAND_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, + CONF_PRESET_MODE_VALUE_TEMPLATE, + CONF_PRESET_MODES_LIST, CONF_QOS, CONF_RED_TEMPLATE, CONF_RETAIN, @@ -196,6 +217,8 @@ from .const import ( CONF_SCHEMA, CONF_SET_POSITION_TEMPLATE, CONF_SET_POSITION_TOPIC, + CONF_SPEED_RANGE_MAX, + CONF_SPEED_RANGE_MIN, CONF_STATE_CLOSED, CONF_STATE_CLOSING, CONF_STATE_OPEN, @@ -239,7 +262,10 @@ from .const import ( DEFAULT_PAYLOAD_OFF, DEFAULT_PAYLOAD_ON, DEFAULT_PAYLOAD_OPEN, + DEFAULT_PAYLOAD_OSCILLATE_OFF, + DEFAULT_PAYLOAD_OSCILLATE_ON, DEFAULT_PAYLOAD_PRESS, + DEFAULT_PAYLOAD_RESET, DEFAULT_PAYLOAD_STOP, DEFAULT_PORT, DEFAULT_POSITION_CLOSED, @@ -247,6 +273,8 @@ from .const import ( DEFAULT_PREFIX, DEFAULT_PROTOCOL, DEFAULT_QOS, + DEFAULT_SPEED_RANGE_MAX, + DEFAULT_SPEED_RANGE_MIN, DEFAULT_STATE_STOPPED, DEFAULT_TILT_CLOSED_POSITION, DEFAULT_TILT_MAX, @@ -353,6 +381,7 @@ SUBENTRY_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, + Platform.FAN, Platform.LIGHT, Platform.NOTIFY, Platform.SENSOR, @@ -437,6 +466,17 @@ TIMEOUT_SELECTOR = NumberSelector( # Cover specific selectors POSITION_SELECTOR = NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX)) +# Fan specific selectors +FAN_SPEED_RANGE_MIN_SELECTOR = vol.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1)), + vol.Coerce(int), +) +FAN_SPEED_RANGE_MAX_SELECTOR = vol.All( + NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=2)), + vol.Coerce(int), +) +PRESET_MODES_SELECTOR = OPTIONS_SELECTOR + # Switch specific selectors SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector( SelectSelectorConfig( @@ -537,6 +577,29 @@ def validate_cover_platform_config( return errors +@callback +def validate_fan_platform_config(config: dict[str, Any]) -> dict[str, str]: + """Validate the fan config options.""" + errors: dict[str, str] = {} + if ( + CONF_SPEED_RANGE_MIN in config + and CONF_SPEED_RANGE_MAX in config + and config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX] + ): + errors["fan_speed_settings"] = ( + "fan_speed_range_max_must_be_greater_than_speed_range_min" + ) + if ( + CONF_PRESET_MODES_LIST in config + and config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config[CONF_PRESET_MODES_LIST] + ): + errors["fan_preset_mode_settings"] = ( + "fan_preset_mode_reset_in_preset_modes_list" + ) + + return errors + + @callback def validate_sensor_platform_config( config: dict[str, Any], @@ -597,9 +660,12 @@ class PlatformField: required: bool validator: Callable[..., Any] | None = None error: str | None = None - default: str | int | bool | None | vol.Undefined = vol.UNDEFINED + default: ( + str | int | bool | None | Callable[[dict[str, Any]], Any] | vol.Undefined + ) = vol.UNDEFINED is_schema_default: bool = False exclude_from_reconfig: bool = False + exclude_from_config: bool = False conditions: tuple[dict[str, Any], ...] | None = None custom_filtering: bool = False section: str | None = None @@ -634,7 +700,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: return errors -COMMON_ENTITY_FIELDS = { +COMMON_ENTITY_FIELDS: dict[str, PlatformField] = { CONF_PLATFORM: PlatformField( selector=SUBENTRY_PLATFORM_SELECTOR, required=True, @@ -651,7 +717,7 @@ COMMON_ENTITY_FIELDS = { ), } -PLATFORM_ENTITY_FIELDS = { +PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { Platform.BINARY_SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, @@ -670,6 +736,32 @@ PLATFORM_ENTITY_FIELDS = { required=False, ), }, + Platform.FAN.value: { + "fan_feature_speed": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_PERCENTAGE_COMMAND_TOPIC)), + ), + "fan_feature_preset_modes": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_PRESET_MODE_COMMAND_TOPIC)), + ), + "fan_feature_oscillation": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_OSCILLATION_COMMAND_TOPIC)), + ), + "fan_feature_direction": PlatformField( + selector=BOOLEAN_SELECTOR, + required=False, + exclude_from_config=True, + default=lambda config: bool(config.get(CONF_DIRECTION_COMMAND_TOPIC)), + ), + }, Platform.NOTIFY.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( @@ -715,7 +807,7 @@ PLATFORM_ENTITY_FIELDS = { ), }, } -PLATFORM_MQTT_FIELDS = { +PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { Platform.BINARY_SENSOR.value: { CONF_STATE_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -951,6 +1043,226 @@ PLATFORM_MQTT_FIELDS = { section="cover_tilt_settings", ), }, + Platform.FAN.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_PAYLOAD_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OFF, + ), + CONF_PAYLOAD_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ON, + ), + CONF_RETAIN: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=bool + ), + CONF_OPTIMISTIC: PlatformField( + selector=BOOLEAN_SELECTOR, required=False, validator=bool + ), + CONF_PERCENTAGE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_PERCENTAGE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_PERCENTAGE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_PERCENTAGE_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_SPEED_RANGE_MIN: PlatformField( + selector=FAN_SPEED_RANGE_MIN_SELECTOR, + required=False, + validator=int, + default=DEFAULT_SPEED_RANGE_MIN, + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_SPEED_RANGE_MAX: PlatformField( + selector=FAN_SPEED_RANGE_MAX_SELECTOR, + required=False, + validator=int, + default=DEFAULT_SPEED_RANGE_MAX, + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_PAYLOAD_RESET_PERCENTAGE: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_RESET, + section="fan_speed_settings", + conditions=({"fan_feature_speed": True},), + ), + CONF_PRESET_MODES_LIST: PlatformField( + selector=PRESET_MODES_SELECTOR, + required=True, + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_PRESET_MODE_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_PAYLOAD_RESET_PRESET_MODE: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_RESET, + section="fan_preset_mode_settings", + conditions=({"fan_feature_preset_modes": True},), + ), + CONF_OSCILLATION_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_OSCILLATION_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_OSCILLATION_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_OSCILLATION_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_PAYLOAD_OSCILLATION_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OSCILLATE_OFF, + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_PAYLOAD_OSCILLATION_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OSCILLATE_ON, + section="fan_oscillation_settings", + conditions=({"fan_feature_oscillation": True},), + ), + CONF_DIRECTION_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + section="fan_direction_settings", + conditions=({"fan_feature_direction": True},), + ), + CONF_DIRECTION_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_direction_settings", + conditions=({"fan_feature_direction": True},), + ), + CONF_DIRECTION_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + section="fan_direction_settings", + conditions=({"fan_feature_direction": True},), + ), + CONF_DIRECTION_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + section="fan_direction_settings", + conditions=({"fan_feature_direction": True},), + ), + }, Platform.NOTIFY.value: { CONF_COMMAND_TOPIC: PlatformField( selector=TEXT_SELECTOR, @@ -1510,6 +1822,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ Platform.BINARY_SENSOR.value: None, Platform.BUTTON.value: None, Platform.COVER.value: validate_cover_platform_config, + Platform.FAN.value: validate_fan_platform_config, Platform.LIGHT.value: validate_light_platform_config, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, @@ -1667,6 +1980,14 @@ def data_schema_from_fields( device_data: MqttDeviceData | None = None, ) -> vol.Schema: """Generate custom data schema from platform fields or device data.""" + + def get_default(field_details: PlatformField) -> Any: + if callable(field_details.default): + if TYPE_CHECKING: + assert component_data is not None + return field_details.default(component_data) + return field_details.default + if device_data is not None: component_data_with_user_input: dict[str, Any] | None = dict(device_data) if TYPE_CHECKING: @@ -1693,7 +2014,7 @@ def data_schema_from_fields( if field_details.required else vol.Optional( field_name, - default=field_details.default + default=get_default(field_details) if field_details.default is not None else vol.UNDEFINED, ): field_details.selector(component_data_with_user_input) # type: ignore[operator] @@ -2581,13 +2902,21 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow): """Update component data defaults.""" for component_data in self._subentry_data["components"].values(): platform = component_data[CONF_PLATFORM] - subentry_default_data = subentry_schema_default_data_from_fields( + platform_fields: dict[str, PlatformField] = ( COMMON_ENTITY_FIELDS | PLATFORM_ENTITY_FIELDS[platform] - | PLATFORM_MQTT_FIELDS[platform], + | PLATFORM_MQTT_FIELDS[platform] + ) + subentry_default_data = subentry_schema_default_data_from_fields( + platform_fields, component_data, ) component_data.update(subentry_default_data) + for key, platform_field in platform_fields.items(): + if not platform_field.exclude_from_config: + continue + if key in component_data: + component_data.pop(key) @callback def _async_create_subentry( diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index be559675dd8..7c0ac1f2a3f 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -78,6 +78,10 @@ CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" +CONF_DIRECTION_COMMAND_TEMPLATE = "direction_command_template" +CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic" +CONF_DIRECTION_STATE_TOPIC = "direction_state_topic" +CONF_DIRECTION_VALUE_TEMPLATE = "direction_value_template" CONF_ENABLED_BY_DEFAULT = "enabled_by_default" CONF_EFFECT_COMMAND_TEMPLATE = "effect_command_template" CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic" @@ -109,16 +113,33 @@ CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" CONF_OFF_DELAY = "off_delay" CONF_ON_COMMAND_TYPE = "on_command_type" +CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" +CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" +CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" +CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" CONF_PAYLOAD_CLOSE = "payload_close" CONF_PAYLOAD_OPEN = "payload_open" +CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" +CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" CONF_PAYLOAD_PRESS = "payload_press" +CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" +CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_PAYLOAD_STOP = "payload_stop" CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" +CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" +CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" +CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" +CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" CONF_POWER_COMMAND_TOPIC = "power_command_topic" CONF_POWER_COMMAND_TEMPLATE = "power_command_template" CONF_PRECISION = "precision" +CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" +CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" +CONF_PRESET_MODES_LIST = "preset_modes" +CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" +CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_RED_TEMPLATE = "red_template" CONF_RGB_COMMAND_TEMPLATE = "rgb_command_template" CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" @@ -134,6 +155,8 @@ CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" CONF_SET_POSITION_TEMPLATE = "set_position_template" CONF_SET_POSITION_TOPIC = "set_position_topic" +CONF_SPEED_RANGE_MAX = "speed_range_max" +CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" @@ -204,8 +227,11 @@ DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OPEN = "OPEN" +DEFAULT_PAYLOAD_OSCILLATE_OFF = "oscillate_off" +DEFAULT_PAYLOAD_OSCILLATE_ON = "oscillate_on" DEFAULT_PAYLOAD_PRESS = "PRESS" DEFAULT_PAYLOAD_STOP = "STOP" +DEFAULT_PAYLOAD_RESET = "None" DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -218,6 +244,8 @@ DEFAULT_WS_PATH = "/" DEFAULT_POSITION_CLOSED = 0 DEFAULT_POSITION_OPEN = 100 DEFAULT_RETAIN = False +DEFAULT_SPEED_RANGE_MAX = 100 +DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_STATE_STOPPED = "stopped" DEFAULT_WHITE_SCALE = 255 diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 3fac4d4ffe0..39ea543c809 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -43,8 +43,38 @@ from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_DIRECTION_COMMAND_TEMPLATE, + CONF_DIRECTION_COMMAND_TOPIC, + CONF_DIRECTION_STATE_TOPIC, + CONF_DIRECTION_VALUE_TEMPLATE, + CONF_OSCILLATION_COMMAND_TEMPLATE, + CONF_OSCILLATION_COMMAND_TOPIC, + CONF_OSCILLATION_STATE_TOPIC, + CONF_OSCILLATION_VALUE_TEMPLATE, + CONF_PAYLOAD_OSCILLATION_OFF, + CONF_PAYLOAD_OSCILLATION_ON, + CONF_PAYLOAD_RESET_PERCENTAGE, + CONF_PAYLOAD_RESET_PRESET_MODE, + CONF_PERCENTAGE_COMMAND_TEMPLATE, + CONF_PERCENTAGE_COMMAND_TOPIC, + CONF_PERCENTAGE_STATE_TOPIC, + CONF_PERCENTAGE_VALUE_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TEMPLATE, + CONF_PRESET_MODE_COMMAND_TOPIC, + CONF_PRESET_MODE_STATE_TOPIC, + CONF_PRESET_MODE_VALUE_TEMPLATE, + CONF_PRESET_MODES_LIST, + CONF_SPEED_RANGE_MAX, + CONF_SPEED_RANGE_MIN, CONF_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, + DEFAULT_PAYLOAD_OFF, + DEFAULT_PAYLOAD_ON, + DEFAULT_PAYLOAD_OSCILLATE_OFF, + DEFAULT_PAYLOAD_OSCILLATE_ON, + DEFAULT_PAYLOAD_RESET, + DEFAULT_SPEED_RANGE_MAX, + DEFAULT_SPEED_RANGE_MIN, PAYLOAD_NONE, ) from .entity import MqttEntity, async_setup_entity_entry_helper @@ -59,39 +89,7 @@ from .util import valid_publish_topic, valid_subscribe_topic PARALLEL_UPDATES = 0 -CONF_DIRECTION_STATE_TOPIC = "direction_state_topic" -CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic" -CONF_DIRECTION_VALUE_TEMPLATE = "direction_value_template" -CONF_DIRECTION_COMMAND_TEMPLATE = "direction_command_template" -CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" -CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" -CONF_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template" -CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" -CONF_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage" -CONF_SPEED_RANGE_MIN = "speed_range_min" -CONF_SPEED_RANGE_MAX = "speed_range_max" -CONF_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic" -CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" -CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" -CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" -CONF_PRESET_MODES_LIST = "preset_modes" -CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" -CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" -CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" -CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" -CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" -CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" -CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" - DEFAULT_NAME = "MQTT Fan" -DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_SPEED_RANGE_MIN = 1 -DEFAULT_SPEED_RANGE_MAX = 100 - -OSCILLATE_ON_PAYLOAD = "oscillate_on" -OSCILLATE_OFF_PAYLOAD = "oscillate_off" MQTT_FAN_ATTRIBUTES_BLOCKED = frozenset( { @@ -165,10 +163,10 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional( - CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD + CONF_PAYLOAD_OSCILLATION_OFF, default=DEFAULT_PAYLOAD_OSCILLATE_OFF ): cv.string, vol.Optional( - CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD + CONF_PAYLOAD_OSCILLATION_ON, default=DEFAULT_PAYLOAD_OSCILLATE_ON ): cv.string, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index dd2186481d1..3ffd487cecb 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -214,6 +214,10 @@ "description": "Please configure specific details for {platform} entity \"{entity}\":", "data": { "device_class": "Device class", + "fan_feature_speed": "Speed support", + "fan_feature_preset_modes": "Preset modes support", + "fan_feature_oscillation": "Oscillation support", + "fan_feature_direction": "Direction support", "options": "Add option", "schema": "Schema", "state_class": "State class", @@ -222,6 +226,10 @@ }, "data_description": { "device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)", + "fan_feature_speed": "The fan supports multiple speeds.", + "fan_feature_preset_modes": "The fan supports preset modes.", + "fan_feature_oscillation": "The fan supports oscillation.", + "fan_feature_direction": "The fan supports direction.", "options": "Options for allowed sensor state values. The sensor’s Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement.", "schema": "The schema to use. [Learn more.]({url}#comparison-of-light-mqtt-schemas)", "state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)", @@ -404,6 +412,80 @@ "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." } }, + "fan_direction_settings": { + "name": "Direction settings", + "data": { + "direction_command_topic": "Direction command topic", + "direction_command_template": "Direction command template", + "direction_state_topic": "Direction state topic", + "direction_value_template": "Direction value template" + }, + "data_description": { + "direction_command_topic": "The MQTT topic to publish commands to change the fan direction payload, either `forward` or `reverse`. Use the direction command template to customize the payload. [Learn more.]({url}#direction_command_topic)", + "direction_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the direction command topic. The template variable `value` will be either `forward` or `reverse`.", + "direction_state_topic": "The MQTT topic subscribed to receive fan direction state. Accepted state payloads are `forward` or `reverse`. [Learn more.]({url}#direction_state_topic)", + "direction_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan direction state value. The template should return either `forward` or `reverse`. When the template returns an empty string, the direction will be ignored." + } + }, + "fan_oscillation_settings": { + "name": "Oscillation settings", + "data": { + "oscillation_command_topic": "Oscillation command topic", + "oscillation_command_template": "Oscillation command template", + "oscillation_state_topic": "Oscillation state topic", + "oscillation_value_template": "Oscillation value template", + "payload_oscillation_off": "Payload \"oscillation off\"", + "payload_oscillation_on": "Payload \"oscillation on\"" + }, + "data_description": { + "oscillation_command_topic": "The MQTT topic to publish commands to change the fan oscillation state. [Learn more.]({url}#oscillation_command_topic)", + "oscillation_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the oscillation command topic.", + "oscillation_state_topic": "The MQTT topic subscribed to receive fan oscillation state. [Learn more.]({url}#oscillation_state_topic)", + "oscillation_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan oscillation state value.", + "payload_oscillation_off": "The payload that represents the oscillation \"off\" state.", + "payload_oscillation_on": "The payload that represents the oscillation \"on\" state." + } + }, + "fan_preset_mode_settings": { + "name": "Preset mode settings", + "data": { + "payload_reset_preset_mode": "Payload \"reset preset mode\"", + "preset_modes": "Preset modes", + "preset_mode_command_topic": "Preset mode command topic", + "preset_mode_command_template": "Preset mode command template", + "preset_mode_state_topic": "Preset mode state topic", + "preset_mode_value_template": "Preset mode value template" + }, + "data_description": { + "payload_reset_preset_mode": "A special payload that resets the fan preset mode state attribute to unknown when received at the preset mode state topic.", + "preset_modes": "List of preset modes this fan is capable of running at. Common examples include auto, smart, whoosh, eco and breeze.", + "preset_mode_command_topic": "The MQTT topic to publish commands to change the fan preset mode. [Learn more.]({url}#preset_mode_command_topic)", + "preset_mode_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the preset mode command topic.", + "preset_mode_state_topic": "The MQTT topic subscribed to receive fan preset mode. [Learn more.]({url}#preset_mode_state_topic)", + "preset_mode_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract fan preset mode value." + } + }, + "fan_speed_settings": { + "name": "Speed settings", + "data": { + "payload_reset_percentage": "Payload \"reset percentage\"", + "percentage_command_topic": "Percentage command topic", + "percentage_command_template": "Percentage command template", + "percentage_state_topic": "Percentage state topic", + "percentage_value_template": "Percentage value template", + "speed_range_min": "Speed range min", + "speed_range_max": "Speed range max" + }, + "data_description": { + "payload_reset_percentage": "A special payload that resets the fan speed percentage state attribute to unknown when received at the percentage state topic.", + "percentage_command_topic": "The MQTT topic to publish commands to change the fan speed state based on a percentage. [Learn more.]({url}#percentage_command_topic)", + "percentage_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the percentage command topic.", + "percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)", + "percentage_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the speed percentage value.", + "speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step.", + "speed_range_max": "The maximum of numeric output range (representing 100 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step." + } + }, "light_color_mode_settings": { "name": "Color mode settings", "data": { @@ -551,6 +633,8 @@ "cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic", "cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic", "cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option", + "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", + "fan_preset_mode_reset_in_preset_modes_list": "Payload \"preset mode reset\" is not a valid preset mode", "invalid_input": "Invalid value", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_template": "Invalid template", @@ -817,6 +901,7 @@ "binary_sensor": "[%key:component::binary_sensor::title%]", "button": "[%key:component::button::title%]", "cover": "[%key:component::cover::title%]", + "fan": "[%key:component::fan::title%]", "light": "[%key:component::light::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index d1951c638a4..ab5ffe28518 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -127,6 +127,44 @@ MOCK_SUBENTRY_COVER_COMPONENT = { "entity_picture": "https://example.com/b37acf667fa04c688ad7dfb27de2178b", }, } +MOCK_SUBENTRY_FAN_COMPONENT = { + "717f924ae9ca4fe9864d845d75d23c9f": { + "platform": "fan", + "name": "Breezer", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{ value }}", + "value_template": "{{ value_json.value }}", + "percentage_command_topic": "test-topic/pct", + "percentage_state_topic": "test-topic/pct", + "percentage_command_template": "{{ value }}", + "percentage_value_template": "{{ value_json.percentage }}", + "payload_reset_percentage": "None", + "preset_modes": ["eco", "auto"], + "preset_mode_command_topic": "test-topic/prm", + "preset_mode_state_topic": "test-topic/prm", + "preset_mode_command_template": "{{ value }}", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "payload_reset_preset_mode": "None", + "oscillation_command_topic": "test-topic/osc", + "oscillation_state_topic": "test-topic/osc", + "oscillation_command_template": "{{ value }}", + "oscillation_value_template": "{{ value_json.oscillation }}", + "payload_oscillation_off": "oscillate_off", + "payload_oscillation_on": "oscillate_on", + "direction_command_topic": "test-topic/dir", + "direction_state_topic": "test-topic/dir", + "direction_command_template": "{{ value }}", + "direction_value_template": "{{ value_json.direction }}", + "payload_off": "OFF", + "payload_on": "ON", + "entity_picture": "https://example.com/717f924ae9ca4fe9864d845d75d23c9f", + "optimistic": False, + "retain": False, + "speed_range_max": 100, + "speed_range_min": 1, + }, +} MOCK_SUBENTRY_NOTIFY_COMPONENT1 = { "363a7ecad6be4a19b939a016ea93e994": { "platform": "notify", @@ -264,6 +302,10 @@ MOCK_COVER_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_COVER_COMPONENT, } +MOCK_FAN_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_FAN_COMPONENT, +} MOCK_NOTIFY_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 1}}, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 56633b2280d..a43617badb0 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -36,6 +36,7 @@ from .common import ( MOCK_BINARY_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_BUTTON_SUBENTRY_DATA_SINGLE, MOCK_COVER_SUBENTRY_DATA_SINGLE, + MOCK_FAN_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, @@ -2785,6 +2786,157 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Blind", ), + ( + MOCK_FAN_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Breezer"}, + { + "fan_feature_speed": True, + "fan_feature_preset_modes": True, + "fan_feature_oscillation": True, + "fan_feature_direction": True, + }, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "fan_speed_settings": { + "percentage_command_template": "{{ value }}", + "percentage_command_topic": "test-topic/pct", + "percentage_state_topic": "test-topic/pct", + "percentage_value_template": "{{ value_json.percentage }}", + "speed_range_min": 1, + "speed_range_max": 100, + "payload_reset_percentage": "None", + }, + "fan_preset_mode_settings": { + "preset_modes": ["eco", "auto"], + "preset_mode_command_template": "{{ value }}", + "preset_mode_command_topic": "test-topic/prm", + "preset_mode_state_topic": "test-topic/prm", + "preset_mode_value_template": "{{ value_json.preset_mode }}", + "payload_reset_preset_mode": "None", + }, + "fan_oscillation_settings": { + "oscillation_command_template": "{{ value }}", + "oscillation_command_topic": "test-topic/osc", + "oscillation_state_topic": "test-topic/osc", + "oscillation_value_template": "{{ value_json.oscillation }}", + }, + "fan_direction_settings": { + "direction_command_template": "{{ value }}", + "direction_command_topic": "test-topic/dir", + "direction_state_topic": "test-topic/dir", + "direction_value_template": "{{ value_json.direction }}", + }, + "retain": False, + "optimistic": False, + }, + ( + ( + { + "command_topic": "test-topic#invalid", + "fan_speed_settings": { + "percentage_command_topic": "test-topic#invalid", + }, + "fan_preset_mode_settings": { + "preset_modes": ["eco", "auto"], + "preset_mode_command_topic": "test-topic#invalid", + }, + "fan_oscillation_settings": { + "oscillation_command_topic": "test-topic#invalid", + }, + "fan_direction_settings": { + "direction_command_topic": "test-topic#invalid", + }, + }, + { + "command_topic": "invalid_publish_topic", + "fan_preset_mode_settings": "invalid_publish_topic", + "fan_speed_settings": "invalid_publish_topic", + "fan_oscillation_settings": "invalid_publish_topic", + "fan_direction_settings": "invalid_publish_topic", + }, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + "fan_speed_settings": { + "percentage_command_topic": "test-topic", + "percentage_state_topic": "test-topic#invalid", + }, + "fan_preset_mode_settings": { + "preset_modes": ["eco", "auto"], + "preset_mode_command_topic": "test-topic", + "preset_mode_state_topic": "test-topic#invalid", + }, + "fan_oscillation_settings": { + "oscillation_command_topic": "test-topic", + "oscillation_state_topic": "test-topic#invalid", + }, + "fan_direction_settings": { + "direction_command_topic": "test-topic", + "direction_state_topic": "test-topic#invalid", + }, + }, + { + "state_topic": "invalid_subscribe_topic", + "fan_preset_mode_settings": "invalid_subscribe_topic", + "fan_speed_settings": "invalid_subscribe_topic", + "fan_oscillation_settings": "invalid_subscribe_topic", + "fan_direction_settings": "invalid_subscribe_topic", + }, + ), + ( + { + "command_topic": "test-topic", + "fan_speed_settings": { + "percentage_command_topic": "test-topic", + }, + "fan_preset_mode_settings": { + "preset_modes": ["None", "auto"], + "preset_mode_command_topic": "test-topic", + }, + "fan_oscillation_settings": { + "oscillation_command_topic": "test-topic", + }, + "fan_direction_settings": { + "direction_command_topic": "test-topic", + }, + }, + { + "fan_preset_mode_settings": "fan_preset_mode_reset_in_preset_modes_list", + }, + ), + ( + { + "command_topic": "test-topic", + "fan_speed_settings": { + "percentage_command_topic": "test-topic", + "speed_range_min": 100, + "speed_range_max": 10, + }, + "fan_preset_mode_settings": { + "preset_modes": ["eco", "auto"], + "preset_mode_command_topic": "test-topic", + }, + "fan_oscillation_settings": { + "oscillation_command_topic": "test-topic", + }, + "fan_direction_settings": { + "direction_command_topic": "test-topic", + }, + }, + { + "fan_speed_settings": "fan_speed_range_max_must_be_greater_than_speed_range_min", + }, + ), + ), + "Milk notifier Breezer", + ), ( MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, @@ -2971,6 +3123,7 @@ async def test_migrate_of_incompatible_config_entry( "binary_sensor", "button", "cover", + "fan", "notify_with_entity_name", "notify_no_entity_name", "sensor_options", From 102230bf9d8190801ae25f53aed9328c8eb26b1e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 23 May 2025 17:46:09 +0200 Subject: [PATCH 0840/1175] Remove repoze.lru from license exceptions (#145519) Co-authored-by: Joost Lekkerkerker --- script/licenses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/script/licenses.py b/script/licenses.py index 44a046a099b..9932e61b080 100644 --- a/script/licenses.py +++ b/script/licenses.py @@ -201,7 +201,6 @@ EXCEPTIONS = { "pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5 "pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164 - "repoze.lru", "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 } From 19259d5cad8ecb4f026cb6d8e3a4548c1816ba91 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 23 May 2025 08:58:45 -0700 Subject: [PATCH 0841/1175] Add read_only selectors to Statistics Options Flow (#145522) --- homeassistant/components/statistics/config_flow.py | 13 +++++++++++++ homeassistant/components/statistics/strings.json | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/statistics/config_flow.py b/homeassistant/components/statistics/config_flow.py index 4c78afbde9c..fb8c09868d5 100644 --- a/homeassistant/components/statistics/config_flow.py +++ b/homeassistant/components/statistics/config_flow.py @@ -106,6 +106,19 @@ DATA_SCHEMA_SETUP = vol.Schema( ) DATA_SCHEMA_OPTIONS = vol.Schema( { + vol.Optional(CONF_ENTITY_ID): EntitySelector( + EntitySelectorConfig(read_only=True) + ), + vol.Optional(CONF_STATE_CHARACTERISTIC): SelectSelector( + SelectSelectorConfig( + options=list( + set(list(STATS_BINARY_SUPPORT) + list(STATS_NUMERIC_SUPPORT)) + ), + translation_key=CONF_STATE_CHARACTERISTIC, + mode=SelectSelectorMode.DROPDOWN, + read_only=True, + ) + ), vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): NumberSelector( NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) ), diff --git a/homeassistant/components/statistics/strings.json b/homeassistant/components/statistics/strings.json index e1085a016ce..e0093fd08c8 100644 --- a/homeassistant/components/statistics/strings.json +++ b/homeassistant/components/statistics/strings.json @@ -32,6 +32,8 @@ "options": { "description": "Read the documentation for further details on how to configure the statistics sensor using these options.", "data": { + "entity_id": "[%key:component::statistics::config::step::user::data::entity_id%]", + "state_characteristic": "[%key:component::statistics::config::step::state_characteristic::data::state_characteristic%]", "sampling_size": "Sampling size", "max_age": "Max age", "keep_last_sample": "Keep last sample", @@ -39,6 +41,8 @@ "precision": "Precision" }, "data_description": { + "entity_id": "[%key:component::statistics::config::step::user::data_description::entity_id%]", + "state_characteristic": "[%key:component::statistics::config::step::state_characteristic::data_description::state_characteristic%]", "sampling_size": "Maximum number of source sensor measurements stored.", "max_age": "Maximum age of source sensor measurements stored.", "keep_last_sample": "Defines whether the most recent sampled value should be preserved regardless of the 'Max age' setting.", @@ -60,6 +64,8 @@ "init": { "description": "[%key:component::statistics::config::step::options::description%]", "data": { + "entity_id": "[%key:component::statistics::config::step::user::data::entity_id%]", + "state_characteristic": "[%key:component::statistics::config::step::state_characteristic::data::state_characteristic%]", "sampling_size": "[%key:component::statistics::config::step::options::data::sampling_size%]", "max_age": "[%key:component::statistics::config::step::options::data::max_age%]", "keep_last_sample": "[%key:component::statistics::config::step::options::data::keep_last_sample%]", @@ -67,6 +73,8 @@ "precision": "[%key:component::statistics::config::step::options::data::precision%]" }, "data_description": { + "entity_id": "[%key:component::statistics::config::step::user::data_description::entity_id%]", + "state_characteristic": "[%key:component::statistics::config::step::state_characteristic::data_description::state_characteristic%]", "sampling_size": "[%key:component::statistics::config::step::options::data_description::sampling_size%]", "max_age": "[%key:component::statistics::config::step::options::data_description::max_age%]", "keep_last_sample": "[%key:component::statistics::config::step::options::data_description::keep_last_sample%]", From d8ed10bcc766b5384dad01698a4322ab23dc4c40 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 23 May 2025 21:10:26 +0200 Subject: [PATCH 0842/1175] Use _handle_coordinator_update() instead of own callback in Feedreader event entity (#145520) use _handle_coordinator_update() instead of own callback --- homeassistant/components/feedreader/event.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/feedreader/event.py b/homeassistant/components/feedreader/event.py index 578b5b1e175..dc7c9e880d5 100644 --- a/homeassistant/components/feedreader/event.py +++ b/homeassistant/components/feedreader/event.py @@ -61,15 +61,9 @@ class FeedReaderEvent(CoordinatorEntity[FeedReaderCoordinator], EventEntity): entry_type=DeviceEntryType.SERVICE, ) - async def async_added_to_hass(self) -> None: - """Entity added to hass.""" - await super().async_added_to_hass() - self.async_on_remove( - self.coordinator.async_add_listener(self._async_handle_update) - ) - @callback - def _async_handle_update(self) -> None: + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" if (data := self.coordinator.data) is None or not data: return From c359765a29c6013aed9eb12856021aadd9aa6337 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 23 May 2025 17:59:22 -0400 Subject: [PATCH 0843/1175] Remove inactive codeowner from template integration (#145535) --- CODEOWNERS | 4 ++-- homeassistant/components/template/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b80b9bc6591..5bc9a2dd8d7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1546,8 +1546,8 @@ build.json @home-assistant/supervisor /tests/components/tedee/ @patrickhilker @zweckj /homeassistant/components/tellduslive/ @fredrike /tests/components/tellduslive/ @fredrike -/homeassistant/components/template/ @Petro31 @PhracturedBlue @home-assistant/core -/tests/components/template/ @Petro31 @PhracturedBlue @home-assistant/core +/homeassistant/components/template/ @Petro31 @home-assistant/core +/tests/components/template/ @Petro31 @home-assistant/core /homeassistant/components/tesla_fleet/ @Bre77 /tests/components/tesla_fleet/ @Bre77 /homeassistant/components/tesla_wall_connector/ @einarhauks diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index 32bfd8ce02e..61c0bd1179a 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -2,7 +2,7 @@ "domain": "template", "name": "Template", "after_dependencies": ["group"], - "codeowners": ["@Petro31", "@PhracturedBlue", "@home-assistant/core"], + "codeowners": ["@Petro31", "@home-assistant/core"], "config_flow": true, "dependencies": ["blueprint"], "documentation": "https://www.home-assistant.io/integrations/template", From 2d3a6d780ce467fb66a3e375a717552fd9198b0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 May 2025 02:52:48 -0500 Subject: [PATCH 0844/1175] Bump aiohttp to 3.12.0rc0 (#145540) changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.0b3...v3.12.0rc0 --- 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 643deb72a51..1cd6fe95cb5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.0b3 +aiohttp==3.12.0rc0 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 5904ef4f48b..08d3d741f5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.0b3", + "aiohttp==3.12.0rc0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index d6986a8872c..673a2b85c07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.0b3 +aiohttp==3.12.0rc0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From f92d14d87c19b3643eb8a39e98bcce60268b95c5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 24 May 2025 10:53:08 +0200 Subject: [PATCH 0845/1175] Bump incomfort-client to v0.6.9 (#145546) --- homeassistant/components/incomfort/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 6214eb03f40..6ab9f560496 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -11,5 +11,5 @@ "iot_class": "local_polling", "loggers": ["incomfortclient"], "quality_scale": "platinum", - "requirements": ["incomfort-client==0.6.8"] + "requirements": ["incomfort-client==0.6.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8b88a29ee65..808a1312794 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1230,7 +1230,7 @@ imeon_inverter_api==0.3.12 imgw_pib==1.0.10 # homeassistant.components.incomfort -incomfort-client==0.6.8 +incomfort-client==0.6.9 # homeassistant.components.influxdb influxdb-client==1.48.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40fa341fe5c..0f074ec5a3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1045,7 +1045,7 @@ imeon_inverter_api==0.3.12 imgw_pib==1.0.10 # homeassistant.components.incomfort -incomfort-client==0.6.8 +incomfort-client==0.6.9 # homeassistant.components.influxdb influxdb-client==1.48.0 From 5c7aa833ec93d00a4d8d8ffdf6f30473d43419a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sat, 24 May 2025 10:41:16 +0100 Subject: [PATCH 0846/1175] Simplify ZBT-1 setup string (#145532) --- homeassistant/components/homeassistant_hardware/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 6dda01561f1..e184f9b3a85 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -28,7 +28,7 @@ }, "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." + "description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration." }, "install_otbr_addon": { "title": "Installing OpenThread Border Router add-on", @@ -44,7 +44,7 @@ }, "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." + "description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration." } }, "abort": { From 8356bdb506e8556b68b99debf342e58114907b07 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 24 May 2025 08:41:40 -0700 Subject: [PATCH 0847/1175] Bump androidtvremote2 to 0.2.2 (#145542) --- homeassistant/components/androidtv_remote/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv_remote/manifest.json b/homeassistant/components/androidtv_remote/manifest.json index 89cc0fc3965..7896f7eefc8 100644 --- a/homeassistant/components/androidtv_remote/manifest.json +++ b/homeassistant/components/androidtv_remote/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["androidtvremote2"], - "requirements": ["androidtvremote2==0.2.1"], + "requirements": ["androidtvremote2==0.2.2"], "zeroconf": ["_androidtvremote2._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 808a1312794..9fefe8bf42e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -468,7 +468,7 @@ amcrest==1.9.8 androidtv[async]==0.0.75 # homeassistant.components.androidtv_remote -androidtvremote2==0.2.1 +androidtvremote2==0.2.2 # homeassistant.components.anel_pwrctrl anel-pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f074ec5a3f..bdaff331cc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -444,7 +444,7 @@ amberelectric==2.0.12 androidtv[async]==0.0.75 # homeassistant.components.androidtv_remote -androidtvremote2==0.2.1 +androidtvremote2==0.2.2 # homeassistant.components.anova anova-wifi==0.17.0 From adf8e5031321023f04851129864d52e2f2eb3806 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 24 May 2025 12:20:04 -0700 Subject: [PATCH 0848/1175] Add data descriptions in the Android TV Remote Configure Android apps (#145537) * Add data descriptions in the Android TV Remote Configure Android apps * Update homeassistant/components/androidtv_remote/strings.json --------- Co-authored-by: Josef Zweck --- homeassistant/components/androidtv_remote/strings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/androidtv_remote/strings.json b/homeassistant/components/androidtv_remote/strings.json index 106cac3a63d..c82b815e27a 100644 --- a/homeassistant/components/androidtv_remote/strings.json +++ b/homeassistant/components/androidtv_remote/strings.json @@ -51,6 +51,10 @@ "app_id": "Application ID", "app_icon": "Application icon", "app_delete": "Check to delete this application" + }, + "data_description": { + "app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android", + "app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename" } } } From a707cbc51b996a19474a0d965ba2209bea84d642 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 24 May 2025 21:26:49 +0200 Subject: [PATCH 0849/1175] Fix translation strings for MQTT subentries (#145529) --- homeassistant/components/mqtt/strings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 3ffd487cecb..5e4c2612592 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -372,8 +372,8 @@ "name": "Tilt settings", "data": { "tilt_closed_value": "Tilt \"closed\" value", - "tilt_command_template": "Set tilt template", - "tilt_command_topic": "Set tilt topic", + "tilt_command_template": "Tilt command template", + "tilt_command_topic": "Tilt command topic", "tilt_max": "Tilt max", "tilt_min": "Tilt min", "tilt_opened_value": "Tilt \"opened\" value", @@ -482,8 +482,8 @@ "percentage_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the percentage command topic.", "percentage_state_topic": "The MQTT topic subscribed to receive fan speed based on percentage. [Learn more.]({url}#percentage_state_topic)", "percentage_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the speed percentage value.", - "speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step.", - "speed_range_max": "The maximum of numeric output range (representing 100 %). The number of speeds within the \"speed range\" / 100 will determine the percentage step." + "speed_range_min": "The minimum of numeric output range (off not included, so speed_range_min - 1 represents 0 %). The percentage step is 100 / the number of speeds within the \"speed range\".", + "speed_range_max": "The maximum of numeric output range (representing 100 %). The percentage step is 100 / number of speeds within the \"speed range\"." } }, "light_color_mode_settings": { @@ -628,13 +628,13 @@ }, "error": { "cover_get_and_set_position_must_be_set_together": "The get position and set position topic options must be set together", - "cover_get_position_template_must_be_used_with_get_position_topic": "The position value template must be used together with the position state topic setting", + "cover_get_position_template_must_be_used_with_get_position_topic": "The position value template must be used together with the position state topic", "cover_set_position_template_must_be_used_with_set_position_topic": "The set position template must be used with the set position topic", "cover_tilt_command_template_must_be_used_with_tilt_command_topic": "The tilt command template must be used with the tilt command topic", "cover_tilt_status_template_must_be_used_with_tilt_status_topic": "The tilt value template must be used with the tilt status topic", "cover_value_template_must_be_used_with_state_topic": "The value template must be used with the state topic option", "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", - "fan_preset_mode_reset_in_preset_modes_list": "Payload \"preset mode reset\" is not a valid preset mode", + "fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode", "invalid_input": "Invalid value", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_template": "Invalid template", From 1044a5341d0b6e8ffff4106d41667a2d9d258eda Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 24 May 2025 21:53:41 +0200 Subject: [PATCH 0850/1175] Bump python-linkplay to v0.2.8 (#145550) * Bump linkplay to v0.2.7 * Bump linkplay to v0.2.8 --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index ac89d2ff399..fafc9e66514 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.5"], + "requirements": ["python-linkplay==0.2.8"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9fefe8bf42e..072794a2114 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2446,7 +2446,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.5 +python-linkplay==0.2.8 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdaff331cc1..6536ed7d3d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1989,7 +1989,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.5 +python-linkplay==0.2.8 # homeassistant.components.matter python-matter-server==7.0.0 From ce02a5544d88433e2c4ab2c4409abb5617f05066 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 May 2025 16:12:16 -0500 Subject: [PATCH 0851/1175] Bump aiohttp to 3.12.0rc1 (#145562) --- homeassistant/package_constraints.txt | 2 +- homeassistant/util/aiohttp.py | 3 +++ pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1cd6fe95cb5..43740736da6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.0rc0 +aiohttp==3.12.0rc1 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 5b6774a08a5..e5b319195ff 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -42,6 +42,9 @@ class MockPayloadWriter: def enable_chunking(self) -> None: """Enable chunking.""" + async def send_headers(self, *args: Any, **kwargs: Any) -> None: + """Write headers.""" + async def write_headers(self, *args: Any, **kwargs: Any) -> None: """Write headers.""" diff --git a/pyproject.toml b/pyproject.toml index 08d3d741f5d..eb701b4b540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.0rc0", + "aiohttp==3.12.0rc1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 673a2b85c07..5dcda4c1268 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.0rc0 +aiohttp==3.12.0rc1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 526a8ee31fd6d7c24aff7637d2bdd98a1f9f82a2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 May 2025 00:37:21 +0300 Subject: [PATCH 0852/1175] Add preset mode to Comelit climate (#145195) --- homeassistant/components/comelit/climate.py | 65 +++++++++++----- homeassistant/components/comelit/const.py | 5 ++ homeassistant/components/comelit/icons.json | 12 +++ homeassistant/components/comelit/strings.json | 12 +++ .../comelit/snapshots/test_climate.ambr | 17 ++-- tests/components/comelit/test_climate.py | 78 ++++++++++++++++++- 6 files changed, 162 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index 69d95da01bf..6b05ed80b13 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -20,7 +20,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN +from .const import ( + DOMAIN, + PRESET_MODE_AUTO, + PRESET_MODE_AUTO_TARGET_TEMP, + PRESET_MODE_MANUAL, +) from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity from .utils import bridge_api_call @@ -41,11 +46,13 @@ class ClimaComelitMode(StrEnum): class ClimaComelitCommand(StrEnum): """Serial Bridge clima commands.""" + AUTO = "auto" + MANUAL = "man" OFF = "off" ON = "on" - MANUAL = "man" SET = "set" - AUTO = "auto" + SNOW = "lower" + SUN = "upper" class ClimaComelitApiStatus(TypedDict): @@ -67,11 +74,15 @@ API_STATUS: dict[str, ClimaComelitApiStatus] = { ), } -MODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = { +HVACMODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = { HVACMode.OFF: ClimaComelitCommand.OFF, - HVACMode.AUTO: ClimaComelitCommand.AUTO, - HVACMode.COOL: ClimaComelitCommand.MANUAL, - HVACMode.HEAT: ClimaComelitCommand.MANUAL, + HVACMode.COOL: ClimaComelitCommand.SNOW, + HVACMode.HEAT: ClimaComelitCommand.SUN, +} + +PRESET_MODE_TO_ACTION: dict[str, ClimaComelitCommand] = { + PRESET_MODE_MANUAL: ClimaComelitCommand.MANUAL, + PRESET_MODE_AUTO: ClimaComelitCommand.AUTO, } @@ -93,17 +104,20 @@ async def async_setup_entry( class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): """Climate device.""" - _attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] + _attr_hvac_modes = [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF] + _attr_preset_modes = [PRESET_MODE_AUTO, PRESET_MODE_MANUAL] _attr_max_temp = 30 _attr_min_temp = 5 _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.PRESET_MODE ) _attr_target_temperature_step = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_name = None + _attr_translation_key = "thermostat" def __init__( self, @@ -132,6 +146,8 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): _mode = values[2] # Values from API: "O", "L", "U" _automatic = values[3] == ClimaComelitMode.AUTO + self._attr_preset_mode = PRESET_MODE_AUTO if _automatic else PRESET_MODE_MANUAL + self._attr_current_temperature = values[0] / 10 self._attr_hvac_action = None @@ -141,10 +157,6 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): self._attr_hvac_action = API_STATUS[_mode]["hvac_action"] self._attr_hvac_mode = None - if _mode == ClimaComelitMode.OFF: - self._attr_hvac_mode = HVACMode.OFF - if _automatic: - self._attr_hvac_mode = HVACMode.AUTO if _mode in API_STATUS: self._attr_hvac_mode = API_STATUS[_mode]["hvac_mode"] @@ -160,13 +172,12 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ( - target_temp := kwargs.get(ATTR_TEMPERATURE) - ) is None or self.hvac_mode == HVACMode.OFF: + (target_temp := kwargs.get(ATTR_TEMPERATURE)) is None + or self.hvac_mode == HVACMode.OFF + or self._attr_preset_mode == PRESET_MODE_AUTO + ): return - await self.coordinator.api.set_clima_status( - self._device.index, ClimaComelitCommand.MANUAL - ) await self.coordinator.api.set_clima_status( self._device.index, ClimaComelitCommand.SET, target_temp ) @@ -177,12 +188,28 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" - if hvac_mode != HVACMode.OFF: + if self._attr_hvac_mode == HVACMode.OFF: await self.coordinator.api.set_clima_status( self._device.index, ClimaComelitCommand.ON ) await self.coordinator.api.set_clima_status( - self._device.index, MODE_TO_ACTION[hvac_mode] + self._device.index, HVACMODE_TO_ACTION[hvac_mode] ) self._attr_hvac_mode = hvac_mode self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new target preset mode.""" + + if self._attr_hvac_mode == HVACMode.OFF: + return + + await self.coordinator.api.set_clima_status( + self._device.index, PRESET_MODE_TO_ACTION[preset_mode] + ) + self._attr_preset_mode = preset_mode + + if preset_mode == PRESET_MODE_AUTO: + self._attr_target_temperature = PRESET_MODE_AUTO_TARGET_TEMP + + self.async_write_ha_state() diff --git a/homeassistant/components/comelit/const.py b/homeassistant/components/comelit/const.py index f52f33fd6da..4baaf0ee426 100644 --- a/homeassistant/components/comelit/const.py +++ b/homeassistant/components/comelit/const.py @@ -11,3 +11,8 @@ DEFAULT_PORT = 80 DEVICE_TYPE_LIST = [BRIDGE, VEDO] SCAN_INTERVAL = 5 + +PRESET_MODE_AUTO = "automatic" +PRESET_MODE_MANUAL = "manual" + +PRESET_MODE_AUTO_TARGET_TEMP = 20 diff --git a/homeassistant/components/comelit/icons.json b/homeassistant/components/comelit/icons.json index 6c42d20de65..6ac83cfc8e0 100644 --- a/homeassistant/components/comelit/icons.json +++ b/homeassistant/components/comelit/icons.json @@ -4,6 +4,18 @@ "zone_status": { "default": "mdi:shield-check" } + }, + "climate": { + "thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "automatic": "mdi:refresh-auto", + "manual": "mdi:alpha-m" + } + } + } + } } } } diff --git a/homeassistant/components/comelit/strings.json b/homeassistant/components/comelit/strings.json index 7a04b5d2d04..d63d22f307a 100644 --- a/homeassistant/components/comelit/strings.json +++ b/homeassistant/components/comelit/strings.json @@ -74,6 +74,18 @@ "dehumidifier": { "name": "Dehumidifier" } + }, + "climate": { + "thermostat": { + "state_attributes": { + "preset_mode": { + "state": { + "automatic": "[%key:common::state::auto%]", + "manual": "[%key:common::state::manual%]" + } + } + } + } } }, "exceptions": { diff --git a/tests/components/comelit/snapshots/test_climate.ambr b/tests/components/comelit/snapshots/test_climate.ambr index 0233359bc45..1f8ce4a3caf 100644 --- a/tests/components/comelit/snapshots/test_climate.ambr +++ b/tests/components/comelit/snapshots/test_climate.ambr @@ -6,13 +6,16 @@ 'area_id': None, 'capabilities': dict({ 'hvac_modes': list([ - , , , , ]), 'max_temp': 30, 'min_temp': 5, + 'preset_modes': list([ + 'automatic', + 'manual', + ]), 'target_temp_step': 0.1, }), 'config_entry_id': , @@ -37,8 +40,8 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, - 'supported_features': , - 'translation_key': None, + 'supported_features': , + 'translation_key': 'thermostat', 'unique_id': 'serial_bridge_config_entry_id-0', 'unit_of_measurement': None, }) @@ -50,14 +53,18 @@ 'friendly_name': 'Climate0', 'hvac_action': , 'hvac_modes': list([ - , , , , ]), 'max_temp': 30, 'min_temp': 5, - 'supported_features': , + 'preset_mode': 'manual', + 'preset_modes': list([ + 'automatic', + 'manual', + ]), + 'supported_features': , 'target_temp_step': 0.1, 'temperature': 5.0, }), diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index 1938211c9dd..5027106cb5b 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -11,12 +11,19 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.climate import ( ATTR_HVAC_MODE, + ATTR_PRESET_MODE, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, HVACMode, ) -from homeassistant.components.comelit.const import SCAN_INTERVAL +from homeassistant.components.comelit.const import ( + PRESET_MODE_AUTO, + PRESET_MODE_MANUAL, + SCAN_INTERVAL, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -273,10 +280,75 @@ async def test_climate_hvac_mode_when_off( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.AUTO}, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.COOL}, blocking=True, ) mock_serial_bridge.set_clima_status.assert_called() assert (state := hass.states.get(ENTITY_ID)) - assert state.state == HVACMode.AUTO + assert state.state == HVACMode.COOL + + +async def test_climate_preset_mode( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test climate preset mode service.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 5.0 + assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_MANUAL + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_MODE_AUTO}, + blocking=True, + ) + mock_serial_bridge.set_clima_status.assert_called() + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 20.0 + assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_AUTO + + +async def test_climate_preset_mode_when_off( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test climate preset mode service when off.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 5.0 + assert state.attributes[ATTR_PRESET_MODE] == PRESET_MODE_MANUAL + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + mock_serial_bridge.set_clima_status.assert_called() + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.OFF + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_MODE_AUTO}, + blocking=True, + ) + mock_serial_bridge.set_clima_status.assert_called() + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.OFF From 1e0a2b704f64ceb4174e5f0cbda4923d48aa6419 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 24 May 2025 23:46:51 +0200 Subject: [PATCH 0853/1175] Bump pylamarzocco to 2.0.5 (#145560) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 44ca31427c0..a40f252f822 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.4"] + "requirements": ["pylamarzocco==2.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 072794a2114..39b4ebd9062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.4 +pylamarzocco==2.0.5 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6536ed7d3d7..d779a50ef99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.4 +pylamarzocco==2.0.5 # homeassistant.components.lastfm pylast==5.1.0 From 57f754b42b96c702b92e7f0c9177949ad1189fc5 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Sat, 24 May 2025 19:04:26 -0400 Subject: [PATCH 0854/1175] Bump aiokem to 0.5.12 (#145565) --- homeassistant/components/rehlko/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rehlko/manifest.json b/homeassistant/components/rehlko/manifest.json index 798fd4b61d2..24c9608e661 100644 --- a/homeassistant/components/rehlko/manifest.json +++ b/homeassistant/components/rehlko/manifest.json @@ -13,5 +13,5 @@ "iot_class": "cloud_polling", "loggers": ["aiokem"], "quality_scale": "silver", - "requirements": ["aiokem==0.5.11"] + "requirements": ["aiokem==0.5.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index 39b4ebd9062..10b5b8468d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ aiokafka==0.10.0 aiokef==0.2.16 # homeassistant.components.rehlko -aiokem==0.5.11 +aiokem==0.5.12 # homeassistant.components.lifx aiolifx-effects==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d779a50ef99..911dfd16b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ aioimmich==0.6.0 aiokafka==0.10.0 # homeassistant.components.rehlko -aiokem==0.5.11 +aiokem==0.5.12 # homeassistant.components.lifx aiolifx-effects==0.3.2 From 13d530d11062fa109dbd98a3a37fe096ef48a4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 24 May 2025 18:10:58 -0500 Subject: [PATCH 0855/1175] Bump aiohttp to 3.12.0 (#145570) --- 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 43740736da6..075d6a7f502 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.0rc1 +aiohttp==3.12.0 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index eb701b4b540..30862625712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.0rc1", + "aiohttp==3.12.0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 5dcda4c1268..53502f0d8df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.0rc1 +aiohttp==3.12.0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 535d128f8a98827185ace3df2bd8f7761da9a535 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 May 2025 03:03:07 +0300 Subject: [PATCH 0856/1175] Remove global registry reference in coordinator for UptimeRobot (#142938) * Remove global registry reference in coordinator for UptimeRobot * rework current_monitors listing * fix logic --- .../components/uptimerobot/coordinator.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 2f6225fa498..7ecb1ee3313 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -39,7 +39,6 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon name=DOMAIN, update_interval=COORDINATOR_UPDATE_INTERVAL, ) - self._device_registry = dr.async_get(hass) self.api = api async def _async_update_data(self) -> list[UptimeRobotMonitor]: @@ -56,23 +55,21 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon monitors: list[UptimeRobotMonitor] = response.data - current_monitors = { - list(device.identifiers)[0][1] - for device in dr.async_entries_for_config_entry( - self._device_registry, self.config_entry.entry_id - ) - } + current_monitors = ( + {str(monitor.id) for monitor in self.data} if self.data else set() + ) new_monitors = {str(monitor.id) for monitor in monitors} if stale_monitors := current_monitors - new_monitors: for monitor_id in stale_monitors: - if device := self._device_registry.async_get_device( + device_registry = dr.async_get(self.hass) + if device := device_registry.async_get_device( identifiers={(DOMAIN, monitor_id)} ): - self._device_registry.async_remove_device(device.id) + device_registry.async_remove_device(device.id) # If there are new monitors, we should reload the config entry so we can # create new devices and entities. - if self.data and new_monitors - {str(monitor.id) for monitor in self.data}: + if self.data and new_monitors - current_monitors: self.hass.async_create_task( self.hass.config_entries.async_reload(self.config_entry.entry_id) ) From fa37bc272e47f279683d78701fc3aa3ba361642f Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 25 May 2025 01:37:50 -0700 Subject: [PATCH 0857/1175] Bump opower to 0.12.2 (#145573) --- 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 beaf63ad59d..7ac9f4cc943 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.12.1"] + "requirements": ["opower==0.12.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10b5b8468d2..a7f8bdcc110 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1614,7 +1614,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.1 +opower==0.12.2 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 911dfd16b3c..1914f1abf88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1351,7 +1351,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.1 +opower==0.12.2 # homeassistant.components.oralb oralb-ble==0.17.6 From 5eebadc730cc9dd9e8054ccbf49014c0c86b71c6 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 25 May 2025 10:38:57 +0200 Subject: [PATCH 0858/1175] Add SmartThings freezer and cooler temperatures (#145468) --- .../components/smartthings/sensor.py | 10 + .../components/smartthings/strings.json | 6 + .../smartthings/snapshots/test_sensor.ambr | 312 ++++++++++++++++++ 3 files changed, 328 insertions(+) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 8ae479e58f5..ef066c02130 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -151,6 +151,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): exists_fn: Callable[[Status], bool] | None = None use_temperature_unit: bool = False deprecated: Callable[[ComponentStatus], tuple[str, str] | None] | None = None + component_translation_key: dict[str, str] | None = None CAPABILITY_TO_SENSORS: dict[ @@ -862,6 +863,11 @@ CAPABILITY_TO_SENSORS: dict[ if Capability.CUSTOM_OUTING_MODE in status else None ), + component_fn=lambda component: component in {"freezer", "cooler"}, + component_translation_key={ + "freezer": "freezer_temperature", + "cooler": "cooler_temperature", + }, ) ] }, @@ -1207,6 +1213,10 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): self._attr_translation_placeholders = ( self.entity_description.translation_placeholders_fn(component) ) + if self.entity_description.component_translation_key and component != MAIN: + self._attr_translation_key = ( + self.entity_description.component_translation_key[component] + ) @property def native_value(self) -> str | float | datetime | int | None: diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c13fd0e7932..dbbc01c34b2 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -242,6 +242,9 @@ "finished": "[%key:component::smartthings::entity::sensor::oven_job_state::state::finished%]" } }, + "cooler_temperature": { + "name": "Cooler temperature" + }, "manual_level": { "name": "Burner {burner_id} level" }, @@ -325,6 +328,9 @@ "equivalent_carbon_dioxide": { "name": "Equivalent carbon dioxide" }, + "freezer_temperature": { + "name": "Freezer temperature" + }, "formaldehyde": { "name": "Formaldehyde" }, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 4197837112c..569838471fc 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -4589,6 +4589,58 @@ 'state': '218', }) # --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Cooler temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4754,6 +4806,58 @@ 'state': '0.0', }) # --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_freezer_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Freezer temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-18', + }) +# --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4866,6 +4970,58 @@ 'state': '0.0135559777781698', }) # --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Cooler temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5031,6 +5187,58 @@ 'state': '0.0', }) # --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Freezer temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-18', + }) +# --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5143,6 +5351,58 @@ 'state': '0.0270189050030708', }) # --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_cooler_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frigo_cooler_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': 'Cooler temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_cooler_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Cooler temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_cooler_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5308,6 +5568,58 @@ 'state': '0.0', }) # --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frigo_freezer_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': 'Freezer temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'freezer_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Freezer temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-17', + }) +# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From d0bc71752b69d667874fc3d2f7146994ad221976 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 25 May 2025 14:01:15 +0200 Subject: [PATCH 0859/1175] Safe get for backflush status in lamarzocco (#145559) * Safe get for backflush status in lamarzocco * add correct default --- homeassistant/components/lamarzocco/binary_sensor.py | 5 ++++- homeassistant/components/lamarzocco/sensor.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index c108bdb02d8..aacfca929ad 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -61,7 +61,10 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.RUNNING, is_on_fn=( lambda machine: cast( - BackFlush, machine.dashboard.config[WidgetType.CM_BACK_FLUSH] + BackFlush, + machine.dashboard.config.get( + WidgetType.CM_BACK_FLUSH, BackFlush(status=BackFlushStatus.OFF) + ), ).status is BackFlushStatus.REQUESTED ), diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index aecb2ff7f04..afe34005108 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import datetime from typing import cast -from pylamarzocco.const import ModelName, WidgetType +from pylamarzocco.const import BackFlushStatus, ModelName, WidgetType from pylamarzocco.models import ( BackFlush, BaseWidgetOutput, @@ -106,7 +106,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TIMESTAMP, value_fn=( lambda config: cast( - BackFlush, config[WidgetType.CM_BACK_FLUSH] + BackFlush, + config.get( + WidgetType.CM_BACK_FLUSH, BackFlush(status=BackFlushStatus.OFF) + ), ).last_cleaning_start_time ), entity_category=EntityCategory.DIAGNOSTIC, From 8c971904caba54c14b4fb72343966d22dc6f3beb Mon Sep 17 00:00:00 2001 From: Florian von Garrel Date: Sun, 25 May 2025 14:03:13 +0200 Subject: [PATCH 0860/1175] Add reauth and reconfigure to paperless (#145469) * Add reauth and reconfigure * Reauth and reconfigure in different functions * Add duplicate check * Add test for reconfigure duplicate * Removed seconds config entry fixture --- .../components/paperless_ngx/config_flow.py | 121 ++++++++++--- .../components/paperless_ngx/coordinator.py | 14 +- .../components/paperless_ngx/manifest.json | 2 +- .../paperless_ngx/quality_scale.yaml | 4 +- .../components/paperless_ngx/strings.json | 24 ++- tests/components/paperless_ngx/conftest.py | 6 +- tests/components/paperless_ngx/const.py | 11 +- .../paperless_ngx/snapshots/test_sensor.ambr | 12 +- .../paperless_ngx/test_config_flow.py | 166 +++++++++++++++++- tests/components/paperless_ngx/test_sensor.py | 22 +-- 10 files changed, 319 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/paperless_ngx/config_flow.py b/homeassistant/components/paperless_ngx/config_flow.py index 039cb23a470..c0c1dc4ce19 100644 --- a/homeassistant/components/paperless_ngx/config_flow.py +++ b/homeassistant/components/paperless_ngx/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any from pypaperless import Paperless @@ -36,6 +37,7 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle the initial step.""" + errors: dict[str, str] = {} if user_input is not None: self._async_abort_entries_match( { @@ -44,31 +46,9 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN): } ) - errors: dict[str, str] = {} - if user_input is not None: - client = Paperless( - user_input[CONF_URL], - user_input[CONF_API_KEY], - session=async_get_clientsession(self.hass), - ) + errors = await self._validate_input(user_input) - try: - await client.initialize() - await client.statistics() - except PaperlessConnectionError: - errors[CONF_URL] = "cannot_connect" - except PaperlessInvalidTokenError: - errors[CONF_API_KEY] = "invalid_api_key" - except PaperlessInactiveOrDeletedError: - errors[CONF_API_KEY] = "user_inactive_or_deleted" - except PaperlessForbiddenError: - errors[CONF_API_KEY] = "forbidden" - except InitializationError: - errors[CONF_URL] = "cannot_connect" - except Exception as err: # noqa: BLE001 - LOGGER.exception("Unexpected exception: %s", err) - errors["base"] = "unknown" - else: + if not errors: return self.async_create_entry( title=user_input[CONF_URL], data=user_input ) @@ -76,3 +56,96 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Reconfigure flow for Paperless-ngx integration.""" + + entry = self._get_reconfigure_entry() + + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match( + { + CONF_URL: user_input[CONF_URL], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) + + errors = await self._validate_input(user_input) + + if not errors: + return self.async_update_reload_and_abort(entry, data=user_input) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values={ + CONF_URL: user_input[CONF_URL] + if user_input is not None + else entry.data[CONF_URL], + }, + ), + errors=errors, + ) + + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-auth.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Reauth flow for Paperless-ngx integration.""" + + entry = self._get_reauth_entry() + + errors: dict[str, str] = {} + if user_input is not None: + updated_data = {**entry.data, CONF_API_KEY: user_input[CONF_API_KEY]} + + errors = await self._validate_input(updated_data) + + if not errors: + return self.async_update_reload_and_abort( + entry, + data=updated_data, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + + async def _validate_input(self, user_input: dict[str, str]) -> dict[str, str]: + errors: dict[str, str] = {} + + client = Paperless( + user_input[CONF_URL], + user_input[CONF_API_KEY], + session=async_get_clientsession(self.hass), + ) + + try: + await client.initialize() + await client.statistics() # test permissions on api + except PaperlessConnectionError: + errors[CONF_URL] = "cannot_connect" + except PaperlessInvalidTokenError: + errors[CONF_API_KEY] = "invalid_api_key" + except PaperlessInactiveOrDeletedError: + errors[CONF_API_KEY] = "user_inactive_or_deleted" + except PaperlessForbiddenError: + errors[CONF_API_KEY] = "forbidden" + except InitializationError: + errors[CONF_URL] = "cannot_connect" + except Exception as err: # noqa: BLE001 + LOGGER.exception("Unexpected exception: %s", err) + errors["base"] = "unknown" + + return errors diff --git a/homeassistant/components/paperless_ngx/coordinator.py b/homeassistant/components/paperless_ngx/coordinator.py index 542c0fee71f..a8296bbda89 100644 --- a/homeassistant/components/paperless_ngx/coordinator.py +++ b/homeassistant/components/paperless_ngx/coordinator.py @@ -17,7 +17,11 @@ from pypaperless.models import Statistic from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -63,12 +67,12 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): translation_key="cannot_connect", ) from err except PaperlessInvalidTokenError as err: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_api_key", ) from err except PaperlessInactiveOrDeletedError as err: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="user_inactive_or_deleted", ) from err @@ -98,12 +102,12 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): translation_key="forbidden", ) from err except PaperlessInvalidTokenError as err: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_api_key", ) from err except PaperlessInactiveOrDeletedError as err: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="user_inactive_or_deleted", ) from err diff --git a/homeassistant/components/paperless_ngx/manifest.json b/homeassistant/components/paperless_ngx/manifest.json index 2ff8aaed4ab..0be3562c76f 100644 --- a/homeassistant/components/paperless_ngx/manifest.json +++ b/homeassistant/components/paperless_ngx/manifest.json @@ -7,6 +7,6 @@ "integration_type": "service", "iot_class": "local_polling", "loggers": ["pypaperless"], - "quality_scale": "bronze", + "quality_scale": "silver", "requirements": ["pypaperless==4.1.0"] } diff --git a/homeassistant/components/paperless_ngx/quality_scale.yaml b/homeassistant/components/paperless_ngx/quality_scale.yaml index 31fdc781c2e..827d4425132 100644 --- a/homeassistant/components/paperless_ngx/quality_scale.yaml +++ b/homeassistant/components/paperless_ngx/quality_scale.yaml @@ -38,7 +38,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: done # Gold @@ -66,7 +66,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: todo stale-devices: status: exempt diff --git a/homeassistant/components/paperless_ngx/strings.json b/homeassistant/components/paperless_ngx/strings.json index 224568f4082..dbcd3cf37e1 100644 --- a/homeassistant/components/paperless_ngx/strings.json +++ b/homeassistant/components/paperless_ngx/strings.json @@ -11,6 +11,26 @@ "api_key": "API key to connect to the Paperless-ngx API" }, "title": "Add Paperless-ngx instance" + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]" + }, + "title": "Re-auth Paperless-ngx instance" + }, + "reconfigure": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "url": "[%key:component::paperless_ngx::config::step::user::data_description::url%]", + "api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]" + }, + "title": "Reconfigure Paperless-ngx instance" } }, "error": { @@ -21,7 +41,9 @@ "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%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "entity": { diff --git a/tests/components/paperless_ngx/conftest.py b/tests/components/paperless_ngx/conftest.py index 758856f6912..a96a0b115e1 100644 --- a/tests/components/paperless_ngx/conftest.py +++ b/tests/components/paperless_ngx/conftest.py @@ -11,7 +11,7 @@ from homeassistant.components.paperless_ngx.const import DOMAIN from homeassistant.core import HomeAssistant from . import setup_integration -from .const import USER_INPUT +from .const import USER_INPUT_ONE from tests.common import MockConfigEntry, load_fixture @@ -59,10 +59,10 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]: def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" return MockConfigEntry( - entry_id="paperless_ngx_test", + entry_id="0KLG00V55WEVTJ0CJHM0GADNGH", title="Paperless-ngx", domain=DOMAIN, - data=USER_INPUT, + data=USER_INPUT_ONE, ) diff --git a/tests/components/paperless_ngx/const.py b/tests/components/paperless_ngx/const.py index 361acaedc6d..addfd54a001 100644 --- a/tests/components/paperless_ngx/const.py +++ b/tests/components/paperless_ngx/const.py @@ -2,7 +2,14 @@ from homeassistant.const import CONF_API_KEY, CONF_URL -USER_INPUT = { +USER_INPUT_ONE = { CONF_URL: "https://192.168.69.16:8000", - CONF_API_KEY: "test_token", + CONF_API_KEY: "12345678", } + +USER_INPUT_TWO = { + CONF_URL: "https://paperless.example.de", + CONF_API_KEY: "87654321", +} + +USER_INPUT_REAUTH = {CONF_API_KEY: "192837465"} diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index ccd48ff8c09..cc197e23ff5 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -31,7 +31,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'correspondent_count', - 'unique_id': 'paperless_ngx_test_correspondent_count', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_correspondent_count', 'unit_of_measurement': 'correspondents', }) # --- @@ -82,7 +82,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'document_type_count', - 'unique_id': 'paperless_ngx_test_document_type_count', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_document_type_count', 'unit_of_measurement': 'document types', }) # --- @@ -133,7 +133,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'documents_inbox', - 'unique_id': 'paperless_ngx_test_documents_inbox', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_inbox', 'unit_of_measurement': 'documents', }) # --- @@ -184,7 +184,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'tag_count', - 'unique_id': 'paperless_ngx_test_tag_count', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_tag_count', 'unit_of_measurement': 'tags', }) # --- @@ -235,7 +235,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'characters_count', - 'unique_id': 'paperless_ngx_test_characters_count', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_characters_count', 'unit_of_measurement': 'characters', }) # --- @@ -286,7 +286,7 @@ 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'documents_total', - 'unique_id': 'paperless_ngx_test_documents_total', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_total', 'unit_of_measurement': 'documents', }) # --- diff --git a/tests/components/paperless_ngx/test_config_flow.py b/tests/components/paperless_ngx/test_config_flow.py index 1674296e9a7..b9960818ceb 100644 --- a/tests/components/paperless_ngx/test_config_flow.py +++ b/tests/components/paperless_ngx/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import USER_INPUT +from .const import USER_INPUT_ONE, USER_INPUT_REAUTH, USER_INPUT_TWO from tests.common import MockConfigEntry, patch @@ -46,13 +46,58 @@ async def test_full_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + USER_INPUT_ONE, ) config_entry = result["result"] - assert config_entry.title == USER_INPUT[CONF_URL] + assert config_entry.title == USER_INPUT_ONE[CONF_URL] assert result["type"] is FlowResultType.CREATE_ENTRY - assert config_entry.data == USER_INPUT + assert config_entry.data == USER_INPUT_ONE + + +async def test_full_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauth an integration and finishing flow works.""" + + mock_config_entry.add_to_hass(hass) + + reauth_flow = await mock_config_entry.start_reauth_flow(hass) + assert reauth_flow["type"] is FlowResultType.FORM + assert reauth_flow["step_id"] == "reauth_confirm" + + result_configure = await hass.config_entries.flow.async_configure( + reauth_flow["flow_id"], USER_INPUT_REAUTH + ) + + assert result_configure["type"] is FlowResultType.ABORT + assert result_configure["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_API_KEY] == USER_INPUT_REAUTH[CONF_API_KEY] + + +async def test_full_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test reconfigure an integration and finishing flow works.""" + + mock_config_entry.add_to_hass(hass) + + reconfigure_flow = await mock_config_entry.start_reconfigure_flow(hass) + assert reconfigure_flow["type"] is FlowResultType.FORM + assert reconfigure_flow["step_id"] == "reconfigure" + + result_configure = await hass.config_entries.flow.async_configure( + reconfigure_flow["flow_id"], + USER_INPUT_TWO, + ) + + assert result_configure["type"] is FlowResultType.ABORT + assert result_configure["reason"] == "reconfigure_successful" + assert mock_config_entry.data == USER_INPUT_TWO @pytest.mark.parametrize( @@ -78,7 +123,7 @@ async def test_config_flow_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=USER_INPUT, + data=USER_INPUT_ONE, ) assert result["type"] is FlowResultType.FORM @@ -89,12 +134,87 @@ async def test_config_flow_error_handling( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=USER_INPUT, + user_input=USER_INPUT_ONE, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == USER_INPUT[CONF_URL] - assert result["data"] == USER_INPUT + assert result["title"] == USER_INPUT_ONE[CONF_URL] + assert result["data"] == USER_INPUT_ONE + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PaperlessConnectionError(), {CONF_URL: "cannot_connect"}), + (PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}), + (PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}), + (PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}), + (InitializationError(), {CONF_URL: "cannot_connect"}), + (Exception("BOOM!"), {"base": "unknown"}), + ], +) +async def test_reauth_flow_error_handling( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_paperless: AsyncMock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test reauth flow with various initialization errors.""" + + mock_config_entry.add_to_hass(hass) + mock_paperless.initialize.side_effect = side_effect + + reauth_flow = await mock_config_entry.start_reauth_flow(hass) + assert reauth_flow["type"] is FlowResultType.FORM + assert reauth_flow["step_id"] == "reauth_confirm" + + result_configure = await hass.config_entries.flow.async_configure( + reauth_flow["flow_id"], USER_INPUT_REAUTH + ) + + await hass.async_block_till_done() + + assert result_configure["type"] is FlowResultType.FORM + assert result_configure["errors"] == expected_error + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PaperlessConnectionError(), {CONF_URL: "cannot_connect"}), + (PaperlessInvalidTokenError(), {CONF_API_KEY: "invalid_api_key"}), + (PaperlessInactiveOrDeletedError(), {CONF_API_KEY: "user_inactive_or_deleted"}), + (PaperlessForbiddenError(), {CONF_API_KEY: "forbidden"}), + (InitializationError(), {CONF_URL: "cannot_connect"}), + (Exception("BOOM!"), {"base": "unknown"}), + ], +) +async def test_reconfigure_flow_error_handling( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_paperless: AsyncMock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test reconfigure flow with various initialization errors.""" + + mock_config_entry.add_to_hass(hass) + mock_paperless.initialize.side_effect = side_effect + + reauth_flow = await mock_config_entry.start_reconfigure_flow(hass) + assert reauth_flow["type"] is FlowResultType.FORM + assert reauth_flow["step_id"] == "reconfigure" + + result_configure = await hass.config_entries.flow.async_configure( + reauth_flow["flow_id"], + USER_INPUT_TWO, + ) + + await hass.async_block_till_done() + + assert result_configure["type"] is FlowResultType.FORM + assert result_configure["errors"] == expected_error async def test_config_already_exists( @@ -105,8 +225,36 @@ async def test_config_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, - data=USER_INPUT, + data=USER_INPUT_ONE, context={"source": config_entries.SOURCE_USER}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_config_already_exists_reconfigure( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test we only allow a single config if reconfiguring an entry.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry_two = MockConfigEntry( + entry_id="J87G00V55WEVTJ0CJHM0GADBH5", + title="Paperless-ngx - Two", + domain=DOMAIN, + data=USER_INPUT_TWO, + ) + mock_config_entry_two.add_to_hass(hass) + + reconfigure_flow = await mock_config_entry_two.start_reconfigure_flow(hass) + assert reconfigure_flow["type"] is FlowResultType.FORM + assert reconfigure_flow["step_id"] == "reconfigure" + + result_configure = await hass.config_entries.flow.async_configure( + reconfigure_flow["flow_id"], + USER_INPUT_ONE, + ) + + assert result_configure["type"] is FlowResultType.ABORT + assert result_configure["reason"] == "already_configured" diff --git a/tests/components/paperless_ngx/test_sensor.py b/tests/components/paperless_ngx/test_sensor.py index 2025bba6965..33610d9b6d6 100644 --- a/tests/components/paperless_ngx/test_sensor.py +++ b/tests/components/paperless_ngx/test_sensor.py @@ -12,6 +12,7 @@ from pypaperless.exceptions import ( from pypaperless.models import Statistic import pytest +from homeassistant.components.paperless_ngx.coordinator import UPDATE_INTERVAL from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -60,7 +61,7 @@ async def test_statistic_sensor_state( ) ) - freezer.tick(timedelta(seconds=120)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -70,12 +71,12 @@ async def test_statistic_sensor_state( @pytest.mark.usefixtures("init_integration") @pytest.mark.parametrize( - "error_cls", + ("error_cls", "assert_state"), [ - PaperlessForbiddenError, - PaperlessConnectionError, - PaperlessInactiveOrDeletedError, - PaperlessInvalidTokenError, + (PaperlessForbiddenError, "420"), + (PaperlessConnectionError, "420"), + (PaperlessInactiveOrDeletedError, STATE_UNAVAILABLE), + (PaperlessInvalidTokenError, STATE_UNAVAILABLE), ], ) async def test__statistic_sensor_state_on_error( @@ -84,28 +85,29 @@ async def test__statistic_sensor_state_on_error( freezer: FrozenDateTimeFactory, mock_statistic_data_update, error_cls, + assert_state, ) -> None: """Ensure sensor entities are added automatically.""" # simulate error mock_paperless.statistics.side_effect = error_cls - freezer.tick(timedelta(seconds=120)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.paperless_ngx_total_documents") assert state.state == STATE_UNAVAILABLE - # recover from error + # recover from not auth errors mock_paperless.statistics = AsyncMock( return_value=Statistic.create_with_data( mock_paperless, data=mock_statistic_data_update, fetched=True ) ) - freezer.tick(timedelta(seconds=120)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("sensor.paperless_ngx_total_documents") - assert state.state == "420" + assert state.state == assert_state From 565f051ffc924b4820968a293ccf08a42637c4d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 May 2025 14:38:08 +0200 Subject: [PATCH 0861/1175] Fix aiohttp MockPayloadWriter (#145579) --- homeassistant/util/aiohttp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index e5b319195ff..888da368053 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -42,7 +42,7 @@ class MockPayloadWriter: def enable_chunking(self) -> None: """Enable chunking.""" - async def send_headers(self, *args: Any, **kwargs: Any) -> None: + def send_headers(self, *args: Any, **kwargs: Any) -> None: """Write headers.""" async def write_headers(self, *args: Any, **kwargs: Any) -> None: From 46951bf2230792aed4c17ed1825ce746741fd2aa Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 25 May 2025 16:16:55 +0200 Subject: [PATCH 0862/1175] Add `returned energy` sensor for Shelly RPC switch component (#145490) * Add returned energy sensor for switch component * Add test * More tests * Make returned energy sensor disabled by default --- homeassistant/components/shelly/sensor.py | 15 +++ .../shelly/snapshots/test_sensor.ambr | 116 ++++++++++++++++++ tests/components/shelly/test_sensor.py | 54 ++++++++ 3 files changed, 185 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 986127b5836..78eff171daf 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -836,6 +836,21 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + "ret_energy": RpcSensorDescription( + key="switch", + sub_key="ret_aenergy", + name="Returned energy", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + value=lambda status, _: status["total"], + suggested_display_precision=2, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + removal_condition=lambda _config, status, key: ( + status[key].get("ret_aenergy") is None + ), + ), "energy_light": RpcSensorDescription( key="light", sub_key="aenergy", diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index cb39b148c8a..c5c1427e3dc 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -154,3 +154,119 @@ 'state': '0', }) # --- +# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_switch_0_energy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'test switch_0 energy', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-switch:0-energy', + 'unit_of_measurement': , + }) +# --- +# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'test switch_0 energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_switch_0_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234.56789', + }) +# --- +# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.test_switch_0_returned_energy', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'test switch_0 returned energy', + 'platform': 'shelly', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '123456789ABC-switch:0-ret_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'test switch_0 returned energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.test_switch_0_returned_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '98.76543', + }) +# --- diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 3bf63546419..a3d0a0f59c9 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -1519,3 +1519,57 @@ async def test_rpc_device_virtual_number_sensor_with_device_class( assert state.state == "34" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_switch_energy_sensors( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + snapshot: SnapshotAssertion, +) -> None: + """Test energy sensors for switch component.""" + status = { + "sys": {}, + "switch:0": { + "id": 0, + "output": True, + "apower": 85.3, + "aenergy": {"total": 1234567.89}, + "ret_aenergy": {"total": 98765.43}, + }, + } + monkeypatch.setattr(mock_rpc_device, "status", status) + await init_integration(hass, 3) + + for entity in ("energy", "returned_energy"): + entity_id = f"{SENSOR_DOMAIN}.test_switch_0_{entity}" + + state = hass.states.get(entity_id) + assert state == snapshot(name=f"{entity_id}-state") + + entry = entity_registry.async_get(entity_id) + assert entry == snapshot(name=f"{entity_id}-entry") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_rpc_switch_no_returned_energy_sensor( + hass: HomeAssistant, + mock_rpc_device: Mock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test switch component without returned energy sensor.""" + status = { + "sys": {}, + "switch:0": { + "id": 0, + "output": True, + "apower": 85.3, + "aenergy": {"total": 1234567.89}, + }, + } + monkeypatch.setattr(mock_rpc_device, "status", status) + await init_integration(hass, 3) + + assert hass.states.get("sensor.test_switch_0_returned_energy") is None From d0b2331a5f58681f6beea8c3d5f01a7fa3712540 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 25 May 2025 18:42:07 +0300 Subject: [PATCH 0863/1175] New integration Amazon Devices (#144422) * New integration Amazon Devices * apply review comments * bump aioamazondevices * Add notify platform * pylance * full coverage for coordinator tests * cleanup imports * Add switch platform * update quality scale: docs items * update quality scale: brands * apply review comments * fix new ruff rule * simplify EntityDescription code * remove additional platforms for first PR * apply review comments * update IQS * apply last review comments * snapshot update * apply review comments * apply review comments --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/brands/amazon.json | 1 + .../components/amazon_devices/__init__.py | 28 ++++ .../amazon_devices/binary_sensor.py | 72 ++++++++++ .../components/amazon_devices/config_flow.py | 63 ++++++++ .../components/amazon_devices/const.py | 8 ++ .../components/amazon_devices/coordinator.py | 58 ++++++++ .../components/amazon_devices/entity.py | 57 ++++++++ .../components/amazon_devices/icons.json | 12 ++ .../components/amazon_devices/manifest.json | 12 ++ .../amazon_devices/quality_scale.yaml | 72 ++++++++++ .../components/amazon_devices/strings.json | 47 ++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/amazon_devices/__init__.py | 13 ++ tests/components/amazon_devices/conftest.py | 76 ++++++++++ tests/components/amazon_devices/const.py | 7 + .../snapshots/test_binary_sensor.ambr | 97 +++++++++++++ .../amazon_devices/snapshots/test_init.ambr | 34 +++++ .../amazon_devices/test_binary_sensor.py | 71 ++++++++++ .../amazon_devices/test_config_flow.py | 134 ++++++++++++++++++ tests/components/amazon_devices/test_init.py | 30 ++++ 26 files changed, 918 insertions(+) create mode 100644 homeassistant/components/amazon_devices/__init__.py create mode 100644 homeassistant/components/amazon_devices/binary_sensor.py create mode 100644 homeassistant/components/amazon_devices/config_flow.py create mode 100644 homeassistant/components/amazon_devices/const.py create mode 100644 homeassistant/components/amazon_devices/coordinator.py create mode 100644 homeassistant/components/amazon_devices/entity.py create mode 100644 homeassistant/components/amazon_devices/icons.json create mode 100644 homeassistant/components/amazon_devices/manifest.json create mode 100644 homeassistant/components/amazon_devices/quality_scale.yaml create mode 100644 homeassistant/components/amazon_devices/strings.json create mode 100644 tests/components/amazon_devices/__init__.py create mode 100644 tests/components/amazon_devices/conftest.py create mode 100644 tests/components/amazon_devices/const.py create mode 100644 tests/components/amazon_devices/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/amazon_devices/snapshots/test_init.ambr create mode 100644 tests/components/amazon_devices/test_binary_sensor.py create mode 100644 tests/components/amazon_devices/test_config_flow.py create mode 100644 tests/components/amazon_devices/test_init.py diff --git a/.strict-typing b/.strict-typing index 7cd54374616..4febfd68486 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.alarm_control_panel.* homeassistant.components.alert.* homeassistant.components.alexa.* homeassistant.components.alpha_vantage.* +homeassistant.components.amazon_devices.* homeassistant.components.amazon_polly.* homeassistant.components.amberelectric.* homeassistant.components.ambient_network.* diff --git a/CODEOWNERS b/CODEOWNERS index 5bc9a2dd8d7..25c842cc6fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,8 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh +/homeassistant/components/amazon_devices/ @chemelli74 +/tests/components/amazon_devices/ @chemelli74 /homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index 624a8a17b7d..d2e25468388 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -3,6 +3,7 @@ "name": "Amazon", "integrations": [ "alexa", + "amazon_devices", "amazon_polly", "aws", "aws_s3", diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/amazon_devices/__init__.py new file mode 100644 index 00000000000..a7318824b4c --- /dev/null +++ b/homeassistant/components/amazon_devices/__init__.py @@ -0,0 +1,28 @@ +"""Amazon Devices integration.""" + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator + +PLATFORMS = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: + """Set up Amazon Devices platform.""" + + coordinator = AmazonDevicesCoordinator(hass, entry) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: + """Unload a config entry.""" + await entry.runtime_data.api.close() + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/amazon_devices/binary_sensor.py new file mode 100644 index 00000000000..0528ffbe1e4 --- /dev/null +++ b/homeassistant/components/amazon_devices/binary_sensor.py @@ -0,0 +1,72 @@ +"""Support for binary sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AmazonConfigEntry +from .entity import AmazonEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription): + """Amazon Devices binary sensor entity description.""" + + is_on_fn: Callable[[AmazonDevice], bool] + + +BINARY_SENSORS: Final = ( + AmazonBinarySensorEntityDescription( + key="online", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + is_on_fn=lambda _device: _device.online, + ), + AmazonBinarySensorEntityDescription( + key="bluetooth", + translation_key="bluetooth", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + is_on_fn=lambda _device: _device.bluetooth_state, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Amazon Devices binary sensors based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc) + for sensor_desc in BINARY_SENSORS + for serial_num in coordinator.data + ) + + +class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity): + """Binary sensor device.""" + + entity_description: AmazonBinarySensorEntityDescription + + @property + def is_on(self) -> bool: + """Return True if the binary sensor is on.""" + return self.entity_description.is_on_fn(self.device) diff --git a/homeassistant/components/amazon_devices/config_flow.py b/homeassistant/components/amazon_devices/config_flow.py new file mode 100644 index 00000000000..5566c16602b --- /dev/null +++ b/homeassistant/components/amazon_devices/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for Amazon Devices integration.""" + +from __future__ import annotations + +from typing import Any + +from aioamazondevices.api import AmazonEchoApi +from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import CountrySelector + +from .const import CONF_LOGIN_DATA, DOMAIN + + +class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Amazon Devices.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors = {} + if user_input: + client = AmazonEchoApi( + user_input[CONF_COUNTRY], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + try: + data = await client.login_mode_interactive(user_input[CONF_CODE]) + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotAuthenticate: + errors["base"] = "invalid_auth" + else: + await self.async_set_unique_id(data["customer_info"]["user_id"]) + self._abort_if_unique_id_configured() + user_input.pop(CONF_CODE) + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input | {CONF_LOGIN_DATA: data}, + ) + finally: + await client.close() + + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=vol.Schema( + { + vol.Required( + CONF_COUNTRY, default=self.hass.config.country + ): CountrySelector(), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CODE): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/amazon_devices/const.py b/homeassistant/components/amazon_devices/const.py new file mode 100644 index 00000000000..b8cf2c264b1 --- /dev/null +++ b/homeassistant/components/amazon_devices/const.py @@ -0,0 +1,8 @@ +"""Amazon Devices constants.""" + +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "amazon_devices" +CONF_LOGIN_DATA = "login_data" diff --git a/homeassistant/components/amazon_devices/coordinator.py b/homeassistant/components/amazon_devices/coordinator.py new file mode 100644 index 00000000000..48e31cb3f94 --- /dev/null +++ b/homeassistant/components/amazon_devices/coordinator.py @@ -0,0 +1,58 @@ +"""Support for Amazon Devices.""" + +from datetime import timedelta + +from aioamazondevices.api import AmazonDevice, AmazonEchoApi +from aioamazondevices.exceptions import ( + CannotAuthenticate, + CannotConnect, + CannotRetrieveData, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import _LOGGER, CONF_LOGIN_DATA + +SCAN_INTERVAL = 30 + +type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator] + + +class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): + """Base coordinator for Amazon Devices.""" + + config_entry: AmazonConfigEntry + + def __init__( + self, + hass: HomeAssistant, + entry: AmazonConfigEntry, + ) -> None: + """Initialize the scanner.""" + super().__init__( + hass, + _LOGGER, + name=entry.title, + config_entry=entry, + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + self.api = AmazonEchoApi( + entry.data[CONF_COUNTRY], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_LOGIN_DATA], + ) + + async def _async_update_data(self) -> dict[str, AmazonDevice]: + """Update device data.""" + try: + await self.api.login_mode_stored_data() + return await self.api.get_devices_data() + except (CannotConnect, CannotRetrieveData) as err: + raise UpdateFailed(f"Error occurred while updating {self.name}") from err + except CannotAuthenticate as err: + raise ConfigEntryError("Could not authenticate") from err diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py new file mode 100644 index 00000000000..2ac90410bec --- /dev/null +++ b/homeassistant/components/amazon_devices/entity.py @@ -0,0 +1,57 @@ +"""Defines a base Amazon Devices entity.""" + +from typing import cast + +from aioamazondevices.api import AmazonDevice +from aioamazondevices.const import DEVICE_TYPE_TO_MODEL, SPEAKER_GROUP_MODEL + +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 AmazonDevicesCoordinator + + +class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): + """Defines a base Amazon Devices entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AmazonDevicesCoordinator, + serial_num: str, + description: EntityDescription, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._serial_num = serial_num + model_details: dict[str, str] = cast( + "dict", DEVICE_TYPE_TO_MODEL.get(self.device.device_type) + ) + model = model_details["model"] if model_details else None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_num)}, + name=self.device.account_name, + model=model, + model_id=self.device.device_type, + manufacturer="Amazon", + hw_version=model_details["hw_version"] if model_details else None, + sw_version=( + self.device.software_version if model != SPEAKER_GROUP_MODEL else None + ), + serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None, + ) + self.entity_description = description + self._attr_unique_id = f"{serial_num}-{description.key}" + + @property + def device(self) -> AmazonDevice: + """Return the device.""" + return self.coordinator.data[self._serial_num] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self._serial_num in self.coordinator.data diff --git a/homeassistant/components/amazon_devices/icons.json b/homeassistant/components/amazon_devices/icons.json new file mode 100644 index 00000000000..e3b20eb2c4a --- /dev/null +++ b/homeassistant/components/amazon_devices/icons.json @@ -0,0 +1,12 @@ +{ + "entity": { + "binary_sensor": { + "bluetooth": { + "default": "mdi:bluetooth", + "state": { + "off": "mdi:bluetooth-off" + } + } + } + } +} diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json new file mode 100644 index 00000000000..675433387bb --- /dev/null +++ b/homeassistant/components/amazon_devices/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "amazon_devices", + "name": "Amazon Devices", + "codeowners": ["@chemelli74"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/amazon_devices", + "integration_type": "hub", + "iot_class": "cloud_polling", + "loggers": ["aioamazondevices"], + "quality_scale": "bronze", + "requirements": ["aioamazondevices==2.0.1"] +} diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/amazon_devices/quality_scale.yaml new file mode 100644 index 00000000000..1234fd574a3 --- /dev/null +++ b/homeassistant/components/amazon_devices/quality_scale.yaml @@ -0,0 +1,72 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: no actions + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: no actions + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: entities do not explicitly subscribe to events + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: todo + config-entry-unloading: done + docs-configuration-parameters: todo + docs-installation-parameters: todo + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: todo + test-coverage: + status: todo + comment: all tests missing + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: todo + icon-translations: done + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: no known use cases for repair issues or flows, yet + stale-devices: + status: todo + comment: automate the cleanup process + + # Platinum + async-dependency: done + inject-websession: todo + strict-typing: done diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json new file mode 100644 index 00000000000..edc10aa9d40 --- /dev/null +++ b/homeassistant/components/amazon_devices/strings.json @@ -0,0 +1,47 @@ +{ + "common": { + "data_country": "Country code", + "data_code": "One-time password (OTP code)", + "data_description_country": "The country of your Amazon account.", + "data_description_username": "The email address of your Amazon account.", + "data_description_password": "The password of your Amazon account.", + "data_description_code": "The one-time password sent to your email address." + }, + "config": { + "flow_title": "{username}", + "step": { + "user": { + "data": { + "country": "[%key:component::amazon_devices::common::data_country%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "code": "[%key:component::amazon_devices::common::data_description_code%]" + }, + "data_description": { + "country": "[%key:component::amazon_devices::common::data_description_country%]", + "username": "[%key:component::amazon_devices::common::data_description_username%]", + "password": "[%key:component::amazon_devices::common::data_description_password%]", + "code": "[%key:component::amazon_devices::common::data_description_code%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "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%]" + }, + "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%]" + } + }, + "entity": { + "binary_sensor": { + "bluetooth": { + "name": "Bluetooth" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 43db3f5be10..1cba78af0b0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,6 +47,7 @@ FLOWS = { "airzone", "airzone_cloud", "alarmdecoder", + "amazon_devices", "amberelectric", "ambient_network", "ambient_station", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9357424dc76..66693d41396 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -207,6 +207,12 @@ "amazon": { "name": "Amazon", "integrations": { + "amazon_devices": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Amazon Devices" + }, "amazon_polly": { "integration_type": "hub", "config_flow": false, diff --git a/mypy.ini b/mypy.ini index f09e68bdcbe..da76e4ae2cd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -415,6 +415,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.amazon_devices.*] +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.amazon_polly.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a7f8bdcc110..3e722a9b329 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,6 +181,9 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 +# homeassistant.components.amazon_devices +aioamazondevices==2.0.1 + # homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.08.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1914f1abf88..c9d2e340806 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,6 +169,9 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 +# homeassistant.components.amazon_devices +aioamazondevices==2.0.1 + # homeassistant.components.ambient_network # homeassistant.components.ambient_station aioambient==2024.08.0 diff --git a/tests/components/amazon_devices/__init__.py b/tests/components/amazon_devices/__init__.py new file mode 100644 index 00000000000..47ee520b124 --- /dev/null +++ b/tests/components/amazon_devices/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Amazon Devices integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/amazon_devices/conftest.py b/tests/components/amazon_devices/conftest.py new file mode 100644 index 00000000000..5978faa0b31 --- /dev/null +++ b/tests/components/amazon_devices/conftest.py @@ -0,0 +1,76 @@ +"""Amazon Devices tests configuration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from aioamazondevices.api import AmazonDevice +import pytest + +from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME + +from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.amazon_devices.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_amazon_devices_client() -> Generator[AsyncMock]: + """Mock an Amazon Devices client.""" + with ( + patch( + "homeassistant.components.amazon_devices.coordinator.AmazonEchoApi", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.amazon_devices.config_flow.AmazonEchoApi", + new=mock_client, + ), + ): + client = mock_client.return_value + client.login_mode_interactive.return_value = { + "customer_info": {"user_id": TEST_USERNAME}, + } + client.get_devices_data.return_value = { + TEST_SERIAL_NUMBER: AmazonDevice( + account_name="Echo Test", + capabilities=["AUDIO_PLAYER", "MICROPHONE"], + device_family="mine", + device_type="echo", + device_owner_customer_id="amazon_ower_id", + device_cluster_members=[TEST_SERIAL_NUMBER], + online=True, + serial_number=TEST_SERIAL_NUMBER, + software_version="echo_test_software_version", + do_not_disturb=False, + response_style=None, + bluetooth_state=True, + ) + } + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Amazon Test Account", + data={ + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: {"session": "test-session"}, + }, + unique_id=TEST_USERNAME, + ) diff --git a/tests/components/amazon_devices/const.py b/tests/components/amazon_devices/const.py new file mode 100644 index 00000000000..94b5b7052e6 --- /dev/null +++ b/tests/components/amazon_devices/const.py @@ -0,0 +1,7 @@ +"""Amazon Devices tests const.""" + +TEST_CODE = 123123 +TEST_COUNTRY = "IT" +TEST_PASSWORD = "fake_password" +TEST_SERIAL_NUMBER = "echo_test_serial_number" +TEST_USERNAME = "fake_email@gmail.com" diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr new file mode 100644 index 00000000000..647fa39540f --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.echo_test_bluetooth-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.echo_test_bluetooth', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Bluetooth', + 'platform': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'bluetooth', + 'unique_id': 'echo_test_serial_number-bluetooth', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.echo_test_bluetooth-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Echo Test Bluetooth', + }), + 'context': , + 'entity_id': 'binary_sensor.echo_test_bluetooth', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.echo_test_connectivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.echo_test_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': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'echo_test_serial_number-online', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.echo_test_connectivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'connectivity', + 'friendly_name': 'Echo Test Connectivity', + }), + 'context': , + 'entity_id': 'binary_sensor.echo_test_connectivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/amazon_devices/snapshots/test_init.ambr b/tests/components/amazon_devices/snapshots/test_init.ambr new file mode 100644 index 00000000000..be0a5894eea --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_init.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'amazon_devices', + 'echo_test_serial_number', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': 'Amazon', + 'model': None, + 'model_id': 'echo', + 'name': 'Echo Test', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': 'echo_test_serial_number', + 'suggested_area': None, + 'sw_version': 'echo_test_software_version', + 'via_device_id': None, + }) +# --- diff --git a/tests/components/amazon_devices/test_binary_sensor.py b/tests/components/amazon_devices/test_binary_sensor.py new file mode 100644 index 00000000000..bbe8af17a8e --- /dev/null +++ b/tests/components/amazon_devices/test_binary_sensor.py @@ -0,0 +1,71 @@ +"""Tests for the Amazon Devices binary sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from aioamazondevices.exceptions import ( + CannotAuthenticate, + CannotConnect, + CannotRetrieveData, +) +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch( + "homeassistant.components.amazon_devices.PLATFORMS", [Platform.BINARY_SENSOR] + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "side_effect", + [ + CannotConnect, + CannotRetrieveData, + CannotAuthenticate, + ], +) +async def test_coordinator_data_update_fails( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, +) -> None: + """Test coordinator data update exceptions.""" + + entity_id = "binary_sensor.echo_test_connectivity" + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_ON + + mock_amazon_devices_client.get_devices_data.side_effect = side_effect + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py new file mode 100644 index 00000000000..e60ae9543a3 --- /dev/null +++ b/tests/components/amazon_devices/test_config_flow.py @@ -0,0 +1,134 @@ +"""Tests for the Amazon Devices config flow.""" + +from unittest.mock import AsyncMock + +from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect +import pytest + +from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + }, + } + assert result["result"].unique_id == TEST_USERNAME + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_amazon_devices_client.login_mode_interactive.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_amazon_devices_client.login_mode_interactive.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_already_configured( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/amazon_devices/test_init.py b/tests/components/amazon_devices/test_init.py new file mode 100644 index 00000000000..489952dbd4c --- /dev/null +++ b/tests/components/amazon_devices/test_init.py @@ -0,0 +1,30 @@ +"""Tests for the Amazon Devices integration.""" + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device_entry is not None + assert device_entry == snapshot From 6634efa3aaead863eee14e5b9bac98485e43aa28 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 25 May 2025 18:20:44 +0200 Subject: [PATCH 0864/1175] Add DHCP discovery to Amazon Devices (#145587) * Add DHCP discovery to Amazon Devices * Add DHCP discovery to Amazon Devices * Add DHCP discovery to Amazon Devices --- .../components/amazon_devices/manifest.json | 11 ++++ .../amazon_devices/quality_scale.yaml | 6 +- homeassistant/generated/dhcp.py | 36 +++++++++++ .../amazon_devices/test_config_flow.py | 64 ++++++++++++++++++- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 675433387bb..3f8dcd4c4df 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -3,6 +3,17 @@ "name": "Amazon Devices", "codeowners": ["@chemelli74"], "config_flow": true, + "dhcp": [ + { "macaddress": "10BF67*" }, + { "macaddress": "48B423*" }, + { "macaddress": "4C1744*" }, + { "macaddress": "50DCE7*" }, + { "macaddress": "74D637*" }, + { "macaddress": "9CC8E9*" }, + { "macaddress": "C095CF*" }, + { "macaddress": "D8BE65*" }, + { "macaddress": "EC2BEB*" } + ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", "iot_class": "cloud_polling", diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/amazon_devices/quality_scale.yaml index 1234fd574a3..23a7cd22a66 100644 --- a/homeassistant/components/amazon_devices/quality_scale.yaml +++ b/homeassistant/components/amazon_devices/quality_scale.yaml @@ -42,8 +42,10 @@ rules: # Gold devices: done diagnostics: todo - discovery-update-info: todo - discovery: todo + discovery-update-info: + status: exempt + comment: Network information not relevant + discovery: done docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 20b49919ace..cbdf31387e6 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,6 +26,42 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, + { + "domain": "amazon_devices", + "macaddress": "10BF67*", + }, + { + "domain": "amazon_devices", + "macaddress": "48B423*", + }, + { + "domain": "amazon_devices", + "macaddress": "4C1744*", + }, + { + "domain": "amazon_devices", + "macaddress": "50DCE7*", + }, + { + "domain": "amazon_devices", + "macaddress": "74D637*", + }, + { + "domain": "amazon_devices", + "macaddress": "9CC8E9*", + }, + { + "domain": "amazon_devices", + "macaddress": "C095CF*", + }, + { + "domain": "amazon_devices", + "macaddress": "D8BE65*", + }, + { + "domain": "amazon_devices", + "macaddress": "EC2BEB*", + }, { "domain": "august", "hostname": "connect", diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py index e60ae9543a3..68ab7f4ffa6 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/amazon_devices/test_config_flow.py @@ -6,15 +6,22 @@ from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect import pytest from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry +DHCP_DISCOVERY = DhcpServiceInfo( + ip="1.1.1.1", + hostname="", + macaddress="c095cfebf19f", +) + async def test_full_flow( hass: HomeAssistant, @@ -132,3 +139,58 @@ async def test_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_dhcp_flow( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full DHCP flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_CODE: TEST_CODE, + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_COUNTRY: TEST_COUNTRY, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_LOGIN_DATA: { + "customer_info": {"user_id": TEST_USERNAME}, + }, + } + assert result["result"].unique_id == TEST_USERNAME + + +async def test_dhcp_already_configured( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" From bc9683312ea3a2257a0dbd08aa6d0f06fb37abdd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 25 May 2025 18:40:04 +0200 Subject: [PATCH 0865/1175] Change cooler name to fridge in SmartThings (#145590) --- .../components/smartthings/strings.json | 6 +- .../snapshots/test_binary_sensor.ambr | 124 +++---- .../smartthings/snapshots/test_number.ambr | 154 ++++----- .../smartthings/snapshots/test_sensor.ambr | 312 +++++++++--------- .../smartthings/test_binary_sensor.py | 12 +- 5 files changed, 304 insertions(+), 304 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index dbbc01c34b2..7b5edde2d10 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -49,7 +49,7 @@ "name": "Freezer door" }, "cooler_door": { - "name": "Cooler door" + "name": "Fridge door" }, "cool_select_plus_door": { "name": "CoolSelect+ door" @@ -116,7 +116,7 @@ "name": "Freezer temperature" }, "cooler_temperature": { - "name": "Cooler temperature" + "name": "Fridge temperature" }, "cool_select_plus_temperature": { "name": "CoolSelect+ temperature" @@ -243,7 +243,7 @@ } }, "cooler_temperature": { - "name": "Cooler temperature" + "name": "Fridge temperature" }, "manual_level": { "name": "Burner {burner_id} level" diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 583c256042e..4f6d0d6d634 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -665,54 +665,6 @@ 'state': 'on', }) # --- -# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_cooler_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.refrigerator_cooler_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Cooler door', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cooler_door', - 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_contactSensor_contact_contact', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_cooler_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'Refrigerator Cooler door', - }), - 'context': , - 'entity_id': 'binary_sensor.refrigerator_cooler_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -761,7 +713,7 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-entry] +# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_fridge_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -774,7 +726,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.refrigerator_cooler_door', + 'entity_id': 'binary_sensor.refrigerator_fridge_door', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -786,23 +738,23 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cooler door', + 'original_name': 'Fridge door', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'cooler_door', - 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_contactSensor_contact_contact', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_cooler_door-state] +# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_fridge_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', - 'friendly_name': 'Refrigerator Cooler door', + 'friendly_name': 'Refrigerator Fridge door', }), 'context': , - 'entity_id': 'binary_sensor.refrigerator_cooler_door', + 'entity_id': 'binary_sensor.refrigerator_fridge_door', 'last_changed': , 'last_reported': , 'last_updated': , @@ -905,7 +857,7 @@ 'state': 'off', }) # --- -# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-entry] +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_fridge_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -918,7 +870,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.frigo_cooler_door', + 'entity_id': 'binary_sensor.refrigerator_fridge_door', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -930,23 +882,23 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cooler door', + 'original_name': 'Fridge door', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'cooler_door', - 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_contactSensor_contact_contact', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_cooler_door-state] +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_fridge_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', - 'friendly_name': 'Frigo Cooler door', + 'friendly_name': 'Refrigerator Fridge door', }), 'context': , - 'entity_id': 'binary_sensor.frigo_cooler_door', + 'entity_id': 'binary_sensor.refrigerator_fridge_door', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1001,6 +953,54 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_fridge_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.frigo_fridge_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fridge door', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_door', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_contactSensor_contact_contact', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_fridge_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Frigo Fridge door', + }), + 'context': , + 'entity_id': 'binary_sensor.frigo_fridge_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_wm_dw_000001][binary_sensor.dishwasher_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_number.ambr b/tests/components/smartthings/snapshots/test_number.ambr index 34073173861..37af2200899 100644 --- a/tests/components/smartthings/snapshots/test_number.ambr +++ b/tests/components/smartthings/snapshots/test_number.ambr @@ -55,64 +55,6 @@ 'state': '0', }) # --- -# name: test_all_entities[da_ref_normal_000001][number.refrigerator_cooler_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'max': 7.0, - 'min': 1.0, - 'mode': , - 'step': 1, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.refrigerator_cooler_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': 'Cooler temperature', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cooler_temperature', - 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ref_normal_000001][number.refrigerator_cooler_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Refrigerator Cooler temperature', - 'max': 7.0, - 'min': 1.0, - 'mode': , - 'step': 1, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.refrigerator_cooler_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3.0', - }) -# --- # name: test_all_entities[da_ref_normal_000001][number.refrigerator_freezer_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -171,7 +113,7 @@ 'state': '-18.0', }) # --- -# name: test_all_entities[da_ref_normal_01001][number.refrigerator_cooler_temperature-entry] +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_fridge_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -189,7 +131,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.refrigerator_cooler_temperature', + 'entity_id': 'number.refrigerator_fridge_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -201,20 +143,20 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cooler temperature', + 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', - 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', 'unit_of_measurement': , }) # --- -# name: test_all_entities[da_ref_normal_01001][number.refrigerator_cooler_temperature-state] +# name: test_all_entities[da_ref_normal_000001][number.refrigerator_fridge_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Refrigerator Cooler temperature', + 'friendly_name': 'Refrigerator Fridge temperature', 'max': 7.0, 'min': 1.0, 'mode': , @@ -222,7 +164,7 @@ 'unit_of_measurement': , }), 'context': , - 'entity_id': 'number.refrigerator_cooler_temperature', + 'entity_id': 'number.refrigerator_fridge_temperature', 'last_changed': , 'last_reported': , 'last_updated': , @@ -287,14 +229,14 @@ 'state': '-18.0', }) # --- -# name: test_all_entities[da_ref_normal_01011][number.frigo_cooler_temperature-entry] +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_fridge_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'max': 7, - 'min': 1, + 'max': 7.0, + 'min': 1.0, 'mode': , 'step': 1, }), @@ -305,7 +247,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.frigo_cooler_temperature', + 'entity_id': 'number.refrigerator_fridge_temperature', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -317,32 +259,32 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Cooler temperature', + 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', - 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', 'unit_of_measurement': , }) # --- -# name: test_all_entities[da_ref_normal_01011][number.frigo_cooler_temperature-state] +# name: test_all_entities[da_ref_normal_01001][number.refrigerator_fridge_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'temperature', - 'friendly_name': 'Frigo Cooler temperature', - 'max': 7, - 'min': 1, + 'friendly_name': 'Refrigerator Fridge temperature', + 'max': 7.0, + 'min': 1.0, 'mode': , 'step': 1, 'unit_of_measurement': , }), 'context': , - 'entity_id': 'number.frigo_cooler_temperature', + 'entity_id': 'number.refrigerator_fridge_temperature', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6', + 'state': '3.0', }) # --- # name: test_all_entities[da_ref_normal_01011][number.frigo_freezer_temperature-entry] @@ -403,6 +345,64 @@ 'state': '-17', }) # --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_fridge_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 7, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.frigo_fridge_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': 'Fridge temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][number.frigo_fridge_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Fridge temperature', + 'max': 7, + 'min': 1, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.frigo_fridge_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- # name: test_all_entities[da_wm_wm_000001][number.washer_rinse_cycles-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 569838471fc..8b3e91ee263 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -4589,58 +4589,6 @@ 'state': '218', }) # --- -# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_cooler_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.refrigerator_cooler_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': 'Cooler temperature', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cooler_temperature', - 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_cooler_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Refrigerator Cooler temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.refrigerator_cooler_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3', - }) -# --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4858,6 +4806,58 @@ 'state': '-18', }) # --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_fridge_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_fridge_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': 'Fridge temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_fridge_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Fridge temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_fridge_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4970,58 +4970,6 @@ 'state': '0.0135559777781698', }) # --- -# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_cooler_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.refrigerator_cooler_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': 'Cooler temperature', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cooler_temperature', - 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_cooler_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Refrigerator Cooler temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.refrigerator_cooler_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '3', - }) -# --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5239,6 +5187,58 @@ 'state': '-18', }) # --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_fridge_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_fridge_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': 'Fridge temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_fridge_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Fridge temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_fridge_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5351,58 +5351,6 @@ 'state': '0.0270189050030708', }) # --- -# name: test_all_entities[da_ref_normal_01011][sensor.frigo_cooler_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.frigo_cooler_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': 'Cooler temperature', - 'platform': 'smartthings', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'cooler_temperature', - 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_temperatureMeasurement_temperature_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_all_entities[da_ref_normal_01011][sensor.frigo_cooler_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'Frigo Cooler temperature', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.frigo_cooler_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }) -# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5620,6 +5568,58 @@ 'state': '-17', }) # --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_fridge_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frigo_fridge_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': 'Fridge temperature', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooler_temperature', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_temperatureMeasurement_temperature_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_fridge_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Frigo Fridge temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.frigo_fridge_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6', + }) +# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 42534e5b691..45643f80d2c 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -51,7 +51,7 @@ async def test_state_update( """Test state update.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF + assert hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_OFF await trigger_update( hass, @@ -63,7 +63,7 @@ async def test_state_update( component="cooler", ) - assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_ON + assert hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_ON @pytest.mark.parametrize("device_fixture", ["da_ref_normal_000001"]) @@ -75,14 +75,14 @@ async def test_availability( """Test availability.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF + assert hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_OFF await trigger_health_update( hass, devices, "7db87911-7dce-1cf2-7119-b953432a2f09", HealthStatus.OFFLINE ) assert ( - hass.states.get("binary_sensor.refrigerator_cooler_door").state + hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_UNAVAILABLE ) @@ -90,7 +90,7 @@ async def test_availability( hass, devices, "7db87911-7dce-1cf2-7119-b953432a2f09", HealthStatus.ONLINE ) - assert hass.states.get("binary_sensor.refrigerator_cooler_door").state == STATE_OFF + assert hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_OFF @pytest.mark.parametrize("device_fixture", ["da_ref_normal_000001"]) @@ -102,7 +102,7 @@ async def test_availability_at_start( """Test unavailable at boot.""" await setup_integration(hass, mock_config_entry) assert ( - hass.states.get("binary_sensor.refrigerator_cooler_door").state + hass.states.get("binary_sensor.refrigerator_fridge_door").state == STATE_UNAVAILABLE ) From f472bf7c8716b43bb0dce6a94a0358e02ef3ce5e Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 25 May 2025 18:42:02 +0200 Subject: [PATCH 0866/1175] Bump uiprotect to version 7.9.2 (#145583) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_views.py | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index e23568480ca..f09dfd2c1ab 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.6.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.9.2", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 3e722a9b329..03da75c9d9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2984,7 +2984,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.6.0 +uiprotect==7.9.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9d2e340806..88e3f006709 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2416,7 +2416,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.6.0 +uiprotect==7.9.2 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index f787089b83f..9e477e1b8e7 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -678,6 +678,7 @@ async def test_video( mock_response.content.iter_chunked = Mock(return_value=content) ufp.api.request = AsyncMock(return_value=mock_response) + ufp.api._raise_for_status = AsyncMock() await init_entry(hass, ufp, [camera]) event_start = fixed_now - timedelta(seconds=30) @@ -722,6 +723,7 @@ async def test_video_entity_id( mock_response.content.iter_chunked = Mock(return_value=content) ufp.api.request = AsyncMock(return_value=mock_response) + ufp.api._raise_for_status = AsyncMock() await init_entry(hass, ufp, [camera]) event_start = fixed_now - timedelta(seconds=30) @@ -937,6 +939,7 @@ async def test_event_video( mock_response.content.iter_chunked = Mock(return_value=content) ufp.api.request = AsyncMock(return_value=mock_response) + ufp.api._raise_for_status = AsyncMock() await init_entry(hass, ufp, [camera]) event_start = fixed_now - timedelta(seconds=30) event = Event( From 1cc2baa95e69c0cdaa25aa7c16dd1c5fc27d37d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 May 2025 15:59:07 -0400 Subject: [PATCH 0867/1175] Pipeline to stream TTS on tool call (#145477) --- .../components/assist_pipeline/pipeline.py | 37 ++- .../snapshots/test_pipeline.ambr | 240 +++++++++++++++++- .../assist_pipeline/test_pipeline.py | 157 ++++++++---- 3 files changed, 376 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 7d5f98e87f6..34f590574d4 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1178,25 +1178,33 @@ class PipelineRun: if role := delta.get("role"): chat_log_role = role - # We are only interested in assistant deltas with content - if chat_log_role != "assistant" or not ( - content := delta.get("content") - ): + # We are only interested in assistant deltas + if chat_log_role != "assistant": return - tts_input_stream.put_nowait(content) + if content := delta.get("content"): + tts_input_stream.put_nowait(content) if self._streamed_response_text: return nonlocal delta_character_count - delta_character_count += len(content) - if delta_character_count < STREAM_RESPONSE_CHARS: + # Streamed responses are not cached. That's why we only start streaming text after + # we have received enough characters that indicates it will be a long response + # or if we have received text, and then a tool call. + + # Tool call after we already received text + start_streaming = delta_character_count > 0 and delta.get("tool_calls") + + # Count characters in the content and test if we exceed streaming threshold + if not start_streaming and content: + delta_character_count += len(content) + start_streaming = delta_character_count > STREAM_RESPONSE_CHARS + + if not start_streaming: return - # Streamed responses are not cached. We only start streaming text after - # we have received a couple of words that indicates it will be a long response. self._streamed_response_text = True async def tts_input_stream_generator() -> AsyncGenerator[str]: @@ -1204,6 +1212,17 @@ class PipelineRun: while (tts_input := await tts_input_stream.get()) is not None: yield tts_input + # Concatenate all existing queue items + parts = [] + while not tts_input_stream.empty(): + parts.append(tts_input_stream.get_nowait()) + tts_input_stream.put_nowait( + "".join( + # At this point parts is only strings, None indicates end of queue + cast(list[str], parts) + ) + ) + assert self.tts_stream is not None self.tts_stream.async_set_message_stream(tts_input_stream_generator()) diff --git a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr index 2e005fb4c13..8431e32ed87 100644 --- a/tests/components/assist_pipeline/snapshots/test_pipeline.ambr +++ b/tests/components/assist_pipeline/snapshots/test_pipeline.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_chat_log_tts_streaming[to_stream_tts0-0-] +# name: test_chat_log_tts_streaming[to_stream_deltas0-0-] list([ dict({ 'data': dict({ @@ -154,7 +154,7 @@ }), ]) # --- -# name: test_chat_log_tts_streaming[to_stream_tts1-16-hello, how are you? I'm doing well, thank you. What about you?] +# name: test_chat_log_tts_streaming[to_stream_deltas1-3-hello, how are you? I'm doing well, thank you. What about you?!] list([ dict({ 'data': dict({ @@ -317,10 +317,18 @@ }), 'type': , }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '!', + }), + }), + 'type': , + }), dict({ 'data': dict({ 'intent_output': dict({ - 'continue_conversation': True, + 'continue_conversation': False, 'conversation_id': , 'response': dict({ 'card': dict({ @@ -338,7 +346,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': "hello, how are you? I'm doing well, thank you. What about you?", + 'speech': "hello, how are you? I'm doing well, thank you. What about you?!", }), }), }), @@ -351,7 +359,229 @@ 'data': dict({ 'engine': 'tts.test', 'language': 'en_US', - 'tts_input': "hello, how are you? I'm doing well, thank you. What about you?", + 'tts_input': "hello, how are you? I'm doing well, thank you. What about you?!", + 'voice': None, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'tts_output': dict({ + 'media_id': 'media-source://tts/-stream-/mocked-token.mp3', + 'mime_type': 'audio/mpeg', + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': None, + 'type': , + }), + ]) +# --- +# name: test_chat_log_tts_streaming[to_stream_deltas2-8-hello, how are you? I'm doing well, thank you.] + list([ + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'language': 'en', + 'pipeline': , + 'tts_output': dict({ + 'mime_type': 'audio/mpeg', + 'stream_response': True, + 'token': 'mocked-token.mp3', + 'url': '/api/tts_proxy/mocked-token.mp3', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'conversation_id': 'mock-ulid', + 'device_id': None, + 'engine': 'test-agent', + 'intent_input': 'Set a timer', + 'language': 'en', + 'prefer_local_intents': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'role': 'assistant', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'hello, ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'how ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'are ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '? ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'tool_calls': list([ + dict({ + 'id': 'test_tool_id', + 'tool_args': dict({ + }), + 'tool_name': 'test_tool', + }), + ]), + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'agent_id': 'test-agent', + 'role': 'tool_result', + 'tool_call_id': 'test_tool_id', + 'tool_name': 'test_tool', + 'tool_result': 'Test response', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'role': 'assistant', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': "I'm ", + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'doing ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'well', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': ', ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'thank ', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': 'you', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'chat_log_delta': dict({ + 'content': '.', + }), + }), + 'type': , + }), + dict({ + 'data': dict({ + 'intent_output': dict({ + 'continue_conversation': False, + 'conversation_id': , + 'response': dict({ + 'card': dict({ + }), + 'data': dict({ + 'failed': list([ + ]), + 'success': list([ + ]), + 'targets': list([ + ]), + }), + 'language': 'en', + 'response_type': 'action_done', + 'speech': dict({ + 'plain': dict({ + 'extra_data': None, + 'speech': "I'm doing well, thank you.", + }), + }), + }), + }), + 'processed_locally': False, + }), + 'type': , + }), + dict({ + 'data': dict({ + 'engine': 'tts.test', + 'language': 'en_US', + 'tts_input': "I'm doing well, thank you.", 'voice': None, }), 'type': , diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index d8550f34deb..abdcb55054c 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -2,11 +2,12 @@ from collections.abc import AsyncGenerator, Generator from typing import Any -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch from hassil.recognize import Intent, IntentData, RecognizeResult import pytest from syrupy.assertion import SnapshotAssertion +import voluptuous as vol from homeassistant.components import ( assist_pipeline, @@ -33,7 +34,7 @@ from homeassistant.components.assist_pipeline.pipeline import ( ) from homeassistant.const import MATCH_ALL from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import chat_session, intent +from homeassistant.helpers import chat_session, intent, llm from homeassistant.setup import async_setup_component from . import MANY_LANGUAGES, process_events @@ -1575,47 +1576,86 @@ async def test_pipeline_language_used_instead_of_conversation_language( @pytest.mark.parametrize( - ("to_stream_tts", "expected_chunks", "chunk_text"), + ("to_stream_deltas", "expected_chunks", "chunk_text"), [ # Size below STREAM_RESPONSE_CHUNKS ( - [ - "hello,", - " ", - "how", - " ", - "are", - " ", - "you", - "?", - ], + ( + [ + "hello,", + " ", + "how", + " ", + "are", + " ", + "you", + "?", + ], + ), # We are not streaming, so 0 chunks via streaming method 0, "", ), # Size above STREAM_RESPONSE_CHUNKS ( - [ - "hello, ", - "how ", - "are ", - "you", - "? ", - "I'm ", - "doing ", - "well", - ", ", - "thank ", - "you", - ". ", - "What ", - "about ", - "you", - "?", - ], - # We are streamed, so equal to count above list items - 16, - "hello, how are you? I'm doing well, thank you. What about you?", + ( + [ + "hello, ", + "how ", + "are ", + "you", + "? ", + "I'm ", + "doing ", + "well", + ", ", + "thank ", + "you", + ". ", + "What ", + "about ", + "you", + "?", + "!", + ], + ), + # We are streamed. First 15 chunks are grouped into 1 chunk + # and the rest are streamed + 3, + "hello, how are you? I'm doing well, thank you. What about you?!", + ), + # Stream a bit, then a tool call, then stream some more + ( + ( + [ + "hello, ", + "how ", + "are ", + "you", + "? ", + ], + { + "tool_calls": [ + llm.ToolInput( + tool_name="test_tool", + tool_args={}, + id="test_tool_id", + ) + ], + }, + [ + "I'm ", + "doing ", + "well", + ", ", + "thank ", + "you", + ".", + ], + ), + # 1 chunk before tool call, then 7 after + 8, + "hello, how are you? I'm doing well, thank you.", ), ], ) @@ -1627,11 +1667,18 @@ async def test_chat_log_tts_streaming( snapshot: SnapshotAssertion, mock_tts_entity: MockTTSEntity, pipeline_data: assist_pipeline.pipeline.PipelineData, - to_stream_tts: list[str], + to_stream_deltas: tuple[dict | list[str]], expected_chunks: int, chunk_text: str, ) -> None: """Test that chat log events are streamed to the TTS entity.""" + text_deltas = [ + delta + for deltas in to_stream_deltas + if isinstance(deltas, list) + for delta in deltas + ] + events: list[assist_pipeline.PipelineEvent] = [] pipeline_store = pipeline_data.pipeline_store @@ -1678,7 +1725,7 @@ async def test_chat_log_tts_streaming( options: dict[str, Any] | None = None, ) -> tts.TtsAudioType: """Mock get TTS audio.""" - return ("mp3", b"".join([chunk.encode() for chunk in to_stream_tts])) + return ("mp3", b"".join([chunk.encode() for chunk in text_deltas])) mock_tts_entity.async_get_tts_audio = async_get_tts_audio mock_tts_entity.async_stream_tts_audio = async_stream_tts_audio @@ -1716,9 +1763,13 @@ async def test_chat_log_tts_streaming( ) async def stream_llm_response(): - yield {"role": "assistant"} - for chunk in to_stream_tts: - yield {"content": chunk} + for deltas in to_stream_deltas: + if isinstance(deltas, dict): + yield deltas + else: + yield {"role": "assistant"} + for chunk in deltas: + yield {"content": chunk} with ( chat_session.async_get_chat_session(hass, conversation_id) as session, @@ -1728,21 +1779,39 @@ async def test_chat_log_tts_streaming( conversation_input, ) as chat_log, ): + await chat_log.async_update_llm_data( + conversing_domain="test", + user_input=conversation_input, + user_llm_hass_api="assist", + user_llm_prompt=None, + ) async for _content in chat_log.async_add_delta_content_stream( agent_id, stream_llm_response() ): pass intent_response = intent.IntentResponse(language) - intent_response.async_set_speech("".join(to_stream_tts)) + intent_response.async_set_speech("".join(to_stream_deltas[-1])) return conversation.ConversationResult( response=intent_response, conversation_id=chat_log.conversation_id, continue_conversation=chat_log.continue_conversation, ) - with patch( - "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", - mock_converse, + mock_tool = AsyncMock() + mock_tool.name = "test_tool" + mock_tool.description = "Test function" + mock_tool.parameters = vol.Schema({}) + mock_tool.async_call.return_value = "Test response" + + with ( + patch( + "homeassistant.helpers.llm.AssistAPI._async_get_tools", + return_value=[mock_tool], + ), + patch( + "homeassistant.components.assist_pipeline.pipeline.conversation.async_converse", + mock_converse, + ), ): await pipeline_input.execute() @@ -1752,7 +1821,7 @@ async def test_chat_log_tts_streaming( [chunk.decode() async for chunk in stream.async_stream_result()] ) - streamed_text = "".join(to_stream_tts) + streamed_text = "".join(text_deltas) assert tts_result == streamed_text assert len(received_tts) == expected_chunks assert "".join(received_tts) == chunk_text From 14c4cf7b63dfa1d4990918a8b91e6e397dbc8923 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Sun, 25 May 2025 23:51:52 +0200 Subject: [PATCH 0868/1175] Bump uiprotect to version 7.10.0 (#145596) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index f09dfd2c1ab..f825e0a5eaf 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.9.2", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 03da75c9d9e..ead9b4ba6d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2984,7 +2984,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.9.2 +uiprotect==7.10.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88e3f006709..65b663b15a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2416,7 +2416,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.9.2 +uiprotect==7.10.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From e4b519d77a13165e6f277e24f32e9a116960a43c Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 25 May 2025 23:59:06 +0200 Subject: [PATCH 0869/1175] Bump pylamarzocco to 2.0.6 (#145595) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index a40f252f822..6118e364c15 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.5"] + "requirements": ["pylamarzocco==2.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index ead9b4ba6d9..7391ed7d801 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.5 +pylamarzocco==2.0.6 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65b663b15a0..3e09852a2c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1711,7 +1711,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.5 +pylamarzocco==2.0.6 # homeassistant.components.lastfm pylast==5.1.0 From 32eb4af6efedb270453be3c5179d93d0c714c552 Mon Sep 17 00:00:00 2001 From: Ivan Lopez Hernandez Date: Sun, 25 May 2025 18:50:55 -0700 Subject: [PATCH 0870/1175] Enable message Streaming in the Gemini integration. (#144937) * Added streaming implementation * Indicate the entity supports streaming * Added tests * Removed unused snapshots --------- Co-authored-by: Paulus Schoutsen --- .../conversation.py | 157 +-- .../__init__.py | 16 +- .../snapshots/test_conversation.ambr | 100 -- .../test_config_flow.py | 4 +- .../test_conversation.py | 901 ++++++++---------- .../test_init.py | 6 +- 6 files changed, 534 insertions(+), 650 deletions(-) delete mode 100644 tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr diff --git a/homeassistant/components/google_generative_ai_conversation/conversation.py b/homeassistant/components/google_generative_ai_conversation/conversation.py index c642bfd94e6..c466101e7e4 100644 --- a/homeassistant/components/google_generative_ai_conversation/conversation.py +++ b/homeassistant/components/google_generative_ai_conversation/conversation.py @@ -3,16 +3,17 @@ from __future__ import annotations import codecs -from collections.abc import Callable +from collections.abc import AsyncGenerator, Callable from dataclasses import replace from typing import Any, Literal, cast -from google.genai.errors import APIError +from google.genai.errors import APIError, ClientError from google.genai.types import ( AutomaticFunctionCallingConfig, Content, FunctionDeclaration, GenerateContentConfig, + GenerateContentResponse, GoogleSearch, HarmCategory, Part, @@ -233,6 +234,81 @@ def _convert_content( return Content(role="model", parts=parts) +async def _transform_stream( + result: AsyncGenerator[GenerateContentResponse], +) -> AsyncGenerator[conversation.AssistantContentDeltaDict]: + new_message = True + try: + async for response in result: + LOGGER.debug("Received response chunk: %s", response) + chunk: conversation.AssistantContentDeltaDict = {} + + if new_message: + chunk["role"] = "assistant" + new_message = False + + # According to the API docs, this would mean no candidate is returned, so we can safely throw an error here. + if response.prompt_feedback or not response.candidates: + reason = ( + response.prompt_feedback.block_reason_message + if response.prompt_feedback + else "unknown" + ) + raise HomeAssistantError( + f"The message got blocked due to content violations, reason: {reason}" + ) + + candidate = response.candidates[0] + + if ( + candidate.finish_reason is not None + and candidate.finish_reason != "STOP" + ): + # The message ended due to a content error as explained in: https://ai.google.dev/api/generate-content#FinishReason + LOGGER.error( + "Error in Google Generative AI response: %s, see: https://ai.google.dev/api/generate-content#FinishReason", + candidate.finish_reason, + ) + raise HomeAssistantError( + f"{ERROR_GETTING_RESPONSE} Reason: {candidate.finish_reason}" + ) + + response_parts = ( + candidate.content.parts + if candidate.content is not None and candidate.content.parts is not None + else [] + ) + + content = "".join([part.text for part in response_parts if part.text]) + tool_calls = [] + for part in response_parts: + if not part.function_call: + continue + tool_call = part.function_call + tool_name = tool_call.name if tool_call.name else "" + tool_args = _escape_decode(tool_call.args) + tool_calls.append( + llm.ToolInput(tool_name=tool_name, tool_args=tool_args) + ) + + if tool_calls: + chunk["tool_calls"] = tool_calls + + chunk["content"] = content + yield chunk + except ( + APIError, + ValueError, + ) as err: + LOGGER.error("Error sending message: %s %s", type(err), err) + if isinstance(err, APIError): + message = err.message + else: + message = type(err).__name__ + error = f"{ERROR_GETTING_RESPONSE}: {message}" + raise HomeAssistantError(error) from err + + class GoogleGenerativeAIConversationEntity( conversation.ConversationEntity, conversation.AbstractConversationAgent ): @@ -240,6 +316,7 @@ class GoogleGenerativeAIConversationEntity( _attr_has_entity_name = True _attr_name = None + _attr_supports_streaming = True def __init__(self, entry: ConfigEntry) -> None: """Initialize the agent.""" @@ -426,80 +503,40 @@ class GoogleGenerativeAIConversationEntity( # To prevent infinite loops, we limit the number of iterations for _iteration in range(MAX_TOOL_ITERATIONS): try: - chat_response = await chat.send_message(message=chat_request) - - if chat_response.prompt_feedback: - raise HomeAssistantError( - f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}" - ) - if not chat_response.candidates: - LOGGER.error( - "No candidates found in the response: %s", - chat_response, - ) - raise HomeAssistantError(ERROR_GETTING_RESPONSE) - + chat_response_generator = await chat.send_message_stream( + message=chat_request + ) except ( APIError, + ClientError, ValueError, ) as err: LOGGER.error("Error sending message: %s %s", type(err), err) - error = f"Sorry, I had a problem talking to Google Generative AI: {err}" + error = ERROR_GETTING_RESPONSE raise HomeAssistantError(error) from err - if (usage_metadata := chat_response.usage_metadata) is not None: - chat_log.async_trace( - { - "stats": { - "input_tokens": usage_metadata.prompt_token_count, - "cached_input_tokens": usage_metadata.cached_content_token_count - or 0, - "output_tokens": usage_metadata.candidates_token_count, - } - } - ) - - response_parts = chat_response.candidates[0].content.parts - if not response_parts: - raise HomeAssistantError(ERROR_GETTING_RESPONSE) - content = " ".join( - [part.text.strip() for part in response_parts if part.text] - ) - - tool_calls = [] - for part in response_parts: - if not part.function_call: - continue - tool_call = part.function_call - tool_name = tool_call.name - tool_args = _escape_decode(tool_call.args) - tool_calls.append( - llm.ToolInput( - tool_name=self._fix_tool_name(tool_name), - tool_args=tool_args, - ) - ) - chat_request = _create_google_tool_response_parts( [ - tool_response - async for tool_response in chat_log.async_add_assistant_content( - conversation.AssistantContent( - agent_id=user_input.agent_id, - content=content, - tool_calls=tool_calls or None, - ) + content + async for content in chat_log.async_add_delta_content_stream( + user_input.agent_id, + _transform_stream(chat_response_generator), ) + if isinstance(content, conversation.ToolResultContent) ] ) - if not tool_calls: + if not chat_log.unresponded_tool_results: break response = intent.IntentResponse(language=user_input.language) - response.async_set_speech( - " ".join([part.text.strip() for part in response_parts if part.text]) - ) + if not isinstance(chat_log.content[-1], conversation.AssistantContent): + LOGGER.error( + "Last content in chat log is not an AssistantContent: %s. This could be due to the model not returning a valid response", + chat_log.content[-1], + ) + raise HomeAssistantError(f"{ERROR_GETTING_RESPONSE}") + response.async_set_speech(chat_log.content[-1].content or "") return conversation.ConversationResult( response=response, conversation_id=chat_log.conversation_id, diff --git a/tests/components/google_generative_ai_conversation/__init__.py b/tests/components/google_generative_ai_conversation/__init__.py index fbf9ee545db..18b3c8e07f0 100644 --- a/tests/components/google_generative_ai_conversation/__init__.py +++ b/tests/components/google_generative_ai_conversation/__init__.py @@ -2,10 +2,10 @@ from unittest.mock import Mock -from google.genai.errors import ClientError +from google.genai.errors import APIError, ClientError import httpx -CLIENT_ERROR_500 = ClientError( +API_ERROR_500 = APIError( 500, Mock( __class__=httpx.Response, @@ -17,6 +17,18 @@ CLIENT_ERROR_500 = ClientError( ), ), ) +CLIENT_ERROR_BAD_REQUEST = ClientError( + 400, + Mock( + __class__=httpx.Response, + json=Mock( + return_value={ + "message": "Bad Request", + "status": "invalid-argument", + } + ), + ), +) CLIENT_ERROR_API_KEY_INVALID = ClientError( 400, Mock( diff --git a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr b/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr deleted file mode 100644 index ce257e61d53..00000000000 --- a/tests/components/google_generative_ai_conversation/snapshots/test_conversation.ambr +++ /dev/null @@ -1,100 +0,0 @@ -# serializer version: 1 -# name: test_function_call - list([ - tuple( - '', - tuple( - ), - dict({ - 'config': GenerateContentConfig(http_options=None, system_instruction="You are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.\nCurrent time is 05:00:00. Today's date is 2024-05-24.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=1500, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties={'param1': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description='Test parameters', enum=None, format=None, items=Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=), max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=), 'param2': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=None), 'param3': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties={'json': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=)}, property_ordering=None, required=[], type=)}, property_ordering=None, required=[], type=))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None), - 'history': list([ - ]), - 'model': 'models/gemini-2.0-flash', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': 'Please call the test function', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': list([ - Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=FunctionResponse(id=None, name='test_tool', response={'result': 'Test response'}), inline_data=None, text=None), - ]), - }), - ), - ]) -# --- -# name: test_function_call_without_parameters - list([ - tuple( - '', - tuple( - ), - dict({ - 'config': GenerateContentConfig(http_options=None, system_instruction="You are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.\nCurrent time is 05:00:00. Today's date is 2024-05-24.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=1500, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=None)], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None), - 'history': list([ - ]), - 'model': 'models/gemini-2.0-flash', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': 'Please call the test function', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': list([ - Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=FunctionResponse(id=None, name='test_tool', response={'result': 'Test response'}), inline_data=None, text=None), - ]), - }), - ), - ]) -# --- -# name: test_use_google_search - list([ - tuple( - '', - tuple( - ), - dict({ - 'config': GenerateContentConfig(http_options=None, system_instruction="You are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.\nCurrent time is 05:00:00. Today's date is 2024-05-24.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=1500, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=), SafetySetting(method=None, category=, threshold=)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties={'param1': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description='Test parameters', enum=None, format=None, items=Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=), max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=), 'param2': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=None), 'param3': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties={'json': Schema(example=None, pattern=None, default=None, max_length=None, title=None, min_length=None, min_properties=None, max_properties=None, any_of=None, description=None, enum=None, format=None, items=None, max_items=None, maximum=None, min_items=None, minimum=None, nullable=None, properties=None, property_ordering=None, required=None, type=)}, property_ordering=None, required=[], type=)}, property_ordering=None, required=[], type=))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None), Tool(function_declarations=None, retrieval=None, google_search=GoogleSearch(), google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None), - 'history': list([ - ]), - 'model': 'models/gemini-2.0-flash', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': 'Please call the test function', - }), - ), - tuple( - '().send_message', - tuple( - ), - dict({ - 'message': list([ - Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=FunctionResponse(id=None, name='test_tool', response={'result': 'Test response'}), inline_data=None, text=None), - ]), - }), - ), - ]) -# --- 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 13063580c95..4234355cb5b 100644 --- a/tests/components/google_generative_ai_conversation/test_config_flow.py +++ b/tests/components/google_generative_ai_conversation/test_config_flow.py @@ -34,7 +34,7 @@ from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import CLIENT_ERROR_500, CLIENT_ERROR_API_KEY_INVALID +from . import API_ERROR_500, CLIENT_ERROR_API_KEY_INVALID from tests.common import MockConfigEntry @@ -339,7 +339,7 @@ async def test_options_switching( ("side_effect", "error"), [ ( - CLIENT_ERROR_500, + API_ERROR_500, "cannot_connect", ), ( diff --git a/tests/components/google_generative_ai_conversation/test_conversation.py b/tests/components/google_generative_ai_conversation/test_conversation.py index 75cb308d5de..2d1a46393fd 100644 --- a/tests/components/google_generative_ai_conversation/test_conversation.py +++ b/tests/components/google_generative_ai_conversation/test_conversation.py @@ -1,16 +1,14 @@ """Tests for the Google Generative AI Conversation integration conversation platform.""" -from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch from freezegun import freeze_time -from google.genai.types import FunctionCall +from google.genai.types import GenerateContentResponse import pytest -from syrupy.assertion import SnapshotAssertion -import voluptuous as vol from homeassistant.components import conversation -from homeassistant.components.conversation import UserContent, async_get_chat_log, trace +from homeassistant.components.conversation import UserContent from homeassistant.components.google_generative_ai_conversation.conversation import ( ERROR_GETTING_RESPONSE, _escape_decode, @@ -18,12 +16,15 @@ from homeassistant.components.google_generative_ai_conversation.conversation imp ) from homeassistant.const import CONF_LLM_HASS_API from homeassistant.core import Context, HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import chat_session, intent, llm +from homeassistant.helpers import intent -from . import CLIENT_ERROR_500 +from . import API_ERROR_500, CLIENT_ERROR_BAD_REQUEST from tests.common import MockConfigEntry +from tests.components.conversation import ( + MockChatLog, + mock_chat_log, # noqa: F401 +) @pytest.fixture(autouse=True) @@ -40,396 +41,44 @@ def mock_ulid_tools(): yield -@patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" +@pytest.fixture +def mock_send_message_stream() -> Generator[AsyncMock]: + """Mock stream response.""" + + async def mock_generator(stream): + for value in stream: + yield value + + with patch( + "google.genai.chats.AsyncChat.send_message_stream", + AsyncMock(), + ) as mock_send_message_stream: + mock_send_message_stream.side_effect = lambda **kwargs: mock_generator( + mock_send_message_stream.return_value.pop(0) + ) + + yield mock_send_message_stream + + +@pytest.mark.parametrize( + ("error"), + [ + (API_ERROR_500,), + (CLIENT_ERROR_BAD_REQUEST,), + ], ) -@pytest.mark.usefixtures("mock_init_component") -@pytest.mark.usefixtures("mock_ulid_tools") -async def test_function_call( - mock_get_tools, - hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - snapshot: SnapshotAssertion, -) -> None: - """Test function calling.""" - agent_id = "conversation.google_generative_ai_conversation" - context = Context() - - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - { - vol.Optional("param1", description="Test parameters"): [ - vol.All(str, vol.Lower) - ], - vol.Optional("param2"): vol.Any(float, int), - vol.Optional("param3"): dict, - } - ) - - mock_get_tools.return_value = [mock_tool] - - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - mock_part = Mock() - mock_part.text = "" - mock_part.function_call = FunctionCall( - name="test_tool", - args={ - "param1": ["test_value", "param1\\'s value"], - "param2": 2.7, - }, - ) - - def tool_call( - hass: HomeAssistant, tool_input: llm.ToolInput, tool_context: llm.LLMContext - ) -> dict[str, Any]: - mock_part.function_call = None - mock_part.text = "Hi there!" - return {"result": "Test response"} - - mock_tool.async_call.side_effect = tool_call - chat_response.candidates = [Mock(content=Mock(parts=[mock_part]))] - result = await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - device_id="test_device", - ) - - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" - mock_tool_response_parts = mock_create.mock_calls[2][2]["message"] - assert len(mock_tool_response_parts) == 1 - assert mock_tool_response_parts[0].model_dump() == { - "code_execution_result": None, - "executable_code": None, - "file_data": None, - "function_call": None, - "function_response": { - "id": None, - "name": "test_tool", - "response": { - "result": "Test response", - }, - }, - "inline_data": None, - "text": None, - "thought": None, - "video_metadata": None, - } - - mock_tool.async_call.assert_awaited_once_with( - hass, - llm.ToolInput( - id="mock-tool-call", - tool_name="test_tool", - tool_args={ - "param1": ["test_value", "param1's value"], - "param2": 2.7, - }, - ), - llm.LLMContext( - platform="google_generative_ai_conversation", - context=context, - user_prompt="Please call the test function", - language="en", - assistant="conversation", - device_id="test_device", - ), - ) - assert [tuple(mock_call) for mock_call in mock_create.mock_calls] == snapshot - - # Test conversating tracing - traces = trace.async_get_traces() - assert traces - last_trace = traces[-1].as_dict() - trace_events = last_trace.get("events", []) - assert [event["event_type"] for event in trace_events] == [ - trace.ConversationTraceEventType.ASYNC_PROCESS, - trace.ConversationTraceEventType.AGENT_DETAIL, # prompt and tools - trace.ConversationTraceEventType.AGENT_DETAIL, # stats for response - trace.ConversationTraceEventType.TOOL_CALL, - trace.ConversationTraceEventType.AGENT_DETAIL, # stats for response - ] - # AGENT_DETAIL event contains the raw prompt passed to the model - detail_event = trace_events[1] - assert "Answer in plain text" in detail_event["data"]["messages"][0]["content"] - assert [ - p["tool_name"] for p in detail_event["data"]["messages"][2]["tool_calls"] - ] == ["test_tool"] - - detail_event = trace_events[2] - assert set(detail_event["data"]["stats"].keys()) == { - "input_tokens", - "cached_input_tokens", - "output_tokens", - } - - -@patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" -) -@pytest.mark.usefixtures("mock_init_component") -@pytest.mark.usefixtures("mock_ulid_tools") -async def test_use_google_search( - mock_get_tools, - hass: HomeAssistant, - mock_config_entry_with_google_search: MockConfigEntry, - snapshot: SnapshotAssertion, -) -> None: - """Test function calling.""" - agent_id = "conversation.google_generative_ai_conversation" - context = Context() - - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - { - vol.Optional("param1", description="Test parameters"): [ - vol.All(str, vol.Lower) - ], - vol.Optional("param2"): vol.Any(float, int), - vol.Optional("param3"): dict, - } - ) - - mock_get_tools.return_value = [mock_tool] - - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - mock_part = Mock() - mock_part.text = "" - mock_part.function_call = FunctionCall( - name="test_tool", - args={ - "param1": ["test_value", "param1\\'s value"], - "param2": 2.7, - }, - ) - - def tool_call( - hass: HomeAssistant, tool_input: llm.ToolInput, tool_context: llm.LLMContext - ) -> dict[str, Any]: - mock_part.function_call = None - mock_part.text = "Hi there!" - return {"result": "Test response"} - - mock_tool.async_call.side_effect = tool_call - chat_response.candidates = [Mock(content=Mock(parts=[mock_part]))] - await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - device_id="test_device", - ) - - assert [tuple(mock_call) for mock_call in mock_create.mock_calls] == snapshot - - -@patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" -) -@pytest.mark.usefixtures("mock_init_component") -async def test_function_call_without_parameters( - mock_get_tools, - hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - snapshot: SnapshotAssertion, -) -> None: - """Test function calling without parameters.""" - agent_id = "conversation.google_generative_ai_conversation" - context = Context() - - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema({}) - - mock_get_tools.return_value = [mock_tool] - - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - mock_part = Mock() - mock_part.text = "" - mock_part.function_call = FunctionCall(name="test_tool", args={}) - - def tool_call( - hass: HomeAssistant, tool_input: llm.ToolInput, tool_context: llm.LLMContext - ) -> dict[str, Any]: - mock_part.function_call = None - mock_part.text = "Hi there!" - return {"result": "Test response"} - - mock_tool.async_call.side_effect = tool_call - chat_response.candidates = [Mock(content=Mock(parts=[mock_part]))] - result = await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - device_id="test_device", - ) - - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" - mock_tool_response_parts = mock_create.mock_calls[2][2]["message"] - assert len(mock_tool_response_parts) == 1 - assert mock_tool_response_parts[0].model_dump() == { - "code_execution_result": None, - "executable_code": None, - "file_data": None, - "function_call": None, - "function_response": { - "id": None, - "name": "test_tool", - "response": { - "result": "Test response", - }, - }, - "inline_data": None, - "text": None, - "thought": None, - "video_metadata": None, - } - - mock_tool.async_call.assert_awaited_once_with( - hass, - llm.ToolInput( - id="mock-tool-call", - tool_name="test_tool", - tool_args={}, - ), - llm.LLMContext( - platform="google_generative_ai_conversation", - context=context, - user_prompt="Please call the test function", - language="en", - assistant="conversation", - device_id="test_device", - ), - ) - assert [tuple(mock_call) for mock_call in mock_create.mock_calls] == snapshot - - -@patch( - "homeassistant.components.google_generative_ai_conversation.conversation.llm.AssistAPI._async_get_tools" -) -@pytest.mark.usefixtures("mock_init_component") -async def test_function_exception( - mock_get_tools, - hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, -) -> None: - """Test exception in function calling.""" - agent_id = "conversation.google_generative_ai_conversation" - context = Context() - - mock_tool = AsyncMock() - mock_tool.name = "test_tool" - mock_tool.description = "Test function" - mock_tool.parameters = vol.Schema( - { - vol.Optional("param1", description="Test parameters"): vol.All( - vol.Coerce(int), vol.Range(0, 100) - ) - } - ) - - mock_get_tools.return_value = [mock_tool] - - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - mock_part = Mock() - mock_part.text = "" - mock_part.function_call = FunctionCall(name="test_tool", args={"param1": 1}) - - def tool_call( - hass: HomeAssistant, tool_input: llm.ToolInput, tool_context: llm.LLMContext - ) -> dict[str, Any]: - mock_part.function_call = None - mock_part.text = "Hi there!" - raise HomeAssistantError("Test tool exception") - - mock_tool.async_call.side_effect = tool_call - chat_response.candidates = [Mock(content=Mock(parts=[mock_part]))] - result = await conversation.async_converse( - hass, - "Please call the test function", - None, - context, - agent_id=agent_id, - device_id="test_device", - ) - - assert result.response.response_type == intent.IntentResponseType.ACTION_DONE - assert result.response.as_dict()["speech"]["plain"]["speech"] == "Hi there!" - mock_tool_response_parts = mock_create.mock_calls[2][2]["message"] - assert len(mock_tool_response_parts) == 1 - assert mock_tool_response_parts[0].model_dump() == { - "code_execution_result": None, - "executable_code": None, - "file_data": None, - "function_call": None, - "function_response": { - "id": None, - "name": "test_tool", - "response": { - "error": "HomeAssistantError", - "error_text": "Test tool exception", - }, - }, - "inline_data": None, - "text": None, - "thought": None, - "video_metadata": None, - } - mock_tool.async_call.assert_awaited_once_with( - hass, - llm.ToolInput( - id="mock-tool-call", - tool_name="test_tool", - tool_args={"param1": 1}, - ), - llm.LLMContext( - platform="google_generative_ai_conversation", - context=context, - user_prompt="Please call the test function", - language="en", - assistant="conversation", - device_id="test_device", - ), - ) - - -@pytest.mark.usefixtures("mock_init_component") async def test_error_handling( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_init_component, + error, ) -> None: """Test that client errors are caught.""" - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - mock_chat.side_effect = CLIENT_ERROR_500 + with patch( + "google.genai.chats.AsyncChat.send_message_stream", + new_callable=AsyncMock, + side_effect=error, + ): result = await conversation.async_converse( hass, "hello", @@ -437,32 +86,251 @@ async def test_error_handling( Context(), agent_id="conversation.google_generative_ai_conversation", ) - assert result.response.response_type == intent.IntentResponseType.ERROR, result assert result.response.error_code == "unknown", result - assert result.response.as_dict()["speech"]["plain"]["speech"] == ( - "Sorry, I had a problem talking to Google Generative AI: 500 internal-error. {'message': 'Internal Server Error', 'status': 'internal-error'}" + assert ( + result.response.as_dict()["speech"]["plain"]["speech"] == ERROR_GETTING_RESPONSE ) +@pytest.mark.usefixtures("mock_init_component") +@pytest.mark.usefixtures("mock_ulid_tools") +async def test_function_call( + hass: HomeAssistant, + mock_config_entry_with_assist: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, +) -> None: + """Test function calling.""" + agent_id = "conversation.google_generative_ai_conversation" + context = Context() + + messages = [ + # Function call stream + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": "Hi there!", + } + ], + "role": "model", + } + } + ] + ), + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "function_call": { + "name": "test_tool", + "args": { + "param1": [ + "test_value", + "param1\\'s value", + ], + "param2": 2.7, + }, + }, + } + ], + "role": "model", + }, + "finish_reason": "STOP", + } + ] + ), + ], + # Messages after function response is sent + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": "I've called the ", + } + ], + "role": "model", + }, + } + ], + ), + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": "test function with the provided parameters.", + } + ], + "role": "model", + }, + "finish_reason": "STOP", + } + ], + ), + ], + ] + + mock_send_message_stream.return_value = messages + + mock_chat_log.mock_tool_results( + { + "mock-tool-call": {"result": "Test response"}, + } + ) + + result = await conversation.async_converse( + hass, + "Please call the test function", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert ( + result.response.as_dict()["speech"]["plain"]["speech"] + == "I've called the test function with the provided parameters." + ) + mock_tool_response_parts = mock_send_message_stream.mock_calls[1][2]["message"] + assert len(mock_tool_response_parts) == 1 + assert mock_tool_response_parts[0].model_dump() == { + "code_execution_result": None, + "executable_code": None, + "file_data": None, + "function_call": None, + "function_response": { + "id": None, + "name": "test_tool", + "response": { + "result": "Test response", + }, + }, + "inline_data": None, + "text": None, + "thought": None, + "video_metadata": None, + } + + +@pytest.mark.usefixtures("mock_init_component") +@pytest.mark.usefixtures("mock_ulid_tools") +async def test_google_search_tool_is_sent( + hass: HomeAssistant, + mock_config_entry_with_google_search: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, +) -> None: + """Test if the Google Search tool is sent to the model.""" + agent_id = "conversation.google_generative_ai_conversation" + context = Context() + + messages = [ + # Messages from the model which contain the google search answer (the usage of the Google Search tool is server side) + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": "The last winner ", + } + ], + "role": "model", + }, + } + ], + ), + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + {"text": "of the 2024 FIFA World Cup was Argentina."} + ], + "role": "model", + }, + "finish_reason": "STOP", + } + ], + ), + ], + ] + + mock_send_message_stream.return_value = messages + + with patch( + "google.genai.chats.AsyncChats.create", return_value=AsyncMock() + ) as mock_create: + mock_create.return_value.send_message_stream = mock_send_message_stream + result = await conversation.async_converse( + hass, + "Who won the 2024 FIFA World Cup?", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert ( + result.response.as_dict()["speech"]["plain"]["speech"] + == "The last winner of the 2024 FIFA World Cup was Argentina." + ) + assert mock_create.mock_calls[0][2]["config"].tools[-1].google_search is not None + + @pytest.mark.usefixtures("mock_init_component") async def test_blocked_response( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, ) -> None: """Test blocked response.""" - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=Mock(block_reason_message="SAFETY")) - mock_chat.return_value = chat_response + agent_id = "conversation.google_generative_ai_conversation" + context = Context() - result = await conversation.async_converse( - hass, - "hello", - None, - Context(), - agent_id="conversation.google_generative_ai_conversation", - ) + messages = [ + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": "I've called the ", + } + ], + "role": "model", + }, + } + ], + ), + GenerateContentResponse(prompt_feedback={"block_reason_message": "SAFETY"}), + ], + ] + + mock_send_message_stream.return_value = messages + + result = await conversation.async_converse( + hass, + "Please call the test function", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", + ) assert result.response.response_type == intent.IntentResponseType.ERROR, result assert result.response.error_code == "unknown", result @@ -473,23 +341,41 @@ async def test_blocked_response( @pytest.mark.usefixtures("mock_init_component") async def test_empty_response( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, ) -> None: """Test empty response.""" - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - chat_response.candidates = [Mock(content=Mock(parts=[]))] - result = await conversation.async_converse( - hass, - "hello", - None, - Context(), - agent_id="conversation.google_generative_ai_conversation", - ) + agent_id = "conversation.google_generative_ai_conversation" + context = Context() + + messages = [ + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [], + "role": "model", + }, + } + ], + ), + ], + ] + + mock_send_message_stream.return_value = messages + + result = await conversation.async_converse( + hass, + "Hello", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", + ) assert result.response.response_type == intent.IntentResponseType.ERROR, result assert result.response.error_code == "unknown", result assert result.response.as_dict()["speech"]["plain"]["speech"] == ( @@ -499,27 +385,36 @@ async def test_empty_response( @pytest.mark.usefixtures("mock_init_component") async def test_none_response( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, ) -> None: - """Test empty response.""" - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - chat_response.candidates = None - result = await conversation.async_converse( - hass, - "hello", - None, - Context(), - agent_id="conversation.google_generative_ai_conversation", - ) + """Test None response.""" + agent_id = "conversation.google_generative_ai_conversation" + context = Context() + + messages = [ + [ + GenerateContentResponse(), + ], + ] + + mock_send_message_stream.return_value = messages + + result = await conversation.async_converse( + hass, + "Hello", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", + ) assert result.response.response_type == intent.IntentResponseType.ERROR, result assert result.response.error_code == "unknown", result assert result.response.as_dict()["speech"]["plain"]["speech"] == ( - ERROR_GETTING_RESPONSE + "The message got blocked due to content violations, reason: unknown" ) @@ -712,69 +607,109 @@ async def test_format_schema(openapi, genai_schema) -> None: @pytest.mark.usefixtures("mock_init_component") async def test_empty_content_in_chat_history( - hass: HomeAssistant, mock_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, ) -> None: """Tests that in case of an empty entry in the chat history the google API will receive an injected space sign instead.""" - with ( - patch("google.genai.chats.AsyncChats.create") as mock_create, - chat_session.async_get_chat_session(hass) as session, - async_get_chat_log(hass, session) as chat_log, - ): - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat + agent_id = "conversation.google_generative_ai_conversation" + context = Context() - # Chat preparation with two inputs, one being an empty string - first_input = "First request" - second_input = "" - chat_log.async_add_user_content(UserContent(first_input)) - chat_log.async_add_user_content(UserContent(second_input)) + messages = [ + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [{"text": "Hi there!"}], + "role": "model", + }, + } + ], + ), + ], + ] + mock_send_message_stream.return_value = messages + + # Chat preparation with two inputs, one being an empty string + first_input = "First request" + second_input = "" + mock_chat_log.async_add_user_content(UserContent(first_input)) + mock_chat_log.async_add_user_content(UserContent(second_input)) + + with patch( + "google.genai.chats.AsyncChats.create", return_value=AsyncMock() + ) as mock_create: + mock_create.return_value.send_message_stream = mock_send_message_stream await conversation.async_converse( hass, - "Second request", - session.conversation_id, - Context(), - agent_id="conversation.google_generative_ai_conversation", + "Hello", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", ) - _, kwargs = mock_create.call_args - actual_history = kwargs.get("history") + _, kwargs = mock_create.call_args + actual_history = kwargs.get("history") - assert actual_history[0].parts[0].text == first_input - assert actual_history[1].parts[0].text == " " + assert actual_history[0].parts[0].text == first_input + assert actual_history[1].parts[0].text == " " @pytest.mark.usefixtures("mock_init_component") async def test_history_always_user_first_turn( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - snapshot: SnapshotAssertion, + mock_chat_log: MockChatLog, # noqa: F811 + mock_send_message_stream: AsyncMock, ) -> None: """Test that the user is always first in the chat history.""" - with ( - chat_session.async_get_chat_session(hass) as session, - async_get_chat_log(hass, session) as chat_log, - ): - chat_log.async_add_assistant_content_without_tools( - conversation.AssistantContent( - agent_id="conversation.google_generative_ai_conversation", - content="Garage door left open, do you want to close it?", - ) + + agent_id = "conversation.google_generative_ai_conversation" + context = Context() + + messages = [ + [ + GenerateContentResponse( + candidates=[ + { + "content": { + "parts": [ + { + "text": " Yes, I can help with that. ", + } + ], + "role": "model", + }, + } + ], + ), + ], + ] + + mock_send_message_stream.return_value = messages + + mock_chat_log.async_add_assistant_content_without_tools( + conversation.AssistantContent( + agent_id="conversation.google_generative_ai_conversation", + content="Garage door left open, do you want to close it?", ) + ) - with patch("google.genai.chats.AsyncChats.create") as mock_create: - mock_chat = AsyncMock() - mock_create.return_value.send_message = mock_chat - chat_response = Mock(prompt_feedback=None) - mock_chat.return_value = chat_response - chat_response.candidates = [Mock(content=Mock(parts=[]))] - + with patch( + "google.genai.chats.AsyncChats.create", return_value=AsyncMock() + ) as mock_create: + mock_create.return_value.send_message_stream = mock_send_message_stream await conversation.async_converse( hass, - "hello", - chat_log.conversation_id, - Context(), - agent_id="conversation.google_generative_ai_conversation", + "Hello", + mock_chat_log.conversation_id, + context, + agent_id=agent_id, + device_id="test_device", ) _, kwargs = mock_create.call_args diff --git a/tests/components/google_generative_ai_conversation/test_init.py b/tests/components/google_generative_ai_conversation/test_init.py index 94308260f74..6cc0bdd5f44 100644 --- a/tests/components/google_generative_ai_conversation/test_init.py +++ b/tests/components/google_generative_ai_conversation/test_init.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import CLIENT_ERROR_500, CLIENT_ERROR_API_KEY_INVALID +from . import API_ERROR_500, CLIENT_ERROR_API_KEY_INVALID from tests.common import MockConfigEntry @@ -212,7 +212,7 @@ async def test_generate_content_service_error( with ( patch( "google.genai.models.AsyncModels.generate_content", - side_effect=CLIENT_ERROR_500, + side_effect=API_ERROR_500, ), pytest.raises( HomeAssistantError, @@ -311,7 +311,7 @@ async def test_generate_content_service_with_image_not_exists( ("side_effect", "state", "reauth"), [ ( - CLIENT_ERROR_500, + API_ERROR_500, ConfigEntryState.SETUP_ERROR, False, ), From ba0c03ddbbe172f78c85695ab291d09df20ecaf3 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 26 May 2025 06:53:09 +0200 Subject: [PATCH 0871/1175] Bump ZHA to 0.0.59 (#145597) --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/number.py | 12 ------------ homeassistant/components/zha/strings.json | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/common.py | 4 ++-- tests/components/zha/test_number.py | 2 +- tests/components/zha/test_sensor.py | 14 +++++++------- 8 files changed, 19 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ae337c2a5f5..4a5ec7be1dc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["zha==0.0.57"], + "requirements": ["zha==0.0.59"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 567e2a5b37a..7a6e40af7e7 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -11,7 +11,6 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.typing import UndefinedType from .entity import ZHAEntity from .helpers import ( @@ -46,17 +45,6 @@ async def async_setup_entry( class ZhaNumber(ZHAEntity, RestoreNumber): """Representation of a ZHA Number entity.""" - @property - def name(self) -> str | UndefinedType | None: - """Return the name of the number entity.""" - if (description := self.entity_data.entity.description) is None: - return super().name - - # The name of this entity is reported by the device itself. - # For backwards compatibility, we keep the same format as before. This - # should probably be changed in the future to omit the prefix. - return f"{super().name} {description}" - @property def native_value(self) -> float | None: """Return the current value.""" diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 05ee1f2ac7e..a330fa6b0ee 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1137,6 +1137,9 @@ }, "external_temperature_sensor_value": { "name": "External temperature sensor value" + }, + "update_frequency": { + "name": "Update frequency" } }, "select": { @@ -1367,6 +1370,9 @@ }, "alarm_sound_mode": { "name": "Alarm sound mode" + }, + "external_switch_type": { + "name": "External switch type" } }, "sensor": { diff --git a/requirements_all.txt b/requirements_all.txt index 7391ed7d801..0a0c49ad306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3177,7 +3177,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.57 +zha==0.0.59 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3e09852a2c4..3267bf3bd18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2579,7 +2579,7 @@ zeroconf==0.147.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.57 +zha==0.0.59 # homeassistant.components.zwave_js zwave-js-server-python==0.63.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 89526f6431e..3935b66cc32 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -75,7 +75,7 @@ def update_attribute_cache(cluster): attrs.append(make_attribute(attrid, value)) hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) - hdr.frame_control.disable_default_response = True + hdr.frame_control = hdr.frame_control.replace(disable_default_response=True) msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema( attribute_reports=attrs ) @@ -119,7 +119,7 @@ async def send_attributes_report( ) hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes) - hdr.frame_control.disable_default_response = True + hdr.frame_control = hdr.frame_control.replace(disable_default_response=True) cluster.handle_message(hdr, msg) await hass.async_block_till_done() diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 180f16e9ae2..91f5e32942f 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -92,7 +92,7 @@ async def test_number(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None assert ( hass.states.get(entity_id).attributes.get("friendly_name") - == "FakeManufacturer FakeModel Number PWM1" + == "FakeManufacturer FakeModel PWM1" ) # change value from device diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 863ea3964ab..2e6b9e8bd6a 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -62,10 +62,10 @@ async def async_test_temperature(hass: HomeAssistant, cluster: Cluster, entity_i async def async_test_pressure(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test pressure sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) - assert_state(hass, entity_id, "1000.0", UnitOfPressure.HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000}) - assert_state(hass, entity_id, "1000.0", UnitOfPressure.HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) async def async_test_illuminance(hass: HomeAssistant, cluster: Cluster, entity_id: str): @@ -211,17 +211,17 @@ async def async_test_em_power_factor( # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) await send_attributes_report(hass, cluster, {0: 1, 0x0510: 100, 10: 1000}) - assert_state(hass, entity_id, "100.0", PERCENTAGE) + assert_state(hass, entity_id, "100", PERCENTAGE) await send_attributes_report(hass, cluster, {0: 1, 0x0510: 99, 10: 1000}) - assert_state(hass, entity_id, "99.0", PERCENTAGE) + assert_state(hass, entity_id, "99", PERCENTAGE) await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 0x0510: 100, 10: 5000}) - assert_state(hass, entity_id, "100.0", PERCENTAGE) + assert_state(hass, entity_id, "100", PERCENTAGE) await send_attributes_report(hass, cluster, {0: 1, 0x0510: 99, 10: 5000}) - assert_state(hass, entity_id, "99.0", PERCENTAGE) + assert_state(hass, entity_id, "99", PERCENTAGE) async def async_test_em_rms_current( @@ -317,7 +317,7 @@ async def async_test_pi_heating_demand( await send_attributes_report( hass, cluster, {Thermostat.AttributeDefs.pi_heating_demand.id: 1} ) - assert_state(hass, entity_id, "1.0", "%") + assert_state(hass, entity_id, "1", "%") @pytest.mark.parametrize( From d4333665fc73f3af884a96680b81a9cb76812a2f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 10:21:38 +0200 Subject: [PATCH 0872/1175] Add issue trackers to requirements script exceptions (#145608) --- script/hassfest/requirements.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 356e44986e5..8c1892f20a7 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -48,97 +48,122 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # - package is the package (can be transitive) referencing the dependency # - reasonX should be the name of the invalid dependency "azure_devops": { + # https://github.com/timmo001/aioazuredevops/issues/67 # aioazuredevops > incremental > setuptools "incremental": {"setuptools"} }, "cmus": { + # https://github.com/mtreinish/pycmus/issues/4 # pycmus > pbr > setuptools "pbr": {"setuptools"} }, "concord232": { + # https://bugs.launchpad.net/python-stevedore/+bug/2111694 # concord232 > stevedore > pbr > setuptools "pbr": {"setuptools"} }, "efergy": { + # https://github.com/tkdrob/pyefergy/issues/46 # pyefergy > codecov # pyefergy > types-pytz "pyefergy": {"codecov", "types-pytz"} }, "fitbit": { + # https://github.com/orcasgit/python-fitbit/pull/178 + # but project seems unmaintained # fitbit > setuptools "fitbit": {"setuptools"} }, "guardian": { + # https://github.com/jsbronder/asyncio-dgram/issues/20 # aioguardian > asyncio-dgram > setuptools "asyncio-dgram": {"setuptools"} }, "hive": { + # https://github.com/Pyhass/Pyhiveapi/pull/88 # pyhive-integration > unasync > setuptools "unasync": {"setuptools"} }, "influxdb": { + # https://github.com/influxdata/influxdb-client-python/issues/695 # influxdb-client > setuptools "influxdb-client": {"setuptools"} }, "keba": { + # https://github.com/jsbronder/asyncio-dgram/issues/20 # keba-kecontact > asyncio-dgram > setuptools "asyncio-dgram": {"setuptools"} }, "lyric": { + # https://github.com/timmo001/aiolyric/issues/115 # aiolyric > incremental > setuptools "incremental": {"setuptools"} }, "microbees": { + # https://github.com/microBeesTech/pythonSDK/issues/6 # microbeespy > setuptools "microbeespy": {"setuptools"} }, "minecraft_server": { + # https://github.com/jsbronder/asyncio-dgram/issues/20 # mcstatus > asyncio-dgram > setuptools "asyncio-dgram": {"setuptools"} }, "mochad": { + # https://github.com/mtreinish/pymochad/issues/8 # pymochad > pbr > setuptools "pbr": {"setuptools"} }, "mystrom": { + # https://github.com/home-assistant-ecosystem/python-mystrom/issues/55 # python-mystrom > setuptools "python-mystrom": {"setuptools"} }, "nx584": { + # https://bugs.launchpad.net/python-stevedore/+bug/2111694 # pynx584 > stevedore > pbr > setuptools "pbr": {"setuptools"} }, "opnsense": { + # https://github.com/mtreinish/pyopnsense/issues/27 # pyopnsense > pbr > setuptools "pbr": {"setuptools"} }, "opower": { + # https://github.com/arrow-py/arrow/issues/1169 (fixed not yet released) # opower > arrow > types-python-dateutil "arrow": {"types-python-dateutil"} }, "osoenergy": { + # https://github.com/osohotwateriot/apyosohotwaterapi/pull/4 # pyosoenergyapi > unasync > setuptools "unasync": {"setuptools"} }, "ovo_energy": { + # https://github.com/timmo001/ovoenergy/issues/132 # ovoenergy > incremental > setuptools "incremental": {"setuptools"} }, "remote_rpi_gpio": { + # https://github.com/waveform80/colorzero/issues/9 # gpiozero > colorzero > setuptools "colorzero": {"setuptools"} }, "system_bridge": { + # https://github.com/timmo001/system-bridge-connector/pull/78 # systembridgeconnector > incremental > setuptools "incremental": {"setuptools"} }, "travisci": { - # travisci > pytest-rerunfailures > pytest + # https://github.com/menegazzo/travispy seems to be unmaintained + # and unused https://www.home-assistant.io/integrations/travisci + # travispy > pytest-rerunfailures > pytest "pytest-rerunfailures": {"pytest"}, - # travisci > pytest + # travispy > pytest "travispy": {"pytest"}, }, "zha": { + # https://github.com/waveform80/colorzero/issues/9 # zha > zigpy-zigate > gpiozero > colorzero > setuptools "colorzero": {"setuptools"} }, From 7f4cc99a3ebc79ce364db60fdebdedf9ad2ba046 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 26 May 2025 10:47:22 +0200 Subject: [PATCH 0873/1175] Use sub-devices for Shelly multi-channel devices (#144100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Shelly RPC sub-devices * Better varaible name * Add get_rpc_device_info helper * Revert channel name changes * Use get_rpc_device_info * Add get_rpc_device_info helper * Use get_block_device_info * Use helpers in the button platform * Fix channel name and roller mode for block devices * Fix EM3 gen1 * Fix channel name for RPC devices * Revert test changes * Fix/improve test_block_get_block_channel_name * Fix test_get_rpc_channel_name_multiple_components * Fix tests * Fix tests * Fix tests * Use key instead of index to generate sub-device identifier * Improve logic for Pro RGBWW PM * Split channels for em1 * Better channel name * Cleaning * has_entity_name is True * Add get_block_sub_device_name() function * Improve block functions * Add get_rpc_sub_device_name() function * Remove _attr_name * Remove name for button with device class * Fix names of virtual components * Better Input name * Fix get_rpc_channel_name() * Fix names for Inputs * get_rpc_channel_name() improvement * Better variable name * Clean RPC functions * Fix input_name type * Fix test * Fix entity_ids for Blu Trv * Fix get_block_channel_name() * Fix for Blu Trv, once again * Revert name for reboot button * Fix button tests * Fix tests * Fix coordinator tests * Fix tests for cover platform * Fix tests for event platform * Fix entity_ids in init tests * Fix get_block_channel_name() for lights * Fix tests for light platform * Fix test for logbook * Update snapshots for number platform * Fix tests for sensor platform * Fix tests for switch platform * Fix tests for utils * Uncomment * Fix tests for flood * Fix Valve entity name * Fix climate tests * Fix test for diagnostics * Fix tests for init * Remove old snapshots * Add tests for 2PM Gen3 * Add comment * More tests * Cleaning * Clean fixtures * Update tests * Anonymize coordinates in fixtures * Split Pro 3EM entities into sub-devices * Make sub-device names more unique * 3EM (gen1) does not support sub-devices * Coverage * Rename "device temperature" sensor to the "relay temperature" * Update tests after rebase * Support sub-devices for 3EM (gen1) * Mark has-entity-name rule as done 🎉 * Rename `relay temperature` to `temperature` --- .../components/shelly/binary_sensor.py | 8 +- homeassistant/components/shelly/button.py | 36 +- homeassistant/components/shelly/climate.py | 34 +- homeassistant/components/shelly/const.py | 3 + homeassistant/components/shelly/entity.py | 30 +- homeassistant/components/shelly/event.py | 8 +- homeassistant/components/shelly/logbook.py | 2 +- homeassistant/components/shelly/number.py | 7 +- .../components/shelly/quality_scale.yaml | 2 +- homeassistant/components/shelly/select.py | 1 - homeassistant/components/shelly/sensor.py | 137 +++-- homeassistant/components/shelly/switch.py | 4 - homeassistant/components/shelly/text.py | 1 - homeassistant/components/shelly/utils.py | 194 +++++-- tests/components/shelly/__init__.py | 2 +- tests/components/shelly/conftest.py | 4 +- .../components/shelly/fixtures/2pm_gen3.json | 259 ++++++++++ .../shelly/fixtures/2pm_gen3_cover.json | 242 +++++++++ tests/components/shelly/fixtures/pro_3em.json | 216 ++++++++ .../shelly/snapshots/test_binary_sensor.ambr | 34 +- .../shelly/snapshots/test_button.ambr | 8 +- .../shelly/snapshots/test_climate.ambr | 32 +- .../shelly/snapshots/test_number.ambr | 12 +- .../shelly/snapshots/test_sensor.ambr | 42 +- tests/components/shelly/test_binary_sensor.py | 7 +- tests/components/shelly/test_climate.py | 16 +- tests/components/shelly/test_coordinator.py | 40 +- tests/components/shelly/test_cover.py | 13 +- tests/components/shelly/test_devices.py | 479 ++++++++++++++++++ tests/components/shelly/test_diagnostics.py | 2 +- tests/components/shelly/test_event.py | 8 +- tests/components/shelly/test_init.py | 9 +- tests/components/shelly/test_light.py | 25 +- tests/components/shelly/test_logbook.py | 2 +- tests/components/shelly/test_sensor.py | 52 +- tests/components/shelly/test_switch.py | 22 +- tests/components/shelly/test_utils.py | 73 ++- 37 files changed, 1744 insertions(+), 322 deletions(-) create mode 100644 tests/components/shelly/fixtures/2pm_gen3.json create mode 100644 tests/components/shelly/fixtures/2pm_gen3_cover.json create mode 100644 tests/components/shelly/fixtures/pro_3em.json create mode 100644 tests/components/shelly/test_devices.py diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index ed5a00fffb3..e7d7b46b322 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.const import STATE_ON, EntityCategory from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -36,6 +35,7 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_virtual_component_ids, is_block_momentary_input, @@ -87,8 +87,8 @@ class RpcBluTrvBinarySensor(RpcBinarySensor): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -190,7 +190,6 @@ RPC_SENSORS: Final = { "input": RpcBinarySensorDescription( key="input", sub_key="state", - name="Input", device_class=BinarySensorDeviceClass.POWER, entity_registry_enabled_default=False, removal_condition=is_rpc_momentary_input, @@ -264,7 +263,6 @@ RPC_SENSORS: Final = { "boolean": RpcBinarySensorDescription( key="boolean", sub_key="value", - has_entity_name=True, ), "calibration": RpcBinarySensorDescription( key="blutrv", diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 77b4021b03b..44f81cc8b36 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -19,18 +19,20 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import ( - CONNECTION_BLUETOOTH, - CONNECTION_NETWORK_MAC, - DeviceInfo, -) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator -from .utils import get_device_entry_gen, get_rpc_key_ids +from .utils import ( + get_block_device_info, + get_blu_trv_device_info, + get_device_entry_gen, + get_rpc_device_info, + get_rpc_key_ids, +) PARALLEL_UPDATES = 0 @@ -168,6 +170,7 @@ class ShellyBaseButton( ): """Defines a Shelly base button.""" + _attr_has_entity_name = True entity_description: ShellyButtonDescription[ ShellyRpcCoordinator | ShellyBlockCoordinator ] @@ -228,8 +231,15 @@ class ShellyButton(ShellyBaseButton): """Initialize Shelly button.""" super().__init__(coordinator, description) - self._attr_name = f"{coordinator.device.name} {description.name}" self._attr_unique_id = f"{coordinator.mac}_{description.key}" + if isinstance(coordinator, ShellyBlockCoordinator): + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac + ) + else: + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac + ) self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) @@ -256,15 +266,11 @@ class ShellyBluTrvButton(ShellyBaseButton): """Initialize.""" super().__init__(coordinator, description) - ble_addr: str = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["addr"] - device_name = ( - coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"]["name"] - or f"shellyblutrv-{ble_addr.replace(':', '')}" - ) - self._attr_name = f"{device_name} {description.name}" + config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"] + ble_addr: str = config["addr"] self._attr_unique_id = f"{ble_addr}_{description.key}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + config, ble_addr, coordinator.mac ) self._id = id_ diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index e1c55591da0..26fabe7e8b5 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -7,7 +7,7 @@ from dataclasses import asdict, dataclass from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.const import BLU_TRV_IDENTIFIER, BLU_TRV_MODEL_NAME, RPC_GENERATIONS +from aioshelly.const import BLU_TRV_IDENTIFIER, RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from homeassistant.components.climate import ( @@ -22,11 +22,6 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.helpers.device_registry import ( - CONNECTION_BLUETOOTH, - CONNECTION_NETWORK_MAC, - DeviceInfo, -) from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity @@ -46,6 +41,9 @@ from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoo from .entity import ShellyRpcEntity, rpc_call from .utils import ( async_remove_shelly_entity, + get_block_device_info, + get_block_entity_name, + get_blu_trv_device_info, get_device_entry_gen, get_rpc_key_ids, is_rpc_thermostat_internal_actuator, @@ -181,6 +179,7 @@ class BlockSleepingClimate( ) _attr_target_temperature_step = SHTRV_01_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -199,7 +198,6 @@ class BlockSleepingClimate( self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] - self._attr_name = coordinator.name if self.block is not None and self.device_block is not None: self._unique_id = f"{self.coordinator.mac}-{self.block.description}" @@ -212,8 +210,11 @@ class BlockSleepingClimate( ] elif entry is not None: self._unique_id = entry.unique_id - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}, + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac, sensor_block + ) + self._attr_name = get_block_entity_name( + self.coordinator.device, sensor_block, None ) self._channel = cast(int, self._unique_id.split("_")[1]) @@ -553,7 +554,6 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity): _attr_hvac_mode = HVACMode.HEAT _attr_target_temperature_step = BLU_TRV_TEMPERATURE_SETTINGS["step"] _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_has_entity_name = True def __init__(self, coordinator: ShellyRpcCoordinator, id_: int) -> None: """Initialize.""" @@ -563,19 +563,9 @@ class RpcBluTrvClimate(ShellyRpcEntity, ClimateEntity): self._config = coordinator.device.config[f"{BLU_TRV_IDENTIFIER}:{id_}"] ble_addr: str = self._config["addr"] self._attr_unique_id = f"{ble_addr}-{self.key}" - name = self._config["name"] or f"shellyblutrv-{ble_addr.replace(':', '')}" - model_id = self._config.get("local_name") - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)}, - identifiers={(DOMAIN, ble_addr)}, - via_device=(DOMAIN, self.coordinator.mac), - manufacturer="Shelly", - model=BLU_TRV_MODEL_NAME.get(model_id), - model_id=model_id, - name=name, + self._attr_device_info = get_blu_trv_device_info( + self._config, ble_addr, self.coordinator.mac ) - # Added intentionally to the constructor to avoid double name from base class - self._attr_name = None @property def target_temperature(self) -> float | None: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 87fc50a6666..7462766e2d4 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -258,6 +258,7 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( CONF_GEN = "gen" +VIRTUAL_COMPONENTS = ("boolean", "enum", "input", "number", "text") VIRTUAL_COMPONENTS_MAP = { "binary_sensor": {"types": ["boolean"], "modes": ["label"]}, "number": {"types": ["number"], "modes": ["field", "slider"]}, @@ -285,3 +286,5 @@ ROLE_TO_DEVICE_CLASS_MAP = { # We want to check only the first 5 KB of the script if it contains emitEvent() # so that the integration startup remains fast. MAX_SCRIPT_SIZE = 5120 + +All_LIGHT_TYPES = ("cct", "light", "rgb", "rgbw") diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 806f5fea700..1b0078890af 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -13,7 +13,6 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry @@ -24,7 +23,9 @@ from .const import CONF_SLEEP_PERIOD, DOMAIN, LOGGER from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator from .utils import ( async_remove_shelly_entity, + get_block_device_info, get_block_entity_name, + get_rpc_device_info, get_rpc_entity_name, get_rpc_key_instances, ) @@ -353,13 +354,15 @@ def rpc_call[_T: ShellyRpcEntity, **_P]( class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Helper class to represent a block entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyBlockCoordinator, block: Block) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) self.block = block self._attr_name = get_block_entity_name(coordinator.device, block) - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac, block ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" @@ -395,12 +398,14 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): """Helper class to represent a rpc entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: ShellyRpcCoordinator, key: str) -> None: """Initialize Shelly entity.""" super().__init__(coordinator) self.key = key - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) @@ -497,6 +502,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): """Class to load info from REST.""" + _attr_has_entity_name = True entity_description: RestEntityDescription def __init__( @@ -514,8 +520,8 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): coordinator.device, None, description.name ) self._attr_unique_id = f"{coordinator.mac}-{attribute}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac ) self._last_value = None @@ -623,8 +629,8 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): self.block: Block | None = block # type: ignore[assignment] self.entity_description = description - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_block_device_info( + coordinator.device, coordinator.mac, block ) if block is not None: @@ -632,7 +638,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity): f"{self.coordinator.mac}-{block.description}-{attribute}" ) self._attr_name = get_block_entity_name( - self.coordinator.device, block, self.entity_description.name + coordinator.device, block, description.name ) elif entry is not None: self._attr_unique_id = entry.unique_id @@ -691,8 +697,8 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity): self.attribute = attribute self.entity_description = description - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = self._attr_unique_id = ( f"{coordinator.mac}-{key}-{attribute}" diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index c858e7b591f..677ea1f6138 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -17,7 +17,6 @@ from homeassistant.components.event import ( EventEntityDescription, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -32,6 +31,7 @@ from .utils import ( async_remove_orphaned_entities, async_remove_shelly_entity, get_device_entry_gen, + get_rpc_device_info, get_rpc_entity_name, get_rpc_key_instances, is_block_momentary_input, @@ -77,7 +77,6 @@ SCRIPT_EVENT: Final = ShellyRpcEventDescription( translation_key="script", device_class=None, entity_registry_enabled_default=False, - has_entity_name=True, ) @@ -195,6 +194,7 @@ class ShellyBlockEvent(ShellyBlockEntity, EventEntity): class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): """Represent RPC event entity.""" + _attr_has_entity_name = True entity_description: ShellyRpcEventDescription def __init__( @@ -206,8 +206,8 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity): """Initialize Shelly entity.""" super().__init__(coordinator) self.event_id = int(key.split(":")[-1]) - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key ) self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index e18cd7ca465..e10b5cb57cf 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -43,7 +43,7 @@ def async_describe_events( rpc_coordinator = get_rpc_coordinator_by_device_id(hass, device_id) if rpc_coordinator and rpc_coordinator.device.initialized: key = f"input:{channel - 1}" - input_name = get_rpc_entity_name(rpc_coordinator.device, key) + input_name = f"{rpc_coordinator.device.name} {get_rpc_entity_name(rpc_coordinator.device, key)}" elif click_type in BLOCK_INPUTS_EVENTS_TYPES: block_coordinator = get_block_coordinator_by_device_id(hass, device_id) diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 49726f436d0..e406d63bdc2 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -21,7 +21,6 @@ from homeassistant.components.number import ( from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry @@ -38,6 +37,7 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_virtual_component_ids, ) @@ -124,8 +124,8 @@ class RpcBluTrvNumber(RpcNumber): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -183,7 +183,6 @@ RPC_NUMBERS: Final = { "number": RpcNumberDescription( key="number", sub_key="value", - has_entity_name=True, max_fn=lambda config: config["max"], min_fn=lambda config: config["min"], mode_fn=lambda config: VIRTUAL_NUMBER_MODE_MAP.get( diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 39a032a57f6..753b2ee4a93 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -17,7 +17,7 @@ rules: docs-removal-instructions: done entity-event-setup: done entity-unique-id: done - has-entity-name: todo + has-entity-name: done runtime-data: done test-before-configure: done test-before-setup: done diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index aec368f356b..0e367a9df37 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -40,7 +40,6 @@ RPC_SELECT_ENTITIES: Final = { "enum": RpcSelectDescription( key="enum", sub_key="value", - has_entity_name=True, ), } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 78eff171daf..0ea246c7734 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -34,7 +34,6 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType @@ -56,8 +55,10 @@ from .entity import ( ) from .utils import ( async_remove_orphaned_entities, + get_blu_trv_device_info, get_device_entry_gen, get_device_uptime, + get_rpc_device_info, get_shelly_air_lamp_life, get_virtual_component_ids, is_rpc_wifi_stations_disabled, @@ -76,6 +77,7 @@ class RpcSensorDescription(RpcEntityDescription, SensorEntityDescription): """Class to describe a RPC sensor.""" device_class_fn: Callable[[dict], SensorDeviceClass | None] | None = None + emeter_phase: str | None = None @dataclass(frozen=True, kw_only=True) @@ -121,6 +123,26 @@ class RpcSensor(ShellyRpcAttributeEntity, SensorEntity): return self.option_map[attribute_value] +class RpcEmeterPhaseSensor(RpcSensor): + """Represent a RPC energy meter phase sensor.""" + + entity_description: RpcSensorDescription + + def __init__( + self, + coordinator: ShellyRpcCoordinator, + key: str, + attribute: str, + description: RpcSensorDescription, + ) -> None: + """Initialize select.""" + super().__init__(coordinator, key, attribute, description) + + self._attr_device_info = get_rpc_device_info( + coordinator.device, coordinator.mac, key, description.emeter_phase + ) + + class RpcBluTrvSensor(RpcSensor): """Represent a RPC BluTrv sensor.""" @@ -135,8 +157,8 @@ class RpcBluTrvSensor(RpcSensor): super().__init__(coordinator, key, attribute, description) ble_addr: str = coordinator.device.config[key]["addr"] - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_BLUETOOTH, ble_addr)} + self._attr_device_info = get_blu_trv_device_info( + coordinator.device.config[key], ble_addr, coordinator.mac ) @@ -507,26 +529,32 @@ RPC_SENSORS: Final = { "a_act_power": RpcSensorDescription( key="em", sub_key="a_act_power", - name="Phase A active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_act_power": RpcSensorDescription( key="em", sub_key="b_act_power", - name="Phase B active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_act_power": RpcSensorDescription( key="em", sub_key="c_act_power", - name="Phase C active power", + name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "total_act_power": RpcSensorDescription( key="em", @@ -539,26 +567,32 @@ RPC_SENSORS: Final = { "a_aprt_power": RpcSensorDescription( key="em", sub_key="a_aprt_power", - name="Phase A apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_aprt_power": RpcSensorDescription( key="em", sub_key="b_aprt_power", - name="Phase B apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_aprt_power": RpcSensorDescription( key="em", sub_key="c_aprt_power", - name="Phase C apparent power", + name="Apparent power", native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, device_class=SensorDeviceClass.APPARENT_POWER, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "aprt_power_em1": RpcSensorDescription( key="em1", @@ -586,23 +620,29 @@ RPC_SENSORS: Final = { "a_pf": RpcSensorDescription( key="em", sub_key="a_pf", - name="Phase A power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_pf": RpcSensorDescription( key="em", sub_key="b_pf", - name="Phase B power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_pf": RpcSensorDescription( key="em", sub_key="c_pf", - name="Phase C power factor", + name="Power factor", device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "voltage": RpcSensorDescription( key="switch", @@ -684,29 +724,35 @@ RPC_SENSORS: Final = { "a_voltage": RpcSensorDescription( key="em", sub_key="a_voltage", - name="Phase A voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_voltage": RpcSensorDescription( key="em", sub_key="b_voltage", - name="Phase B voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_voltage": RpcSensorDescription( key="em", sub_key="c_voltage", - name="Phase C voltage", + name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "current": RpcSensorDescription( key="switch", @@ -781,29 +827,35 @@ RPC_SENSORS: Final = { "a_current": RpcSensorDescription( key="em", sub_key="a_current", - name="Phase A current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_current": RpcSensorDescription( key="em", sub_key="b_current", - name="Phase B current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_current": RpcSensorDescription( key="em", sub_key="c_current", - name="Phase C current", + name="Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "n_current": RpcSensorDescription( key="em", @@ -944,7 +996,7 @@ RPC_SENSORS: Final = { "a_total_act_energy": RpcSensorDescription( key="emdata", sub_key="a_total_act_energy", - name="Phase A total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -952,11 +1004,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_total_act_energy": RpcSensorDescription( key="emdata", sub_key="b_total_act_energy", - name="Phase B total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -964,11 +1018,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_total_act_energy": RpcSensorDescription( key="emdata", sub_key="c_total_act_energy", - name="Phase C total active energy", + name="Total active energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -976,6 +1032,8 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "total_act_ret": RpcSensorDescription( key="emdata", @@ -1003,7 +1061,7 @@ RPC_SENSORS: Final = { "a_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="a_total_act_ret_energy", - name="Phase A total active returned energy", + name="Total active returned energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1011,11 +1069,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="b_total_act_ret_energy", - name="Phase B total active returned energy", + name="Total active returned energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1023,11 +1083,13 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="c_total_act_ret_energy", - name="Phase C total active returned energy", + name="Total active returned energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1035,6 +1097,8 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "freq": RpcSensorDescription( key="switch", @@ -1069,32 +1133,38 @@ RPC_SENSORS: Final = { "a_freq": RpcSensorDescription( key="em", sub_key="a_freq", - name="Phase A frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="A", + entity_class=RpcEmeterPhaseSensor, ), "b_freq": RpcSensorDescription( key="em", sub_key="b_freq", - name="Phase B frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="B", + entity_class=RpcEmeterPhaseSensor, ), "c_freq": RpcSensorDescription( key="em", sub_key="c_freq", - name="Phase C frequency", + name="Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, suggested_display_precision=0, device_class=SensorDeviceClass.FREQUENCY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + emeter_phase="C", + entity_class=RpcEmeterPhaseSensor, ), "illuminance": RpcSensorDescription( key="illuminance", @@ -1107,7 +1177,7 @@ RPC_SENSORS: Final = { "temperature": RpcSensorDescription( key="switch", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1120,7 +1190,7 @@ RPC_SENSORS: Final = { "temperature_light": RpcSensorDescription( key="light", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1133,7 +1203,7 @@ RPC_SENSORS: Final = { "temperature_cct": RpcSensorDescription( key="cct", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1146,7 +1216,7 @@ RPC_SENSORS: Final = { "temperature_rgb": RpcSensorDescription( key="rgb", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1159,7 +1229,7 @@ RPC_SENSORS: Final = { "temperature_rgbw": RpcSensorDescription( key="rgbw", sub_key="temperature", - name="Device temperature", + name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: status["tC"], suggested_display_precision=1, @@ -1308,12 +1378,10 @@ RPC_SENSORS: Final = { "text": RpcSensorDescription( key="text", sub_key="value", - has_entity_name=True, ), "number": RpcSensorDescription( key="number", sub_key="value", - has_entity_name=True, unit=lambda config: config["meta"]["ui"]["unit"] if config["meta"]["ui"]["unit"] else None, @@ -1324,7 +1392,6 @@ RPC_SENSORS: Final = { "enum": RpcSensorDescription( key="enum", sub_key="value", - has_entity_name=True, options_fn=lambda config: config["options"], device_class=SensorDeviceClass.ENUM, ), diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 507f701795e..1c184d260f8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -291,7 +291,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity): """Entity that controls a switch on RPC based Shelly devices.""" entity_description: RpcSwitchDescription - _attr_has_entity_name = True @property def is_on(self) -> bool: @@ -316,9 +315,6 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity): class RpcRelaySwitch(RpcSwitch): """Entity that controls a switch on RPC based Shelly devices.""" - # False to avoid double naming as True is inerithed from base class - _attr_has_entity_name = False - def __init__( self, coordinator: ShellyRpcCoordinator, diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index a780c464947..d89531e2338 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -40,7 +40,6 @@ RPC_TEXT_ENTITIES: Final = { "text": RpcTextDescription( key="text", sub_key="value", - has_entity_name=True, ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 0c8048d34e4..eff5c95125c 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -11,6 +11,8 @@ from aiohttp.web import Request, WebSocketResponse from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import ( BLOCK_GENERATIONS, + BLU_TRV_IDENTIFIER, + BLU_TRV_MODEL_NAME, DEFAULT_COAP_PORT, DEFAULT_HTTP_PORT, MODEL_1L, @@ -40,7 +42,11 @@ from homeassistant.helpers import ( issue_registry as ir, singleton, ) -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + CONNECTION_NETWORK_MAC, + DeviceInfo, +) from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.util.dt import utcnow @@ -65,7 +71,9 @@ from .const import ( SHELLY_EMIT_EVENT_PATTERN, SHIX3_1_INPUTS_EVENTS_TYPES, UPTIME_DEVIATION, + VIRTUAL_COMPONENTS, VIRTUAL_COMPONENTS_MAP, + All_LIGHT_TYPES, ) @@ -109,26 +117,24 @@ def get_block_entity_name( device: BlockDevice, block: Block | None, description: str | None = None, -) -> str: +) -> str | None: """Naming for block based switch and sensors.""" channel_name = get_block_channel_name(device, block) if description: - return f"{channel_name} {description.lower()}" + return f"{channel_name} {description.lower()}" if channel_name else description return channel_name -def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: +def get_block_channel_name(device: BlockDevice, block: Block | None) -> str | None: """Get name based on device and channel name.""" - entity_name = device.name - if ( not block - or block.type == "device" + or block.type in ("device", "light", "relay", "emeter") or get_number_of_channels(device, block) == 1 ): - return entity_name + return None assert block.channel @@ -140,12 +146,28 @@ def get_block_channel_name(device: BlockDevice, block: Block | None) -> str: if channel_name: return channel_name + base = ord("1") + + return f"Channel {chr(int(block.channel) + base)}" + + +def get_block_sub_device_name(device: BlockDevice, block: Block) -> str: + """Get name of block sub-device.""" + if TYPE_CHECKING: + assert block.channel + + mode = cast(str, block.type) + "s" + if mode in device.settings: + if channel_name := device.settings[mode][int(block.channel)].get("name"): + return cast(str, channel_name) + if device.settings["device"]["type"] == MODEL_EM3: base = ord("A") - else: - base = ord("1") + return f"{device.name} Phase {chr(int(block.channel) + base)}" - return f"{entity_name} channel {chr(int(block.channel) + base)}" + base = ord("1") + + return f"{device.name} Channel {chr(int(block.channel) + base)}" def is_block_momentary_input( @@ -364,39 +386,64 @@ def get_shelly_model_name( return cast(str, MODEL_NAMES.get(model)) -def get_rpc_channel_name(device: RpcDevice, key: str) -> str: +def get_rpc_channel_name(device: RpcDevice, key: str) -> str | None: """Get name based on device and channel name.""" + if BLU_TRV_IDENTIFIER in key: + return None + + instances = len( + get_rpc_key_instances(device.status, key.split(":")[0], all_lights=True) + ) + component = key.split(":")[0] + component_id = key.split(":")[-1] + + if key in device.config and key != "em:0": + # workaround for Pro 3EM, we don't want to get name for em:0 + if component_name := device.config[key].get("name"): + if component in (*VIRTUAL_COMPONENTS, "script"): + return cast(str, component_name) + + return cast(str, component_name) if instances == 1 else None + + if component in VIRTUAL_COMPONENTS: + return f"{component.title()} {component_id}" + + return None + + +def get_rpc_sub_device_name( + device: RpcDevice, key: str, emeter_phase: str | None = None +) -> str: + """Get name based on device and channel name.""" + if key in device.config and key != "em:0": + # workaround for Pro 3EM, we don't want to get name for em:0 + if entity_name := device.config[key].get("name"): + return cast(str, entity_name) + key = key.replace("emdata", "em") key = key.replace("em1data", "em1") - device_name = device.name - entity_name: str | None = None - if key in device.config: - entity_name = device.config[key].get("name") - if entity_name is None: - channel = key.split(":")[0] - channel_id = key.split(":")[-1] - if key.startswith(("cover:", "input:", "light:", "switch:", "thermostat:")): - return f"{device_name} {channel.title()} {channel_id}" - if key.startswith(("cct", "rgb:", "rgbw:")): - return f"{device_name} {channel.upper()} light {channel_id}" - if key.startswith("em1"): - return f"{device_name} EM{channel_id}" - if key.startswith(("boolean:", "enum:", "number:", "text:")): - return f"{channel.title()} {channel_id}" - return device_name + component = key.split(":")[0] + component_id = key.split(":")[-1] - return entity_name + if component in ("cct", "rgb", "rgbw"): + return f"{device.name} {component.upper()} light {component_id}" + if component == "em1": + return f"{device.name} Energy Meter {component_id}" + if component == "em" and emeter_phase is not None: + return f"{device.name} Phase {emeter_phase}" + + return f"{device.name} {component.title()} {component_id}" def get_rpc_entity_name( device: RpcDevice, key: str, description: str | None = None -) -> str: +) -> str | None: """Naming for RPC based switch and sensors.""" channel_name = get_rpc_channel_name(device, key) if description: - return f"{channel_name} {description.lower()}" + return f"{channel_name} {description.lower()}" if channel_name else description return channel_name @@ -406,7 +453,9 @@ def get_device_entry_gen(entry: ConfigEntry) -> int: return entry.data.get(CONF_GEN, 1) -def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: +def get_rpc_key_instances( + keys_dict: dict[str, Any], key: str, all_lights: bool = False +) -> list[str]: """Return list of key instances for RPC device from a dict.""" if key in keys_dict: return [key] @@ -414,6 +463,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key == "switch" and "cover:0" in keys_dict: key = "cover" + if key in All_LIGHT_TYPES and all_lights: + return [k for k in keys_dict if k.startswith(All_LIGHT_TYPES)] + return [k for k in keys_dict if k.startswith(f"{key}:")] @@ -691,3 +743,81 @@ async def get_rpc_scripts_event_types( script_events[script_id] = await get_rpc_script_event_types(device, script_id) return script_events + + +def get_rpc_device_info( + device: RpcDevice, + mac: str, + key: str | None = None, + emeter_phase: str | None = None, +) -> DeviceInfo: + """Return device info for RPC device.""" + if key is None: + return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)}) + + # workaround for Pro EM50 + key = key.replace("em1data", "em1") + # workaround for Pro 3EM + key = key.replace("emdata", "em") + + key_parts = key.split(":") + component = key_parts[0] + idx = key_parts[1] if len(key_parts) > 1 else None + + if emeter_phase is not None: + return DeviceInfo( + identifiers={(DOMAIN, f"{mac}-{key}-{emeter_phase.lower()}")}, + name=get_rpc_sub_device_name(device, key, emeter_phase), + manufacturer="Shelly", + via_device=(DOMAIN, mac), + ) + + if ( + component not in (*All_LIGHT_TYPES, "cover", "em1", "switch") + or idx is None + or len(get_rpc_key_instances(device.status, component, all_lights=True)) < 2 + ): + return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)}) + + return DeviceInfo( + identifiers={(DOMAIN, f"{mac}-{key}")}, + name=get_rpc_sub_device_name(device, key), + manufacturer="Shelly", + via_device=(DOMAIN, mac), + ) + + +def get_blu_trv_device_info( + config: dict[str, Any], ble_addr: str, parent_mac: str +) -> DeviceInfo: + """Return device info for RPC device.""" + model_id = config.get("local_name") + return DeviceInfo( + connections={(CONNECTION_BLUETOOTH, ble_addr)}, + identifiers={(DOMAIN, ble_addr)}, + via_device=(DOMAIN, parent_mac), + manufacturer="Shelly", + model=BLU_TRV_MODEL_NAME.get(model_id) if model_id else None, + model_id=config.get("local_name"), + name=config["name"] or f"shellyblutrv-{ble_addr.replace(':', '')}", + ) + + +def get_block_device_info( + device: BlockDevice, mac: str, block: Block | None = None +) -> DeviceInfo: + """Return device info for Block device.""" + if ( + block is None + or block.type not in ("light", "relay", "emeter") + or device.settings.get("mode") == "roller" + or get_number_of_channels(device, block) < 2 + ): + return DeviceInfo(connections={(CONNECTION_NETWORK_MAC, mac)}) + + return DeviceInfo( + identifiers={(DOMAIN, f"{mac}-{block.description}")}, + name=get_block_sub_device_name(device, block), + manufacturer="Shelly", + via_device=(DOMAIN, mac), + ) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index ec2d3d2c829..6c835d2a636 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -53,7 +53,7 @@ async def init_integration( data[CONF_GEN] = gen entry = MockConfigEntry( - domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options + domain=DOMAIN, data=data, unique_id=MOCK_MAC, options=options, title="Test name" ) entry.add_to_hass(hass) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index dd17fe34cc8..ac70226a20a 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -189,7 +189,7 @@ MOCK_BLOCKS = [ ] MOCK_CONFIG = { - "input:0": {"id": 0, "name": "Test name input 0", "type": "button"}, + "input:0": {"id": 0, "name": "Test input 0", "type": "button"}, "input:1": { "id": 1, "type": "analog", @@ -204,7 +204,7 @@ MOCK_CONFIG = { "xcounts": {"expr": None, "unit": None}, "xfreq": {"expr": None, "unit": None}, }, - "flood:0": {"id": 0, "name": "Test name"}, + "flood:0": {"id": 0, "name": "Kitchen"}, "light:0": {"name": "test light_0"}, "light:1": {"name": "test light_1"}, "light:2": {"name": "test light_2"}, diff --git a/tests/components/shelly/fixtures/2pm_gen3.json b/tests/components/shelly/fixtures/2pm_gen3.json new file mode 100644 index 00000000000..bf3b4867585 --- /dev/null +++ b/tests/components/shelly/fixtures/2pm_gen3.json @@ -0,0 +1,259 @@ +{ + "config": { + "ble": { + "enable": true, + "rpc": { + "enable": true + } + }, + "bthome": {}, + "cloud": { + "enable": false, + "server": "iot.shelly.cloud:6012/jrpc" + }, + "input:0": { + "enable": true, + "factory_reset": true, + "id": 0, + "invert": false, + "name": null, + "type": "switch" + }, + "input:1": { + "enable": true, + "factory_reset": true, + "id": 1, + "invert": false, + "name": null, + "type": "switch" + }, + "knx": { + "enable": false, + "ia": "15.15.255", + "routing": { + "addr": "224.0.23.12:3671" + } + }, + "matter": { + "enable": false + }, + "mqtt": { + "client_id": "shelly2pmg3-aabbccddeeff", + "enable": true, + "enable_control": true, + "enable_rpc": true, + "rpc_ntf": true, + "server": "mqtt.test.server", + "ssl_ca": null, + "status_ntf": true, + "topic_prefix": "shelly2pmg3-aabbccddeeff", + "use_client_cert": false, + "user": "iot" + }, + "switch:0": { + "auto_off": false, + "auto_off_delay": 60.0, + "auto_on": false, + "auto_on_delay": 60.0, + "autorecover_voltage_errors": false, + "current_limit": 10.0, + "id": 0, + "in_locked": false, + "in_mode": "follow", + "initial_state": "match_input", + "name": null, + "power_limit": 2800, + "reverse": false, + "undervoltage_limit": 0, + "voltage_limit": 280 + }, + "switch:1": { + "auto_off": false, + "auto_off_delay": 60.0, + "auto_on": false, + "auto_on_delay": 60.0, + "autorecover_voltage_errors": false, + "current_limit": 10.0, + "id": 1, + "in_locked": false, + "in_mode": "follow", + "initial_state": "match_input", + "name": null, + "power_limit": 2800, + "reverse": false, + "undervoltage_limit": 0, + "voltage_limit": 280 + }, + "sys": { + "cfg_rev": 170, + "debug": { + "file_level": null, + "level": 2, + "mqtt": { + "enable": false + }, + "udp": { + "addr": null + }, + "websocket": { + "enable": true + } + }, + "device": { + "addon_type": null, + "discoverable": true, + "eco_mode": true, + "fw_id": "20250508-110823/1.6.1-g8dbd358", + "mac": "AABBCCDDEEFF", + "name": "Test Name", + "profile": "switch" + }, + "location": { + "lat": 15.2201, + "lon": 33.0121, + "tz": "Europe/Warsaw" + }, + "rpc_udp": { + "dst_addr": null, + "listen_port": null + }, + "sntp": { + "server": "sntp.test.server" + } + }, + "wifi": { + "sta": { + "ssid": "Wifi-Network-Name", + "is_open": false, + "enable": true, + "ipv4mode": "dhcp", + "ip": null, + "netmask": null, + "gw": null, + "nameserver": null + } + }, + "ws": { + "enable": false, + "server": null, + "ssl_ca": "ca.pem" + } + }, + "shelly": { + "app": "S2PMG3", + "auth_domain": null, + "auth_en": false, + "fw_id": "20250508-110823/1.6.1-g8dbd358", + "gen": 3, + "id": "shelly2pmg3-aabbccddeeff", + "mac": "AABBCCDDEEFF", + "matter": false, + "model": "S3SW-002P16EU", + "name": "Test Name", + "profile": "switch", + "slot": 0, + "ver": "1.6.1" + }, + "status": { + "ble": {}, + "bthome": {}, + "cloud": { + "connected": false + }, + "input:0": { + "id": 0, + "state": false + }, + "input:1": { + "id": 1, + "state": false + }, + "knx": {}, + "matter": { + "commissionable": false, + "num_fabrics": 0 + }, + "mqtt": { + "connected": true + }, + "switch:0": { + "aenergy": { + "by_minute": [0.0, 0.0, 0.0], + "minute_ts": 1747488720, + "total": 0.0 + }, + "apower": 0.0, + "current": 0.0, + "freq": 50.0, + "id": 0, + "output": false, + "pf": 0.0, + "ret_aenergy": { + "by_minute": [0.0, 0.0, 0.0], + "minute_ts": 1747488720, + "total": 0.0 + }, + "source": "init", + "temperature": { + "tC": 40.6, + "tF": 105.1 + }, + "voltage": 216.2 + }, + "switch:1": { + "aenergy": { + "by_minute": [0.0, 0.0, 0.0], + "minute_ts": 1747488720, + "total": 0.0 + }, + "apower": 0.0, + "current": 0.0, + "freq": 50.0, + "id": 1, + "output": false, + "pf": 0.0, + "ret_aenergy": { + "by_minute": [0.0, 0.0, 0.0], + "minute_ts": 1747488720, + "total": 0.0 + }, + "source": "init", + "temperature": { + "tC": 40.6, + "tF": 105.1 + }, + "voltage": 216.3 + }, + "sys": { + "available_updates": {}, + "btrelay_rev": 0, + "cfg_rev": 170, + "fs_free": 430080, + "fs_size": 917504, + "kvs_rev": 0, + "last_sync_ts": 1747488676, + "mac": "AABBCCDDEEFF", + "ram_free": 66440, + "ram_min_free": 49448, + "ram_size": 245788, + "reset_reason": 3, + "restart_required": false, + "schedule_rev": 22, + "time": "15:32", + "unixtime": 1747488776, + "uptime": 103, + "utc_offset": 7200, + "webhook_rev": 22 + }, + "wifi": { + "rssi": -52, + "ssid": "Wifi-Network-Name", + "sta_ip": "192.168.2.24", + "sta_ip6": [], + "status": "got ip" + }, + "ws": { + "connected": false + } + } +} diff --git a/tests/components/shelly/fixtures/2pm_gen3_cover.json b/tests/components/shelly/fixtures/2pm_gen3_cover.json new file mode 100644 index 00000000000..4aa2bad677e --- /dev/null +++ b/tests/components/shelly/fixtures/2pm_gen3_cover.json @@ -0,0 +1,242 @@ +{ + "config": { + "ble": { + "enable": true, + "rpc": { + "enable": true + } + }, + "bthome": {}, + "cloud": { + "enable": false, + "server": "iot.shelly.cloud:6012/jrpc" + }, + "cover:0": { + "current_limit": 10.0, + "id": 0, + "in_locked": false, + "in_mode": "dual", + "initial_state": "stopped", + "invert_directions": false, + "maintenance_mode": false, + "maxtime_close": 60.0, + "maxtime_open": 60.0, + "motor": { + "idle_confirm_period": 0.25, + "idle_power_thr": 2.0 + }, + "name": null, + "obstruction_detection": { + "action": "stop", + "direction": "both", + "enable": false, + "holdoff": 1.0, + "power_thr": 1000 + }, + "power_limit": 2800, + "safety_switch": { + "action": "stop", + "allowed_move": null, + "direction": "both", + "enable": false + }, + "slat": { + "close_time": 1.5, + "enable": false, + "open_time": 1.5, + "precise_ctl": false, + "retain_pos": false, + "step": 20 + }, + "swap_inputs": false, + "undervoltage_limit": 0, + "voltage_limit": 280 + }, + "input:0": { + "enable": true, + "factory_reset": true, + "id": 0, + "invert": false, + "name": null, + "type": "switch" + }, + "input:1": { + "enable": true, + "factory_reset": true, + "id": 1, + "invert": false, + "name": null, + "type": "switch" + }, + "knx": { + "enable": false, + "ia": "15.15.255", + "routing": { + "addr": "224.0.23.12:3671" + } + }, + "matter": { + "enable": false + }, + "mqtt": { + "client_id": "shelly2pmg3-aabbccddeeff", + "enable": true, + "enable_control": true, + "enable_rpc": true, + "rpc_ntf": true, + "server": "mqtt.test.server", + "ssl_ca": null, + "status_ntf": true, + "topic_prefix": "shellies-gen3/shelly-2pm-gen3-365730", + "use_client_cert": false, + "user": "iot" + }, + "sys": { + "cfg_rev": 171, + "debug": { + "file_level": null, + "level": 2, + "mqtt": { + "enable": false + }, + "udp": { + "addr": null + }, + "websocket": { + "enable": true + } + }, + "device": { + "addon_type": null, + "discoverable": true, + "eco_mode": true, + "fw_id": "20250508-110823/1.6.1-g8dbd358", + "mac": "AABBCCDDEEFF", + "name": "Test Name", + "profile": "cover" + }, + "location": { + "lat": 19.2201, + "lon": 34.0121, + "tz": "Europe/Warsaw" + }, + "rpc_udp": { + "dst_addr": null, + "listen_port": null + }, + "sntp": { + "server": "sntp.test.server" + }, + "ui_data": { + "consumption_types": ["", "light"] + } + }, + "wifi": { + "sta": { + "ssid": "Wifi-Network-Name", + "is_open": false, + "enable": true, + "ipv4mode": "dhcp", + "ip": null, + "netmask": null, + "gw": null, + "nameserver": null + } + }, + "ws": { + "enable": false, + "server": null, + "ssl_ca": "ca.pem" + } + }, + "shelly": { + "app": "S2PMG3", + "auth_domain": null, + "auth_en": false, + "fw_id": "20250508-110823/1.6.1-g8dbd358", + "gen": 3, + "id": "shelly2pmg3-aabbccddeeff", + "mac": "AABBCCDDEEFF", + "matter": false, + "model": "S3SW-002P16EU", + "name": "Test Name", + "profile": "cover", + "slot": 0, + "ver": "1.6.1" + }, + "status": { + "ble": {}, + "bthome": {}, + "cloud": { + "connected": false + }, + "cover:0": { + "aenergy": { + "by_minute": [0.0, 0.0, 0.0], + "minute_ts": 1747492440, + "total": 0.0 + }, + "apower": 0.0, + "current": 0.0, + "freq": 50.0, + "id": 0, + "last_direction": null, + "pf": 0.0, + "pos_control": false, + "source": "init", + "state": "stopped", + "temperature": { + "tC": 36.4, + "tF": 97.5 + }, + "voltage": 217.7 + }, + "input:0": { + "id": 0, + "state": false + }, + "input:1": { + "id": 1, + "state": false + }, + "knx": {}, + "matter": { + "commissionable": false, + "num_fabrics": 0 + }, + "mqtt": { + "connected": true + }, + "sys": { + "available_updates": {}, + "btrelay_rev": 0, + "cfg_rev": 171, + "fs_free": 430080, + "fs_size": 917504, + "kvs_rev": 0, + "last_sync_ts": 1747492085, + "mac": "AABBCCDDEEFF", + "ram_free": 64632, + "ram_min_free": 51660, + "ram_size": 245568, + "reset_reason": 3, + "restart_required": false, + "schedule_rev": 23, + "time": "16:34", + "unixtime": 1747492463, + "uptime": 381, + "utc_offset": 7200, + "webhook_rev": 23 + }, + "wifi": { + "rssi": -53, + "ssid": "Wifi-Network-Name", + "sta_ip": "192.168.2.24", + "sta_ip6": [], + "status": "got ip" + }, + "ws": { + "connected": false + } + } +} diff --git a/tests/components/shelly/fixtures/pro_3em.json b/tests/components/shelly/fixtures/pro_3em.json new file mode 100644 index 00000000000..93351e9bc65 --- /dev/null +++ b/tests/components/shelly/fixtures/pro_3em.json @@ -0,0 +1,216 @@ +{ + "config": { + "ble": { + "enable": false, + "rpc": { + "enable": true + } + }, + "bthome": {}, + "cloud": { + "enable": false, + "server": "iot.shelly.cloud:6012/jrpc" + }, + "em:0": { + "blink_mode_selector": "active_energy", + "ct_type": "120A", + "id": 0, + "monitor_phase_sequence": false, + "name": null, + "phase_selector": "all", + "reverse": {} + }, + "emdata:0": {}, + "eth": { + "enable": false, + "gw": null, + "ip": null, + "ipv4mode": "dhcp", + "nameserver": null, + "netmask": null, + "server_mode": false + }, + "modbus": { + "enable": true + }, + "mqtt": { + "client_id": "shellypro3em-aabbccddeeff", + "enable": false, + "enable_control": true, + "enable_rpc": true, + "rpc_ntf": true, + "server": "mqtt.test.server", + "ssl_ca": null, + "status_ntf": true, + "topic_prefix": "shellypro3em-aabbccddeeff", + "use_client_cert": false, + "user": "iot" + }, + "sys": { + "cfg_rev": 50, + "debug": { + "file_level": null, + "level": 2, + "mqtt": { + "enable": false + }, + "udp": { + "addr": null + }, + "websocket": { + "enable": false + } + }, + "device": { + "addon_type": null, + "discoverable": true, + "eco_mode": false, + "fw_id": "20250508-110717/1.6.1-g8dbd358", + "mac": "AABBCCDDEEFF", + "name": "Test Name", + "profile": "triphase", + "sys_btn_toggle": true + }, + "location": { + "lat": 22.55775, + "lon": 54.94637, + "tz": "Europe/Warsaw" + }, + "rpc_udp": { + "dst_addr": null, + "listen_port": null + }, + "sntp": { + "server": "sntp.test.server" + }, + "ui_data": {} + }, + "temperature:0": { + "id": 0, + "name": null, + "offset_C": 0.0, + "report_thr_C": 5.0 + }, + "wifi": { + "sta": { + "ssid": "Wifi-Network-Name", + "is_open": false, + "enable": true, + "ipv4mode": "dhcp", + "ip": null, + "netmask": null, + "gw": null, + "nameserver": null + } + }, + "ws": { + "enable": false, + "server": null, + "ssl_ca": "ca.pem" + } + }, + "shelly": { + "app": "Pro3EM", + "auth_domain": "shellypro3em-aabbccddeeff", + "auth_en": true, + "fw_id": "20250508-110717/1.6.1-g8dbd358", + "gen": 2, + "id": "shellypro3em-aabbccddeeff", + "mac": "AABBCCDDEEFF", + "model": "SPEM-003CEBEU", + "name": "Test Name", + "profile": "triphase", + "slot": 0, + "ver": "1.6.1" + }, + "status": { + "ble": {}, + "bthome": { + "errors": ["bluetooth_disabled"] + }, + "cloud": { + "connected": false + }, + "em:0": { + "a_act_power": 2166.2, + "a_aprt_power": 2175.9, + "a_current": 9.592, + "a_freq": 49.9, + "a_pf": 0.99, + "a_voltage": 227.0, + "b_act_power": 3.6, + "b_aprt_power": 10.1, + "b_current": 0.044, + "b_freq": 49.9, + "b_pf": 0.36, + "b_voltage": 230.0, + "c_act_power": 244.0, + "c_aprt_power": 339.7, + "c_current": 1.479, + "c_freq": 49.9, + "c_pf": 0.72, + "c_voltage": 230.2, + "id": 0, + "n_current": null, + "total_act_power": 2413.825, + "total_aprt_power": 2525.779, + "total_current": 11.116, + "user_calibrated_phase": [] + }, + "emdata:0": { + "a_total_act_energy": 3105576.42, + "a_total_act_ret_energy": 0.0, + "b_total_act_energy": 195765.72, + "b_total_act_ret_energy": 0.0, + "c_total_act_energy": 2114072.05, + "c_total_act_ret_energy": 0.0, + "id": 0, + "total_act": 5415414.19, + "total_act_ret": 0.0 + }, + "eth": { + "ip": null, + "ip6": null + }, + "modbus": {}, + "mqtt": { + "connected": false + }, + "sys": { + "available_updates": {}, + "btrelay_rev": 0, + "cfg_rev": 50, + "fs_free": 180224, + "fs_size": 524288, + "kvs_rev": 1, + "last_sync_ts": 1747561099, + "mac": "AABBCCDDEEFF", + "ram_free": 113080, + "ram_min_free": 97524, + "ram_size": 247524, + "reset_reason": 3, + "restart_required": false, + "schedule_rev": 0, + "time": "11:38", + "unixtime": 1747561101, + "uptime": 501683, + "utc_offset": 7200, + "webhook_rev": 0 + }, + "temperature:0": { + "id": 0, + "tC": 46.3, + "tF": 115.4 + }, + "wifi": { + "rssi": -57, + "ssid": "Wifi-Network-Name", + "sta_ip": "192.168.2.151", + "sta_ip6": [], + "status": "got ip" + }, + "ws": { + "connected": false + } + } +} diff --git a/tests/components/shelly/snapshots/test_binary_sensor.ambr b/tests/components/shelly/snapshots/test_binary_sensor.ambr index fcc6377837e..df8ed9cff4f 100644 --- a/tests/components/shelly/snapshots/test_binary_sensor.ambr +++ b/tests/components/shelly/snapshots/test_binary_sensor.ambr @@ -13,7 +13,7 @@ 'domain': 'binary_sensor', 'entity_category': , 'entity_id': 'binary_sensor.trv_name_calibration', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -24,7 +24,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name calibration', + 'original_name': 'Calibration', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -37,7 +37,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'problem', - 'friendly_name': 'TRV-Name calibration', + 'friendly_name': 'TRV-Name Calibration', }), 'context': , 'entity_id': 'binary_sensor.trv_name_calibration', @@ -47,7 +47,7 @@ 'state': 'off', }) # --- -# name: test_rpc_flood_entities[binary_sensor.test_name_flood-entry] +# name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_flood-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -60,8 +60,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.test_name_flood', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.test_name_kitchen_flood', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -72,7 +72,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Test name flood', + 'original_name': 'Kitchen flood', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -81,21 +81,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_rpc_flood_entities[binary_sensor.test_name_flood-state] +# name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_flood-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'moisture', - 'friendly_name': 'Test name flood', + 'friendly_name': 'Test name Kitchen flood', }), 'context': , - 'entity_id': 'binary_sensor.test_name_flood', + 'entity_id': 'binary_sensor.test_name_kitchen_flood', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_rpc_flood_entities[binary_sensor.test_name_mute-entry] +# name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_mute-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -108,8 +108,8 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.test_name_mute', - 'has_entity_name': False, + 'entity_id': 'binary_sensor.test_name_kitchen_mute', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -120,7 +120,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name mute', + 'original_name': 'Kitchen mute', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -129,13 +129,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_rpc_flood_entities[binary_sensor.test_name_mute-state] +# name: test_rpc_flood_entities[binary_sensor.test_name_kitchen_mute-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Test name mute', + 'friendly_name': 'Test name Kitchen mute', }), 'context': , - 'entity_id': 'binary_sensor.test_name_mute', + 'entity_id': 'binary_sensor.test_name_kitchen_mute', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/snapshots/test_button.ambr b/tests/components/shelly/snapshots/test_button.ambr index f5a38f1b847..33410ec2bbf 100644 --- a/tests/components/shelly/snapshots/test_button.ambr +++ b/tests/components/shelly/snapshots/test_button.ambr @@ -13,7 +13,7 @@ 'domain': 'button', 'entity_category': , 'entity_id': 'button.trv_name_calibrate', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -24,7 +24,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name Calibrate', + 'original_name': 'Calibrate', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -60,7 +60,7 @@ 'domain': 'button', 'entity_category': , 'entity_id': 'button.test_name_reboot', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -71,7 +71,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Test name Reboot', + 'original_name': 'Reboot', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, diff --git a/tests/components/shelly/snapshots/test_climate.ambr b/tests/components/shelly/snapshots/test_climate.ambr index 991c570172e..a434e1d8a9b 100644 --- a/tests/components/shelly/snapshots/test_climate.ambr +++ b/tests/components/shelly/snapshots/test_climate.ambr @@ -90,7 +90,7 @@ 'domain': 'climate', 'entity_category': None, 'entity_id': 'climate.test_name', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -101,7 +101,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -140,7 +140,7 @@ 'state': 'off', }) # --- -# name: test_rpc_climate_hvac_mode[climate.test_name_thermostat_0-entry] +# name: test_rpc_climate_hvac_mode[climate.test_name-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -161,8 +161,8 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.test_name_thermostat_0', - 'has_entity_name': False, + 'entity_id': 'climate.test_name', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -173,7 +173,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name Thermostat 0', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -182,12 +182,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_rpc_climate_hvac_mode[climate.test_name_thermostat_0-state] +# name: test_rpc_climate_hvac_mode[climate.test_name-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 44.4, 'current_temperature': 12.3, - 'friendly_name': 'Test name Thermostat 0', + 'friendly_name': 'Test name', 'hvac_action': , 'hvac_modes': list([ , @@ -200,14 +200,14 @@ 'temperature': 23, }), 'context': , - 'entity_id': 'climate.test_name_thermostat_0', + 'entity_id': 'climate.test_name', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'heat', }) # --- -# name: test_wall_display_thermostat_mode[climate.test_name_thermostat_0-entry] +# name: test_wall_display_thermostat_mode[climate.test_name-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -228,8 +228,8 @@ 'disabled_by': None, 'domain': 'climate', 'entity_category': None, - 'entity_id': 'climate.test_name_thermostat_0', - 'has_entity_name': False, + 'entity_id': 'climate.test_name', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -240,7 +240,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Test name Thermostat 0', + 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': , @@ -249,12 +249,12 @@ 'unit_of_measurement': None, }) # --- -# name: test_wall_display_thermostat_mode[climate.test_name_thermostat_0-state] +# name: test_wall_display_thermostat_mode[climate.test_name-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'current_humidity': 44.4, 'current_temperature': 12.3, - 'friendly_name': 'Test name Thermostat 0', + 'friendly_name': 'Test name', 'hvac_action': , 'hvac_modes': list([ , @@ -267,7 +267,7 @@ 'temperature': 23, }), 'context': , - 'entity_id': 'climate.test_name_thermostat_0', + 'entity_id': 'climate.test_name', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr index 07fda999556..d715b342e79 100644 --- a/tests/components/shelly/snapshots/test_number.ambr +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -18,7 +18,7 @@ 'domain': 'number', 'entity_category': , 'entity_id': 'number.trv_name_external_temperature', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -29,7 +29,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name external temperature', + 'original_name': 'External temperature', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -41,7 +41,7 @@ # name: test_blu_trv_number_entity[number.trv_name_external_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name external temperature', + 'friendly_name': 'TRV-Name External temperature', 'max': 50, 'min': -50, 'mode': , @@ -75,7 +75,7 @@ 'domain': 'number', 'entity_category': None, 'entity_id': 'number.trv_name_valve_position', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -86,7 +86,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name valve position', + 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -98,7 +98,7 @@ # name: test_blu_trv_number_entity[number.trv_name_valve_position-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name valve position', + 'friendly_name': 'TRV-Name Valve position', 'max': 100, 'min': 0, 'mode': , diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index c5c1427e3dc..6fd0bd716b7 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -15,7 +15,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_battery', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -26,7 +26,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name battery', + 'original_name': 'Battery', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -39,7 +39,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'TRV-Name battery', + 'friendly_name': 'TRV-Name Battery', 'state_class': , 'unit_of_measurement': '%', }), @@ -67,7 +67,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_signal_strength', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -78,7 +78,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'TRV-Name signal strength', + 'original_name': 'Signal strength', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -91,7 +91,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'signal_strength', - 'friendly_name': 'TRV-Name signal strength', + 'friendly_name': 'TRV-Name Signal strength', 'state_class': , 'unit_of_measurement': 'dBm', }), @@ -119,7 +119,7 @@ 'domain': 'sensor', 'entity_category': , 'entity_id': 'sensor.trv_name_valve_position', - 'has_entity_name': False, + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -130,7 +130,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'TRV-Name valve position', + 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, 'supported_features': 0, @@ -142,7 +142,7 @@ # name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'TRV-Name valve position', + 'friendly_name': 'TRV-Name Valve position', 'state_class': , 'unit_of_measurement': '%', }), @@ -154,7 +154,7 @@ 'state': '0', }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-entry] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -169,8 +169,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_switch_0_energy', - 'has_entity_name': False, + 'entity_id': 'sensor.test_name_test_switch_0_energy', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -196,23 +196,23 @@ 'unit_of_measurement': , }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_energy-state] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'test switch_0 energy', + 'friendly_name': 'Test name test switch_0 energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_switch_0_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1234.56789', }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-entry] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -227,8 +227,8 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_switch_0_returned_energy', - 'has_entity_name': False, + 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', + 'has_entity_name': True, 'hidden_by': None, 'icon': None, 'id': , @@ -254,16 +254,16 @@ 'unit_of_measurement': , }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_switch_0_returned_energy-state] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'test switch_0 returned energy', + 'friendly_name': 'Test name test switch_0 returned energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index fc79853f29e..f67e0bbb564 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -36,7 +36,8 @@ async def test_block_binary_sensor( entity_registry: EntityRegistry, ) -> None: """Test block binary sensor.""" - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_channel_1_overpowering" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_overpowering" await init_integration(hass, 1) assert (state := hass.states.get(entity_id)) @@ -239,7 +240,7 @@ async def test_rpc_binary_sensor( entity_registry: EntityRegistry, ) -> None: """Test RPC binary sensor.""" - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_cover_0_overpowering" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_test_cover_0_overpowering" await init_integration(hass, 2) assert (state := hass.states.get(entity_id)) @@ -521,7 +522,7 @@ async def test_rpc_flood_entities( await init_integration(hass, 4) for entity in ("flood", "mute"): - entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_{entity}" + entity_id = f"{BINARY_SENSOR_DOMAIN}.test_name_kitchen_{entity}" state = hass.states.get(entity_id) assert state == snapshot(name=f"{entity_id}-state") diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index eddd9ab6fd0..c19bd916fed 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -613,7 +613,7 @@ async def test_rpc_climate_hvac_mode( snapshot: SnapshotAssertion, ) -> None: """Test climate hvac mode service.""" - entity_id = "climate.test_name_thermostat_0" + entity_id = "climate.test_name" await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) @@ -651,7 +651,7 @@ async def test_rpc_climate_without_humidity( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test climate entity without the humidity value.""" - entity_id = "climate.test_name_thermostat_0" + entity_id = "climate.test_name" new_status = deepcopy(mock_rpc_device.status) new_status.pop("humidity:0") monkeypatch.setattr(mock_rpc_device, "status", new_status) @@ -673,7 +673,7 @@ async def test_rpc_climate_set_temperature( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate set target temperature.""" - entity_id = "climate.test_name_thermostat_0" + entity_id = "climate.test_name" await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) @@ -700,7 +700,7 @@ async def test_rpc_climate_hvac_mode_cool( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test climate with hvac mode cooling.""" - entity_id = "climate.test_name_thermostat_0" + entity_id = "climate.test_name" new_config = deepcopy(mock_rpc_device.config) new_config["thermostat:0"]["type"] = "cooling" monkeypatch.setattr(mock_rpc_device, "config", new_config) @@ -720,8 +720,8 @@ async def test_wall_display_thermostat_mode( snapshot: SnapshotAssertion, ) -> None: """Test Wall Display in thermostat mode.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) @@ -745,8 +745,8 @@ async def test_wall_display_thermostat_mode_external_actuator( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test Wall Display in thermostat mode with an external actuator.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" new_status = deepcopy(mock_rpc_device.status) new_status["sys"]["relay_in_thermostat"] = False diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index cf7f82014a0..5b4372fe938 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -56,6 +56,8 @@ async def test_block_reload_on_cfg_change( ) -> None: """Test block reload on config change.""" await init_integration(hass, 1) + # num_outputs is 2, devicename and channel name is used + entity_id = "switch.test_name_channel_1" monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) mock_block_device.mock_update() @@ -71,7 +73,7 @@ async def test_block_reload_on_cfg_change( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Generate config change from switch to light monkeypatch.setitem( @@ -81,14 +83,14 @@ async def test_block_reload_on_cfg_change( mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") is None + assert hass.states.get(entity_id) is None async def test_block_no_reload_on_bulb_changes( @@ -98,6 +100,9 @@ async def test_block_no_reload_on_bulb_changes( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block no reload on bulb mode/effect change.""" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + # num_outputs is 1, device name is used + entity_id = "switch.test_name" await init_integration(hass, 1, model=MODEL_BULB) monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", 1) @@ -113,14 +118,14 @@ async def test_block_no_reload_on_bulb_changes( mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Test no reload on effect change monkeypatch.setattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "effect", 1) @@ -128,14 +133,14 @@ async def test_block_no_reload_on_bulb_changes( mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_name_channel_1") + assert hass.states.get(entity_id) async def test_block_polling_auth_error( @@ -242,9 +247,11 @@ async def test_block_polling_connection_error( "update", AsyncMock(side_effect=DeviceConnectionError), ) + # num_outputs is 2, device name and channel name is used + entity_id = "switch.test_name_channel_1" await init_integration(hass, 1) - assert (state := hass.states.get("switch.test_name_channel_1")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON # Move time to generate polling @@ -252,7 +259,7 @@ async def test_block_polling_connection_error( async_fire_time_changed(hass) await hass.async_block_till_done() - assert (state := hass.states.get("switch.test_name_channel_1")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE @@ -391,6 +398,7 @@ async def test_rpc_reload_on_cfg_change( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC reload on config change.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) @@ -421,14 +429,14 @@ async def test_rpc_reload_on_cfg_change( ) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") + assert hass.states.get(entity_id) # Wait for debouncer freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("switch.test_switch_0") is None + assert hass.states.get(entity_id) is None async def test_rpc_reload_with_invalid_auth( @@ -719,11 +727,12 @@ async def test_rpc_reconnect_error( exc: Exception, ) -> None: """Test RPC reconnect error.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON monkeypatch.setattr(mock_rpc_device, "connected", False) @@ -734,7 +743,7 @@ async def test_rpc_reconnect_error( async_fire_time_changed(hass) await hass.async_block_till_done() - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE @@ -746,6 +755,7 @@ async def test_rpc_error_running_connected_events( caplog: pytest.LogCaptureFixture, ) -> None: """Test RPC error while running connected events.""" + entity_id = "switch.test_name_test_switch_0" monkeypatch.delitem(mock_rpc_device.status, "cover:0") monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) with patch( @@ -758,7 +768,7 @@ async def test_rpc_error_running_connected_events( assert "Error running connected events for device" in caplog.text - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE # Move time to generate reconnect without error @@ -766,7 +776,7 @@ async def test_rpc_error_running_connected_events( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index df3ab4f288d..4f8e8a7650d 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -116,7 +116,7 @@ async def test_rpc_device_services( entity_registry: EntityRegistry, ) -> None: """Test RPC device cover services.""" - entity_id = "cover.test_cover_0" + entity_id = "cover.test_name_test_cover_0" await init_integration(hass, 2) await hass.services.async_call( @@ -178,23 +178,24 @@ async def test_rpc_device_no_cover_keys( monkeypatch.delitem(mock_rpc_device.status, "cover:0") await init_integration(hass, 2) - assert hass.states.get("cover.test_cover_0") is None + assert hass.states.get("cover.test_name_test_cover_0") is None async def test_rpc_device_update( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC device update.""" + entity_id = "cover.test_name_test_cover_0" mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed") await init_integration(hass, 2) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get(entity_id) assert state assert state.state == CoverState.CLOSED mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open") mock_rpc_device.mock_update() - state = hass.states.get("cover.test_cover_0") + state = hass.states.get(entity_id) assert state assert state.state == CoverState.OPEN @@ -208,7 +209,7 @@ async def test_rpc_device_no_position_control( ) await init_integration(hass, 2) - state = hass.states.get("cover.test_cover_0") + state = hass.states.get("cover.test_name_test_cover_0") assert state assert state.state == CoverState.OPEN @@ -220,7 +221,7 @@ async def test_rpc_cover_tilt( entity_registry: EntityRegistry, ) -> None: """Test RPC cover that supports tilt.""" - entity_id = "cover.test_cover_0" + entity_id = "cover.test_name_test_cover_0" config = deepcopy(mock_rpc_device.config) config["cover:0"]["slat"] = {"enable": True} diff --git a/tests/components/shelly/test_devices.py b/tests/components/shelly/test_devices.py new file mode 100644 index 00000000000..e894a393ac5 --- /dev/null +++ b/tests/components/shelly/test_devices.py @@ -0,0 +1,479 @@ +"""Test real devices.""" + +from unittest.mock import Mock + +from aioshelly.const import MODEL_2PM_G3, MODEL_PRO_EM3 +import pytest + +from homeassistant.components.shelly.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry + +from . import init_integration + +from tests.common import load_json_object_fixture + + +async def test_shelly_2pm_gen3_no_relay_names( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly 2PM Gen3 without relay names. + + This device has two relays/channels,we should get a main device and two sub + devices. + """ + device_fixture = load_json_object_fixture("2pm_gen3.json", DOMAIN) + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=3, model=MODEL_2PM_G3) + + # Relay 0 sub-device + entity_id = "switch.test_name_switch_0" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Switch 0" + + entity_id = "sensor.test_name_switch_0_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Switch 0" + + # Relay 1 sub-device + entity_id = "switch.test_name_switch_1" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Switch 1" + + entity_id = "sensor.test_name_switch_1_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Switch 1" + + # Main device + entity_id = "update.test_name_firmware" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + +async def test_shelly_2pm_gen3_relay_names( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly 2PM Gen3 with relay names. + + This device has two relays/channels,we should get a main device and two sub + devices. + """ + device_fixture = load_json_object_fixture("2pm_gen3.json", DOMAIN) + device_fixture["config"]["switch:0"]["name"] = "Kitchen light" + device_fixture["config"]["switch:1"]["name"] = "Living room light" + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=3, model=MODEL_2PM_G3) + + # Relay 0 sub-device + entity_id = "switch.kitchen_light" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Kitchen light" + + entity_id = "sensor.kitchen_light_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Kitchen light" + + # Relay 1 sub-device + entity_id = "switch.living_room_light" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Living room light" + + entity_id = "sensor.living_room_light_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Living room light" + + # Main device + entity_id = "update.test_name_firmware" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + +async def test_shelly_2pm_gen3_cover( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly 2PM Gen3 with cover profile. + + With the cover profile we should only get the main device and no subdevices. + """ + device_fixture = load_json_object_fixture("2pm_gen3_cover.json", DOMAIN) + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=3, model=MODEL_2PM_G3) + + entity_id = "cover.test_name" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + entity_id = "sensor.test_name_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + entity_id = "update.test_name_firmware" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + +async def test_shelly_2pm_gen3_cover_with_name( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly 2PM Gen3 with cover profile and the cover name. + + With the cover profile we should only get the main device and no subdevices. + """ + device_fixture = load_json_object_fixture("2pm_gen3_cover.json", DOMAIN) + device_fixture["config"]["cover:0"]["name"] = "Bedroom blinds" + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=3, model=MODEL_2PM_G3) + + entity_id = "cover.test_name_bedroom_blinds" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + entity_id = "sensor.test_name_bedroom_blinds_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + entity_id = "update.test_name_firmware" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + +async def test_shelly_pro_3em( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly Pro 3EM. + + We should get the main device and three subdevices, one subdevice per one phase. + """ + device_fixture = load_json_object_fixture("pro_3em.json", DOMAIN) + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=2, model=MODEL_PRO_EM3) + + # Main device + entity_id = "sensor.test_name_total_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + # Phase A sub-device + entity_id = "sensor.test_name_phase_a_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase A" + + # Phase B sub-device + entity_id = "sensor.test_name_phase_b_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase B" + + # Phase C sub-device + entity_id = "sensor.test_name_phase_c_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase C" + + +async def test_shelly_pro_3em_with_emeter_name( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly Pro 3EM when the name for Emeter is set. + + We should get the main device and three subdevices, one subdevice per one phase. + """ + device_fixture = load_json_object_fixture("pro_3em.json", DOMAIN) + device_fixture["config"]["em:0"]["name"] = "Emeter name" + monkeypatch.setattr(mock_rpc_device, "shelly", device_fixture["shelly"]) + monkeypatch.setattr(mock_rpc_device, "status", device_fixture["status"]) + monkeypatch.setattr(mock_rpc_device, "config", device_fixture["config"]) + + await init_integration(hass, gen=2, model=MODEL_PRO_EM3) + + # Main device + entity_id = "sensor.test_name_total_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" + + # Phase A sub-device + entity_id = "sensor.test_name_phase_a_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase A" + + # Phase B sub-device + entity_id = "sensor.test_name_phase_b_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase B" + + # Phase C sub-device + entity_id = "sensor.test_name_phase_c_active_power" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name Phase C" + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_block_channel_with_name( + hass: HomeAssistant, + mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, +) -> None: + """Test block channel with name.""" + monkeypatch.setitem( + mock_block_device.settings["relays"][0], "name", "Kitchen light" + ) + + await init_integration(hass, 1) + + # channel 1 sub-device; num_outputs is 2 so the name of the channel should be used + entity_id = "switch.kitchen_light" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Kitchen light" + + # main device + entity_id = "update.test_name_firmware" + + state = hass.states.get(entity_id) + assert state + + entry = entity_registry.async_get(entity_id) + assert entry + + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Test name" diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 84ebd50c425..300b67abe75 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -147,7 +147,7 @@ async def test_rpc_config_entry_diagnostics( ], "last_detection": ANY, "monotonic_time": ANY, - "name": "Mock Title (12:34:56:78:9A:BE)", + "name": "Test name (12:34:56:78:9A:BE)", "scanning": True, "start_time": ANY, "source": "12:34:56:78:9A:BE", diff --git a/tests/components/shelly/test_event.py b/tests/components/shelly/test_event.py index a3c96b6b247..520233eaf60 100644 --- a/tests/components/shelly/test_event.py +++ b/tests/components/shelly/test_event.py @@ -31,7 +31,7 @@ async def test_rpc_button( ) -> None: """Test RPC device event.""" await init_integration(hass, 2) - entity_id = "event.test_name_input_0" + entity_id = "event.test_name_test_input_0" assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNKNOWN @@ -176,6 +176,7 @@ async def test_block_event( ) -> None: """Test block device event.""" await init_integration(hass, 1) + # num_outputs is 2, device name and channel name is used entity_id = "event.test_name_channel_1" assert (state := hass.states.get(entity_id)) @@ -201,11 +202,12 @@ async def test_block_event( async def test_block_event_shix3_1( - hass: HomeAssistant, mock_block_device: Mock + hass: HomeAssistant, mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block device event for SHIX3-1.""" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) await init_integration(hass, 1, model=MODEL_I3) - entity_id = "event.test_name_channel_1" + entity_id = "event.test_name" assert (state := hass.states.get(entity_id)) assert state.attributes.get(ATTR_EVENT_TYPES) == unordered( diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 4cf49a2dab8..283de897d8d 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -346,7 +346,7 @@ async def test_sleeping_rpc_device_offline_during_setup( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload( @@ -378,7 +378,7 @@ async def test_entry_unload( ("gen", "entity_id"), [ (1, "switch.test_name_channel_1"), - (2, "switch.test_switch_0"), + (2, "switch.test_name_test_switch_0"), ], ) async def test_entry_unload_device_not_ready( @@ -417,7 +417,7 @@ async def test_entry_unload_not_connected( ) assert entry.state is ConfigEntryState.LOADED - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get("switch.test_name_test_switch_0")) assert state.state == STATE_ON assert not mock_stop_scanner.call_count @@ -448,7 +448,7 @@ async def test_entry_unload_not_connected_but_we_think_we_are( ) assert entry.state is ConfigEntryState.LOADED - assert (state := hass.states.get("switch.test_switch_0")) + assert (state := hass.states.get("switch.test_name_test_switch_0")) assert state.state == STATE_ON assert not mock_stop_scanner.call_count @@ -483,6 +483,7 @@ async def test_entry_missing_gen(hass: HomeAssistant, mock_block_device: Mock) - assert entry.state is ConfigEntryState.LOADED + # num_outputs is 2, channel name is used assert (state := hass.states.get("switch.test_name_channel_1")) assert state.state == STATE_ON diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index 0dab06f53a9..9c79cf5d988 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -58,10 +58,14 @@ SHELLY_PLUS_RGBW_CHANNELS = 4 async def test_block_device_rgbw_bulb( - hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry + hass: HomeAssistant, + mock_block_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device RGBW bulb.""" - entity_id = "light.test_name_channel_1" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + entity_id = "light.test_name" await init_integration(hass, 1, model=MODEL_BULB) # Test initial @@ -142,7 +146,8 @@ async def test_block_device_rgb_bulb( caplog: pytest.LogCaptureFixture, ) -> None: """Test block device RGB bulb.""" - entity_id = "light.test_name_channel_1" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + entity_id = "light.test_name" monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "mode") monkeypatch.setattr( mock_block_device.blocks[LIGHT_BLOCK_ID], "description", "light_1" @@ -246,7 +251,8 @@ async def test_block_device_white_bulb( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device white bulb.""" - entity_id = "light.test_name_channel_1" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + entity_id = "light.test_name" monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "red") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "green") monkeypatch.delattr(mock_block_device.blocks[LIGHT_BLOCK_ID], "blue") @@ -322,6 +328,7 @@ async def test_block_device_support_transition( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block device supports transition.""" + # num_outputs is 2, device name and channel name is used entity_id = "light.test_name_channel_1" monkeypatch.setitem( mock_block_device.settings, "fw", "20220809-122808/v1.12-g99f7e0b" @@ -448,7 +455,7 @@ async def test_rpc_device_switch_type_lights_mode( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device with switch in consumption type lights mode.""" - entity_id = "light.test_switch_0" + entity_id = "light.test_name_test_switch_0" monkeypatch.setitem( mock_rpc_device.config["sys"]["ui_data"], "consumption_types", ["lights"] ) @@ -595,7 +602,7 @@ async def test_rpc_device_rgb_profile( 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" + entity_id = "light.test_name_test_rgb_0" await init_integration(hass, 2) # Test initial @@ -639,7 +646,7 @@ async def test_rpc_device_rgbw_profile( 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" + entity_id = "light.test_name_test_rgbw_0" await init_integration(hass, 2) # Test initial @@ -753,7 +760,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others( # 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}" + entity_id = f"light.test_name_test_light_{i}" register_entity( hass, LIGHT_DOMAIN, @@ -781,7 +788,7 @@ async def test_rpc_rgbw_device_rgb_w_modes_remove_others( await hass.async_block_till_done() # verify we have RGB/w light - entity_id = f"light.test_{active_mode}_0" + entity_id = f"light.test_name_test_{active_mode}_0" assert (state := hass.states.get(entity_id)) assert state.state == STATE_ON diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 8962b26544b..08256e03f4e 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -108,7 +108,7 @@ async def test_humanify_shelly_click_event_rpc_device( assert event1["domain"] == DOMAIN assert ( event1["message"] - == "'single_push' click event for Test name input 0 Input was fired" + == "'single_push' click event for Test name Test input 0 Input was fired" ) assert event2["name"] == "Shelly" diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index a3d0a0f59c9..e95d4cfaeb2 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -62,6 +62,7 @@ async def test_block_sensor( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test block sensor.""" + # num_outputs is 2, channel name is used entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_power" await init_integration(hass, 1) @@ -82,6 +83,7 @@ async def test_energy_sensor( hass: HomeAssistant, mock_block_device: Mock, entity_registry: EntityRegistry ) -> None: """Test energy sensor.""" + # num_outputs is 2, channel name is used entity_id = f"{SENSOR_DOMAIN}.test_name_channel_1_energy" await init_integration(hass, 1) @@ -430,7 +432,9 @@ async def test_block_shelly_air_lamp_life( percentage: float, ) -> None: """Test block Shelly Air lamp life percentage sensor.""" - entity_id = f"{SENSOR_DOMAIN}.{'test_name_channel_1_lamp_life'}" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + # num_outputs is 1, device name is used + entity_id = f"{SENSOR_DOMAIN}.{'test_name_lamp_life'}" monkeypatch.setattr( mock_block_device.blocks[RELAY_BLOCK_ID], "totalWorkTime", lamp_life_seconds ) @@ -444,7 +448,7 @@ async def test_rpc_sensor( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test RPC sensor.""" - entity_id = f"{SENSOR_DOMAIN}.test_cover_0_power" + entity_id = f"{SENSOR_DOMAIN}.test_name_test_cover_0_power" await init_integration(hass, 2) assert (state := hass.states.get(entity_id)) @@ -673,37 +677,45 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_rpc_em1_sensors( +async def test_rpc_energy_meter_1_sensors( hass: HomeAssistant, entity_registry: EntityRegistry, mock_rpc_device: Mock ) -> None: """Test RPC sensors for EM1 component.""" await init_integration(hass, 2) - assert (state := hass.states.get("sensor.test_name_em0_power")) + assert (state := hass.states.get("sensor.test_name_energy_meter_0_power")) assert state.state == "85.3" - assert (entry := entity_registry.async_get("sensor.test_name_em0_power")) + assert (entry := entity_registry.async_get("sensor.test_name_energy_meter_0_power")) assert entry.unique_id == "123456789ABC-em1:0-power_em1" - assert (state := hass.states.get("sensor.test_name_em1_power")) + assert (state := hass.states.get("sensor.test_name_energy_meter_1_power")) assert state.state == "123.3" - assert (entry := entity_registry.async_get("sensor.test_name_em1_power")) + assert (entry := entity_registry.async_get("sensor.test_name_energy_meter_1_power")) assert entry.unique_id == "123456789ABC-em1:1-power_em1" - assert (state := hass.states.get("sensor.test_name_em0_total_active_energy")) + assert ( + state := hass.states.get("sensor.test_name_energy_meter_0_total_active_energy") + ) assert state.state == "123.4564" assert ( - entry := entity_registry.async_get("sensor.test_name_em0_total_active_energy") + entry := entity_registry.async_get( + "sensor.test_name_energy_meter_0_total_active_energy" + ) ) assert entry.unique_id == "123456789ABC-em1data:0-total_act_energy" - assert (state := hass.states.get("sensor.test_name_em1_total_active_energy")) + assert ( + state := hass.states.get("sensor.test_name_energy_meter_1_total_active_energy") + ) assert state.state == "987.6543" assert ( - entry := entity_registry.async_get("sensor.test_name_em1_total_active_energy") + entry := entity_registry.async_get( + "sensor.test_name_energy_meter_1_total_active_energy" + ) ) assert entry.unique_id == "123456789ABC-em1data:1-total_act_energy" @@ -901,7 +913,7 @@ async def test_rpc_pulse_counter_sensors( await init_integration(hass, 2) - entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter" assert (state := hass.states.get(entity_id)) assert state.state == "56174" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "pulse" @@ -910,7 +922,7 @@ async def test_rpc_pulse_counter_sensors( assert (entry := entity_registry.async_get(entity_id)) assert entry.unique_id == "123456789ABC-input:2-pulse_counter" - entity_id = f"{SENSOR_DOMAIN}.gas_counter_value" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_counter_value" assert (state := hass.states.get(entity_id)) assert state.state == "561.74" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit @@ -949,11 +961,11 @@ async def test_rpc_disabled_xtotal_counter( ) await init_integration(hass, 2) - entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter" assert (state := hass.states.get(entity_id)) assert state.state == "20635" - entity_id = f"{SENSOR_DOMAIN}.gas_counter_value" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_counter_value" assert hass.states.get(entity_id) is None @@ -980,7 +992,7 @@ async def test_rpc_pulse_counter_frequency_sensors( await init_integration(hass, 2) - entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter_frequency" assert (state := hass.states.get(entity_id)) assert state.state == "208.0" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ @@ -989,7 +1001,7 @@ async def test_rpc_pulse_counter_frequency_sensors( assert (entry := entity_registry.async_get(entity_id)) assert entry.unique_id == "123456789ABC-input:2-counter_frequency" - entity_id = f"{SENSOR_DOMAIN}.gas_pulse_counter_frequency_value" + entity_id = f"{SENSOR_DOMAIN}.test_name_gas_pulse_counter_frequency_value" assert (state := hass.states.get(entity_id)) assert state.state == "6.11" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit @@ -1411,7 +1423,7 @@ async def test_rpc_rgbw_sensors( assert (entry := entity_registry.async_get(entity_id)) assert entry.unique_id == f"123456789ABC-{light_type}:0-voltage_{light_type}" - entity_id = f"sensor.test_name_{light_type}_light_0_device_temperature" + entity_id = f"sensor.test_name_{light_type}_light_0_temperature" assert (state := hass.states.get(entity_id)) assert state.state == "54.3" @@ -1544,7 +1556,7 @@ async def test_rpc_switch_energy_sensors( await init_integration(hass, 3) for entity in ("energy", "returned_energy"): - entity_id = f"{SENSOR_DOMAIN}.test_switch_0_{entity}" + entity_id = f"{SENSOR_DOMAIN}.test_name_test_switch_0_{entity}" state = hass.states.get(entity_id) assert state == snapshot(name=f"{entity_id}-state") @@ -1572,4 +1584,4 @@ async def test_rpc_switch_no_returned_energy_sensor( monkeypatch.setattr(mock_rpc_device, "status", status) await init_integration(hass, 3) - assert hass.states.get("sensor.test_switch_0_returned_energy") is None + assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index 824742d1798..54923b538f6 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -42,6 +42,7 @@ async def test_block_device_services( ) -> None: """Test block device turn on/off services.""" await init_integration(hass, 1) + # num_outputs is 2, device_name and channel name is used entity_id = "switch.test_name_channel_1" await hass.services.async_call( @@ -192,7 +193,7 @@ async def test_block_restored_motion_switch_no_last_state( @pytest.mark.parametrize( ("model", "sleep", "entity", "unique_id"), [ - (MODEL_1PM, 0, "switch.test_name_channel_1", "123456789ABC-relay_0"), + (MODEL_1PM, 0, "switch.test_name", "123456789ABC-relay_0"), ( MODEL_MOTION, 1000, @@ -205,12 +206,15 @@ async def test_block_device_unique_ids( hass: HomeAssistant, entity_registry: EntityRegistry, mock_block_device: Mock, + monkeypatch: pytest.MonkeyPatch, model: str, sleep: int, entity: str, unique_id: str, ) -> None: """Test block device unique_ids.""" + monkeypatch.setitem(mock_block_device.shelly, "num_outputs", 1) + # num_outputs is 1, device name is used await init_integration(hass, 1, model=model, sleep_period=sleep) if sleep: @@ -332,7 +336,7 @@ async def test_rpc_device_services( monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) - entity_id = "switch.test_switch_0" + entity_id = "switch.test_name_test_switch_0" await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -365,7 +369,7 @@ async def test_rpc_device_unique_ids( monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False) await init_integration(hass, 2) - assert (entry := entity_registry.async_get("switch.test_switch_0")) + assert (entry := entity_registry.async_get("switch.test_name_test_switch_0")) assert entry.unique_id == "123456789ABC-switch:0" @@ -386,11 +390,11 @@ async def test_rpc_device_switch_type_lights_mode( [ ( DeviceConnectionError, - "Device communication error occurred while calling action for switch.test_switch_0 of Test name", + "Device communication error occurred while calling action for switch.test_name_test_switch_0 of Test name", ), ( RpcCallError(-1, "error"), - "RPC call error occurred while calling action for switch.test_switch_0 of Test name", + "RPC call error occurred while calling action for switch.test_name_test_switch_0 of Test name", ), ], ) @@ -411,7 +415,7 @@ async def test_rpc_set_state_errors( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -434,7 +438,7 @@ async def test_rpc_auth_error( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.test_switch_0"}, + {ATTR_ENTITY_ID: "switch.test_name_test_switch_0"}, blocking=True, ) @@ -476,8 +480,8 @@ async def test_wall_display_relay_mode( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test Wall Display in relay mode.""" - climate_entity_id = "climate.test_name_thermostat_0" - switch_entity_id = "switch.test_switch_0" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_name_test_switch_0" config_entry = await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) diff --git a/tests/components/shelly/test_utils.py b/tests/components/shelly/test_utils.py index ae3caa93825..0cdd1640e65 100644 --- a/tests/components/shelly/test_utils.py +++ b/tests/components/shelly/test_utils.py @@ -79,37 +79,38 @@ async def test_block_get_block_channel_name( mock_block_device: Mock, monkeypatch: pytest.MonkeyPatch ) -> None: """Test block get block channel name.""" - monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "Test name channel 1" + result = get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], ) + # when has_entity_name is True the result should be None + assert result is None + + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "type", "relay") + result = get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], + ) + # when has_entity_name is True the result should be None + assert result is None monkeypatch.setitem(mock_block_device.settings["device"], "type", MODEL_EM3) - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "Test name channel A" + result = get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], ) + # when has_entity_name is True the result should be None + assert result is None monkeypatch.setitem( mock_block_device.settings, "relays", [{"name": "test-channel"}] ) - - assert ( - get_block_channel_name( - mock_block_device, - mock_block_device.blocks[DEVICE_BLOCK_ID], - ) - == "test-channel" + result = get_block_channel_name( + mock_block_device, + mock_block_device.blocks[DEVICE_BLOCK_ID], ) + # when has_entity_name is True the result should be None + assert result is None async def test_is_block_momentary_input( @@ -241,20 +242,19 @@ async def test_get_block_input_triggers( async def test_get_rpc_channel_name(mock_rpc_device: Mock) -> None: """Test get RPC channel name.""" - assert get_rpc_channel_name(mock_rpc_device, "input:0") == "Test name input 0" - assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Test name Input 3" + assert get_rpc_channel_name(mock_rpc_device, "input:0") == "Test input 0" + assert get_rpc_channel_name(mock_rpc_device, "input:3") == "Input 3" @pytest.mark.parametrize( ("component", "expected"), [ - ("cover", "Cover"), - ("input", "Input"), - ("light", "Light"), - ("rgb", "RGB light"), - ("rgbw", "RGBW light"), - ("switch", "Switch"), - ("thermostat", "Thermostat"), + ("cover", None), + ("light", None), + ("rgb", None), + ("rgbw", None), + ("switch", None), + ("thermostat", None), ], ) async def test_get_rpc_channel_name_multiple_components( @@ -270,14 +270,9 @@ async def test_get_rpc_channel_name_multiple_components( } monkeypatch.setattr(mock_rpc_device, "config", config) - assert ( - get_rpc_channel_name(mock_rpc_device, f"{component}:0") - == f"Test name {expected} 0" - ) - assert ( - get_rpc_channel_name(mock_rpc_device, f"{component}:1") - == f"Test name {expected} 1" - ) + # we use sub-devices, so the entity name is not set + assert get_rpc_channel_name(mock_rpc_device, f"{component}:0") == expected + assert get_rpc_channel_name(mock_rpc_device, f"{component}:1") == expected async def test_get_rpc_input_triggers( From 19ee8886d65c10c2edaa27be61363e26509e7d9f Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 11:59:13 +0300 Subject: [PATCH 0874/1175] Add more mac-addresses for Amazon Devices autodiscovery (#145598) * Add more mac-addresses for Amazon Devices autodiscovery * some more --- .../components/amazon_devices/manifest.json | 10 +++++ homeassistant/generated/dhcp.py | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 3f8dcd4c4df..f20c226230d 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -4,12 +4,22 @@ "codeowners": ["@chemelli74"], "config_flow": true, "dhcp": [ + { "macaddress": "08A6BC*" }, { "macaddress": "10BF67*" }, + { "macaddress": "440049*" }, + { "macaddress": "443D54*" }, { "macaddress": "48B423*" }, { "macaddress": "4C1744*" }, + { "macaddress": "50D45C*" }, { "macaddress": "50DCE7*" }, + { "macaddress": "68F63B*" }, { "macaddress": "74D637*" }, + { "macaddress": "7C6166*" }, + { "macaddress": "901195*" }, + { "macaddress": "943A91*" }, + { "macaddress": "98226E*" }, { "macaddress": "9CC8E9*" }, + { "macaddress": "A8E621*" }, { "macaddress": "C095CF*" }, { "macaddress": "D8BE65*" }, { "macaddress": "EC2BEB*" } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index cbdf31387e6..19fa6cc706a 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,10 +26,22 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, + { + "domain": "amazon_devices", + "macaddress": "08A6BC*", + }, { "domain": "amazon_devices", "macaddress": "10BF67*", }, + { + "domain": "amazon_devices", + "macaddress": "440049*", + }, + { + "domain": "amazon_devices", + "macaddress": "443D54*", + }, { "domain": "amazon_devices", "macaddress": "48B423*", @@ -38,18 +50,46 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "4C1744*", }, + { + "domain": "amazon_devices", + "macaddress": "50D45C*", + }, { "domain": "amazon_devices", "macaddress": "50DCE7*", }, + { + "domain": "amazon_devices", + "macaddress": "68F63B*", + }, { "domain": "amazon_devices", "macaddress": "74D637*", }, + { + "domain": "amazon_devices", + "macaddress": "7C6166*", + }, + { + "domain": "amazon_devices", + "macaddress": "901195*", + }, + { + "domain": "amazon_devices", + "macaddress": "943A91*", + }, + { + "domain": "amazon_devices", + "macaddress": "98226E*", + }, { "domain": "amazon_devices", "macaddress": "9CC8E9*", }, + { + "domain": "amazon_devices", + "macaddress": "A8E621*", + }, { "domain": "amazon_devices", "macaddress": "C095CF*", From d975135a7cd2acc2a2c1520559f4b3a796058598 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 12:00:09 +0300 Subject: [PATCH 0875/1175] Improve Bluetooth binary_sensor for Amazon Devices (#145600) Improve blueetooth binary_sensor for Amazon Devices --- homeassistant/components/amazon_devices/binary_sensor.py | 1 - .../amazon_devices/snapshots/test_binary_sensor.ambr | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/amazon_devices/binary_sensor.py index 0528ffbe1e4..2e41983dda4 100644 --- a/homeassistant/components/amazon_devices/binary_sensor.py +++ b/homeassistant/components/amazon_devices/binary_sensor.py @@ -39,7 +39,6 @@ BINARY_SENSORS: Final = ( AmazonBinarySensorEntityDescription( key="bluetooth", translation_key="bluetooth", - device_class=BinarySensorDeviceClass.CONNECTIVITY, is_on_fn=lambda _device: _device.bluetooth_state, ), ) diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr index 647fa39540f..1033d63eba4 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr @@ -22,7 +22,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': , + 'original_device_class': None, 'original_icon': None, 'original_name': 'Bluetooth', 'platform': 'amazon_devices', @@ -36,7 +36,6 @@ # name: test_all_entities[binary_sensor.echo_test_bluetooth-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'connectivity', 'friendly_name': 'Echo Test Bluetooth', }), 'context': , From 301d308d5ac25cbfd18dec8b27e17876a2958ac3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 May 2025 11:12:42 +0200 Subject: [PATCH 0876/1175] Add payload ON and OFF options to MQTT switch subentry component (#144627) * Add payload ON and OFF options to MQTT switch component * Add `state_on` and `state_off` options --- homeassistant/components/mqtt/config_flow.py | 20 ++++++++++++++++++++ homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/strings.json | 4 ++++ homeassistant/components/mqtt/switch.py | 8 ++++---- tests/components/mqtt/common.py | 2 ++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 78d2305c4e2..bb884d6392f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -221,6 +221,8 @@ from .const import ( CONF_SPEED_RANGE_MIN, CONF_STATE_CLOSED, CONF_STATE_CLOSING, + CONF_STATE_OFF, + CONF_STATE_ON, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATE_STOPPED, @@ -1330,6 +1332,24 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { validator=validate(cv.template), error="invalid_template", ), + CONF_PAYLOAD_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_OFF, + ), + CONF_PAYLOAD_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_ON, + ), + CONF_STATE_OFF: PlatformField( + selector=TEXT_SELECTOR, + required=False, + ), + CONF_STATE_ON: PlatformField( + selector=TEXT_SELECTOR, + required=False, + ), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 7c0ac1f2a3f..c60aa674b1b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -159,6 +159,8 @@ CONF_SPEED_RANGE_MAX = "speed_range_max" CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" +CONF_STATE_OFF = "state_off" +CONF_STATE_ON = "state_on" CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 5e4c2612592..281c5a34a45 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -270,6 +270,8 @@ "qos": "QoS", "red_template": "Red template", "retain": "Retain", + "state_off": "State \"off\"", + "state_on": "State \"on\"", "state_template": "State template", "state_topic": "State topic", "state_value_template": "State value template", @@ -295,6 +297,8 @@ "qos": "The QoS value a {platform} entity should use.", "red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.", "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", + "state_off": "The incoming payload that represents the \"off\" state. Use only when the value that represents \"off\" state in the state topic is different from value that should be sent to the command topic to turn the device off.", + "state_on": "The incoming payload that represents the \"on\" state. Use only when the value that represents \"on\" state in the state topic is different from value that should be sent to the command topic to turn the device on.", "state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.", "state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)", "supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)", diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f6996fc77ce..fa33751f37d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -31,7 +31,11 @@ from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_STATE_OFF, + CONF_STATE_ON, CONF_STATE_TOPIC, + DEFAULT_PAYLOAD_OFF, + DEFAULT_PAYLOAD_ON, PAYLOAD_NONE, ) from .entity import MqttEntity, async_setup_entity_entry_helper @@ -46,10 +50,6 @@ from .schemas import MQTT_ENTITY_COMMON_SCHEMA PARALLEL_UPDATES = 0 DEFAULT_NAME = "MQTT Switch" -DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_PAYLOAD_OFF = "OFF" -CONF_STATE_ON = "state_on" -CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index ab5ffe28518..b985a8caffe 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -234,6 +234,8 @@ MOCK_SUBENTRY_SWITCH_COMPONENT = { "state_topic": "test-topic", "command_template": "{{ value }}", "value_template": "{{ value_json.value }}", + "payload_off": "OFF", + "payload_on": "ON", "entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f12e", "optimistic": True, }, From 561be22a603f84e51ce303c3bbf5b4bd77325701 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Mon, 26 May 2025 11:13:15 +0200 Subject: [PATCH 0877/1175] Disable last cleaning sensor for gs3mp model in lamarzocco (#145576) * Disable last cleaning sensor for gs3mp model in lamarzocco * is comparison --- homeassistant/components/lamarzocco/binary_sensor.py | 2 +- homeassistant/components/lamarzocco/sensor.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/binary_sensor.py b/homeassistant/components/lamarzocco/binary_sensor.py index aacfca929ad..4fc2c0b05df 100644 --- a/homeassistant/components/lamarzocco/binary_sensor.py +++ b/homeassistant/components/lamarzocco/binary_sensor.py @@ -70,7 +70,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = ( ), entity_category=EntityCategory.DIAGNOSTIC, supported_fn=lambda coordinator: ( - coordinator.device.dashboard.model_name != ModelName.GS3_MP + coordinator.device.dashboard.model_name is not ModelName.GS3_MP ), ), LaMarzoccoBinarySensorEntityDescription( diff --git a/homeassistant/components/lamarzocco/sensor.py b/homeassistant/components/lamarzocco/sensor.py index afe34005108..29f1c6209ec 100644 --- a/homeassistant/components/lamarzocco/sensor.py +++ b/homeassistant/components/lamarzocco/sensor.py @@ -113,6 +113,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = ( ).last_cleaning_start_time ), entity_category=EntityCategory.DIAGNOSTIC, + supported_fn=( + lambda coordinator: coordinator.device.dashboard.model_name + is not ModelName.GS3_MP + ), ), ) From 34d11521c0f984e286a257e87e4ed3ce84cdeb33 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 May 2025 11:13:24 +0200 Subject: [PATCH 0878/1175] Fix reference to "tilt command topic" in MQTT translation strings (#145563) * Fix reference to "tilt command topic" in MQTT translation strings * Missed one --- homeassistant/components/mqtt/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 281c5a34a45..3bb467affd6 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -386,12 +386,12 @@ "tilt_optimistic": "Tilt optimistic" }, "data_description": { - "tilt_closed_value": "The value that will be sent to the \"set tilt topic\" when the cover tilt is closed.", - "tilt_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the set tilt topic. Within the template the following variables are available: `entity_id`, `tilt_position` (the target tilt position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_command_template)", + "tilt_closed_value": "The value that will be sent to the \"tilt command topic\" when the cover tilt is closed.", + "tilt_command_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to define the position to be sent to the tilt command topic. Within the template the following variables are available: `entity_id`, `tilt_position` (the target tilt position percentage), `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_command_template)", "tilt_command_topic": "The MQTT topic to publish commands to control the cover tilt. [Learn more.]({url}#tilt_command_topic)", "tilt_max": "The maximum tilt value.", "tilt_min": "The minimum tilt value.", - "tilt_opened_value": "The value that will be sent to the \"set tilt topic\" when the cover tilt is opened.", + "tilt_opened_value": "The value that will be sent to the \"tilt command topic\" when the cover tilt is opened.", "tilt_status_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the payload for the tilt status topic. Within the template the following variables are available: `entity_id`, `position_open`, `position_closed`, `tilt_min` and `tilt_max`. [Learn more.]({url}#tilt_status_template)", "tilt_status_topic": "The MQTT topic subscribed to receive tilt status update values. [Learn more.]({url}#tilt_status_topic)", "tilt_optimistic": "Flag that defines if tilt works in optimistic mode. If tilt status topic is not defined, tilt works in optimisic mode by default. [Learn more.]({url}#tilt_optimistic)" From 8f9f531dd76df4ac8129dc4400e73a31a4026875 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 26 May 2025 19:22:11 +1000 Subject: [PATCH 0879/1175] Bump aiolifx to 1.1.5 to improve the identification of LIFX Luna (#145416) Signed-off-by: Avi Miller --- homeassistant/components/lifx/manifest.json | 3 ++- homeassistant/generated/zeroconf.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 18b9457ebf4..b93714a2cdf 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -32,6 +32,7 @@ "LIFX GU10", "LIFX Indoor Neon", "LIFX Lightstrip", + "LIFX Luna", "LIFX Mini", "LIFX Neon", "LIFX Nightvision", @@ -51,7 +52,7 @@ "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"], "requirements": [ - "aiolifx==1.1.4", + "aiolifx==1.1.5", "aiolifx-effects==0.3.2", "aiolifx-themes==0.6.4" ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 38f90663601..ed5ac37c0cd 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -128,6 +128,10 @@ HOMEKIT = { "always_discover": True, "domain": "lifx", }, + "LIFX Luna": { + "always_discover": True, + "domain": "lifx", + }, "LIFX Mini": { "always_discover": True, "domain": "lifx", diff --git a/requirements_all.txt b/requirements_all.txt index 0a0c49ad306..c280ba3fd7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -298,7 +298,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.6.4 # homeassistant.components.lifx -aiolifx==1.1.4 +aiolifx==1.1.5 # homeassistant.components.lookin aiolookin==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3267bf3bd18..1d40d35fa15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -280,7 +280,7 @@ aiolifx-effects==0.3.2 aiolifx-themes==0.6.4 # homeassistant.components.lifx -aiolifx==1.1.4 +aiolifx==1.1.5 # homeassistant.components.lookin aiolookin==1.0.0 From c1c74a6f61dcdd1330d0168a0d9057f64e84f086 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 26 May 2025 11:22:46 +0200 Subject: [PATCH 0880/1175] Mark Shelly quality as silver (#145610) --- homeassistant/components/shelly/manifest.json | 1 + script/hassfest/quality_scale.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index f60718beca3..78e01e6d8a6 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -8,6 +8,7 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aioshelly"], + "quality_scale": "silver", "requirements": ["aioshelly==13.6.0"], "zeroconf": [ { diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 5df24a1dc0d..11d3af590a0 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -1955,7 +1955,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "sfr_box", "sharkiq", "shell_command", - "shelly", "shodan", "shopping_list", "sia", From 2cf09abb4c6906ac2dc0f1e99c81cbf16805e5cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 26 May 2025 11:24:01 +0200 Subject: [PATCH 0881/1175] Fulfilled quality rules - gold and platinum tiers for Miele integration (#144773) Fulfilled quality rules - gold and platinum tiers --- .../components/miele/quality_scale.yaml | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/miele/quality_scale.yaml b/homeassistant/components/miele/quality_scale.yaml index d0c3677db40..94ce68278ef 100644 --- a/homeassistant/components/miele/quality_scale.yaml +++ b/homeassistant/components/miele/quality_scale.yaml @@ -53,29 +53,37 @@ rules: test-coverage: todo # Gold - devices: todo - diagnostics: todo - discovery-update-info: todo - discovery: todo - docs-data-update: todo + devices: done + diagnostics: done + discovery-update-info: + status: exempt + comment: | + Discovery is just used to initiate setup of the integration. No data from devices is collected. + discovery: done + docs-data-update: done docs-examples: todo - docs-known-limitations: todo - docs-supported-devices: todo - docs-supported-functions: todo + docs-known-limitations: done + docs-supported-devices: done + docs-supported-functions: done docs-troubleshooting: todo docs-use-cases: done - dynamic-devices: todo - entity-category: todo - entity-device-class: todo - entity-disabled-by-default: todo - entity-translations: todo - exception-translations: todo - icon-translations: todo + dynamic-devices: done + entity-category: done + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: done + icon-translations: done reconfiguration-flow: done - repair-issues: todo - stale-devices: todo + repair-issues: + status: exempt + comment: | + No repair issues are created. + stale-devices: + status: done + comment: Stale devices can be deleted from GUI. Automatic deletion will have to wait until we get experience if devices are missing from API data intermittently. # Platinum async-dependency: done inject-websession: done - strict-typing: todo + strict-typing: done From 25f3ab364027aa3040f764d36598ea7b1f226326 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 26 May 2025 04:16:56 -0700 Subject: [PATCH 0882/1175] Add from_hex filter (#145229) --- homeassistant/helpers/template.py | 6 ++++++ tests/helpers/test_template.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index cb6d8fe81b8..1061ac732d6 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2572,6 +2572,11 @@ def struct_unpack(value: bytes, format_string: str, offset: int = 0) -> Any | No return None +def from_hex(value: str) -> bytes: + """Perform hex string decode.""" + return bytes.fromhex(value) + + def base64_encode(value: str) -> str: """Perform base64 encode.""" return base64.b64encode(value.encode("utf-8")).decode("utf-8") @@ -3131,6 +3136,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["flatten"] = flatten self.filters["float"] = forgiving_float_filter self.filters["from_json"] = from_json + self.filters["from_hex"] = from_hex self.filters["iif"] = iif self.filters["int"] = forgiving_int_filter self.filters["intersect"] = intersect diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 4de8d47cc16..0b95010b71b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1632,6 +1632,14 @@ def test_ord(hass: HomeAssistant) -> None: assert template.Template('{{ "d" | ord }}', hass).async_render() == 100 +def test_from_hex(hass: HomeAssistant) -> None: + """Test the fromhex filter.""" + assert ( + template.Template("{{ '0F010003' | from_hex }}", hass).async_render() + == b"\x0f\x01\x00\x03" + ) + + def test_base64_encode(hass: HomeAssistant) -> None: """Test the base64_encode filter.""" assert ( From cc504da03a3ae1ae39ac5edbeaf46e951fa5deeb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 13:21:00 +0200 Subject: [PATCH 0883/1175] Improve type hints in XiaomiGatewayDevice derived entities (#145605) --- homeassistant/components/xiaomi_miio/entity.py | 17 ++++++++++++++--- homeassistant/components/xiaomi_miio/light.py | 2 ++ homeassistant/components/xiaomi_miio/sensor.py | 10 +++++++++- homeassistant/components/xiaomi_miio/switch.py | 12 +++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/entity.py b/homeassistant/components/xiaomi_miio/entity.py index ba1148985ba..cef2137a8c0 100644 --- a/homeassistant/components/xiaomi_miio/entity.py +++ b/homeassistant/components/xiaomi_miio/entity.py @@ -4,9 +4,10 @@ import datetime from enum import Enum from functools import partial import logging -from typing import Any +from typing import TYPE_CHECKING, Any from miio import DeviceException +from miio.gateway.devices import SubDevice from homeassistant.const import ATTR_CONNECTIONS, CONF_MAC, CONF_MODEL from homeassistant.helpers import device_registry as dr @@ -18,6 +19,7 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ATTR_AVAILABLE, DOMAIN +from .typing import XiaomiMiioConfigEntry _LOGGER = logging.getLogger(__name__) @@ -150,10 +152,17 @@ class XiaomiCoordinatedMiioEntity[_T: DataUpdateCoordinator[Any]]( return time.isoformat() -class XiaomiGatewayDevice(CoordinatorEntity, Entity): +class XiaomiGatewayDevice( + CoordinatorEntity[DataUpdateCoordinator[dict[str, bool]]], Entity +): """Representation of a base Xiaomi Gateway Device.""" - def __init__(self, coordinator, sub_device, entry): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, bool]], + sub_device: SubDevice, + entry: XiaomiMiioConfigEntry, + ) -> None: """Initialize the Xiaomi Gateway Device.""" super().__init__(coordinator) self._sub_device = sub_device @@ -174,6 +183,8 @@ class XiaomiGatewayDevice(CoordinatorEntity, Entity): @property def device_info(self) -> DeviceInfo: """Return the device info of the gateway.""" + if TYPE_CHECKING: + assert self._entry.unique_id is not None return DeviceInfo( identifiers={(DOMAIN, self._sub_device.sid)}, via_device=(DOMAIN, self._entry.unique_id), diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 61931cc750a..03341ea9541 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -18,6 +18,7 @@ from miio import ( PhilipsEyecare, PhilipsMoonlight, ) +from miio.gateway.devices.light import LightBulb from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -1093,6 +1094,7 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): _attr_color_mode = ColorMode.COLOR_TEMP _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + _sub_device: LightBulb @property def brightness(self): diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 73581595851..4ed09d93734 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -8,6 +8,7 @@ import logging from typing import TYPE_CHECKING from miio import AirQualityMonitor, Device as MiioDevice, DeviceException +from miio.gateway.devices import SubDevice from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -46,6 +47,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt as dt_util from . import VacuumCoordinatorDataAttributes @@ -977,7 +979,13 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Representation of a XiaomiGatewaySensor.""" - def __init__(self, coordinator, sub_device, entry, description): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, bool]], + sub_device: SubDevice, + entry: XiaomiMiioConfigEntry, + description: XiaomiMiioSensorDescription, + ) -> None: """Initialize the XiaomiSensor.""" super().__init__(coordinator, sub_device, entry) self._unique_id = f"{sub_device.sid}-{description.key}" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 9b2366a8273..ae5dc96075e 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -9,6 +9,8 @@ import logging from typing import Any from miio import AirConditioningCompanionV3, ChuangmiPlug, DeviceException, PowerStrip +from miio.gateway.devices import SubDevice +from miio.gateway.devices.switch import Switch from miio.powerstrip import PowerMode import voluptuous as vol @@ -30,6 +32,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_FLOW_TYPE, @@ -748,8 +751,15 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Representation of a XiaomiGatewaySwitch.""" _attr_device_class = SwitchDeviceClass.SWITCH + _sub_device: Switch - def __init__(self, coordinator, sub_device, entry, variable): + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, bool]], + sub_device: SubDevice, + entry: XiaomiMiioConfigEntry, + variable: str, + ) -> None: """Initialize the XiaomiSensor.""" super().__init__(coordinator, sub_device, entry) self._channel = GATEWAY_SWITCH_VARS[variable][KEY_CHANNEL] From 13a6c13b8939092aa07943ea82ded46cd4c6a382 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 26 May 2025 04:56:11 -0700 Subject: [PATCH 0884/1175] Allow base64_encode to support bytes and strings (#145227) --- homeassistant/helpers/template.py | 6 ++++-- tests/helpers/test_template.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1061ac732d6..408e88ef8b3 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2577,9 +2577,11 @@ def from_hex(value: str) -> bytes: return bytes.fromhex(value) -def base64_encode(value: str) -> str: +def base64_encode(value: str | bytes) -> str: """Perform base64 encode.""" - return base64.b64encode(value.encode("utf-8")).decode("utf-8") + if isinstance(value, str): + value = value.encode("utf-8") + return base64.b64encode(value).decode("utf-8") def base64_decode(value: str, encoding: str | None = "utf-8") -> str | bytes: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 0b95010b71b..6c41b7970da 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1640,12 +1640,17 @@ def test_from_hex(hass: HomeAssistant) -> None: ) -def test_base64_encode(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("value_template", "expected"), + [ + ('{{ "homeassistant" | base64_encode }}', "aG9tZWFzc2lzdGFudA=="), + ("{{ int('0F010003', base=16) | pack('>I') | base64_encode }}", "DwEAAw=="), + ("{{ 'AA01000200150020' | from_hex | base64_encode }}", "qgEAAgAVACA="), + ], +) +def test_base64_encode(hass: HomeAssistant, value_template: str, expected: str) -> None: """Test the base64_encode filter.""" - assert ( - template.Template('{{ "homeassistant" | base64_encode }}', hass).async_render() - == "aG9tZWFzc2lzdGFudA==" - ) + assert template.Template(value_template, hass).async_render() == expected def test_base64_decode(hass: HomeAssistant) -> None: From 87c3e2c7cee85a640c200e505d71faf2298fd4d6 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Mon, 26 May 2025 14:56:37 +0300 Subject: [PATCH 0885/1175] Download backup if restore fails in Z-Wave migration (#145434) * ZWaveJS migration: Download backup if restore fails * update test * PR comment --- homeassistant/components/zwave_js/config_flow.py | 15 +++++++++++---- homeassistant/components/zwave_js/strings.json | 2 +- tests/components/zwave_js/test_config_flow.py | 2 ++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index b539c747c4f..3e899da0538 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import base64 from contextlib import suppress from datetime import datetime import logging @@ -192,7 +193,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.backup_task: asyncio.Task | None = None self.restore_backup_task: asyncio.Task | None = None self.backup_data: bytes | None = None - self.backup_filepath: str | None = None + self.backup_filepath: Path | None = None self.use_addon = False self._migrating = False self._reconfigure_config_entry: ConfigEntry | None = None @@ -1241,11 +1242,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): """Restore failed.""" if user_input is not None: return await self.async_step_restore_nvm() + assert self.backup_filepath is not None + assert self.backup_data is not None return self.async_show_form( step_id="restore_failed", description_placeholders={ "file_path": str(self.backup_filepath), + "file_url": f"data:application/octet-stream;base64,{base64.b64encode(self.backup_data).decode('ascii')}", + "file_name": self.backup_filepath.name, }, ) @@ -1383,12 +1388,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): unsub() # save the backup to a file just in case - self.backup_filepath = self.hass.config.path( - f"zwavejs_nvm_backup_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bin" + self.backup_filepath = Path( + self.hass.config.path( + f"zwavejs_nvm_backup_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.bin" + ) ) try: await self.hass.async_add_executor_job( - Path(self.backup_filepath).write_bytes, + self.backup_filepath.write_bytes, self.backup_data, ) except OSError as err: diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index ac5de91d6e8..fbe43af1f6f 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -123,7 +123,7 @@ }, "restore_failed": { "title": "Restoring unsuccessful", - "description": "Your Z-Wave network could not be restored to the new controller. This means that your Z-Wave devices are not connected to Home Assistant.\n\nThe backup is saved to ”{file_path}”", + "description": "Your Z-Wave network could not be restored to the new controller. This means that your Z-Wave devices are not connected to Home Assistant.\n\nThe backup is saved to ”{file_path}”\n\n'<'a href=\"{file_url}\" download=\"{file_name}\"'>'Download backup file'<'/a'>'", "submit": "Try again" }, "choose_serial_port": { diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 5a1e7b217e0..bae8ae55034 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -4231,6 +4231,8 @@ async def test_reconfigure_migrate_restore_failure( description_placeholders = result["description_placeholders"] assert description_placeholders is not None assert description_placeholders["file_path"] + assert description_placeholders["file_url"] + assert description_placeholders["file_name"] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) From e22fbe553b3b1f773f746e5c477e6c0c0063e69e Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Mon, 26 May 2025 14:00:30 +0200 Subject: [PATCH 0886/1175] Add Homee event platform (#145569) * add event.py * Add strings and code improvements * Add tests for event * last fixes * fix review comments * update test snapshot --- homeassistant/components/homee/__init__.py | 1 + homeassistant/components/homee/event.py | 61 +++++++++++++++ homeassistant/components/homee/strings.json | 21 ++++++ tests/components/homee/fixtures/events.json | 46 ++++++++++++ .../homee/snapshots/test_event.ambr | 75 +++++++++++++++++++ tests/components/homee/test_event.py | 65 ++++++++++++++++ 6 files changed, 269 insertions(+) create mode 100644 homeassistant/components/homee/event.py create mode 100644 tests/components/homee/fixtures/events.json create mode 100644 tests/components/homee/snapshots/test_event.ambr create mode 100644 tests/components/homee/test_event.py diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index 654bdde6211..83705d4fed1 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -20,6 +20,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CLIMATE, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.LIGHT, Platform.LOCK, diff --git a/homeassistant/components/homee/event.py b/homeassistant/components/homee/event.py new file mode 100644 index 00000000000..047d9e2e122 --- /dev/null +++ b/homeassistant/components/homee/event.py @@ -0,0 +1,61 @@ +"""The homee event platform.""" + +from pyHomee.const import AttributeType +from pyHomee.model import HomeeAttribute + +from homeassistant.components.event import EventDeviceClass, EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import HomeeConfigEntry +from .entity import HomeeEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: HomeeConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Add event entities for homee.""" + + async_add_entities( + HomeeEvent(attribute, config_entry) + for node in config_entry.runtime_data.nodes + for attribute in node.attributes + if attribute.type == AttributeType.UP_DOWN_REMOTE + ) + + +class HomeeEvent(HomeeEntity, EventEntity): + """Representation of a homee event.""" + + _attr_translation_key = "up_down_remote" + _attr_event_types = [ + "released", + "up", + "down", + "stop", + "up_long", + "down_long", + "stop_long", + "c_button", + "b_button", + "a_button", + ] + _attr_device_class = EventDeviceClass.BUTTON + + async def async_added_to_hass(self) -> None: + """Add the homee event entity to home assistant.""" + await super().async_added_to_hass() + self.async_on_remove( + self._attribute.add_on_changed_listener(self._event_triggered) + ) + + @callback + def _event_triggered(self, event: HomeeAttribute) -> None: + """Handle a homee event.""" + if event.type == AttributeType.UP_DOWN_REMOTE: + self._trigger_event(self.event_types[int(event.current_value)]) + self.schedule_update_ha_state() diff --git a/homeassistant/components/homee/strings.json b/homeassistant/components/homee/strings.json index 092fca0c0ac..5e124aa427e 100644 --- a/homeassistant/components/homee/strings.json +++ b/homeassistant/components/homee/strings.json @@ -147,6 +147,27 @@ } } }, + "event": { + "up_down_remote": { + "name": "Up/down remote", + "state_attributes": { + "event_type": { + "state": { + "release": "Released", + "up": "Up", + "down": "Down", + "stop": "Stop", + "up_long": "Up (long press)", + "down_long": "Down (long press)", + "stop_long": "Stop (long press)", + "c_button": "C button", + "b_button": "B button", + "a_button": "A button" + } + } + } + } + }, "fan": { "homee": { "state_attributes": { diff --git a/tests/components/homee/fixtures/events.json b/tests/components/homee/fixtures/events.json new file mode 100644 index 00000000000..351d35ec497 --- /dev/null +++ b/tests/components/homee/fixtures/events.json @@ -0,0 +1,46 @@ +{ + "id": 1, + "name": "Remote Control", + "profile": 41, + "image": "default", + "favorite": 0, + "order": 29, + "protocol": 23, + "routing": 0, + "state": 1, + "state_changed": 1715356788, + "added": 1615396304, + "history": 1, + "cube_type": 14, + "note": "", + "services": 0, + "phonetic_name": "", + "owner": 2, + "security": 0, + "attributes": [ + { + "id": 1, + "node_id": 1, + "instance": 0, + "minimum": 0, + "maximum": 9, + "current_value": 0.0, + "target_value": 0.0, + "last_value": 2.0, + "unit": "n/a", + "step_value": 1.0, + "editable": 0, + "type": 300, + "state": 1, + "last_changed": 1713470190, + "changed_by": 1, + "changed_by_id": 0, + "based_on": 1, + "data": "", + "name": "", + "options": { + "observed_by": [145] + } + } + ] +} diff --git a/tests/components/homee/snapshots/test_event.ambr b/tests/components/homee/snapshots/test_event.ambr new file mode 100644 index 00000000000..45194526ef0 --- /dev/null +++ b/tests/components/homee/snapshots/test_event.ambr @@ -0,0 +1,75 @@ +# serializer version: 1 +# name: test_event_snapshot[event.remote_control_up_down_remote-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'released', + 'up', + 'down', + 'stop', + 'up_long', + 'down_long', + 'stop_long', + 'c_button', + 'b_button', + 'a_button', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.remote_control_up_down_remote', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Up/down remote', + 'platform': 'homee', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'up_down_remote', + 'unique_id': '00055511EECC-1-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_event_snapshot[event.remote_control_up_down_remote-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'button', + 'event_type': None, + 'event_types': list([ + 'released', + 'up', + 'down', + 'stop', + 'up_long', + 'down_long', + 'stop_long', + 'c_button', + 'b_button', + 'a_button', + ]), + 'friendly_name': 'Remote Control Up/down remote', + }), + 'context': , + 'entity_id': 'event.remote_control_up_down_remote', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/homee/test_event.py b/tests/components/homee/test_event.py new file mode 100644 index 00000000000..0ffa7cd8530 --- /dev/null +++ b/tests/components/homee/test_event.py @@ -0,0 +1,65 @@ +"""Test homee events.""" + +from unittest.mock import MagicMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.event import ATTR_EVENT_TYPE +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import build_mock_node, setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_event_fires( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the correct event fires when the attribute changes.""" + + EVENT_TYPES = [ + "released", + "up", + "down", + "stop", + "up_long", + "down_long", + "stop_long", + "c_button", + "b_button", + "a_button", + ] + mock_homee.nodes = [build_mock_node("events.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + await setup_integration(hass, mock_config_entry) + + # Simulate the event triggers. + attribute = mock_homee.nodes[0].attributes[0] + for i, event_type in enumerate(EVENT_TYPES): + attribute.current_value = i + attribute.add_on_changed_listener.call_args_list[1][0][0](attribute) + await hass.async_block_till_done() + + # Check if the event was fired + state = hass.states.get("event.remote_control_up_down_remote") + assert state.attributes[ATTR_EVENT_TYPE] == event_type + + +async def test_event_snapshot( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the event entity snapshot.""" + with patch("homeassistant.components.homee.PLATFORMS", [Platform.EVENT]): + mock_homee.nodes = [build_mock_node("events.json")] + mock_homee.get_node_by_id.return_value = mock_homee.nodes[0] + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 970359c6a06a8df316ca5b58f04580be5655f32c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 26 May 2025 14:25:07 +0200 Subject: [PATCH 0887/1175] Empty response returns empty list in Nord Pool (#145514) --- homeassistant/components/nordpool/services.py | 7 ++--- .../components/nordpool/strings.json | 3 -- .../nordpool/snapshots/test_services.ambr | 6 ++++ tests/components/nordpool/test_services.py | 28 ++++++++++++++++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nordpool/services.py b/homeassistant/components/nordpool/services.py index 6607edfdbcb..628962811e3 100644 --- a/homeassistant/components/nordpool/services.py +++ b/homeassistant/components/nordpool/services.py @@ -97,11 +97,8 @@ def async_setup_services(hass: HomeAssistant) -> None: translation_domain=DOMAIN, translation_key="authentication_error", ) from error - except NordPoolEmptyResponseError as error: - raise ServiceValidationError( - translation_domain=DOMAIN, - translation_key="empty_response", - ) from error + except NordPoolEmptyResponseError: + return {area: [] for area in areas} except NordPoolError as error: raise ServiceValidationError( translation_domain=DOMAIN, diff --git a/homeassistant/components/nordpool/strings.json b/homeassistant/components/nordpool/strings.json index 7b33f032de1..73c35673826 100644 --- a/homeassistant/components/nordpool/strings.json +++ b/homeassistant/components/nordpool/strings.json @@ -129,9 +129,6 @@ "authentication_error": { "message": "There was an authentication error as you tried to retrieve data too far in the past." }, - "empty_response": { - "message": "Nord Pool has not posted market prices for the provided date." - }, "connection_error": { "message": "There was a connection error connecting to the API. Try again later." } diff --git a/tests/components/nordpool/snapshots/test_services.ambr b/tests/components/nordpool/snapshots/test_services.ambr index 6a57d7ecce9..b271b433061 100644 --- a/tests/components/nordpool/snapshots/test_services.ambr +++ b/tests/components/nordpool/snapshots/test_services.ambr @@ -1,4 +1,10 @@ # serializer version: 1 +# name: test_empty_response_returns_empty_list + dict({ + 'SE3': list([ + ]), + }) +# --- # name: test_service_call dict({ 'SE3': list([ diff --git a/tests/components/nordpool/test_services.py b/tests/components/nordpool/test_services.py index 6d6af685d28..d59ec4712d7 100644 --- a/tests/components/nordpool/test_services.py +++ b/tests/components/nordpool/test_services.py @@ -74,7 +74,6 @@ async def test_service_call( ("error", "key"), [ (NordPoolAuthenticationError, "authentication_error"), - (NordPoolEmptyResponseError, "empty_response"), (NordPoolError, "connection_error"), ], ) @@ -106,6 +105,33 @@ async def test_service_call_failures( assert err.value.translation_key == key +@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") +async def test_empty_response_returns_empty_list( + hass: HomeAssistant, + load_int: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test get_prices_for_date service call return empty list for empty response.""" + service_data = TEST_SERVICE_DATA.copy() + service_data[ATTR_CONFIG_ENTRY] = load_int.entry_id + + with ( + patch( + "homeassistant.components.nordpool.coordinator.NordPoolClient.async_get_delivery_period", + side_effect=NordPoolEmptyResponseError, + ), + ): + response = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PRICES_FOR_DATE, + service_data, + blocking=True, + return_response=True, + ) + + assert response == snapshot + + @pytest.mark.freeze_time("2024-11-05T18:00:00+00:00") async def test_service_call_config_entry_bad_state( hass: HomeAssistant, From 2d2e0d0fb9b2a93d9f6f87f667b47ec5185a4a65 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 14:45:55 +0200 Subject: [PATCH 0888/1175] Add init type hints to XiaomiCoordinatedMiioEntity derived entities (#145612) --- .../components/xiaomi_miio/binary_sensor.py | 18 +- .../components/xiaomi_miio/button.py | 16 +- .../components/xiaomi_miio/entity.py | 12 +- homeassistant/components/xiaomi_miio/fan.py | 164 +++++++++++++----- .../components/xiaomi_miio/humidifier.py | 50 ++++-- .../components/xiaomi_miio/number.py | 31 ++-- .../components/xiaomi_miio/select.py | 37 +++- .../components/xiaomi_miio/sensor.py | 15 +- .../components/xiaomi_miio/switch.py | 65 ++++--- .../components/xiaomi_miio/vacuum.py | 57 ++++-- 10 files changed, 328 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index b0a990cf9be..205db7cd21c 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -5,7 +5,9 @@ from __future__ import annotations from collections.abc import Callable, Iterable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +from miio import Device as MiioDevice from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,6 +17,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import VacuumCoordinatorDataAttributes from .const import ( @@ -213,12 +216,21 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity): +class XiaomiGenericBinarySensor( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], BinarySensorEntity +): """Representation of a Xiaomi Humidifier binary sensor.""" entity_description: XiaomiMiioBinarySensorDescription - def __init__(self, device, entry, unique_id, coordinator, description): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioBinarySensorDescription, + ) -> None: """Initialize the entity.""" super().__init__(device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 194b73f2372..58236e136cb 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -3,7 +3,9 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any +from miio import Device as MiioDevice from miio.integrations.vacuum.roborock.vacuum import Consumable from homeassistant.components.button import ( @@ -14,6 +16,7 @@ from homeassistant.components.button import ( from homeassistant.const import CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, MODELS_VACUUM from .entity import XiaomiCoordinatedMiioEntity @@ -148,14 +151,23 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): +class XiaomiGenericCoordinatedButton( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], ButtonEntity +): """A button implementation for Xiaomi.""" entity_description: XiaomiMiioButtonDescription _attr_device_class = ButtonDeviceClass.RESTART - def __init__(self, device, entry, unique_id, coordinator, description): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioButtonDescription, + ) -> None: """Initialize the plug switch.""" super().__init__(device, entry, unique_id, coordinator) self.entity_description = description diff --git a/homeassistant/components/xiaomi_miio/entity.py b/homeassistant/components/xiaomi_miio/entity.py index cef2137a8c0..f8cdc69a12e 100644 --- a/homeassistant/components/xiaomi_miio/entity.py +++ b/homeassistant/components/xiaomi_miio/entity.py @@ -6,7 +6,7 @@ from functools import partial import logging from typing import TYPE_CHECKING, Any -from miio import DeviceException +from miio import Device as MiioDevice, DeviceException from miio.gateway.devices import SubDevice from homeassistant.const import ATTR_CONNECTIONS, CONF_MAC, CONF_MODEL @@ -70,7 +70,13 @@ class XiaomiCoordinatedMiioEntity[_T: DataUpdateCoordinator[Any]]( _attr_has_entity_name = True - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: _T, + ) -> None: """Initialize the coordinated Xiaomi Miio Device.""" super().__init__(coordinator) self._device = device @@ -88,6 +94,8 @@ class XiaomiCoordinatedMiioEntity[_T: DataUpdateCoordinator[Any]]( @property def device_info(self) -> DeviceInfo: """Return the device info.""" + if TYPE_CHECKING: + assert self._device_id is not None device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, manufacturer="Xiaomi", diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 4492dcf9f17..aa7069f1e92 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -8,6 +8,7 @@ import logging import math from typing import Any +from miio import Device as MiioDevice from miio.fan_common import ( MoveDirection as FanMoveDirection, OperationMode as FanOperationMode, @@ -34,6 +35,7 @@ from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.percentage import ( percentage_to_ranged_value, ranged_value_to_percentage, @@ -293,22 +295,30 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): +class XiaomiGenericDevice( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], FanEntity +): """Representation of a generic Xiaomi device.""" _attr_name = None - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator) - self._available_attributes = {} - self._state = None - self._mode = None - self._fan_level = None - self._state_attrs = {} + self._available_attributes: dict[str, Any] = {} + self._state: bool | None = None + self._mode: str | None = None + self._fan_level: int | None = None + self._state_attrs: dict[str, Any] = {} self._device_features = 0 - self._preset_modes = [] + self._preset_modes: list[str] = [] @property @abstractmethod @@ -343,7 +353,8 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): ) -> None: """Turn the device on.""" result = await self._try_command( - "Turning the miio device on failed.", self._device.on + "Turning the miio device on failed.", + self._device.on, # type: ignore[attr-defined] ) # If operation mode was set the device must not be turned on. @@ -359,7 +370,8 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" result = await self._try_command( - "Turning the miio device off failed.", self._device.off + "Turning the miio device off failed.", + self._device.off, # type: ignore[attr-defined] ) if result: @@ -370,7 +382,13 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): class XiaomiGenericAirPurifier(XiaomiGenericDevice): """Representation of a generic AirPurifier device.""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the generic AirPurifier device.""" super().__init__(device, entry, unique_id, coordinator) @@ -417,7 +435,13 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the plug switch.""" super().__init__(device, entry, unique_id, coordinator) @@ -528,7 +552,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): if speed_mode: await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class(self.SPEED_MODE_MAPPING[speed_mode]), ) @@ -539,7 +563,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): """ if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ): self._mode = self.operation_mode_class[preset_mode].value @@ -552,7 +576,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): await self._try_command( "Setting the extra features of the miio device failed.", - self._device.set_extra_features, + self._device.set_extra_features, # type: ignore[attr-defined] features, ) @@ -599,7 +623,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): return if await self._try_command( "Setting fan level of the miio device failed.", - self._device.set_fan_level, + self._device.set_fan_level, # type: ignore[attr-defined] fan_level, ): self._fan_level = fan_level @@ -609,7 +633,13 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Purifier MB4.""" - def __init__(self, device, entry, unique_id, coordinator) -> None: + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize Air Purifier MB4.""" super().__init__(device, entry, unique_id, coordinator) @@ -659,7 +689,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): return if await self._try_command( "Setting fan level of the miio device failed.", - self._device.set_favorite_rpm, + self._device.set_favorite_rpm, # type: ignore[attr-defined] favorite_rpm, ): self._favorite_rpm = favorite_rpm @@ -673,7 +703,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ): self._mode = self.operation_mode_class[preset_mode].value @@ -712,7 +742,13 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the miio device.""" super().__init__(device, entry, unique_id, coordinator) @@ -764,7 +800,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): if speed_mode: if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] AirfreshOperationMode(self.SPEED_MODE_MAPPING[speed_mode]), ): self._mode = AirfreshOperationMode( @@ -779,7 +815,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): """ if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ): self._mode = self.operation_mode_class[preset_mode].value @@ -792,7 +828,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): await self._try_command( "Setting the extra features of the miio device failed.", - self._device.set_extra_features, + self._device.set_extra_features, # type: ignore[attr-defined] features, ) @@ -810,10 +846,16 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Fresh A1.""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the miio device.""" super().__init__(device, entry, unique_id, coordinator) - self._favorite_speed = None + self._favorite_speed: int | None = None self._device_features = FEATURE_FLAGS_AIRFRESH_A1 self._preset_modes = PRESET_MODES_AIRFRESH_A1 self._attr_supported_features = ( @@ -857,7 +899,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): return if await self._try_command( "Setting fan level of the miio device failed.", - self._device.set_favorite_speed, + self._device.set_favorite_speed, # type: ignore[attr-defined] favorite_speed, ): self._favorite_speed = favorite_speed @@ -867,7 +909,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Set the preset mode of the fan. This method is a coroutine.""" if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ): self._mode = self.operation_mode_class[preset_mode].value @@ -885,7 +927,13 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): class XiaomiAirFreshT2017(XiaomiAirFreshA1): """Representation of a Xiaomi Air Fresh T2017.""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the miio device.""" super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH_T2017 @@ -897,7 +945,13 @@ class XiaomiGenericFan(XiaomiGenericDevice): _attr_translation_key = "generic_fan" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) @@ -922,9 +976,9 @@ class XiaomiGenericFan(XiaomiGenericDevice): ) if self._model != MODEL_FAN_1C: self._attr_supported_features |= FanEntityFeature.DIRECTION - self._preset_mode = None - self._oscillating = None - self._percentage = None + self._preset_mode: str | None = None + self._oscillating: bool | None = None + self._percentage: int | None = None @property def preset_mode(self) -> str | None: @@ -953,7 +1007,7 @@ class XiaomiGenericFan(XiaomiGenericDevice): """Set oscillation.""" await self._try_command( "Setting oscillate on/off of the miio device failed.", - self._device.set_oscillate, + self._device.set_oscillate, # type: ignore[attr-defined] oscillating, ) self._oscillating = oscillating @@ -966,7 +1020,7 @@ class XiaomiGenericFan(XiaomiGenericDevice): await self._try_command( "Setting move direction of the miio device failed.", - self._device.set_rotate, + self._device.set_rotate, # type: ignore[attr-defined] FanMoveDirection(FAN_DIRECTIONS_MAP[direction]), ) @@ -974,7 +1028,13 @@ class XiaomiGenericFan(XiaomiGenericDevice): class XiaomiFan(XiaomiGenericFan): """Representation of a Xiaomi Fan.""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) @@ -1018,13 +1078,13 @@ class XiaomiFan(XiaomiGenericFan): if preset_mode == ATTR_MODE_NATURE: await self._try_command( "Setting natural fan speed percentage of the miio device failed.", - self._device.set_natural_speed, + self._device.set_natural_speed, # type: ignore[attr-defined] self._percentage, ) else: await self._try_command( "Setting direct fan speed percentage of the miio device failed.", - self._device.set_direct_speed, + self._device.set_direct_speed, # type: ignore[attr-defined] self._percentage, ) @@ -1041,13 +1101,13 @@ class XiaomiFan(XiaomiGenericFan): if self._nature_mode: await self._try_command( "Setting fan speed percentage of the miio device failed.", - self._device.set_natural_speed, + self._device.set_natural_speed, # type: ignore[attr-defined] percentage, ) else: await self._try_command( "Setting fan speed percentage of the miio device failed.", - self._device.set_direct_speed, + self._device.set_direct_speed, # type: ignore[attr-defined] percentage, ) self._percentage = percentage @@ -1061,7 +1121,13 @@ class XiaomiFan(XiaomiGenericFan): class XiaomiFanP5(XiaomiGenericFan): """Representation of a Xiaomi Fan P5.""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) @@ -1089,7 +1155,7 @@ class XiaomiFanP5(XiaomiGenericFan): """Set the preset mode of the fan.""" await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ) self._preset_mode = preset_mode @@ -1104,7 +1170,7 @@ class XiaomiFanP5(XiaomiGenericFan): await self._try_command( "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, + self._device.set_speed, # type: ignore[attr-defined] percentage, ) self._percentage = percentage @@ -1145,7 +1211,7 @@ class XiaomiFanMiot(XiaomiGenericFan): """Set the preset mode of the fan.""" await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ) self._preset_mode = preset_mode @@ -1160,7 +1226,7 @@ class XiaomiFanMiot(XiaomiGenericFan): result = await self._try_command( "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, + self._device.set_speed, # type: ignore[attr-defined] percentage, ) if result: @@ -1184,7 +1250,13 @@ class XiaomiFanZA5(XiaomiFanMiot): class XiaomiFan1C(XiaomiFanMiot): """Representation of a Xiaomi Fan 1C (Standing Fan 2 Lite).""" - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize MIOT fan with speed count.""" super().__init__(device, entry, unique_id, coordinator) self._speed_count = 3 @@ -1221,7 +1293,7 @@ class XiaomiFan1C(XiaomiFanMiot): result = await self._try_command( "Setting fan speed percentage of the miio device failed.", - self._device.set_speed, + self._device.set_speed, # type: ignore[attr-defined] speed, ) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index bf87f18e14a..49ae58ed2ef 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -4,6 +4,7 @@ import logging import math from typing import Any +from miio import Device as MiioDevice from miio.integrations.humidifier.deerma.airhumidifier_mjjsq import ( OperationMode as AirhumidifierMjjsqOperationMode, ) @@ -23,6 +24,7 @@ from homeassistant.components.humidifier import ( from homeassistant.const import ATTR_MODE, CONF_DEVICE, CONF_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.percentage import percentage_to_ranged_value from .const import ( @@ -108,26 +110,35 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): +class XiaomiGenericHumidifier( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], HumidifierEntity +): """Representation of a generic Xiaomi humidifier device.""" _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES _attr_name = None - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the generic Xiaomi device.""" super().__init__(device, entry, unique_id, coordinator=coordinator) - self._attributes = {} - self._mode = None + self._attributes: dict[str, Any] = {} + self._mode: str | int | None = None self._humidity_steps = 100 self._target_humidity: float | None = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" result = await self._try_command( - "Turning the miio device on failed.", self._device.on + "Turning the miio device on failed.", + self._device.on, # type: ignore[attr-defined] ) if result: self._attr_is_on = True @@ -136,7 +147,8 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" result = await self._try_command( - "Turning the miio device off failed.", self._device.off + "Turning the miio device off failed.", + self._device.off, # type: ignore[attr-defined] ) if result: @@ -159,7 +171,13 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): available_modes: list[str] - def __init__(self, device, entry, unique_id, coordinator): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + coordinator: DataUpdateCoordinator[Any], + ) -> None: """Initialize the plug switch.""" super().__init__(device, entry, unique_id, coordinator) @@ -228,7 +246,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): _LOGGER.debug("Setting the target humidity to: %s", target_humidity) if await self._try_command( "Setting target humidity of the miio device failed.", - self._device.set_target_humidity, + self._device.set_target_humidity, # type: ignore[attr-defined] target_humidity, ): self._target_humidity = target_humidity @@ -243,7 +261,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): _LOGGER.debug("Setting the operation mode to: Auto") if await self._try_command( "Setting operation mode of the miio device to MODE_AUTO failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] AirhumidifierOperationMode.Auto, ): self._mode = AirhumidifierOperationMode.Auto.value @@ -261,7 +279,7 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): _LOGGER.debug("Setting the operation mode to: %s", mode) if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] AirhumidifierOperationMode[mode], ): self._mode = mode.lower() @@ -306,7 +324,7 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): _LOGGER.debug("Setting the humidity to: %s", target_humidity) if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_target_humidity, + self._device.set_target_humidity, # type: ignore[attr-defined] target_humidity, ): self._target_humidity = target_humidity @@ -320,7 +338,7 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): _LOGGER.debug("Setting the operation mode to: Auto") if await self._try_command( "Setting operation mode of the miio device to MODE_AUTO failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] AirhumidifierMiotOperationMode.Auto, ): self._mode = 0 @@ -339,7 +357,7 @@ class XiaomiAirHumidifierMiot(XiaomiAirHumidifier): if self.is_on: if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.REVERSE_MODE_MAPPING[mode], ): self._mode = self.REVERSE_MODE_MAPPING[mode].value @@ -381,7 +399,7 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): _LOGGER.debug("Setting the humidity to: %s", target_humidity) if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_target_humidity, + self._device.set_target_humidity, # type: ignore[attr-defined] target_humidity, ): self._target_humidity = target_humidity @@ -395,7 +413,7 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): _LOGGER.debug("Setting the operation mode to: Humidity") if await self._try_command( "Setting operation mode of the miio device to MODE_HUMIDITY failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] AirhumidifierMjjsqOperationMode.Humidity, ): self._mode = 3 @@ -411,7 +429,7 @@ class XiaomiAirHumidifierMjjsq(XiaomiAirHumidifier): if self.is_on: if await self._try_command( "Setting operation mode of the miio device failed.", - self._device.set_mode, + self._device.set_mode, # type: ignore[attr-defined] self.MODE_MAPPING[mode], ): self._mode = self.MODE_MAPPING[mode].value diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 9863397c82a..2f7066c6fdf 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,8 +4,9 @@ from __future__ import annotations import dataclasses from dataclasses import dataclass +from typing import Any -from miio import Device +from miio import Device as MiioDevice from homeassistant.components.number import ( DOMAIN as PLATFORM_DOMAIN, @@ -350,17 +351,19 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): +class XiaomiNumberEntity( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], NumberEntity +): """Representation of a generic Xiaomi attribute selector.""" entity_description: XiaomiMiioNumberDescription def __init__( self, - device: Device, + device: MiioDevice, entry: XiaomiMiioConfigEntry, unique_id: str, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[Any], description: XiaomiMiioNumberDescription, ) -> None: """Initialize the generic Xiaomi attribute selector.""" @@ -402,7 +405,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the target motor speed.""" return await self._try_command( "Setting the target motor speed of the miio device failed.", - self._device.set_speed, + self._device.set_speed, # type: ignore[attr-defined] motor_speed, ) @@ -410,7 +413,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the favorite level.""" return await self._try_command( "Setting the favorite level of the miio device failed.", - self._device.set_favorite_level, + self._device.set_favorite_level, # type: ignore[attr-defined] level, ) @@ -418,7 +421,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the fan level.""" return await self._try_command( "Setting the fan level of the miio device failed.", - self._device.set_fan_level, + self._device.set_fan_level, # type: ignore[attr-defined] level, ) @@ -426,21 +429,23 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the volume.""" return await self._try_command( "Setting the volume of the miio device failed.", - self._device.set_volume, + self._device.set_volume, # type: ignore[attr-defined] volume, ) async def async_set_oscillation_angle(self, angle: int) -> bool: """Set the volume.""" return await self._try_command( - "Setting angle of the miio device failed.", self._device.set_angle, angle + "Setting angle of the miio device failed.", + self._device.set_angle, # type: ignore[attr-defined] + angle, ) async def async_set_delay_off_countdown(self, delay_off_countdown: int) -> bool: """Set the delay off countdown.""" return await self._try_command( "Setting delay off miio device failed.", - self._device.delay_off, + self._device.delay_off, # type: ignore[attr-defined] delay_off_countdown, ) @@ -448,7 +453,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", - self._device.set_led_brightness_level, + self._device.set_led_brightness_level, # type: ignore[attr-defined] level, ) @@ -456,7 +461,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", - self._device.set_led_brightness, + self._device.set_led_brightness, # type: ignore[attr-defined] level, ) @@ -464,6 +469,6 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Set the target motor speed.""" return await self._try_command( "Setting the favorite rpm of the miio device failed.", - self._device.set_favorite_rpm, + self._device.set_favorite_rpm, # type: ignore[attr-defined] rpm, ) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 734de2c0ff4..6dff7cf8ede 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -4,8 +4,9 @@ from __future__ import annotations from dataclasses import dataclass, field import logging -from typing import NamedTuple +from typing import Any, NamedTuple +from miio import Device as MiioDevice from miio.fan_common import LedBrightness as FanLedBrightness from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( DisplayOrientation as AirfreshT2017DisplayOrientation, @@ -32,6 +33,7 @@ from homeassistant.components.select import SelectEntity, SelectEntityDescriptio from homeassistant.const import CONF_DEVICE, CONF_MODEL, EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_FLOW_TYPE, @@ -87,7 +89,7 @@ class AttributeEnumMapping(NamedTuple): enum_class: type -MODEL_TO_ATTR_MAP: dict[str, list] = { +MODEL_TO_ATTR_MAP: dict[str, list[AttributeEnumMapping]] = { MODEL_AIRFRESH_T2017: [ AttributeEnumMapping(ATTR_DISPLAY_ORIENTATION, AirfreshT2017DisplayOrientation), AttributeEnumMapping(ATTR_PTC_LEVEL, AirfreshT2017PtcLevel), @@ -232,10 +234,21 @@ async def async_setup_entry( ) -class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): +class XiaomiSelector( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], SelectEntity +): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, device, entry, unique_id, coordinator, description): + entity_description: XiaomiMiioSelectDescription + + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioSelectDescription, + ) -> None: """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator) self.entity_description = description @@ -244,9 +257,15 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiGenericSelector(XiaomiSelector): """Representation of a Xiaomi generic selector.""" - entity_description: XiaomiMiioSelectDescription - - def __init__(self, device, entry, unique_id, coordinator, description, enum_class): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioSelectDescription, + enum_class: type, + ) -> None: """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator, description) self._current_attr = enum_class( @@ -257,10 +276,10 @@ class XiaomiGenericSelector(XiaomiSelector): if description.options_map: self._options_map = {} - for key, val in enum_class._member_map_.items(): + for key, val in enum_class._member_map_.items(): # type: ignore[attr-defined] self._options_map[description.options_map[key]] = val else: - self._options_map = enum_class._member_map_ + self._options_map = enum_class._member_map_ # type: ignore[attr-defined] self._reverse_map = {val: key for key, val in self._options_map.items()} self._enum_class = enum_class diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 4ed09d93734..c0631d66e56 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from miio import AirQualityMonitor, Device as MiioDevice, DeviceException from miio.gateway.devices import SubDevice @@ -854,12 +854,21 @@ async def async_setup_entry( async_add_entities(entities) -class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): +class XiaomiGenericSensor( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], SensorEntity +): """Representation of a Xiaomi generic sensor.""" entity_description: XiaomiMiioSensorDescription - def __init__(self, device, entry, unique_id, coordinator, description): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioSensorDescription, + ) -> None: """Initialize the entity.""" super().__init__(device, entry, unique_id, coordinator) self.entity_description = description diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ae5dc96075e..6711c45922b 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -8,7 +8,13 @@ from functools import partial import logging from typing import Any -from miio import AirConditioningCompanionV3, ChuangmiPlug, DeviceException, PowerStrip +from miio import ( + AirConditioningCompanionV3, + ChuangmiPlug, + Device as MiioDevice, + DeviceException, + PowerStrip, +) from miio.gateway.devices import SubDevice from miio.gateway.devices.switch import Switch from miio.powerstrip import PowerMode @@ -520,12 +526,21 @@ async def async_setup_other_entry( async_add_entities(entities) -class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): +class XiaomiGenericCoordinatedSwitch( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[Any]], SwitchEntity +): """Representation of a Xiaomi Plug Generic.""" entity_description: XiaomiMiioSwitchDescription - def __init__(self, device, entry, unique_id, coordinator, description): + def __init__( + self, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator[Any], + description: XiaomiMiioSwitchDescription, + ) -> None: """Initialize the plug switch.""" super().__init__(device, entry, unique_id, coordinator) @@ -574,7 +589,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the buzzer on.""" return await self._try_command( "Turning the buzzer of the miio device on failed.", - self._device.set_buzzer, + self._device.set_buzzer, # type: ignore[attr-defined] True, ) @@ -582,7 +597,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the buzzer off.""" return await self._try_command( "Turning the buzzer of the miio device off failed.", - self._device.set_buzzer, + self._device.set_buzzer, # type: ignore[attr-defined] False, ) @@ -590,7 +605,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the child lock on.""" return await self._try_command( "Turning the child lock of the miio device on failed.", - self._device.set_child_lock, + self._device.set_child_lock, # type: ignore[attr-defined] True, ) @@ -598,7 +613,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the child lock off.""" return await self._try_command( "Turning the child lock of the miio device off failed.", - self._device.set_child_lock, + self._device.set_child_lock, # type: ignore[attr-defined] False, ) @@ -606,7 +621,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the display on.""" return await self._try_command( "Turning the display of the miio device on failed.", - self._device.set_display, + self._device.set_display, # type: ignore[attr-defined] True, ) @@ -614,7 +629,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the display off.""" return await self._try_command( "Turning the display of the miio device off failed.", - self._device.set_display, + self._device.set_display, # type: ignore[attr-defined] False, ) @@ -622,7 +637,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the dry mode on.""" return await self._try_command( "Turning the dry mode of the miio device on failed.", - self._device.set_dry, + self._device.set_dry, # type: ignore[attr-defined] True, ) @@ -630,7 +645,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the dry mode off.""" return await self._try_command( "Turning the dry mode of the miio device off failed.", - self._device.set_dry, + self._device.set_dry, # type: ignore[attr-defined] False, ) @@ -638,7 +653,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the dry mode on.""" return await self._try_command( "Turning the clean mode of the miio device on failed.", - self._device.set_clean_mode, + self._device.set_clean_mode, # type: ignore[attr-defined] True, ) @@ -646,7 +661,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the dry mode off.""" return await self._try_command( "Turning the clean mode of the miio device off failed.", - self._device.set_clean_mode, + self._device.set_clean_mode, # type: ignore[attr-defined] False, ) @@ -654,7 +669,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the led on.""" return await self._try_command( "Turning the led of the miio device on failed.", - self._device.set_led, + self._device.set_led, # type: ignore[attr-defined] True, ) @@ -662,7 +677,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the led off.""" return await self._try_command( "Turning the led of the miio device off failed.", - self._device.set_led, + self._device.set_led, # type: ignore[attr-defined] False, ) @@ -670,7 +685,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the learn mode on.""" return await self._try_command( "Turning the learn mode of the miio device on failed.", - self._device.set_learn_mode, + self._device.set_learn_mode, # type: ignore[attr-defined] True, ) @@ -678,7 +693,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn the learn mode off.""" return await self._try_command( "Turning the learn mode of the miio device off failed.", - self._device.set_learn_mode, + self._device.set_learn_mode, # type: ignore[attr-defined] False, ) @@ -686,7 +701,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn auto detect on.""" return await self._try_command( "Turning auto detect of the miio device on failed.", - self._device.set_auto_detect, + self._device.set_auto_detect, # type: ignore[attr-defined] True, ) @@ -694,7 +709,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn auto detect off.""" return await self._try_command( "Turning auto detect of the miio device off failed.", - self._device.set_auto_detect, + self._device.set_auto_detect, # type: ignore[attr-defined] False, ) @@ -702,7 +717,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer on.""" return await self._try_command( "Turning ionizer of the miio device on failed.", - self._device.set_ionizer, + self._device.set_ionizer, # type: ignore[attr-defined] True, ) @@ -710,7 +725,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer off.""" return await self._try_command( "Turning ionizer of the miio device off failed.", - self._device.set_ionizer, + self._device.set_ionizer, # type: ignore[attr-defined] False, ) @@ -718,7 +733,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer on.""" return await self._try_command( "Turning ionizer of the miio device on failed.", - self._device.set_anion, + self._device.set_anion, # type: ignore[attr-defined] True, ) @@ -726,7 +741,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer off.""" return await self._try_command( "Turning ionizer of the miio device off failed.", - self._device.set_anion, + self._device.set_anion, # type: ignore[attr-defined] False, ) @@ -734,7 +749,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer on.""" return await self._try_command( "Turning ionizer of the miio device on failed.", - self._device.set_ptc, + self._device.set_ptc, # type: ignore[attr-defined] True, ) @@ -742,7 +757,7 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Turn ionizer off.""" return await self._try_command( "Turning ionizer of the miio device off failed.", - self._device.set_ptc, + self._device.set_ptc, # type: ignore[attr-defined] False, ) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 62343391cf4..ca6ab084324 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -6,7 +6,7 @@ from functools import partial import logging from typing import Any -from miio import DeviceException +from miio import Device as MiioDevice, DeviceException import voluptuous as vol from homeassistant.components.vacuum import ( @@ -196,9 +196,9 @@ class MiroboVacuum( def __init__( self, - device, - entry, - unique_id, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ) -> None: """Initialize the Xiaomi vacuum cleaner robot handler.""" @@ -281,16 +281,23 @@ class MiroboVacuum( async def async_start(self) -> None: """Start or resume the cleaning task.""" await self._try_command( - "Unable to start the vacuum: %s", self._device.resume_or_start + "Unable to start the vacuum: %s", + self._device.resume_or_start, # type: ignore[attr-defined] ) async def async_pause(self) -> None: """Pause the cleaning task.""" - await self._try_command("Unable to set start/pause: %s", self._device.pause) + await self._try_command( + "Unable to set start/pause: %s", + self._device.pause, # type: ignore[attr-defined] + ) async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" - await self._try_command("Unable to stop: %s", self._device.stop) + await self._try_command( + "Unable to stop: %s", + self._device.stop, # type: ignore[attr-defined] + ) async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" @@ -307,22 +314,31 @@ class MiroboVacuum( ) return await self._try_command( - "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed_int + "Unable to set fan speed: %s", + self._device.set_fan_speed, # type: ignore[attr-defined] + fan_speed_int, ) async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" - await self._try_command("Unable to return home: %s", self._device.home) + await self._try_command( + "Unable to return home: %s", + self._device.home, # type: ignore[attr-defined] + ) async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" await self._try_command( - "Unable to start the vacuum for a spot clean-up: %s", self._device.spot + "Unable to start the vacuum for a spot clean-up: %s", + self._device.spot, # type: ignore[attr-defined] ) async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" - await self._try_command("Unable to locate the botvac: %s", self._device.find) + await self._try_command( + "Unable to locate the botvac: %s", + self._device.find, # type: ignore[attr-defined] + ) async def async_send_command( self, @@ -341,13 +357,15 @@ class MiroboVacuum( async def async_remote_control_start(self) -> None: """Start remote control mode.""" await self._try_command( - "Unable to start remote control the vacuum: %s", self._device.manual_start + "Unable to start remote control the vacuum: %s", + self._device.manual_start, # type: ignore[attr-defined] ) async def async_remote_control_stop(self) -> None: """Stop remote control mode.""" await self._try_command( - "Unable to stop remote control the vacuum: %s", self._device.manual_stop + "Unable to stop remote control the vacuum: %s", + self._device.manual_stop, # type: ignore[attr-defined] ) async def async_remote_control_move( @@ -356,7 +374,7 @@ class MiroboVacuum( """Move vacuum with remote control mode.""" await self._try_command( "Unable to move with remote control the vacuum: %s", - self._device.manual_control, + self._device.manual_control, # type: ignore[attr-defined] velocity=velocity, rotation=rotation, duration=duration, @@ -368,7 +386,7 @@ class MiroboVacuum( """Move vacuum one step with remote control mode.""" await self._try_command( "Unable to remote control the vacuum: %s", - self._device.manual_control_once, + self._device.manual_control_once, # type: ignore[attr-defined] velocity=velocity, rotation=rotation, duration=duration, @@ -378,7 +396,7 @@ class MiroboVacuum( """Goto the specified coordinates.""" await self._try_command( "Unable to send the vacuum cleaner to the specified coordinates: %s", - self._device.goto, + self._device.goto, # type: ignore[attr-defined] x_coord=x_coord, y_coord=y_coord, ) @@ -390,7 +408,7 @@ class MiroboVacuum( await self._try_command( "Unable to start cleaning of the specified segments: %s", - self._device.segment_clean, + self._device.segment_clean, # type: ignore[attr-defined] segments=segments, ) @@ -400,7 +418,10 @@ class MiroboVacuum( _zone.append(repeats) _LOGGER.debug("Zone with repeats: %s", zone) try: - await self.hass.async_add_executor_job(self._device.zoned_clean, zone) + await self.hass.async_add_executor_job( + self._device.zoned_clean, # type: ignore[attr-defined] + zone, + ) await self.coordinator.async_refresh() except (OSError, DeviceException) as exc: _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc) From c68ab714b7f9684d5e3abe57d35613be8ecfeb2b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 14:46:07 +0200 Subject: [PATCH 0889/1175] Add init type hints to XiaomiMiioEntity derived entities (#145611) --- .../components/xiaomi_miio/air_quality.py | 25 ++++-- .../components/xiaomi_miio/entity.py | 12 ++- homeassistant/components/xiaomi_miio/light.py | 76 ++++++++++++++++--- .../components/xiaomi_miio/sensor.py | 12 ++- .../components/xiaomi_miio/switch.py | 65 ++++++++++++---- 5 files changed, 155 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 4190f49e30c..c96a29a423c 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -3,7 +3,12 @@ from collections.abc import Callable import logging -from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException +from miio import ( + AirQualityMonitor, + AirQualityMonitorCGDN1, + Device as MiioDevice, + DeviceException, +) from homeassistant.components.air_quality import AirQualityEntity from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL, CONF_TOKEN @@ -40,12 +45,17 @@ PROP_TO_ATTR = { class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the entity.""" super().__init__(name, device, entry, unique_id) self._icon = "mdi:cloud" - self._available = None self._air_quality_index = None self._carbon_dioxide = None self._carbon_dioxide_equivalent = None @@ -170,12 +180,17 @@ class AirMonitorV1(AirMonitorB1): class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): """Air Quality class for cgllc.airm.cgdn1 device.""" - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the entity.""" super().__init__(name, device, entry, unique_id) self._icon = "mdi:cloud" - self._available = None self._carbon_dioxide = None self._particulate_matter_2_5 = None self._particulate_matter_10 = None diff --git a/homeassistant/components/xiaomi_miio/entity.py b/homeassistant/components/xiaomi_miio/entity.py index f8cdc69a12e..bb4e68f9f71 100644 --- a/homeassistant/components/xiaomi_miio/entity.py +++ b/homeassistant/components/xiaomi_miio/entity.py @@ -27,7 +27,13 @@ _LOGGER = logging.getLogger(__name__) class XiaomiMiioEntity(Entity): """Representation of a base Xiaomi Miio Entity.""" - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: MiioDevice, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the Xiaomi Miio Device.""" self._device = device self._model = entry.data[CONF_MODEL] @@ -35,7 +41,7 @@ class XiaomiMiioEntity(Entity): self._device_id = entry.unique_id self._unique_id = unique_id self._name = name - self._available = None + self._available = False @property def unique_id(self): @@ -50,6 +56,8 @@ class XiaomiMiioEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + if TYPE_CHECKING: + assert self._device_id is not None device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, manufacturer="Xiaomi", diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 03341ea9541..f452c704db2 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -259,15 +259,21 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _device: Ceil | PhilipsBulb | PhilipsEyecare | PhilipsMoonlight - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: Ceil | PhilipsBulb | PhilipsEyecare | PhilipsMoonlight, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) self._brightness = None - self._available = False self._state = None - self._state_attrs = {} + self._state_attrs: dict[str, Any] = {} @property def available(self) -> bool: @@ -348,7 +354,15 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): """Representation of a Generic Xiaomi Philips Light.""" - def __init__(self, name, device, entry, unique_id): + _device: Ceil | PhilipsBulb | PhilipsEyecare | PhilipsMoonlight + + def __init__( + self, + name: str, + device: Ceil | PhilipsBulb | PhilipsEyecare | PhilipsMoonlight, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -390,7 +404,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): """Set delayed turn off.""" await self._try_command( "Setting the turn off delay failed.", - self._device.delay_off, + self._device.delay_off, # type: ignore[union-attr] time_period.total_seconds(), ) @@ -421,12 +435,19 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): _attr_color_mode = ColorMode.COLOR_TEMP _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + _device: Ceil | PhilipsBulb | PhilipsMoonlight - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: Ceil | PhilipsBulb | PhilipsMoonlight, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._color_temp = None + self._color_temp: int | None = None @property def _current_mireds(self): @@ -575,7 +596,15 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Ceiling Lamp.""" - def __init__(self, name, device, entry, unique_id): + _device: Ceil + + def __init__( + self, + name: str, + device: Ceil, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -635,7 +664,15 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - def __init__(self, name, device, entry, unique_id): + _device: PhilipsEyecare + + def __init__( + self, + name: str, + device: PhilipsEyecare, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -748,7 +785,15 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" - def __init__(self, name, device, entry, unique_id): + _device: PhilipsEyecare + + def __init__( + self, + name: str, + device: PhilipsEyecare, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" name = f"{name} Ambient Light" if unique_id is not None: @@ -807,12 +852,19 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} + _device: PhilipsMoonlight - def __init__(self, name, device, entry, unique_id): + def __init__( + self, + name: str, + device: PhilipsMoonlight, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._hs_color = None + self._hs_color: tuple[float, float] | None = None self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) self._state_attrs.update( { diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c0631d66e56..9088dbb3a06 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -925,11 +925,19 @@ class XiaomiGenericSensor( class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" - def __init__(self, name, device, entry, unique_id, description): + _device: AirQualityMonitor + + def __init__( + self, + name: str, + device: AirQualityMonitor, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + description: XiaomiMiioSensorDescription, + ) -> None: """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._available = None self._state = None self._state_attrs = { ATTR_POWER: None, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 6711c45922b..2bd9e406a14 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -803,13 +803,20 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, device, entry, unique_id): + _device: AirConditioningCompanionV3 | ChuangmiPlug | PowerStrip + + def __init__( + self, + name: str, + device: AirConditioningCompanionV3 | ChuangmiPlug | PowerStrip, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) self._icon = "mdi:power-socket" - self._available = False - self._state = None + self._state: bool | None = None self._state_attrs = {ATTR_TEMPERATURE: None, ATTR_MODEL: self._model} self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @@ -918,7 +925,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): await self._try_command( "Setting the power price of the power strip failed", - self._device.set_power_price, + self._device.set_power_price, # type: ignore[union-attr] price, ) @@ -926,9 +933,17 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): """Representation of a Xiaomi Power Strip.""" - def __init__(self, name, plug, model, unique_id): + _device: PowerStrip + + def __init__( + self, + name: str, + plug: PowerStrip, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the plug switch.""" - super().__init__(name, plug, model, unique_id) + super().__init__(name, plug, entry, unique_id) if self._model == MODEL_POWER_STRIP_V2: self._device_features = FEATURE_FLAGS_POWER_STRIP_V2 @@ -995,7 +1010,16 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Representation of a Chuang Mi Plug V1 and V3.""" - def __init__(self, name, plug, entry, unique_id, channel_usb): + _device: ChuangmiPlug + + def __init__( + self, + name: str, + plug: ChuangmiPlug, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + channel_usb: bool, + ) -> None: """Initialize the plug switch.""" name = f"{name} USB" if channel_usb else name @@ -1015,11 +1039,13 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Turn a channel on.""" if self._channel_usb: result = await self._try_command( - "Turning the plug on failed", self._device.usb_on + "Turning the plug on failed", + self._device.usb_on, ) else: result = await self._try_command( - "Turning the plug on failed", self._device.on + "Turning the plug on failed", + self._device.on, ) if result: @@ -1030,7 +1056,8 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): """Turn a channel off.""" if self._channel_usb: result = await self._try_command( - "Turning the plug off failed", self._device.usb_off + "Turning the plug off failed", + self._device.usb_off, ) else: result = await self._try_command( @@ -1075,16 +1102,25 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): """Representation of a Xiaomi AirConditioning Companion.""" - def __init__(self, name, plug, model, unique_id): + _device: AirConditioningCompanionV3 + + def __init__( + self, + name: str, + plug: AirConditioningCompanionV3, + entry: XiaomiMiioConfigEntry, + unique_id: str | None, + ) -> None: """Initialize the acpartner switch.""" - super().__init__(name, plug, model, unique_id) + super().__init__(name, plug, entry, unique_id) self._state_attrs.update({ATTR_TEMPERATURE: None, ATTR_LOAD_POWER: None}) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the socket on.""" result = await self._try_command( - "Turning the socket on failed", self._device.socket_on + "Turning the socket on failed", + self._device.socket_on, ) if result: @@ -1094,7 +1130,8 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the socket off.""" result = await self._try_command( - "Turning the socket off failed", self._device.socket_off + "Turning the socket off failed", + self._device.socket_off, ) if result: From e95e9e1a337550d80a5b880908bb51e6c2c02b5e Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Mon, 26 May 2025 13:47:00 +0100 Subject: [PATCH 0890/1175] bump starlink-grpc-core to 1.2.3 due to API change upstream (#145261) --- homeassistant/components/starlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json index 15bad3ebc2e..cc787076e7a 100644 --- a/homeassistant/components/starlink/manifest.json +++ b/homeassistant/components/starlink/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/starlink", "iot_class": "local_polling", - "requirements": ["starlink-grpc-core==1.2.2"] + "requirements": ["starlink-grpc-core==1.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c280ba3fd7c..67cfc2c49c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2827,7 +2827,7 @@ starline==0.1.5 starlingbank==3.2 # homeassistant.components.starlink -starlink-grpc-core==1.2.2 +starlink-grpc-core==1.2.3 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d40d35fa15..b51c8823c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2295,7 +2295,7 @@ srpenergy==1.3.6 starline==0.1.5 # homeassistant.components.starlink -starlink-grpc-core==1.2.2 +starlink-grpc-core==1.2.3 # homeassistant.components.statsd statsd==3.2.1 From 150110e221c2222331beca3137d0016525ba5341 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Mon, 26 May 2025 14:56:16 +0200 Subject: [PATCH 0891/1175] add/fix miele program ids mapping (#145577) * add/fix miele program ids mapping * fix mistyped keys and base translations --- homeassistant/components/miele/const.py | 156 ++++++++++++++++++-- homeassistant/components/miele/strings.json | 125 +++++++++++++++- 2 files changed, 267 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index a72cf916cf3..0d11cbdd0a5 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -444,7 +444,7 @@ TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = { 7: "cool_air", 8: "express", 9: "cottons_eco", - 10: "automatic_plus", + 10: "gentle_smoothing", 12: "proofing", 13: "denim", 14: "shirts", @@ -505,6 +505,26 @@ OVEN_PROGRAM_ID: dict[int, str] = { 51: "moisture_plus_conventional_heat", 74: "moisture_plus_intensive_bake", 76: "moisture_plus_conventional_heat", + 97: "custom_program_1", + 98: "custom_program_2", + 99: "custom_program_3", + 100: "custom_program_4", + 101: "custom_program_5", + 102: "custom_program_6", + 103: "custom_program_7", + 104: "custom_program_8", + 105: "custom_program_9", + 106: "custom_program_10", + 107: "custom_program_11", + 108: "custom_program_12", + 109: "custom_program_13", + 110: "custom_program_14", + 111: "custom_program_15", + 112: "custom_program_16", + 113: "custom_program_17", + 114: "custom_program_18", + 115: "custom_program_19", + 116: "custom_program_20", 323: "pyrolytic", 326: "descale", 335: "shabbat_program", @@ -515,9 +535,94 @@ OVEN_PROGRAM_ID: dict[int, str] = { 360: "low_temperature_cooking", 361: "steam_cooking", 362: "keeping_warm", - 512: "1_tray", - 513: "2_trays", - 529: "baking_tray", + 364: "apple_sponge", + 365: "apple_pie", + 367: "sponge_base", + 368: "swiss_roll", + 369: "butter_cake", + 373: "marble_cake", + 374: "fruit_streusel_cake", + 375: "madeira_cake", + 378: "blueberry_muffins", + 379: "walnut_muffins", + 382: "baguettes", + 383: "flat_bread", + 384: "plaited_loaf", + 385: "seeded_loaf", + 386: "white_bread_baking_tin", + 387: "white_bread_on_tray", + 394: "duck", + 396: "chicken_whole", + 397: "chicken_thighs", + 401: "turkey_whole", + 402: "turkey_drumsticks", + 406: "veal_fillet_roast", + 407: "veal_fillet_low_temperature_cooking", + 408: "veal_knuckle", + 409: "saddle_of_veal_roast", + 410: "saddle_of_veal_low_temperature_cooking", + 411: "braised_veal", + 415: "leg_of_lamb", + 419: "saddle_of_lamb_roast", + 420: "saddle_of_lamb_low_temperature_cooking", + 422: "beef_fillet_roast", + 423: "beef_fillet_low_temperature_cooking", + 427: "braised_beef", + 428: "roast_beef_roast", + 429: "roast_beef_low_temperature_cooking", + 435: "pork_smoked_ribs_roast", + 436: "pork_smoked_ribs_low_temperature_cooking", + 443: "ham_roast", + 449: "pork_fillet_roast", + 450: "pork_fillet_low_temperature_cooking", + 454: "saddle_of_venison", + 455: "rabbit", + 456: "saddle_of_roebuck", + 461: "salmon_fillet", + 464: "potato_cheese_gratin", + 486: "trout", + 491: "carp", + 492: "salmon_trout", + 496: "springform_tin_15cm", + 497: "springform_tin_20cm", + 498: "springform_tin_25cm", + 499: "fruit_flan_puff_pastry", + 500: "fruit_flan_short_crust_pastry", + 501: "sachertorte", + 502: "chocolate_hazlenut_cake_one_large", + 503: "chocolate_hazlenut_cake_several_small", + 504: "stollen", + 505: "drop_cookies_1_tray", + 506: "drop_cookies_2_trays", + 507: "linzer_augen_1_tray", + 508: "linzer_augen_2_trays", + 509: "almond_macaroons_1_tray", + 510: "almond_macaroons_2_trays", + 512: "biscuits_short_crust_pastry_1_tray", + 513: "biscuits_short_crust_pastry_2_trays", + 514: "vanilla_biscuits_1_tray", + 515: "vanilla_biscuits_2_trays", + 516: "choux_buns", + 518: "spelt_bread", + 519: "walnut_bread", + 520: "mixed_rye_bread", + 522: "dark_mixed_grain_bread", + 525: "multigrain_rolls", + 526: "rye_rolls", + 527: "white_rolls", + 528: "tart_flambe", + 529: "pizza_yeast_dough_baking_tray", + 530: "pizza_yeast_dough_round_baking_tine", + 531: "pizza_oil_cheese_dough_baking_tray", + 532: "pizza_oil_cheese_dough_round_baking_tine", + 533: "quiche_lorraine", + 534: "savoury_flan_puff_pastry", + 535: "savoury_flan_short_crust_pastry", + 536: "osso_buco", + 539: "beef_hash", + 543: "pork_with_crackling", + 550: "potato_gratin", + 551: "cheese_souffle", 554: "baiser_one_large", 555: "baiser_several_small", 556: "lemon_meringue_pie", @@ -525,6 +630,19 @@ OVEN_PROGRAM_ID: dict[int, str] = { 621: "prove_15_min", 622: "prove_30_min", 623: "prove_45_min", + 624: "belgian_sponge_cake", + 625: "goose_unstuffed", + 634: "rack_of_lamb_with_vegetables", + 635: "yorkshire_pudding", + 636: "meat_loaf", + 695: "swiss_farmhouse_bread", + 696: "plaited_swiss_loaf", + 697: "tiger_bread", + 698: "ginger_loaf", + 699: "goose_stuffed", + 700: "beef_wellington", + 701: "pork_belly", + 702: "pikeperch_fillet_with_vegetables", 99001: "steam_bake", 17003: "no_program", } @@ -729,6 +847,26 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { 72: "sous_vide", 75: "eco_steam_cooking", 77: "rapid_steam_cooking", + 97: "custom_program_1", + 98: "custom_program_2", + 99: "custom_program_3", + 100: "custom_program_4", + 101: "custom_program_5", + 102: "custom_program_6", + 103: "custom_program_7", + 104: "custom_program_8", + 105: "custom_program_9", + 106: "custom_program_10", + 107: "custom_program_11", + 108: "custom_program_12", + 109: "custom_program_13", + 110: "custom_program_14", + 111: "custom_program_15", + 112: "custom_program_16", + 113: "custom_program_17", + 114: "custom_program_18", + 115: "custom_program_19", + 116: "custom_program_20", 326: "descale", 330: "menu_cooking", 2018: "reheating_with_steam", @@ -970,7 +1108,7 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = { 2429: "pumpkin_soup", 2430: "meat_with_rice", 2431: "beef_casserole", - 2450: "risotto", + 2450: "pumpkin_risotto", 2451: "risotto", 2453: "rice_pudding_steam_cooking", 2454: "rice_pudding_rapid_steam_cooking", @@ -1146,10 +1284,10 @@ STATE_PROGRAM_ID: dict[int, dict[int, str]] = { MieleAppliance.DISHWASHER: DISHWASHER_PROGRAM_ID, MieleAppliance.DISH_WARMER: DISH_WARMER_PROGRAM_ID, MieleAppliance.OVEN: OVEN_PROGRAM_ID, - MieleAppliance.OVEN_MICROWAVE: OVEN_PROGRAM_ID, - MieleAppliance.STEAM_OVEN_MK2: OVEN_PROGRAM_ID, - MieleAppliance.STEAM_OVEN: OVEN_PROGRAM_ID, - MieleAppliance.STEAM_OVEN_COMBI: OVEN_PROGRAM_ID, + MieleAppliance.OVEN_MICROWAVE: OVEN_PROGRAM_ID | STEAM_OVEN_MICRO_PROGRAM_ID, + MieleAppliance.STEAM_OVEN_MK2: OVEN_PROGRAM_ID | STEAM_OVEN_MICRO_PROGRAM_ID, + MieleAppliance.STEAM_OVEN_COMBI: OVEN_PROGRAM_ID | STEAM_OVEN_MICRO_PROGRAM_ID, + MieleAppliance.STEAM_OVEN: STEAM_OVEN_MICRO_PROGRAM_ID, MieleAppliance.STEAM_OVEN_MICRO: STEAM_OVEN_MICRO_PROGRAM_ID, MieleAppliance.WASHER_DRYER: WASHING_MACHINE_PROGRAM_ID, MieleAppliance.ROBOT_VACUUM_CLEANER: ROBOT_VACUUM_CLEANER_PROGRAM_ID, diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 2cbc4f2f5f4..55d1769daf8 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -330,9 +330,11 @@ "program_id": { "name": "Program", "state": { - "1_tray": "1 tray", - "2_trays": "2 trays", "amaranth": "Amaranth", + "almond_macaroons_1_tray": "Almond macaroons (1 tray)", + "almond_macaroons_2_trays": "Almond macaroons (2 trays)", + "apple_pie": "Apple pie", + "apple_sponge": "Apple sponge", "apples_diced": "Apples (diced)", "apples_halved": "Apples (halved)", "apples_quartered": "Apples (quartered)", @@ -344,6 +346,8 @@ "apricots_halved_steam_cooking": "Apricots (halved, steam cooking)", "apricots_quartered": "Apricots (quartered)", "apricots_wedges": "Apricots (wedges)", + "savoury_flan_puff_pastry": "Savoury flan, puff pastry", + "savoury_flan_short_crust_pastry": "Savoury flan, short crust pastry", "artichokes_large": "Artichokes large", "artichokes_medium": "Artichokes medium", "artichokes_small": "Artichokes small", @@ -353,15 +357,18 @@ "auto_roast": "Auto roast", "automatic": "Automatic", "automatic_plus": "Automatic plus", - "baking_tray": "Baking tray", + "baguettes": "Baguettes", "barista_assistant": "BaristaAssistant", - "baser_one_large": "Baiser one large", - "baser_severall_small": "Baiser several small", + "baser_one_large": "Baiser (one large)", + "baser_severall_small": "Baiser (several small)", "basket_program": "Basket program", "basmati_rice_rapid_steam_cooking": "Basmati rice (rapid steam cooking)", "basmati_rice_steam_cooking": "Basmati rice (steam cooking)", "bed_linen": "Bed linen", "beef_casserole": "Beef casserole", + "beef_fillet_low_temperature_cooking": "Beef fillet (low temperature cooking)", + "beef_fillet_roast": "Beef fillet (roast)", + "beef_hash": "Beef hash", "beef_tenderloin": "Beef tenderloin", "beef_tenderloin_medaillons_1_cm_low_temperature_cooking": "Beef tenderloin (medaillons, 1 cm, low-temperature cooking)", "beef_tenderloin_medaillons_1_cm_steam_cooking": "Beef tenderloin (medaillons, 1 cm, steam cooking)", @@ -369,9 +376,11 @@ "beef_tenderloin_medaillons_2_cm_steam_cooking": "Beef tenderloin (medaillons, 2 cm, steam cooking)", "beef_tenderloin_medaillons_3_cm_low_temperature_cooking": "Beef tenderloin (medaillons, 3 cm, low-temperature cooking)", "beef_tenderloin_medaillons_3_cm_steam_cooking": "Beef tenderloin (medaillons, 3 cm, steam cooking)", + "beef_wellington": "Beef Wellington", "beetroot_whole_large": "Beetroot (whole, large)", "beetroot_whole_medium": "Beetroot (whole, medium)", "beetroot_whole_small": "Beetroot (whole, small)", + "belgian_sponge_cake": "Belgian sponge cake", "beluga_lentils": "Beluga lentils", "black_beans": "Black beans", "black_salsify_medium": "Black salsify (medium)", @@ -379,12 +388,15 @@ "black_salsify_thin": "Black salsify (thin)", "black_tea": "Black tea", "blanching": "Blanching", + "blueberry_muffins": "Blueberry muffins", "bologna_sausage": "Bologna sausage", "bottling": "Bottling", "bottling_hard": "Bottling (hard)", "bottling_medium": "Bottling (medium)", "bottling_soft": "Bottling (soft)", "bottom_heat": "Bottom heat", + "braised_beef": "Braised beef", + "braised_veal": "Braised veal", "bread_dumplings_boil_in_the_bag": "Bread dumplings (boil-in-the-bag)", "bread_dumplings_fresh": "Bread dumplings (fresh)", "brewing_unit_degrease": "Brewing unit degrease", @@ -407,6 +419,7 @@ "bunched_carrots_whole_large": "Bunched carrots (whole, large)", "bunched_carrots_whole_medium": "Bunched carrots (whole, medium)", "bunched_carrots_whole_small": "Bunched carrots (whole, small)", + "butter_cake": "Butter cake", "cafe_au_lait": "Café au lait", "caffe_latte": "Caffè latte", "cappuccino": "Cappuccino", @@ -435,13 +448,19 @@ "chanterelle": "Chanterelle", "char": "Char", "check_appliance": "Check appliance", + "cheese_souffle": "Cheese souffle", "cheesecake_one_large": "Cheesecake (one large)", "cheesecake_several_small": "Cheesecake (several small)", "chick_peas": "Chick peas", + "chicken_thighs": "Chicken thighs", "chicken_tikka_masala_with_rice": "Chicken Tikka Masala with rice", + "chicken_whole": "Chicken", "chinese_cabbage_cut": "Chinese cabbage (cut)", + "chocolate_hazlenut_cake_one_large": "Chocolate hazlenut cake (one large)", + "chocolate_hazlenut_cake_several_small": "Chocolate hazlenut cake (several small)", "chongming_rapid_steam_cooking": "Chongming (rapid steam cooking)", "chongming_steam_cooking": "Chongming (steam cooking)", + "choux_buns": "Choux buns", "christmas_pudding_cooking": "Christmas pudding (cooking)", "christmas_pudding_heating": "Christmas pudding (heating)", "clean_machine": "Clean machine", @@ -458,6 +477,8 @@ "common_sole_fillet_2_cm": "Common sole (fillet, 2 cm)", "conventional_heat": "Conventional heat", "cook_bacon": "Cook bacon", + "biscuits_short_crust_pastry_1_tray": "Biscuits, short crust pastry (1 tray)", + "biscuits_short_crust_pastry_2_trays": "Biscuits, short crust pastry (2 trays)", "cool_air": "Cool air", "corn_on_the_cob": "Corn on the cob", "cottons": "Cottons", @@ -468,7 +489,30 @@ "cranberries": "Cranberries", "crevettes": "Crevettes", "curtains": "Curtains", + "custom_program_1": "Custom program 1", + "custom_program_2": "Custom program 2", + "custom_program_3": "Custom program 3", + "custom_program_4": "Custom program 4", + "custom_program_5": "Custom program 5", + "custom_program_6": "Custom program 6", + "custom_program_7": "Custom program 7", + "custom_program_8": "Custom program 8", + "custom_program_9": "Custom program 9", + "custom_program_10": "Custom program 10", + "custom_program_11": "Custom program 11", + "custom_program_12": "Custom program 12", + "custom_program_13": "Custom program 13", + "custom_program_14": "Custom program 14", + "custom_program_15": "Custom program 15", + "custom_program_16": "Custom program 16", + "custom_program_17": "Custom program 17", + "custom_program_18": "Custom program 18", + "custom_program_19": "Custom program 19", + "custom_program_20": "Custom program 20", + "drop_cookies_1_tray": "Drop cookies (1 tray)", + "drop_cookies_2_trays": "Drop cookies (2 trays)", "dark_garments": "Dark garments", + "dark_mixed_grain_bread": "Dark mixed grain bread", "decrystallise_honey": "Decrystallise honey", "defrost": "Defrost", "defrosting_with_microwave": "Defrosting with microwave", @@ -481,6 +525,7 @@ "down_duvets": "Down duvets", "down_filled_items": "Down-filled items", "drain_spin": "Drain/spin", + "duck": "Duck", "dutch_hash": "Dutch hash", "eco": "ECO", "eco_40_60": "ECO 40-60", @@ -503,8 +548,12 @@ "fennel_quartered": "Fennel (quartered)", "fennel_strips": "Fennel (strips)", "first_wash": "First wash", + "flat_bread": "Flat bread", "flat_white": "Flat white", "freshen_up": "Freshen up", + "fruit_streusel_cake": "Fruit streusel cake", + "fruit_flan_puff_pastry": "Fruit flan, puff pastry", + "fruit_flan_short_crust_pastry": "Fruit flan, short crust pastry", "fruit_tea": "Fruit tea", "full_grill": "Full grill", "gentle": "Gentle", @@ -515,9 +564,12 @@ "german_turnip_diced": "German turnip (diced)", "gilt_head_bream_fillet": "Gilt-head bream (fillet)", "gilt_head_bream_whole": "Gilt-head bream (whole)", + "ginger_loaf": "Ginger loaf", "glasses_warm": "Glasses warm", "gnocchi_fresh": "Gnocchi (fresh)", "goose_barnacles": "Goose barnacles", + "goose_stuffed": "Goose stuffed", + "goose_unstuffed": "Goose unstuffed", "gooseberries": "Gooseberries", "goulash_soup": "Goulash soup", "green_asparagus_medium": "Green asparagus (medium)", @@ -533,6 +585,7 @@ "greenage_plums": "Greenage plums", "halibut_fillet_2_cm": "Halibut (fillet, 2 cm)", "halibut_fillet_3_cm": "Halibut (fillet, 3 cm)", + "ham_roast": "Ham roast", "heating_damp_flannels": "Heating damp flannels", "hens_eggs_size_l_hard": "Hen’s eggs (size „L“, hard)", "hens_eggs_size_l_medium": "Hen’s eggs (size „L“, medium)", @@ -572,17 +625,23 @@ "latte_macchiato": "Latte macchiato", "leek_pieces": "Leek (pieces)", "leek_rings": "Leek (rings)", + "leg_of_lamb": "Leg of lamb", "lemon_meringue_pie": "Lemon meringue pie", + "linzer_augen_1_tray": "Linzer Augen (1 tray)", + "linzer_augen_2_trays": "Linzer Augen (2 trays)", "long_coffee": "Long coffee", "long_grain_rice_general_rapid_steam_cooking": "Long grain rice (general, rapid steam cooking)", "long_grain_rice_general_steam_cooking": "Long grain rice (general, steam cooking)", "low_temperature_cooking": "Low temperature cooking", "maintenance": "Maintenance program", + "madeira_cake": "Madeira cake", "make_yoghurt": "Make yoghurt", "mangel_cut": "Mangel (cut)", + "marble_cake": "Marble cake", "meat_for_soup_back_or_top_rib": "Meat for soup (back or top rib)", "meat_for_soup_brisket": "Meat for soup (brisket)", "meat_for_soup_leg_steak": "Meat for soup (leg steak)", + "meat_loaf": "Meat loaf", "meat_with_rice": "Meat with rice", "melt_chocolate": "Melt chocolate", "menu_cooking": "Menu cooking", @@ -593,10 +652,12 @@ "millet": "Millet", "minimum_iron": "Minimum iron", "mirabelles": "Mirabelles", + "mixed_rye_bread": "Mixed rye bread", "moisture_plus_auto_roast": "Moisture plus + Auto roast", "moisture_plus_conventional_heat": "Moisture plus + Conventional heat", "moisture_plus_fan_plus": "Moisture plus + Fan plus", "moisture_plus_intensive_bake": "Moisture plus + Intensive bake", + "multigrain_rolls": "Multigrain rolls", "mushrooms_diced": "Mushrooms (diced)", "mushrooms_halved": "Mushrooms (halved)", "mushrooms_quartered": "Mushrooms (quartered)", @@ -614,6 +675,7 @@ "normal": "[%key:common::state::normal%]", "oats_cracked": "Oats (cracked)", "oats_whole": "Oats (whole)", + "osso_buco": "Osso buco", "outerwear": "Outerwear", "oyster_mushroom_diced": "Oyster mushroom (diced)", "oyster_mushroom_strips": "Oyster mushroom (strips)", @@ -654,11 +716,17 @@ "pike_piece": "Pike (piece)", "pillows": "Pillows", "pinto_beans": "Pinto beans", + "pizza_oil_cheese_dough_baking_tray": "Pizza, oil cheese dough (baking tray)", + "pizza_oil_cheese_dough_round_baking_tine": "Pizza, oil cheese dough (round baking tine)", + "pizza_yeast_dough_baking_tray": "Pizza, yeast dough (baking tray)", + "pizza_yeast_dough_round_baking_tine": "Pizza, yeast dough (round baking tine)", "plaice_fillet_1_cm": "Plaice (fillet, 1 cm)", "plaice_fillet_2_cm": "Plaice (fillet, 2 cm)", "plaice_whole_2_cm": "Plaice (whole, 2 cm)", "plaice_whole_3_cm": "Plaice (whole, 3 cm)", "plaice_whole_4_cm": "Plaice (whole, 4 cm)", + "plaited_loaf": "Plaited loaf", + "plaited_swiss_loaf": "Plaited swiss loaf", "plums_halved": "Plums (halved)", "plums_whole": "Plums (whole)", "pointed_cabbage_cut": "Pointed cabbage (cut)", @@ -667,13 +735,21 @@ "polenta_swiss_style_fine_polenta": "Polenta Swiss style (fine polenta)", "polenta_swiss_style_medium_polenta": "Polenta Swiss style (medium polenta)", "popcorn": "Popcorn", + "pork_belly": "Pork belly", + "pork_fillet_low_temperature_cooking": "Pork fillet (low temperature cooking)", + "pork_fillet_roast": "Pork fillet (roast)", + "pork_smoked_ribs_low_temperature_cooking": "Pork smoked ribs (low temperature cooking)", + "pork_smoked_ribs_roast": "Pork smoked ribs (roast)", "pork_tenderloin_medaillons_3_cm": "Pork tenderloin (medaillons, 3 cm)", "pork_tenderloin_medaillons_4_cm": "Pork tenderloin (medaillons, 4 cm)", "pork_tenderloin_medaillons_5_cm": "Pork tenderloin (medaillons, 5 cm)", + "pork_with_crackling": "Pork with crackling", + "potato_cheese_gratin": "Potato cheese gratin", "potato_dumplings_half_half_boil_in_bag": "Potato dumplings (half/half, boil-in-bag)", "potato_dumplings_half_half_deep_frozen": "Potato dumplings (half/half, deep-frozen)", "potato_dumplings_raw_boil_in_bag": "Potato dumplings (raw, boil-in-bag)", "potato_dumplings_raw_deep_frozen": "Potato dumplings (raw, deep-frozen)", + "potato_gratin": "Potato gratin", "potatoes_floury_diced": "Potatoes (floury, diced)", "potatoes_floury_halved": "Potatoes (floury, halved)", "potatoes_floury_quartered": "Potatoes (floury, quartered)", @@ -714,12 +790,16 @@ "prove_45_min": "Prove for 45 min", "prove_dough": "Prove dough", "pumpkin_diced": "Pumpkin (diced)", + "pumpkin_risotto": "Pumpkin risotto", "pumpkin_soup": "Pumpkin soup", "pyrolytic": "Pyrolytic", + "quiche_lorraine": "Quiche Lorraine", "quick_mw": "Quick MW", "quick_power_wash": "QuickPowerWash", "quinces_diced": "Quinces (diced)", "quinoa": "Quinoa", + "rabbit": "Rabbit", + "rack_of_lamb_with_vegetables": "Rack of lamb with vegetables", "rapid_steam_cooking": "Rapid steam cooking", "ravioli_fresh": "Ravioli (fresh)", "razor_clams_large": "Razor clams (large)", @@ -742,6 +822,8 @@ "rinse_out_lint": "Rinse out lint", "risotto": "Risotto", "ristretto": "Ristretto", + "roast_beef_low_temperature_cooking": "Roast beef (low temperature cooking)", + "roast_beef_roast": "Roast beef (roast)", "romanesco_florets_large": "Romanesco florets (large)", "romanesco_florets_medium": "Romanesco florets (medium)", "romanesco_florets_small": "Romanesco florets (small)", @@ -754,7 +836,16 @@ "runner_beans_sliced": "Runner beans (sliced)", "runner_beans_whole": "Runner beans (whole)", "rye_cracked": "Rye (cracked)", + "rye_rolls": "Rye rolls", "rye_whole": "Rye (whole)", + "sachertorte": "Sachertorte", + "saddle_of_lamb_low_temperature_cooking": "Saddle of lamb (low temperature cooking)", + "saddle_of_lamb_roast": "Saddle of lamb (roast)", + "saddle_of_roebuck": "Saddle of roebuck", + "saddle_of_veal_low_temperature_cooking": "Saddle of veal (low temperature cooking)", + "saddle_of_veal_roast": "Saddle of veal (roast)", + "saddle_of_venison": "Saddle of venison", + "salmon_fillet": "Salmon fillet", "salmon_fillet_2_cm": "Salmon (fillet, 2 cm)", "salmon_fillet_3_cm": "Salmon (fillet, 3 cm)", "salmon_piece": "Salmon (piece)", @@ -767,6 +858,7 @@ "schupfnudeln_potato_noodels": "Schupfnudeln (potato noodels)", "sea_devil_fillet_3_cm": "Sea devil (fillet, 3 cm)", "sea_devil_fillet_4_cm": "Sea devil (fillet, 4 cm)", + "seeded_loaf": "Seeded loaf", "separate_rinse_starch": "Separate rinse/starch", "shabbat_program": "Shabbat program", "sheyang_rapid_steam_cooking": "Sheyang (rapid steam cooking)", @@ -789,29 +881,39 @@ "sour_cherries": "Sour cherries", "sous_vide": "Sous-vide", "spaetzle_fresh": "Spätzle (fresh)", + "spelt_bread": "Spelt bread", "spelt_cracked": "Spelt (cracked)", "spelt_whole": "Spelt (whole)", "spinach": "Spinach", + "sponge_base": "Sponge base", "sportswear": "Sportswear", "spot": "Spot", + "springform_tin_15cm": "Springform tin 15cm", + "springform_tin_20cm": "Springform tin 20cm", + "springform_tin_25cm": "Springform tin 25cm", "standard_pillows": "Standard pillows", "starch": "Starch", "steam_care": "Steam care", "steam_cooking": "Steam cooking", "steam_smoothing": "Steam smoothing", "sterilize_crockery": "Sterilize crockery", + "stollen": "Stollen", "stuffed_cabbage": "Stuffed cabbage", "sweat_onions": "Sweat onions", "swede_cut_into_batons": "Swede (cut into batons)", "swede_diced": "Swede (diced)", "sweet_cheese_dumplings": "Sweet cheese dumplings", "sweet_cherries": "Sweet cherries", + "swiss_farmhouse_bread": "Swiss farmhouse bread", + "swiss_roll": "Swiss roll", "swiss_toffee_cream_100_ml": "Swiss toffee cream (100 ml)", "swiss_toffee_cream_150_ml": "Swiss toffee cream (150 ml)", "tagliatelli_fresh": "Tagliatelli (fresh)", "tall_items": "Tall items", + "tart_flambe": "Tart flambè", "teltow_turnip_diced": "Teltow turnip (diced)", "teltow_turnip_sliced": "Teltow turnip (sliced)", + "tiger_bread": "Tiger bread", "tilapia_fillet_1_cm": "Tilapia (fillet, 1 cm)", "tilapia_fillet_2_cm": "Tilapia (fillet, 2 cm)", "toffee_date_dessert_one_large": "Toffee-date dessert (one large)", @@ -829,17 +931,26 @@ "turbot_fillet_2_cm": "Turbot (fillet, 2 cm)", "turbot_fillet_3_cm": "Turbot (fillet, 3 cm)", "turkey_breast": "Turkey breast", + "turkey_drumsticks": "Turkey drumsticks", + "turkey_whole": "Turkey", "uonumma_koshihikari_rapid_steam_cooking": "Uonumma Koshihikari (rapid steam cooking)", "uonumma_koshihikari_steam_cooking": "Uonumma Koshihikari (steam cooking)", + "vanilla_biscuits_1_tray": "Vanilla biscuits (1 tray)", + "vanilla_biscuits_2_trays": "Vanilla biscuits (2 trays)", + "veal_fillet_low_temperature_cooking": "Veal fillet (low temperature cooking)", "veal_fillet_medaillons_1_cm": "Veal fillet (medaillons, 1 cm)", "veal_fillet_medaillons_2_cm": "Veal fillet (medaillons, 2 cm)", "veal_fillet_medaillons_3_cm": "Veal fillet (medaillons, 3 cm)", + "veal_fillet_roast": "Veal fillet (roast)", "veal_fillet_whole": "Veal fillet (whole)", + "veal_knuckle": "Veal knuckle", "veal_sausages": "Veal sausages", "venus_clams": "Venus clams", "very_hot_water": "Very hot water", "viennese_apple_strudel": "Viennese apple strudel", "viennese_silverside": "Viennese silverside", + "walnut_bread": "Walnut bread", + "walnut_muffins": "Walnut muffins", "warm_air": "Warm air", "wheat_cracked": "Wheat (cracked)", "wheat_whole": "Wheat (whole)", @@ -847,6 +958,9 @@ "white_asparagus_thick": "White asparagus (thick)", "white_asparagus_thin": "White asparagus (thin)", "white_beans": "White beans", + "white_bread_baking_tin": "White bread (baking tin)", + "white_bread_on_tray": "White bread (tray)", + "white_rolls": "White rolls", "white_tea": "White tea", "whole_ham_reheating": "Whole ham (reheating)", "whole_ham_steam_cooking": "Whole ham (steam cooking)", @@ -864,6 +978,7 @@ "yellow_beans_whole": "Yellow beans (whole)", "yellow_split_peas": "Yellow split peas", "yom_tov": "Yom tov", + "yorkshire_pudding": "Yorkshire pudding", "zander_fillet": "Zander (fillet)" } }, From ba0f6c3ba255fe249bc8b3606dffb86ff58a9748 Mon Sep 17 00:00:00 2001 From: Jan Rieger <271149+jrieger@users.noreply.github.com> Date: Mon, 26 May 2025 14:56:55 +0200 Subject: [PATCH 0892/1175] Add translations to Unifi Protect (#145548) * Add translations to Unifi Protect * address comments * change `CO` to `CO alarm` --- .../components/unifiprotect/binary_sensor.py | 117 +++-- .../components/unifiprotect/button.py | 11 +- .../components/unifiprotect/media_player.py | 4 +- .../components/unifiprotect/number.py | 20 +- .../components/unifiprotect/select.py | 22 +- .../components/unifiprotect/sensor.py | 97 ++-- .../components/unifiprotect/strings.json | 461 +++++++++++++++++- .../components/unifiprotect/switch.py | 78 +-- homeassistant/components/unifiprotect/text.py | 2 +- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_switch.py | 29 +- tests/components/unifiprotect/utils.py | 6 +- 12 files changed, 642 insertions(+), 207 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 0d904d3c3ba..b55fef45229 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -65,13 +65,13 @@ MOUNT_DEVICE_CLASS_MAP = { CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="dark", - name="Is dark", + translation_key="is_dark", icon="mdi:brightness-6", ufp_value="is_dark", ), ProtectBinaryEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -80,7 +80,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_led_status", @@ -89,7 +89,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="hdr_mode", - name="HDR mode", + translation_key="hdr_mode", icon="mdi:brightness-7", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_hdr", @@ -98,7 +98,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="high_fps", - name="High FPS", + translation_key="high_fps", icon="mdi:video-high-definition", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_highfps", @@ -107,7 +107,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="system_sounds", - name="System sounds", + translation_key="system_sounds", icon="mdi:speaker", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="has_speaker", @@ -117,7 +117,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_name", - name="Overlay: show name", + translation_key="overlay_show_name", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_name_enabled", @@ -125,7 +125,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_date", - name="Overlay: show date", + translation_key="overlay_show_date", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_date_enabled", @@ -133,7 +133,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_logo", - name="Overlay: show logo", + translation_key="overlay_show_logo", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_logo_enabled", @@ -141,7 +141,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="osd_bitrate", - name="Overlay: show bitrate", + translation_key="overlay_show_nerd_mode", icon="mdi:fullscreen", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="osd_settings.is_debug_enabled", @@ -149,14 +149,14 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="motion_enabled", - name="Detections: motion", + translation_key="detections_motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( key="smart_person", - name="Detections: person", + translation_key="detections_person", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_person", @@ -165,7 +165,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_vehicle", - name="Detections: vehicle", + translation_key="detections_vehicle", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_vehicle", @@ -174,7 +174,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_animal", - name="Detections: animal", + translation_key="detections_animal", icon="mdi:paw", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_animal", @@ -183,7 +183,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_package", - name="Detections: package", + translation_key="detections_package", icon="mdi:package-variant-closed", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_package", @@ -192,7 +192,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_licenseplate", - name="Detections: license plate", + translation_key="detections_license_plate", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_license_plate", @@ -201,7 +201,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_smoke", - name="Detections: smoke", + translation_key="detections_smoke", icon="mdi:fire", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_smoke", @@ -210,7 +210,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_cmonx", - name="Detections: CO", + translation_key="detections_co_alarm", icon="mdi:molecule-co", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_co", @@ -219,7 +219,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_siren", - name="Detections: siren", + translation_key="detections_siren", icon="mdi:alarm-bell", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_siren", @@ -228,7 +228,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_baby_cry", - name="Detections: baby cry", + translation_key="detections_baby_cry", icon="mdi:cradle", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_baby_cry", @@ -237,7 +237,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_speak", - name="Detections: speaking", + translation_key="detections_speaking", icon="mdi:account-voice", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_speaking", @@ -246,7 +246,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_bark", - name="Detections: barking", + translation_key="detections_barking", icon="mdi:dog", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_bark", @@ -255,7 +255,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_car_alarm", - name="Detections: car alarm", + translation_key="detections_car_alarm", icon="mdi:car", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_car_alarm", @@ -264,7 +264,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_car_horn", - name="Detections: car horn", + translation_key="detections_car_horn", icon="mdi:bugle", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_car_horn", @@ -273,7 +273,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="smart_glass_break", - name="Detections: glass break", + translation_key="detections_glass_break", icon="mdi:glass-fragile", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="can_detect_glass_break", @@ -282,7 +282,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="track_person", - name="Tracking: person", + translation_key="tracking_person", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.is_ptz", @@ -294,19 +294,18 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="dark", - name="Is dark", + translation_key="is_dark", icon="mdi:brightness-6", ufp_value="is_dark", ), ProtectBinaryEntityDescription( key="motion", - name="Motion detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_pir_motion_detected", ), ProtectBinaryEntityDescription( key="light", - name="Flood light", + translation_key="flood_light", icon="mdi:spotlight-beam", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="is_light_on", @@ -314,7 +313,7 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -323,7 +322,7 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="light_device_settings.is_indicator_enabled", @@ -336,7 +335,7 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( MOUNTABLE_SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key=_KEY_DOOR, - name="Contact", + translation_key="contact", device_class=BinarySensorDeviceClass.DOOR, ufp_value="is_opened", ufp_enabled="is_contact_sensor_enabled", @@ -346,34 +345,30 @@ MOUNTABLE_SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="leak", - name="Leak", device_class=BinarySensorDeviceClass.MOISTURE, ufp_value="is_leak_detected", ufp_enabled="is_leak_sensor_enabled", ), ProtectBinaryEntityDescription( key="battery_low", - name="Battery low", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ufp_value="battery_status.is_low", ), ProtectBinaryEntityDescription( key="motion", - name="Motion detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", ufp_enabled="is_motion_sensor_enabled", ), ProtectBinaryEntityDescription( key="tampering", - name="Tampering detected", device_class=BinarySensorDeviceClass.TAMPER, ufp_value="is_tampering_detected", ), ProtectBinaryEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="led_settings.is_enabled", @@ -381,7 +376,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="motion_enabled", - name="Motion detection", + translation_key="detections_motion", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="motion_settings.is_enabled", @@ -389,7 +384,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="temperature", - name="Temperature sensor", + translation_key="temperature_sensor", icon="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="temperature_settings.is_enabled", @@ -397,7 +392,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="humidity", - name="Humidity sensor", + translation_key="humidity_sensor", icon="mdi:water-percent", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="humidity_settings.is_enabled", @@ -405,7 +400,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="light", - name="Light sensor", + translation_key="light_sensor", icon="mdi:brightness-5", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="light_settings.is_enabled", @@ -413,7 +408,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ), ProtectBinaryEntityDescription( key="alarm", - name="Alarm sound detection", + translation_key="alarm_sound_detection", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="alarm_settings.is_enabled", ufp_perm=PermRequired.NO_WRITE, @@ -423,7 +418,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ProtectBinaryEventEntityDescription( key="doorbell", - name="Doorbell", + translation_key="doorbell", device_class=BinarySensorDeviceClass.OCCUPANCY, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", @@ -431,14 +426,13 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="motion", - name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), ProtectBinaryEventEntityDescription( key="smart_obj_any", - name="Object detected", + translation_key="object_detected", icon="mdi:eye", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_detect_event", @@ -446,7 +440,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_person", - name="Person detected", + translation_key="person_detected", icon="mdi:walk", ufp_obj_type=SmartDetectObjectType.PERSON, ufp_required_field="can_detect_person", @@ -455,7 +449,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_vehicle", - name="Vehicle detected", + translation_key="vehicle_detected", icon="mdi:car", ufp_obj_type=SmartDetectObjectType.VEHICLE, ufp_required_field="can_detect_vehicle", @@ -464,7 +458,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_animal", - name="Animal detected", + translation_key="animal_detected", icon="mdi:paw", ufp_obj_type=SmartDetectObjectType.ANIMAL, ufp_required_field="can_detect_animal", @@ -473,7 +467,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_obj_package", - name="Package detected", + translation_key="package_detected", icon="mdi:package-variant-closed", entity_registry_enabled_default=False, ufp_obj_type=SmartDetectObjectType.PACKAGE, @@ -483,7 +477,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_any", - name="Audio object detected", + translation_key="audio_object_detected", icon="mdi:eye", ufp_required_field="feature_flags.has_smart_detect", ufp_event_obj="last_smart_audio_detect_event", @@ -491,7 +485,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_smoke", - name="Smoke alarm detected", + translation_key="smoke_alarm_detected", icon="mdi:fire", ufp_obj_type=SmartDetectObjectType.SMOKE, ufp_required_field="can_detect_smoke", @@ -500,7 +494,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_cmonx", - name="CO alarm detected", + translation_key="co_alarm_detected", icon="mdi:molecule-co", ufp_required_field="can_detect_co", ufp_enabled="is_co_detection_on", @@ -509,7 +503,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_siren", - name="Siren detected", + translation_key="siren_detected", icon="mdi:alarm-bell", ufp_obj_type=SmartDetectObjectType.SIREN, ufp_required_field="can_detect_siren", @@ -518,7 +512,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_baby_cry", - name="Baby cry detected", + translation_key="baby_cry_detected", icon="mdi:cradle", ufp_obj_type=SmartDetectObjectType.BABY_CRY, ufp_required_field="can_detect_baby_cry", @@ -527,7 +521,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_speak", - name="Speaking detected", + translation_key="speaking_detected", icon="mdi:account-voice", ufp_obj_type=SmartDetectObjectType.SPEAK, ufp_required_field="can_detect_speaking", @@ -536,7 +530,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_bark", - name="Barking detected", + translation_key="barking_detected", icon="mdi:dog", ufp_obj_type=SmartDetectObjectType.BARK, ufp_required_field="can_detect_bark", @@ -545,7 +539,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_car_alarm", - name="Car alarm detected", + translation_key="car_alarm_detected", icon="mdi:car", ufp_obj_type=SmartDetectObjectType.BURGLAR, ufp_required_field="can_detect_car_alarm", @@ -554,7 +548,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_car_horn", - name="Car horn detected", + translation_key="car_horn_detected", icon="mdi:bugle", ufp_obj_type=SmartDetectObjectType.CAR_HORN, ufp_required_field="can_detect_car_horn", @@ -563,7 +557,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( ), ProtectBinaryEventEntityDescription( key="smart_audio_glass_break", - name="Glass break detected", + translation_key="glass_break_detected", icon="mdi:glass-fragile", ufp_obj_type=SmartDetectObjectType.GLASS_BREAK, ufp_required_field="can_detect_glass_break", @@ -575,14 +569,13 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="battery_low", - name="Battery low", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, ufp_value="battery_status.is_low", ), ProtectBinaryEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="led_settings.is_enabled", @@ -593,7 +586,7 @@ DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( VIEWER_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 7b766299946..2842f38d8a6 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -52,14 +52,13 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( key="reboot", entity_registry_enabled_default=False, device_class=ButtonDeviceClass.RESTART, - name="Reboot device", ufp_press="reboot", ufp_perm=PermRequired.WRITE, ), ProtectButtonEntityDescription( key="unadopt", + translation_key="unadopt_device", entity_registry_enabled_default=False, - name="Unadopt device", icon="mdi:delete", ufp_press="unadopt", ufp_perm=PermRequired.DELETE, @@ -68,7 +67,7 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( key="adopt", - name="Adopt device", + translation_key="adopt_device", icon="mdi:plus-circle", ufp_press="adopt", ) @@ -76,7 +75,7 @@ ADOPT_BUTTON = ProtectButtonEntityDescription[ProtectAdoptableDeviceModel]( SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="clear_tamper", - name="Clear tamper", + translation_key="clear_tamper", icon="mdi:notification-clear-all", ufp_press="clear_tamper", ufp_perm=PermRequired.WRITE, @@ -86,14 +85,14 @@ SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="play", - name="Play chime", + translation_key="play_chime", device_class=DEVICE_CLASS_CHIME_BUTTON, icon="mdi:play", ufp_press="play", ), ProtectButtonEntityDescription( key="play_buzzer", - name="Play buzzer", + translation_key="play_buzzer", icon="mdi:play", ufp_press="play_buzzer", ), diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index a1e60931026..2c2948823d0 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -29,7 +29,9 @@ from .entity import ProtectDeviceEntity _LOGGER = logging.getLogger(__name__) _SPEAKER_DESCRIPTION = MediaPlayerEntityDescription( - key="speaker", name="Speaker", device_class=MediaPlayerDeviceClass.SPEAKER + key="speaker", + translation_key="speaker", + device_class=MediaPlayerDeviceClass.SPEAKER, ) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 5dbf9f2b00e..0f0790105c5 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -64,7 +64,7 @@ def _get_chime_duration(obj: Camera) -> int: CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="wdr_value", - name="Wide dynamic range", + translation_key="wide_dynamic_range", icon="mdi:state-machine", entity_category=EntityCategory.CONFIG, ufp_min=0, @@ -77,7 +77,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="mic_level", - name="Microphone level", + translation_key="microphone_level", icon="mdi:microphone", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -92,7 +92,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="zoom_position", - name="Zoom level", + translation_key="zoom_level", icon="mdi:magnify-plus-outline", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -106,7 +106,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="chime_duration", - name="Chime duration", + translation_key="chime_duration", icon="mdi:bell", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -121,7 +121,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription( key="icr_lux", - name="Infrared custom lux trigger", + translation_key="infrared_custom_lux_trigger", icon="mdi:white-balance-sunny", entity_category=EntityCategory.CONFIG, ufp_min=0, @@ -138,7 +138,7 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="sensitivity", - name="Motion sensitivity", + translation_key="motion_sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -152,7 +152,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ProtectNumberEntityDescription[Light]( key="duration", - name="Auto-shutoff duration", + translation_key="auto_shutoff_duration", icon="mdi:camera-timer", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -169,7 +169,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="sensitivity", - name="Motion sensitivity", + translation_key="motion_sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, @@ -186,7 +186,7 @@ SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription[Doorlock]( key="auto_lock_time", - name="Auto-lock timeout", + translation_key="auto_lock_timeout", icon="mdi:walk", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, @@ -203,7 +203,7 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="volume", - name="Volume", + translation_key="volume", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 054c9430387..168fab584fa 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -193,7 +193,7 @@ async def _set_liveview(obj: Viewer, liveview_id: str) -> None: CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription( key="recording_mode", - name="Recording mode", + translation_key="recording_mode", icon="mdi:video-outline", entity_category=EntityCategory.CONFIG, ufp_options=DEVICE_RECORDING_MODES, @@ -204,7 +204,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="infrared", - name="Infrared mode", + translation_key="infrared_mode", icon="mdi:circle-opacity", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_led_ir", @@ -216,7 +216,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Camera]( key="doorbell_text", - name="Doorbell text", + translation_key="doorbell_text", icon="mdi:card-text", entity_category=EntityCategory.CONFIG, device_class=DEVICE_CLASS_LCD_MESSAGE, @@ -228,7 +228,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="chime_type", - name="Chime type", + translation_key="chime_type", icon="mdi:bell", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_chime", @@ -240,7 +240,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription( key="hdr_mode", - name="HDR mode", + translation_key="hdr_mode", icon="mdi:brightness-7", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_hdr", @@ -254,7 +254,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription[Light]( key=_KEY_LIGHT_MOTION, - name="Light mode", + translation_key="light_mode", icon="mdi:spotlight", entity_category=EntityCategory.CONFIG, ufp_options=MOTION_MODE_TO_LIGHT_MODE, @@ -264,7 +264,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Light]( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -277,7 +277,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription( key="mount_type", - name="Mount type", + translation_key="mount_type", icon="mdi:screwdriver", entity_category=EntityCategory.CONFIG, ufp_options=MOUNT_TYPES, @@ -288,7 +288,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ), ProtectSelectEntityDescription[Sensor]( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -301,7 +301,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription[Doorlock]( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.CONFIG, ufp_value="camera_id", @@ -314,7 +314,7 @@ DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ProtectSelectEntityDescription[Viewer]( key="viewer", - name="Liveview", + translation_key="liveview", icon="mdi:view-dashboard", entity_category=None, ufp_options_fn=_get_viewer_options, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index a719f36c2b3..f25a0302669 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -125,7 +125,7 @@ def _get_alarm_sound(obj: Sensor) -> str: ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="uptime", - name="Uptime", + translation_key="uptime", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -134,7 +134,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="ble_signal", - name="Bluetooth signal strength", + translation_key="bluetooth_signal_strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, @@ -145,7 +145,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="phy_rate", - name="Link speed", + translation_key="link_speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, entity_category=EntityCategory.DIAGNOSTIC, @@ -156,7 +156,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="wifi_signal", - name="WiFi signal strength", + translation_key="wifi_signal_strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_registry_enabled_default=False, @@ -170,7 +170,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="oldest_recording", - name="Oldest recording", + translation_key="oldest_recording", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -178,7 +178,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="storage_used", - name="Storage used", + translation_key="storage_used", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_category=EntityCategory.DIAGNOSTIC, @@ -189,7 +189,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="write_rate", - name="Disk write rate", + translation_key="disk_write_rate", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, entity_category=EntityCategory.DIAGNOSTIC, @@ -201,7 +201,6 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="voltage", - name="Voltage", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=UnitOfElectricPotential.VOLT, entity_category=EntityCategory.DIAGNOSTIC, @@ -214,7 +213,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="doorbell_last_trip_time", - name="Last doorbell ring", + translation_key="last_doorbell_ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:doorbell-video", ufp_required_field="feature_flags.is_doorbell", @@ -223,7 +222,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="lens_type", - name="Lens type", + translation_key="lens_type", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:camera-iris", ufp_required_field="has_removable_lens", @@ -231,7 +230,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="mic_level", - name="Microphone level", + translation_key="microphone_level", icon="mdi:microphone", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -242,7 +241,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="recording_mode", - name="Recording mode", + translation_key="recording_mode", icon="mdi:video-outline", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="recording_settings.mode.value", @@ -250,7 +249,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="infrared", - name="Infrared mode", + translation_key="infrared_mode", icon="mdi:circle-opacity", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_led_ir", @@ -259,7 +258,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="doorbell_text", - name="Doorbell text", + translation_key="doorbell_text", icon="mdi:card-text", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_lcd_screen", @@ -268,7 +267,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="chime_type", - name="Chime type", + translation_key="chime_type", icon="mdi:bell", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -280,7 +279,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="stats_rx", - name="Received data", + translation_key="received_data", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -292,7 +291,7 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="stats_tx", - name="Transferred data", + translation_key="transferred_data", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, entity_registry_enabled_default=False, @@ -307,7 +306,6 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="battery_level", - name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -316,7 +314,6 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="light_level", - name="Light level", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -325,7 +322,6 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="humidity_level", - name="Humidity level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -334,7 +330,6 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="temperature_level", - name="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -343,34 +338,34 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[Sensor]( key="alarm_sound", - name="Alarm sound detected", + translation_key="alarm_sound_detected", ufp_value_fn=_get_alarm_sound, ufp_enabled="is_alarm_sensor_enabled", ), ProtectSensorEntityDescription( key="door_last_trip_time", - name="Last open", + translation_key="last_open", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="open_status_changed_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last motion detected", + translation_key="last_motion_detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="motion_detected_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="tampering_last_trip_time", - name="Last tampering detected", + translation_key="last_tampering_detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="tampering_detected_at", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="sensitivity", - name="Motion sensitivity", + translation_key="sensitivity", icon="mdi:walk", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -379,7 +374,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="mount_type", - name="Mount type", + translation_key="mount_type", icon="mdi:screwdriver", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="mount_type", @@ -387,7 +382,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -398,7 +393,6 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="battery_level", - name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, @@ -407,7 +401,7 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -418,7 +412,7 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="uptime", - name="Uptime", + translation_key="uptime", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -426,7 +420,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="storage_utilization", - name="Storage utilization", + translation_key="storage_utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", entity_category=EntityCategory.DIAGNOSTIC, @@ -436,7 +430,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_rotating", - name="Type: timelapse video", + translation_key="type_timelapse_video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -446,7 +440,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_timelapse", - name="Type: continuous video", + translation_key="type_continuous_video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -456,7 +450,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="record_detections", - name="Type: detections video", + translation_key="type_detections_video", native_unit_of_measurement=PERCENTAGE, icon="mdi:server", entity_category=EntityCategory.DIAGNOSTIC, @@ -466,7 +460,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_HD", - name="Resolution: HD video", + translation_key="resolution_hd_video", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -476,7 +470,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_4K", - name="Resolution: 4K video", + translation_key="resolution_4k_video", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -486,7 +480,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="resolution_free", - name="Resolution: free space", + translation_key="resolution_free_space", native_unit_of_measurement=PERCENTAGE, icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, @@ -496,7 +490,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[NVR]( key="record_capacity", - name="Recording capacity", + translation_key="recording_capacity", native_unit_of_measurement=UnitOfTime.SECONDS, icon="mdi:record-rec", entity_category=EntityCategory.DIAGNOSTIC, @@ -508,7 +502,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="cpu_utilization", - name="CPU utilization", + translation_key="cpu_utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:speedometer", entity_registry_enabled_default=False, @@ -518,7 +512,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="cpu_temperature", - name="CPU temperature", + translation_key="cpu_temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_registry_enabled_default=False, @@ -528,7 +522,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[NVR]( key="memory_utilization", - name="Memory utilization", + translation_key="memory_utilization", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", entity_registry_enabled_default=False, @@ -542,9 +536,8 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( ProtectSensorEventEntityDescription( key="smart_obj_licenseplate", - name="License plate detected", icon="mdi:car", - translation_key="license_plate", + translation_key="license_plate_detected", ufp_obj_type=SmartDetectObjectType.LICENSE_PLATE, ufp_required_field="can_detect_license_plate", ufp_event_obj="last_license_plate_detect_event", @@ -555,14 +548,14 @@ LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = ( LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last motion detected", + translation_key="last_motion_detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="last_motion", entity_registry_enabled_default=False, ), ProtectSensorEntityDescription( key="sensitivity", - name="Motion sensitivity", + translation_key="motion_sensitivity", icon="mdi:walk", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -571,7 +564,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription[Light]( key="light_motion", - name="Light mode", + translation_key="light_mode", icon="mdi:spotlight", entity_category=EntityCategory.DIAGNOSTIC, ufp_value_fn=async_get_light_motion_current, @@ -579,7 +572,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ProtectSensorEntityDescription( key="paired_camera", - name="Paired camera", + translation_key="paired_camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="camera.display_name", @@ -590,7 +583,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="motion_last_trip_time", - name="Last motion detected", + translation_key="last_motion_detected", device_class=SensorDeviceClass.TIMESTAMP, ufp_value="last_motion", entity_registry_enabled_default=False, @@ -600,14 +593,14 @@ MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="last_ring", - name="Last ring", + translation_key="last_ring", device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:bell", ufp_value="last_ring", ), ProtectSensorEntityDescription( key="volume", - name="Volume", + translation_key="volume", icon="mdi:speaker", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -619,7 +612,7 @@ CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ProtectSensorEntityDescription( key="viewer", - name="Liveview", + translation_key="liveview", icon="mdi:view-dashboard", entity_category=EntityCategory.DIAGNOSTIC, ufp_value="liveview.name", diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d5a7d615399..46a60f4abfd 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -128,16 +128,469 @@ } }, "entity": { + "binary_sensor": { + "is_dark": { + "name": "Is dark" + }, + "ssh_enabled": { + "name": "SSH enabled" + }, + "status_light": { + "name": "Status light" + }, + "hdr_mode": { + "name": "HDR mode" + }, + "high_fps": { + "name": "High FPS" + }, + "system_sounds": { + "name": "System sounds" + }, + "overlay_show_name": { + "name": "Overlay: show name" + }, + "overlay_show_date": { + "name": "Overlay: show date" + }, + "overlay_show_logo": { + "name": "Overlay: show logo" + }, + "overlay_show_nerd_mode": { + "name": "Overlay: show nerd mode" + }, + "detections_motion": { + "name": "Detections: motion" + }, + "detections_person": { + "name": "Detections: person" + }, + "detections_vehicle": { + "name": "Detections: vehicle" + }, + "detections_animal": { + "name": "Detections: animal" + }, + "detections_package": { + "name": "Detections: package" + }, + "detections_license_plate": { + "name": "Detections: license plate" + }, + "detections_smoke": { + "name": "Detections: smoke" + }, + "detections_co_alarm": { + "name": "Detections: CO alarm" + }, + "detections_siren": { + "name": "Detections: siren" + }, + "detections_baby_cry": { + "name": "Detections: baby cry" + }, + "detections_speaking": { + "name": "Detections: speaking" + }, + "detections_barking": { + "name": "Detections: barking" + }, + "detections_car_alarm": { + "name": "Detections: car alarm" + }, + "detections_car_horn": { + "name": "Detections: car horn" + }, + "detections_glass_break": { + "name": "Detections: glass break" + }, + "tracking_person": { + "name": "Tracking: person" + }, + "flood_light": { + "name": "Flood light" + }, + "contact": { + "name": "Contact" + }, + "temperature_sensor": { + "name": "Temperature sensor" + }, + "humidity_sensor": { + "name": "Humidity sensor" + }, + "light_sensor": { + "name": "Light sensor" + }, + "alarm_sound_detection": { + "name": "Alarm sound detection" + }, + "doorbell": { + "name": "[%key:component::event::entity_component::doorbell::name%]" + }, + "object_detected": { + "name": "Object detected" + }, + "person_detected": { + "name": "Person detected" + }, + "vehicle_detected": { + "name": "Vehicle detected" + }, + "animal_detected": { + "name": "Animal detected" + }, + "package_detected": { + "name": "Package detected" + }, + "audio_object_detected": { + "name": "Audio object detected" + }, + "smoke_alarm_detected": { + "name": "Smoke alarm detected" + }, + "co_alarm_detected": { + "name": "CO alarm detected" + }, + "siren_detected": { + "name": "Siren detected" + }, + "baby_cry_detected": { + "name": "Baby cry detected" + }, + "speaking_detected": { + "name": "Speaking detected" + }, + "barking_detected": { + "name": "Barking detected" + }, + "car_alarm_detected": { + "name": "Car alarm detected" + }, + "car_horn_detected": { + "name": "Car horn detected" + }, + "glass_break_detected": { + "name": "Glass break detected" + } + }, + "button": { + "unadopt_device": { + "name": "Unadopt device" + }, + "adopt_device": { + "name": "Adopt device" + }, + "clear_tamper": { + "name": "Clear tamper" + }, + "play_chime": { + "name": "Play chime" + }, + "play_buzzer": { + "name": "Play buzzer" + } + }, + "media_player": { + "speaker": { + "name": "[%key:component::media_player::entity_component::speaker::name%]" + } + }, + "number": { + "wide_dynamic_range": { + "name": "Wide dynamic range" + }, + "microphone_level": { + "name": "Microphone level" + }, + "zoom_level": { + "name": "Zoom level" + }, + "chime_duration": { + "name": "Chime duration" + }, + "infrared_custom_lux_trigger": { + "name": "Infrared custom lux trigger" + }, + "motion_sensitivity": { + "name": "Motion sensitivity" + }, + "auto_shutoff_duration": { + "name": "Auto-shutoff duration" + }, + "auto_lock_timeout": { + "name": "Auto-lock timeout" + }, + "volume": { + "name": "[%key:component::sensor::entity_component::volume::name%]" + } + }, + "select": { + "recording_mode": { + "name": "Recording mode" + }, + "infrared_mode": { + "name": "Infrared mode" + }, + "doorbell_text": { + "name": "Doorbell text" + }, + "chime_type": { + "name": "Chime type" + }, + "hdr_mode": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]" + }, + "light_mode": { + "name": "Light mode" + }, + "paired_camera": { + "name": "Paired camera" + }, + "mount_type": { + "name": "Mount type" + }, + "liveview": { + "name": "Liveview" + } + }, "sensor": { - "license_plate": { + "uptime": { + "name": "Uptime" + }, + "bluetooth_signal_strength": { + "name": "Bluetooth signal strength" + }, + "link_speed": { + "name": "Link speed" + }, + "wifi_signal_strength": { + "name": "WiFi signal strength" + }, + "oldest_recording": { + "name": "Oldest recording" + }, + "storage_used": { + "name": "Storage used" + }, + "disk_write_rate": { + "name": "Disk write rate" + }, + "last_doorbell_ring": { + "name": "Last doorbell ring" + }, + "lens_type": { + "name": "Lens type" + }, + "microphone_level": { + "name": "[%key:component::unifiprotect::entity::number::microphone_level::name%]" + }, + "recording_mode": { + "name": "[%key:component::unifiprotect::entity::select::recording_mode::name%]" + }, + "infrared_mode": { + "name": "[%key:component::unifiprotect::entity::select::infrared_mode::name%]" + }, + "doorbell_text": { + "name": "[%key:component::unifiprotect::entity::select::doorbell_text::name%]" + }, + "chime_type": { + "name": "[%key:component::unifiprotect::entity::select::chime_type::name%]" + }, + "received_data": { + "name": "Received data" + }, + "transferred_data": { + "name": "Transferred data" + }, + "temperature": { + "name": "[%key:component::sensor::entity_component::temperature::name%]" + }, + "alarm_sound_detected": { + "name": "Alarm sound detected" + }, + "last_open": { + "name": "Last open" + }, + "last_motion_detected": { + "name": "Last motion detected" + }, + "last_tampering_detected": { + "name": "Last tampering detected" + }, + "motion_sensitivity": { + "name": "[%key:component::unifiprotect::entity::number::motion_sensitivity::name%]" + }, + "mount_type": { + "name": "[%key:component::unifiprotect::entity::select::mount_type::name%]" + }, + "paired_camera": { + "name": "[%key:component::unifiprotect::entity::select::paired_camera::name%]" + }, + "storage_utilization": { + "name": "Storage utilization" + }, + "type_timelapse_video": { + "name": "Type: timelapse video" + }, + "type_continuous_video": { + "name": "Type: continuous video" + }, + "type_detections_video": { + "name": "Type: detections video" + }, + "resolution_hd_video": { + "name": "Resolution: HD video" + }, + "resolution_4k_video": { + "name": "Resolution: 4K video" + }, + "resolution_free_space": { + "name": "Resolution: free space" + }, + "recording_capacity": { + "name": "Recording capacity" + }, + "cpu_utilization": { + "name": "CPU utilization" + }, + "cpu_temperature": { + "name": "CPU temperature" + }, + "memory_utilization": { + "name": "Memory utilization" + }, + "license_plate_detected": { + "name": "License plate detected", "state": { - "none": "Clear" + "none": "[%key:component::binary_sensor::entity_component::gas::state::off%]" } + }, + "light_mode": { + "name": "[%key:component::unifiprotect::entity::select::light_mode::name%]" + }, + "last_ring": { + "name": "Last ring" + }, + "volume": { + "name": "[%key:component::sensor::entity_component::volume::name%]" + }, + "liveview": { + "name": "[%key:component::unifiprotect::entity::select::liveview::name%]" + } + }, + "switch": { + "ssh_enabled": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::ssh_enabled::name%]" + }, + "status_light": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::status_light::name%]" + }, + "hdr_mode": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]" + }, + "high_fps": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::high_fps::name%]" + }, + "system_sounds": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::system_sounds::name%]" + }, + "overlay_show_name": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::overlay_show_name::name%]" + }, + "overlay_show_date": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::overlay_show_date::name%]" + }, + "overlay_show_logo": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::overlay_show_logo::name%]" + }, + "overlay_show_nerd_mode": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::overlay_show_nerd_mode::name%]" + }, + "color_night_vision": { + "name": "Color night vision" + }, + "motion": { + "name": "[%key:component::binary_sensor::entity_component::motion::name%]" + }, + "detections_motion": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_motion::name%]" + }, + "detections_person": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_person::name%]" + }, + "detections_vehicle": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_vehicle::name%]" + }, + "detections_animal": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_animal::name%]" + }, + "detections_package": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_package::name%]" + }, + "detections_license_plate": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_license_plate::name%]" + }, + "detections_smoke": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_smoke::name%]" + }, + "detections_co_alarm": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_co_alarm::name%]" + }, + "detections_siren": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_siren::name%]" + }, + "detections_baby_cry": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_baby_cry::name%]" + }, + "detections_speak": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_speaking::name%]" + }, + "detections_barking": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_barking::name%]" + }, + "detections_car_alarm": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_car_alarm::name%]" + }, + "detections_car_horn": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_car_horn::name%]" + }, + "detections_glass_break": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_glass_break::name%]" + }, + "tracking_person": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::tracking_person::name%]" + }, + "privacy_mode": { + "name": "Privacy mode" + }, + "temperature_sensor": { + "name": "Temperature sensor" + }, + "humidity_sensor": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::humidity_sensor::name%]" + }, + "light_sensor": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::light_sensor::name%]" + }, + "alarm_sound_detection": { + "name": "[%key:component::unifiprotect::entity::binary_sensor::alarm_sound_detection::name%]" + }, + "analytics_enabled": { + "name": "Analytics enabled" + }, + "insights_enabled": { + "name": "Insights enabled" + } + }, + "text": { + "doorbell": { + "name": "[%key:component::event::entity_component::doorbell::name%]" } }, "event": { "doorbell": { - "name": "Doorbell", + "name": "[%key:component::event::entity_component::doorbell::name%]", "state_attributes": { "event_type": { "state": { @@ -217,7 +670,7 @@ "description": "Removes a privacy zone from a camera.", "fields": { "device_id": { - "name": "Camera", + "name": "[%key:component::camera::title%]", "description": "Camera you want to remove the privacy zone from." }, "name": { diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fce92912a52..29dffa97c3a 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -52,7 +52,7 @@ async def _set_highfps(obj: Camera, value: bool) -> None: CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -62,7 +62,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_led_status", @@ -72,7 +72,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="hdr_mode", - name="HDR mode", + translation_key="hdr_mode", icon="mdi:brightness-7", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, @@ -83,7 +83,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription[Camera]( key="high_fps", - name="High FPS", + translation_key="high_fps", icon="mdi:video-high-definition", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_highfps", @@ -93,7 +93,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="system_sounds", - name="System sounds", + translation_key="system_sounds", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, ufp_required_field="has_speaker", @@ -104,7 +104,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_name", - name="Overlay: show name", + translation_key="overlay_show_name", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_name_enabled", @@ -113,7 +113,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_date", - name="Overlay: show date", + translation_key="overlay_show_date", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_date_enabled", @@ -122,7 +122,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_logo", - name="Overlay: show logo", + translation_key="overlay_show_logo", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_logo_enabled", @@ -131,7 +131,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="osd_bitrate", - name="Overlay: show nerd mode", + translation_key="overlay_show_nerd_mode", icon="mdi:fullscreen", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", @@ -140,7 +140,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="color_night_vision", - name="Color night vision", + translation_key="color_night_vision", icon="mdi:light-flood-down", entity_category=EntityCategory.CONFIG, ufp_required_field="has_color_night_vision", @@ -150,7 +150,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="motion", - name="Detections: motion", + translation_key="motion", icon="mdi:run-fast", entity_category=EntityCategory.CONFIG, ufp_value="recording_settings.enable_motion_detection", @@ -160,7 +160,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_person", - name="Detections: person", + translation_key="detections_person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_person", @@ -171,7 +171,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_vehicle", - name="Detections: vehicle", + translation_key="detections_vehicle", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_vehicle", @@ -182,7 +182,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_animal", - name="Detections: animal", + translation_key="detections_animal", icon="mdi:paw", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_animal", @@ -193,7 +193,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_package", - name="Detections: package", + translation_key="detections_package", icon="mdi:package-variant-closed", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_package", @@ -204,7 +204,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_licenseplate", - name="Detections: license plate", + translation_key="detections_license_plate", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_license_plate", @@ -215,7 +215,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_smoke", - name="Detections: smoke", + translation_key="detections_smoke", icon="mdi:fire", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_smoke", @@ -226,7 +226,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_cmonx", - name="Detections: CO", + translation_key="detections_co_alarm", icon="mdi:molecule-co", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_co", @@ -237,7 +237,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_siren", - name="Detections: siren", + translation_key="detections_siren", icon="mdi:alarm-bell", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_siren", @@ -248,7 +248,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_baby_cry", - name="Detections: baby cry", + translation_key="detections_baby_cry", icon="mdi:cradle", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_baby_cry", @@ -259,7 +259,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_speak", - name="Detections: speaking", + translation_key="detections_speak", icon="mdi:account-voice", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_speaking", @@ -270,7 +270,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_bark", - name="Detections: barking", + translation_key="detections_bark", icon="mdi:dog", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_bark", @@ -281,7 +281,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_car_alarm", - name="Detections: car alarm", + translation_key="detections_car_alarm", icon="mdi:car", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_car_alarm", @@ -292,7 +292,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_car_horn", - name="Detections: car horn", + translation_key="detections_car_horn", icon="mdi:bugle", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_car_horn", @@ -303,7 +303,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="smart_glass_break", - name="Detections: glass break", + translation_key="detections_glass_break", icon="mdi:glass-fragile", entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_glass_break", @@ -314,7 +314,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="track_person", - name="Tracking: person", + translation_key="tracking_person", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.is_ptz", @@ -326,7 +326,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( key="privacy_mode", - name="Privacy mode", + translation_key="privacy_mode", icon="mdi:eye-settings", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_privacy_mask", @@ -337,7 +337,7 @@ PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", @@ -346,7 +346,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="motion", - name="Motion detection", + translation_key="detections_motion", icon="mdi:walk", entity_category=EntityCategory.CONFIG, ufp_value="motion_settings.is_enabled", @@ -355,7 +355,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="temperature", - name="Temperature sensor", + translation_key="temperature_sensor", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ufp_value="temperature_settings.is_enabled", @@ -364,7 +364,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="humidity", - name="Humidity sensor", + translation_key="humidity_sensor", icon="mdi:water-percent", entity_category=EntityCategory.CONFIG, ufp_value="humidity_settings.is_enabled", @@ -373,7 +373,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="light", - name="Light sensor", + translation_key="light_sensor", icon="mdi:brightness-5", entity_category=EntityCategory.CONFIG, ufp_value="light_settings.is_enabled", @@ -382,7 +382,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="alarm", - name="Alarm sound detection", + translation_key="alarm_sound_detection", entity_category=EntityCategory.CONFIG, ufp_value="alarm_settings.is_enabled", ufp_set_method="set_alarm_status", @@ -394,7 +394,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -404,7 +404,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="light_device_settings.is_indicator_enabled", @@ -416,7 +416,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", - name="Status light on", + translation_key="status_light", icon="mdi:led-on", entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", @@ -428,7 +428,7 @@ DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", - name="SSH enabled", + translation_key="ssh_enabled", icon="mdi:lock", entity_registry_enabled_default=False, entity_category=EntityCategory.CONFIG, @@ -441,7 +441,7 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="analytics_enabled", - name="Analytics enabled", + translation_key="analytics_enabled", icon="mdi:google-analytics", entity_category=EntityCategory.CONFIG, ufp_value="is_analytics_enabled", @@ -449,7 +449,7 @@ NVR_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ProtectSwitchEntityDescription( key="insights_enabled", - name="Insights enabled", + translation_key="insights_enabled", icon="mdi:magnify", entity_category=EntityCategory.CONFIG, ufp_value="is_insights_enabled", diff --git a/homeassistant/components/unifiprotect/text.py b/homeassistant/components/unifiprotect/text.py index 1c468d44cc6..2e11c201f5f 100644 --- a/homeassistant/components/unifiprotect/text.py +++ b/homeassistant/components/unifiprotect/text.py @@ -46,7 +46,7 @@ async def _set_doorbell_message(obj: Camera, message: str) -> None: CAMERA: tuple[ProtectTextEntityDescription, ...] = ( ProtectTextEntityDescription( key="doorbell", - name="Doorbell", + translation_key="doorbell", entity_category=EntityCategory.CONFIG, ufp_value_fn=_get_doorbell_current, ufp_set_method_fn=_set_doorbell_message, diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 3a283093179..bcd3e89b784 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -48,7 +48,7 @@ async def test_reboot_button( ufp.api.reboot_device = AsyncMock() unique_id = f"{chime.mac}_reboot" - entity_id = "button.test_chime_reboot_device" + entity_id = "button.test_chime_restart" entity = entity_registry.async_get(entity_id) assert entity diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 194e46681ce..1a899550204 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -34,22 +34,21 @@ CAMERA_SWITCHES_BASIC = [ d for d in CAMERA_SWITCHES if ( - not d.name.startswith("Detections:") - and d.name - not in {"SSH enabled", "Color night vision", "Tracking: person", "HDR mode"} + not d.translation_key.startswith("detections_") + and d.key not in {"ssh", "color_night_vision", "track_person", "hdr_mode"} ) - or d.name + or d.key in { - "Detections: motion", - "Detections: person", - "Detections: vehicle", - "Detections: animal", + "detections_motion", + "detections_person", + "detections_vehicle", + "detections_animal", } ] CAMERA_SWITCHES_NO_EXTRA = [ d for d in CAMERA_SWITCHES_BASIC - if d.name not in ("High FPS", "Privacy mode", "HDR mode") + if d.key not in ("high_fps", "privacy_mode", "hdr_mode") ] @@ -152,7 +151,7 @@ async def test_switch_setup_light( description = LIGHT_SWITCHES[0] unique_id = f"{light.mac}_{description.key}" - entity_id = f"switch.test_light_{description.name.lower().replace(' ', '_')}" + entity_id = f"switch.test_light_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity @@ -194,11 +193,8 @@ async def test_switch_setup_camera_all( description = CAMERA_SWITCHES[0] - description_entity_name = ( - description.name.lower().replace(":", "").replace(" ", "_") - ) unique_id = f"{doorbell.mac}_{description.key}" - entity_id = f"switch.test_camera_{description_entity_name}" + entity_id = f"switch.test_camera_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity @@ -243,11 +239,8 @@ async def test_switch_setup_camera_none( description = CAMERA_SWITCHES[0] - description_entity_name = ( - description.name.lower().replace(":", "").replace(" ", "_") - ) unique_id = f"{camera.mac}_{description.key}" - entity_id = f"switch.test_camera_{description_entity_name}" + entity_id = f"switch.test_camera_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 06ffe16ab87..ddd6fdf0189 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -109,8 +109,10 @@ def ids_from_device_description( entity_name = normalize_name(device.display_name) - if description.name and isinstance(description.name, str): - description_entity_name = normalize_name(description.name) + if getattr(description, "translation_key", None): + description_entity_name = normalize_name(description.translation_key) + elif getattr(description, "device_class", None): + description_entity_name = normalize_name(description.device_class) else: description_entity_name = normalize_name(description.key) From 1c1f5a779be832e678073f7fa9128df642f4724d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 15:59:01 +0300 Subject: [PATCH 0893/1175] Cleanup non-existing climate and humidifier devices for Comelit (#144624) * Cleanup non-existing climate and humidifier devices for Comelit * skip removing main hub device * add tests * complete tests * improve logging * fix post rebase * apply review comments * typos * fix identifiers * fix ruff post merge * clean post merge --- homeassistant/components/comelit/climate.py | 42 +++++------ .../components/comelit/humidifier.py | 32 ++++++--- homeassistant/components/comelit/utils.py | 66 +++++++++++++++++- tests/components/comelit/test_climate.py | 38 ++++++++++ tests/components/comelit/test_humidifier.py | 38 ++++++++++ tests/components/comelit/test_utils.py | 69 +++++++++++++++++-- 6 files changed, 244 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/comelit/climate.py b/homeassistant/components/comelit/climate.py index 6b05ed80b13..84761a89722 100644 --- a/homeassistant/components/comelit/climate.py +++ b/homeassistant/components/comelit/climate.py @@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject from aiocomelit.const import CLIMATE from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, ClimateEntity, ClimateEntityFeature, HVACAction, @@ -17,18 +18,12 @@ from homeassistant.components.climate import ( ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import ( - DOMAIN, - PRESET_MODE_AUTO, - PRESET_MODE_AUTO_TARGET_TEMP, - PRESET_MODE_MANUAL, -) +from .const import PRESET_MODE_AUTO, PRESET_MODE_AUTO_TARGET_TEMP, PRESET_MODE_MANUAL from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity -from .utils import bridge_api_call +from .utils import bridge_api_call, cleanup_stale_entity, load_api_data # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -95,10 +90,23 @@ async def async_setup_entry( coordinator = cast(ComelitSerialBridge, config_entry.runtime_data) - async_add_entities( - ComelitClimateEntity(coordinator, device, config_entry.entry_id) - for device in coordinator.data[CLIMATE].values() - ) + entities: list[ClimateEntity] = [] + for device in coordinator.data[CLIMATE].values(): + values = load_api_data(device, CLIMATE_DOMAIN) + if values[0] == 0 and values[4] == 0: + # No climate data, device is only a humidifier/dehumidifier + + await cleanup_stale_entity( + hass, config_entry, f"{config_entry.entry_id}-{device.index}", device + ) + + continue + + entities.append( + ComelitClimateEntity(coordinator, device, config_entry.entry_id) + ) + + async_add_entities(entities) class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): @@ -132,15 +140,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity): def _update_attributes(self) -> None: """Update class attributes.""" device = self.coordinator.data[CLIMATE][self._device.index] - if not isinstance(device.val, list): - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="invalid_clima_data" - ) - - # CLIMATE has a 2 item tuple: - # - first for Clima - # - second for Humidifier - values = device.val[0] + values = load_api_data(device, CLIMATE_DOMAIN) _active = values[1] _mode = values[2] # Values from API: "O", "L", "U" diff --git a/homeassistant/components/comelit/humidifier.py b/homeassistant/components/comelit/humidifier.py index 0c43744aadd..4a7361022ce 100644 --- a/homeassistant/components/comelit/humidifier.py +++ b/homeassistant/components/comelit/humidifier.py @@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject from aiocomelit.const import CLIMATE from homeassistant.components.humidifier import ( + DOMAIN as HUMIDIFIER_DOMAIN, MODE_AUTO, MODE_NORMAL, HumidifierAction, @@ -17,13 +18,13 @@ from homeassistant.components.humidifier import ( HumidifierEntityFeature, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import DOMAIN from .coordinator import ComelitConfigEntry, ComelitSerialBridge from .entity import ComelitBridgeBaseEntity -from .utils import bridge_api_call +from .utils import bridge_api_call, cleanup_stale_entity, load_api_data # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -67,6 +68,23 @@ async def async_setup_entry( entities: list[ComelitHumidifierEntity] = [] for device in coordinator.data[CLIMATE].values(): + values = load_api_data(device, HUMIDIFIER_DOMAIN) + if values[0] == 0 and values[4] == 0: + # No humidity data, device is only a climate + + for device_class in ( + HumidifierDeviceClass.HUMIDIFIER, + HumidifierDeviceClass.DEHUMIDIFIER, + ): + await cleanup_stale_entity( + hass, + config_entry, + f"{config_entry.entry_id}-{device.index}-{device_class}", + device, + ) + + continue + entities.append( ComelitHumidifierEntity( coordinator, @@ -124,15 +142,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity): def _update_attributes(self) -> None: """Update class attributes.""" device = self.coordinator.data[CLIMATE][self._device.index] - if not isinstance(device.val, list): - raise HomeAssistantError( - translation_domain=DOMAIN, translation_key="invalid_clima_data" - ) - - # CLIMATE has a 2 item tuple: - # - first for Clima - # - second for Humidifier - values = device.val[1] + values = load_api_data(device, HUMIDIFIER_DOMAIN) _active = values[1] _mode = values[2] # Values from API: "O", "L", "U" diff --git a/homeassistant/components/comelit/utils.py b/homeassistant/components/comelit/utils.py index 5d16f6232df..d0f0fbbee3f 100644 --- a/homeassistant/components/comelit/utils.py +++ b/homeassistant/components/comelit/utils.py @@ -4,14 +4,21 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from typing import Any, Concatenate +from aiocomelit import ComelitSerialBridgeObject from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData from aiohttp import ClientSession, CookieJar +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) -from .const import DOMAIN +from .const import _LOGGER, DOMAIN from .entity import ComelitBridgeBaseEntity @@ -22,6 +29,61 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession: ) +def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]: + """Load data from the API.""" + # This function is called when the data is loaded from the API + if not isinstance(device.val, list): + raise HomeAssistantError( + translation_domain=domain, translation_key="invalid_clima_data" + ) + # CLIMATE has a 2 item tuple: + # - first for Clima + # - second for Humidifier + return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1] + + +async def cleanup_stale_entity( + hass: HomeAssistant, + config_entry: ConfigEntry, + entry_unique_id: str, + device: ComelitSerialBridgeObject, +) -> None: + """Cleanup stale entity.""" + entity_reg: er.EntityRegistry = er.async_get(hass) + + identifiers: list[str] = [] + + for entry in er.async_entries_for_config_entry(entity_reg, config_entry.entry_id): + if entry.unique_id == entry_unique_id: + entry_name = entry.name or entry.original_name + _LOGGER.info("Removing entity: %s [%s]", entry.entity_id, entry_name) + entity_reg.async_remove(entry.entity_id) + identifiers.append(f"{config_entry.entry_id}-{device.type}-{device.index}") + + if len(identifiers) > 0: + _async_remove_state_config_entry_from_devices(hass, identifiers, config_entry) + + +def _async_remove_state_config_entry_from_devices( + hass: HomeAssistant, identifiers: list[str], config_entry: ConfigEntry +) -> None: + """Remove config entry from device.""" + + device_registry = dr.async_get(hass) + for identifier in identifiers: + device = device_registry.async_get_device(identifiers={(DOMAIN, identifier)}) + if device: + _LOGGER.info( + "Removing config entry %s from device %s", + config_entry.title, + device.name, + ) + device_registry.async_update_device( + device_id=device.id, + remove_config_entry_id=config_entry.entry_id, + ) + + def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P]( func: Callable[Concatenate[_T, _P], Awaitable[None]], ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: diff --git a/tests/components/comelit/test_climate.py b/tests/components/comelit/test_climate.py index 5027106cb5b..53a84fbc6b8 100644 --- a/tests/components/comelit/test_climate.py +++ b/tests/components/comelit/test_climate.py @@ -352,3 +352,41 @@ async def test_climate_preset_mode_when_off( assert (state := hass.states.get(ENTITY_ID)) assert state.state == HVACMode.OFF + + +async def test_climate_remove_stale( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test removal of stale climate entities.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 5.0 + + mock_serial_bridge.get_all_devices.return_value[CLIMATE] = { + 0: ComelitSerialBridgeObject( + index=0, + name="Climate0", + status=0, + human_status="off", + type="climate", + val=[ + [0, 0, "O", "A", 0, 0, 0, "N"], + [650, 0, "U", "M", 500, 0, 0, "U"], + [0, 0], + ], + protected=0, + zone="Living room", + power=0.0, + power_unit=WATT, + ), + } + + await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID)) is None diff --git a/tests/components/comelit/test_humidifier.py b/tests/components/comelit/test_humidifier.py index c5ba89becfa..6530d33f09b 100644 --- a/tests/components/comelit/test_humidifier.py +++ b/tests/components/comelit/test_humidifier.py @@ -290,3 +290,41 @@ async def test_humidifier_set_status( assert (state := hass.states.get(ENTITY_ID)) assert state.state == STATE_ON + + +async def test_humidifier_dehumidifier_remove_stale( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test removal of stale humidifier/dehumidifier entities.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID)) + assert state.state == STATE_ON + assert state.attributes[ATTR_HUMIDITY] == 50.0 + + mock_serial_bridge.get_all_devices.return_value[CLIMATE] = { + 0: ComelitSerialBridgeObject( + index=0, + name="Climate0", + status=0, + human_status="off", + type="climate", + val=[ + [221, 0, "U", "M", 50, 0, 0, "U"], + [0, 0, "O", "A", 0, 0, 0, "N"], + [0, 0], + ], + protected=0, + zone="Living room", + power=0.0, + power_unit=WATT, + ), + } + + await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID)) is None diff --git a/tests/components/comelit/test_utils.py b/tests/components/comelit/test_utils.py index 413d0d0e561..dbf4904fefe 100644 --- a/tests/components/comelit/test_utils.py +++ b/tests/components/comelit/test_utils.py @@ -1,14 +1,18 @@ -"""Tests for Comelit SimpleHome switch platform.""" +"""Tests for Comelit SimpleHome utils.""" from unittest.mock import AsyncMock +from aiocomelit.api import ComelitSerialBridgeObject +from aiocomelit.const import CLIMATE, WATT from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData import pytest +from homeassistant.components.climate import HVACMode from homeassistant.components.comelit.const import DOMAIN +from homeassistant.components.humidifier import ATTR_HUMIDITY from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -16,7 +20,58 @@ from . import setup_integration from tests.common import MockConfigEntry -ENTITY_ID = "switch.switch0" +ENTITY_ID_0 = "switch.switch0" +ENTITY_ID_1 = "climate.climate0" +ENTITY_ID_2 = "humidifier.climate0_dehumidifier" +ENTITY_ID_3 = "humidifier.climate0_humidifier" + + +async def test_device_remove_stale( + hass: HomeAssistant, + mock_serial_bridge: AsyncMock, + mock_serial_bridge_config_entry: MockConfigEntry, +) -> None: + """Test removal of stale devices with no entities.""" + + await setup_integration(hass, mock_serial_bridge_config_entry) + + assert (state := hass.states.get(ENTITY_ID_1)) + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 5.0 + + assert (state := hass.states.get(ENTITY_ID_2)) + assert state.state == STATE_OFF + assert state.attributes[ATTR_HUMIDITY] == 50.0 + + assert (state := hass.states.get(ENTITY_ID_3)) + assert state.state == STATE_ON + assert state.attributes[ATTR_HUMIDITY] == 50.0 + + mock_serial_bridge.get_all_devices.return_value[CLIMATE] = { + 0: ComelitSerialBridgeObject( + index=0, + name="Climate0", + status=0, + human_status="off", + type="climate", + val=[ + [0, 0, "O", "A", 0, 0, 0, "N"], + [0, 0, "O", "A", 0, 0, 0, "N"], + [0, 0], + ], + protected=0, + zone="Living room", + power=0.0, + power_unit=WATT, + ), + } + + await hass.config_entries.async_reload(mock_serial_bridge_config_entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get(ENTITY_ID_1)) is None + assert (state := hass.states.get(ENTITY_ID_2)) is None + assert (state := hass.states.get(ENTITY_ID_3)) is None @pytest.mark.parametrize( @@ -38,7 +93,7 @@ async def test_bridge_api_call_exceptions( await setup_integration(hass, mock_serial_bridge_config_entry) - assert (state := hass.states.get(ENTITY_ID)) + assert (state := hass.states.get(ENTITY_ID_0)) assert state.state == STATE_OFF mock_serial_bridge.set_device_status.side_effect = side_effect @@ -48,7 +103,7 @@ async def test_bridge_api_call_exceptions( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: ENTITY_ID_0}, blocking=True, ) @@ -66,7 +121,7 @@ async def test_bridge_api_call_reauth( await setup_integration(hass, mock_serial_bridge_config_entry) - assert (state := hass.states.get(ENTITY_ID)) + assert (state := hass.states.get(ENTITY_ID_0)) assert state.state == STATE_OFF mock_serial_bridge.set_device_status.side_effect = CannotAuthenticate @@ -75,7 +130,7 @@ async def test_bridge_api_call_reauth( await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, + {ATTR_ENTITY_ID: ENTITY_ID_0}, blocking=True, ) From c7745e0d0290c09d9e1e1cdd81d0e223caa7c85b Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Mon, 26 May 2025 14:01:17 +0100 Subject: [PATCH 0894/1175] Add support for SEARCH_MEDIA feature (#143261) * initial * initial * add tests * Update for list return * translate exception * tests for errors * review tweaks * test fix * force content_type to lowercase * Allow media_content_type = None * new test --- .../components/squeezebox/browse_media.py | 19 ++- .../components/squeezebox/media_player.py | 71 ++++++++++ .../components/squeezebox/strings.json | 3 + tests/components/squeezebox/conftest.py | 21 ++- .../snapshots/test_media_player.ambr | 4 +- .../squeezebox/test_media_browser.py | 124 ++++++++++++++++++ 6 files changed, 237 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 6e1ec8b37c4..03df289a2fd 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -50,21 +50,33 @@ MEDIA_TYPE_TO_SQUEEZEBOX: dict[str | MediaType, str] = { MediaType.GENRE: "genre", MediaType.APPS: "apps", "radios": "radios", + "favorite": "favorite", } SQUEEZEBOX_ID_BY_TYPE: dict[str | MediaType, str] = { MediaType.ALBUM: "album_id", + "albums": "album_id", MediaType.ARTIST: "artist_id", + "artists": "artist_id", MediaType.TRACK: "track_id", + "tracks": "track_id", MediaType.PLAYLIST: "playlist_id", + "playlists": "playlist_id", MediaType.GENRE: "genre_id", + "genres": "genre_id", + "favorite": "item_id", "favorites": "item_id", MediaType.APPS: "item_id", + "app": "item_id", + "radios": "item_id", + "radio": "item_id", } CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | str]] = { "favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, + "favorite": {"item": "favorite", "children": ""}, "radios": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP}, + "radio": {"item": MediaClass.DIRECTORY, "children": MediaClass.APP}, "artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST}, "albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM}, "tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK}, @@ -100,6 +112,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[ "album artists": MediaType.ARTIST, MediaType.APPS: MediaType.APP, MediaType.APP: MediaType.TRACK, + "favorite": None, } @@ -191,7 +204,7 @@ def _build_response_favorites(item: dict[str, Any]) -> BrowseMedia: return BrowseMedia( media_content_id=item["id"], title=item["title"], - media_content_type="favorites", + media_content_type="favorite", media_class=CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]["item"], can_expand=bool(item.get("hasitems")), can_play=bool(item["isaudio"] and item.get("url")), @@ -236,6 +249,7 @@ async def build_item_response( search_id = payload["search_id"] search_type = payload["search_type"] + search_query = payload.get("search_query") assert ( search_type is not None ) # async_browse_media will not call this function if search_type is None @@ -252,6 +266,7 @@ async def build_item_response( browse_data.media_type_to_squeezebox[search_type], limit=browse_limit, browse_id=browse_id, + search_query=search_query, ) if result is not None and result.get("items"): @@ -261,7 +276,7 @@ async def build_item_response( for item in result["items"]: # Force the item id to a string in case it's numeric from some lms item["id"] = str(item.get("id", "")) - if search_type == "favorites": + if search_type in ["favorites", "favorite"]: child_media = _build_response_favorites(item) elif search_type in ["apps", "radios"]: diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 873bedd13fb..1e803c0e1ef 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -23,6 +23,8 @@ from homeassistant.components.media_player import ( MediaPlayerState, MediaType, RepeatMode, + SearchMedia, + SearchMediaQuery, async_process_play_media_url, ) from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY @@ -204,6 +206,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.MEDIA_ENQUEUE | MediaPlayerEntityFeature.MEDIA_ANNOUNCE + | MediaPlayerEntityFeature.SEARCH_MEDIA ) _attr_has_entity_name = True _attr_name = None @@ -545,6 +548,74 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity): await self._player.async_index(index) await self.coordinator.async_refresh() + async def async_search_media( + self, + query: SearchMediaQuery, + ) -> SearchMedia: + """Search the media player.""" + + _valid_type_list = [ + key + for key in self._browse_data.content_type_media_class + if key not in ["apps", "app", "radios", "radio"] + ] + + _media_content_type_list = ( + query.media_content_type.lower().replace(", ", ",").split(",") + if query.media_content_type + else ["albums", "tracks", "artists", "genres"] + ) + + if query.media_content_type and set(_media_content_type_list).difference( + _valid_type_list + ): + _LOGGER.debug("Invalid Media Content Type: %s", query.media_content_type) + + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_search_media_content_type", + translation_placeholders={ + "media_content_type": ", ".join(_valid_type_list) + }, + ) + + search_response_list: list[BrowseMedia] = [] + + for _content_type in _media_content_type_list: + payload = { + "search_type": _content_type, + "search_id": query.media_content_id, + "search_query": query.search_query, + } + + try: + search_response_list.append( + await build_item_response( + self, + self._player, + payload, + self.browse_limit, + self._browse_data, + ) + ) + except BrowseError: + _LOGGER.debug("Search Failure: Payload %s", payload) + + result: list[BrowseMedia] = [] + + for search_response in search_response_list: + # Apply the media_filter_classes to the result if specified + if query.media_filter_classes and search_response.children: + search_response.children = [ + child + for child in search_response.children + if child.media_content_type in query.media_filter_classes + ] + if search_response.children: + result.extend(list(search_response.children)) + + return SearchMedia(result=result) + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set the repeat mode.""" if repeat == RepeatMode.ALL: diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index b004234c327..a8c0b4bb0ae 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -196,6 +196,9 @@ }, "update_restart_failed": { "message": "Error trying to update LMS Plugins: Restart failed." + }, + "invalid_search_media_content_type": { + "message": "If specified, Media content type must be one of {media_content_type}" } } } diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index fb2428ba758..2cbc1305bcb 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -131,11 +131,15 @@ async def mock_async_play_announcement(media_id: str) -> bool: async def mock_async_browse( - media_type: MediaType, limit: int, browse_id: tuple | None = None + media_type: MediaType, + limit: int, + browse_id: tuple | None = None, + search_query: str | None = None, ) -> dict | None: """Mock the async_browse method of pysqueezebox.Player.""" child_types = { "favorites": "favorites", + "favorite": "favorite", "new music": "album", "album artists": "artists", "albums": "album", @@ -224,6 +228,21 @@ async def mock_async_browse( "items": fake_items, } return None + + if search_query: + if search_query not in [x["title"] for x in fake_items]: + return None + + for item in fake_items: + if ( + item["title"] == search_query + and item["item_type"] == child_types[media_type] + ): + return { + "title": media_type, + "items": [item], + } + if ( media_type in MEDIA_TYPE_TO_SQUEEZEBOX.values() or media_type == "app-fakecommand" diff --git a/tests/components/squeezebox/snapshots/test_media_player.ambr b/tests/components/squeezebox/snapshots/test_media_player.ambr index 7540a448882..5e2e59f447e 100644 --- a/tests/components/squeezebox/snapshots/test_media_player.ambr +++ b/tests/components/squeezebox/snapshots/test_media_player.ambr @@ -65,7 +65,7 @@ 'original_name': None, 'platform': 'squeezebox', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff', 'unit_of_measurement': None, @@ -84,7 +84,7 @@ }), 'repeat': , 'shuffle': False, - 'supported_features': , + 'supported_features': , 'volume_level': 0.01, }), 'context': , diff --git a/tests/components/squeezebox/test_media_browser.py b/tests/components/squeezebox/test_media_browser.py index f1ba187a699..093e4f186d4 100644 --- a/tests/components/squeezebox/test_media_browser.py +++ b/tests/components/squeezebox/test_media_browser.py @@ -10,6 +10,7 @@ from homeassistant.components.media_player import ( DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, BrowseError, + MediaClass, MediaType, ) from homeassistant.components.squeezebox.browse_media import ( @@ -170,6 +171,129 @@ async def test_async_browse_media_for_apps( assert "Fake Invalid Item 1" not in search +@pytest.mark.parametrize( + ("category", "media_filter_classes"), + [ + ("favorites", None), + ("artists", None), + ("albums", None), + ("playlists", None), + ("genres", None), + ("new music", None), + ("album artists", None), + ("albums", [MediaClass.ALBUM]), + ], +) +async def test_async_search_media( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, + category: str, + media_filter_classes: list[MediaClass] | None, +) -> None: + """Test each category with subitems.""" + with patch( + "homeassistant.components.squeezebox.browse_media.is_internal_request", + return_value=False, + ): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/search_media", + "entity_id": "media_player.test_player", + "media_content_id": "", + "media_content_type": category, + "search_query": "Fake Item 1", + "media_filter_classes": media_filter_classes, + } + ) + response = await client.receive_json() + assert response["success"] + category_level = response["result"]["result"] + assert category_level[0]["title"] == "Fake Item 1" + + +async def test_async_search_media_invalid_filter( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test search_media action with invalid media_filter_class.""" + with patch( + "homeassistant.components.squeezebox.browse_media.is_internal_request", + return_value=False, + ): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/search_media", + "entity_id": "media_player.test_player", + "media_content_id": "", + "media_content_type": "albums", + "search_query": "Fake Item 1", + "media_filter_classes": "movie", + } + ) + response = await client.receive_json() + assert response["success"] + assert len(response["result"]["result"]) == 0 + + +async def test_async_search_media_invalid_type( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test search_media action with invalid media_content_type.""" + with patch( + "homeassistant.components.squeezebox.browse_media.is_internal_request", + return_value=False, + ): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/search_media", + "entity_id": "media_player.test_player", + "media_content_id": "", + "media_content_type": "Fake Type", + "search_query": "Fake Item 1", + }, + ) + response = await client.receive_json() + assert not response["success"] + err_message = "If specified, Media content type must be one of" + assert err_message in response["error"]["message"] + + +async def test_async_search_media_not_found( + hass: HomeAssistant, + config_entry: MockConfigEntry, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test trying to play an item that doesn't exist.""" + with patch( + "homeassistant.components.squeezebox.browse_media.is_internal_request", + return_value=False, + ): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/search_media", + "entity_id": "media_player.test_player", + "media_content_id": "", + "media_content_type": "", + "search_query": "Unknown Item", + }, + ) + response = await client.receive_json() + + assert len(response["result"]["result"]) == 0 + + async def test_generate_playlist_for_app( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, From 39906cf65bd56f2c62fcd6363c2a9935b32db3b3 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Mon, 26 May 2025 14:04:26 +0100 Subject: [PATCH 0895/1175] Add state_class to metoffice sensors (#145496) * Add state_class to metoffice sensors * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/metoffice/sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 77118ec382e..b707bf604e6 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -12,9 +12,11 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEGREE, PERCENTAGE, UV_INDEX, UnitOfLength, @@ -71,8 +73,8 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( native_attr_name="screenTemperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, - icon=None, entity_registry_enabled_default=True, ), MetOfficeSensorEntityDescription( @@ -80,6 +82,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( native_attr_name="feelsLikeTemperature", name="Feels like temperature", device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, icon=None, entity_registry_enabled_default=False, @@ -93,12 +96,16 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( # This can be removed if we add a mixed metric/imperial unit system for UK users suggested_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, ), MetOfficeSensorEntityDescription( key="wind_direction", native_attr_name="windDirectionFrom10m", name="Wind direction", + native_unit_of_measurement=DEGREE, + device_class=SensorDeviceClass.WIND_DIRECTION, + state_class=SensorStateClass.MEASUREMENT_ANGLE, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), @@ -111,12 +118,15 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( # This can be removed if we add a mixed metric/imperial unit system for UK users suggested_unit_of_measurement=UnitOfSpeed.MILES_PER_HOUR, device_class=SensorDeviceClass.WIND_SPEED, + state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), MetOfficeSensorEntityDescription( key="visibility", native_attr_name="visibility", name="Visibility distance", + device_class=SensorDeviceClass.DISTANCE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfLength.METERS, icon="mdi:eye", entity_registry_enabled_default=False, @@ -132,6 +142,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( MetOfficeSensorEntityDescription( key="precipitation", native_attr_name="probOfPrecipitation", + state_class=SensorStateClass.MEASUREMENT, name="Probability of precipitation", native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", @@ -142,6 +153,7 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = ( native_attr_name="screenRelativeHumidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon=None, entity_registry_enabled_default=False, From 5202bbb6af33c0ec191012b2dcbcfcc8e336be2e Mon Sep 17 00:00:00 2001 From: Jeef Date: Mon, 26 May 2025 07:05:00 -0600 Subject: [PATCH 0896/1175] Update Weatherflow wind direction icons to use Ranged Icon Translation (#140166) * feat: Wind direction icons * optimize funciton * float to int * no-verify * pre-change for icon translation changes --------- Co-authored-by: Jeff Stein <6491743+jeffor@users.noreply.github.com> --- .../components/weatherflow/icons.json | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/weatherflow/icons.json b/homeassistant/components/weatherflow/icons.json index 71a8b48415d..e0d2459b072 100644 --- a/homeassistant/components/weatherflow/icons.json +++ b/homeassistant/components/weatherflow/icons.json @@ -11,10 +11,32 @@ "default": "mdi:weather-rainy" }, "wind_direction": { - "default": "mdi:compass-outline" + "default": "mdi:compass-outline", + "range": { + "0": "mdi:arrow-up", + "22.5": "mdi:arrow-top-right", + "67.5": "mdi:arrow-right", + "112.5": "mdi:arrow-bottom-right", + "157.5": "mdi:arrow-down", + "202.5": "mdi:arrow-bottom-left", + "247.5": "mdi:arrow-left", + "292.5": "mdi:arrow-top-left", + "337.5": "mdi:arrow-up" + } }, "wind_direction_average": { - "default": "mdi:compass-outline" + "default": "mdi:compass-outline", + "range": { + "0": "mdi:arrow-up", + "22.5": "mdi:arrow-top-right", + "67.5": "mdi:arrow-right", + "112.5": "mdi:arrow-bottom-right", + "157.5": "mdi:arrow-down", + "202.5": "mdi:arrow-bottom-left", + "247.5": "mdi:arrow-left", + "292.5": "mdi:arrow-top-left", + "337.5": "mdi:arrow-up" + } } } } From 6ddc2193d6aceb1db143ad5785e2a9b313162c60 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 26 May 2025 15:05:11 +0200 Subject: [PATCH 0897/1175] Add exception handler and exception translations to eheimdigital (#145476) * Add exception handler and exception translations to eheimdigital * Fix --------- Co-authored-by: Joostlek --- .../components/eheimdigital/climate.py | 39 ++++++++----------- .../components/eheimdigital/entity.py | 26 ++++++++++++- .../components/eheimdigital/light.py | 23 +++++------ .../components/eheimdigital/number.py | 3 +- .../eheimdigital/quality_scale.yaml | 2 +- .../components/eheimdigital/select.py | 3 +- .../components/eheimdigital/strings.json | 5 +++ .../components/eheimdigital/switch.py | 4 +- homeassistant/components/eheimdigital/time.py | 3 +- 9 files changed, 65 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/eheimdigital/climate.py b/homeassistant/components/eheimdigital/climate.py index 3cde9e758cd..7ac0b897507 100644 --- a/homeassistant/components/eheimdigital/climate.py +++ b/homeassistant/components/eheimdigital/climate.py @@ -4,7 +4,7 @@ from typing import Any from eheimdigital.device import EheimDigitalDevice from eheimdigital.heater import EheimDigitalHeater -from eheimdigital.types import EheimDigitalClientError, HeaterMode, HeaterUnit +from eheimdigital.types import HeaterMode, HeaterUnit from homeassistant.components.climate import ( PRESET_NONE, @@ -20,12 +20,11 @@ from homeassistant.const import ( UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -83,34 +82,28 @@ class EheimDigitalHeaterClimate(EheimDigitalEntity[EheimDigitalHeater], ClimateE self._attr_unique_id = self._device_address self._async_update_attrs() + @exception_handler async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" - try: - if preset_mode in HEATER_PRESET_TO_HEATER_MODE: - await self._device.set_operation_mode( - HEATER_PRESET_TO_HEATER_MODE[preset_mode] - ) - except EheimDigitalClientError as err: - raise HomeAssistantError from err + if preset_mode in HEATER_PRESET_TO_HEATER_MODE: + await self._device.set_operation_mode( + HEATER_PRESET_TO_HEATER_MODE[preset_mode] + ) + @exception_handler async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new temperature.""" - try: - if ATTR_TEMPERATURE in kwargs: - await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE]) - except EheimDigitalClientError as err: - raise HomeAssistantError from err + if ATTR_TEMPERATURE in kwargs: + await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE]) + @exception_handler async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the heating mode.""" - try: - match hvac_mode: - case HVACMode.OFF: - await self._device.set_active(active=False) - case HVACMode.AUTO: - await self._device.set_active(active=True) - except EheimDigitalClientError as err: - raise HomeAssistantError from err + match hvac_mode: + case HVACMode.OFF: + await self._device.set_active(active=False) + case HVACMode.AUTO: + await self._device.set_active(active=True) def _async_update_attrs(self) -> None: if self._device.temperature_unit == HeaterUnit.CELSIUS: diff --git a/homeassistant/components/eheimdigital/entity.py b/homeassistant/components/eheimdigital/entity.py index c0f91a4b798..d28087ef82e 100644 --- a/homeassistant/components/eheimdigital/entity.py +++ b/homeassistant/components/eheimdigital/entity.py @@ -1,12 +1,15 @@ """Base entity for EHEIM Digital.""" from abc import ABC, abstractmethod -from typing import TYPE_CHECKING +from collections.abc import Callable, Coroutine +from typing import TYPE_CHECKING, Any, Concatenate from eheimdigital.device import EheimDigitalDevice +from eheimdigital.types import EheimDigitalClientError from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -51,3 +54,24 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice]( """Update attributes when the coordinator updates.""" self._async_update_attrs() super()._handle_coordinator_update() + + +def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P]( + func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate AirGradient calls to handle exceptions. + + A decorator that wraps the passed in function, catches AirGradient errors. + """ + + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except EheimDigitalClientError as error: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="communication_error", + translation_placeholders={"error": str(error)}, + ) from error + + return handler diff --git a/homeassistant/components/eheimdigital/light.py b/homeassistant/components/eheimdigital/light.py index 2725315befd..7960e956859 100644 --- a/homeassistant/components/eheimdigital/light.py +++ b/homeassistant/components/eheimdigital/light.py @@ -4,7 +4,7 @@ from typing import Any from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl from eheimdigital.device import EheimDigitalDevice -from eheimdigital.types import EheimDigitalClientError, LightMode +from eheimdigital.types import LightMode from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -15,13 +15,12 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.color import brightness_to_value, value_to_brightness from .const import EFFECT_DAYCL_MODE, EFFECT_TO_LIGHT_MODE from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler BRIGHTNESS_SCALE = (1, 100) @@ -88,6 +87,7 @@ class EheimDigitalClassicLEDControlLight( """Return whether the entity is available.""" return super().available and self._device.light_level[self._channel] is not None + @exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if ATTR_EFFECT in kwargs: @@ -96,22 +96,17 @@ class EheimDigitalClassicLEDControlLight( if ATTR_BRIGHTNESS in kwargs: if self._device.light_mode == LightMode.DAYCL_MODE: await self._device.set_light_mode(LightMode.MAN_MODE) - try: - await self._device.turn_on( - int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])), - self._channel, - ) - except EheimDigitalClientError as err: - raise HomeAssistantError from err + await self._device.turn_on( + int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])), + self._channel, + ) + @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" if self._device.light_mode == LightMode.DAYCL_MODE: await self._device.set_light_mode(LightMode.MAN_MODE) - try: - await self._device.turn_off(self._channel) - except EheimDigitalClientError as err: - raise HomeAssistantError from err + await self._device.turn_off(self._channel) def _async_update_attrs(self) -> None: light_level = self._device.light_level[self._channel] diff --git a/homeassistant/components/eheimdigital/number.py b/homeassistant/components/eheimdigital/number.py index 7fd0c6b6de7..03f27aa82df 100644 --- a/homeassistant/components/eheimdigital/number.py +++ b/homeassistant/components/eheimdigital/number.py @@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 @@ -182,6 +182,7 @@ class EheimDigitalNumber( self._attr_unique_id = f"{self._device_address}_{description.key}" @override + @exception_handler async def async_set_native_value(self, value: float) -> None: return await self.entity_description.set_value_fn(self._device, value) diff --git a/homeassistant/components/eheimdigital/quality_scale.yaml b/homeassistant/components/eheimdigital/quality_scale.yaml index a56551a14f6..fa13c9bf4ca 100644 --- a/homeassistant/components/eheimdigital/quality_scale.yaml +++ b/homeassistant/components/eheimdigital/quality_scale.yaml @@ -58,7 +58,7 @@ rules: entity-device-class: done entity-disabled-by-default: done entity-translations: done - exception-translations: todo + exception-translations: done icon-translations: todo reconfiguration-flow: todo repair-issues: todo diff --git a/homeassistant/components/eheimdigital/select.py b/homeassistant/components/eheimdigital/select.py index 9311eb01ecc..41ab13e3bd4 100644 --- a/homeassistant/components/eheimdigital/select.py +++ b/homeassistant/components/eheimdigital/select.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 @@ -94,6 +94,7 @@ class EheimDigitalSelect( self._attr_unique_id = f"{self._device_address}_{description.key}" @override + @exception_handler async def async_select_option(self, option: str) -> None: return await self.entity_description.set_value_fn(self._device, option) diff --git a/homeassistant/components/eheimdigital/strings.json b/homeassistant/components/eheimdigital/strings.json index 89f802c9d6d..77cffb4a709 100644 --- a/homeassistant/components/eheimdigital/strings.json +++ b/homeassistant/components/eheimdigital/strings.json @@ -101,5 +101,10 @@ "name": "Night start time" } } + }, + "exceptions": { + "communication_error": { + "message": "An error occurred while communicating with the EHEIM Digital hub: {error}" + } } } diff --git a/homeassistant/components/eheimdigital/switch.py b/homeassistant/components/eheimdigital/switch.py index de23feff322..2a4f3df3861 100644 --- a/homeassistant/components/eheimdigital/switch.py +++ b/homeassistant/components/eheimdigital/switch.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler # Coordinator is used to centralize the data updates PARALLEL_UPDATES = 0 @@ -58,10 +58,12 @@ class EheimDigitalClassicVarioSwitch( self._async_update_attrs() @override + @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: await self._device.set_active(active=False) @override + @exception_handler async def async_turn_on(self, **kwargs: Any) -> None: await self._device.set_active(active=True) diff --git a/homeassistant/components/eheimdigital/time.py b/homeassistant/components/eheimdigital/time.py index ae64fad0c92..49834c827b9 100644 --- a/homeassistant/components/eheimdigital/time.py +++ b/homeassistant/components/eheimdigital/time.py @@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator -from .entity import EheimDigitalEntity +from .entity import EheimDigitalEntity, exception_handler PARALLEL_UPDATES = 0 @@ -122,6 +122,7 @@ class EheimDigitalTime( self._attr_unique_id = f"{device.mac_address}_{description.key}" @override + @exception_handler async def async_set_value(self, value: time) -> None: """Change the time.""" return await self.entity_description.set_value_fn(self._device, value) From 5642d6450f539ebcd2f288d1d3b1818463ffa665 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 26 May 2025 15:05:44 +0200 Subject: [PATCH 0898/1175] Add template to command args in command_line notify (#125170) * Add template to command args in command_line notify * coverage --------- Co-authored-by: Erik Montnemery --- .../components/command_line/notify.py | 35 ++++++- tests/components/command_line/test_notify.py | 94 +++++++++++++++++++ 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index ec1b51a47c7..50bfbe651ef 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -9,10 +9,12 @@ from typing import Any from homeassistant.components.notify import BaseNotificationService from homeassistant.const import CONF_COMMAND from homeassistant.core import HomeAssistant +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.process import kill_subprocess -from .const import CONF_COMMAND_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, LOGGER _LOGGER = logging.getLogger(__name__) @@ -43,8 +45,31 @@ class CommandLineNotificationService(BaseNotificationService): def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a command line.""" + command = self.command + if " " not in command: + prog = command + args = None + args_compiled = None + else: + prog, args = command.split(" ", 1) + args_compiled = Template(args, self.hass) + + rendered_args = None + if args_compiled: + args_to_render = {"arguments": args} + try: + rendered_args = args_compiled.async_render(args_to_render) + except TemplateError as ex: + LOGGER.exception("Error rendering command template: %s", ex) + return + + if rendered_args != args: + command = f"{prog} {rendered_args}" + + LOGGER.debug("Running command: %s, with message: %s", command, message) + with subprocess.Popen( # noqa: S602 # shell by design - self.command, + command, universal_newlines=True, stdin=subprocess.PIPE, close_fds=False, # required for posix_spawn @@ -56,10 +81,10 @@ class CommandLineNotificationService(BaseNotificationService): _LOGGER.error( "Command failed (with return code %s): %s", proc.returncode, - self.command, + command, ) except subprocess.TimeoutExpired: - _LOGGER.error("Timeout for command: %s", self.command) + _LOGGER.error("Timeout for command: %s", command) kill_subprocess(proc) except subprocess.SubprocessError: - _LOGGER.error("Error trying to exec command: %s", self.command) + _LOGGER.error("Error trying to exec command: %s", command) diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 6898b44f062..a0c69765c9a 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -100,6 +100,100 @@ async def test_command_line_output(hass: HomeAssistant) -> None: assert message == await hass.async_add_executor_job(Path(filename).read_text) +async def test_command_line_output_single_command( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the command line output.""" + + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "echo", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": "test message"}, blocking=True + ) + assert "Running command: echo, with message: test message" in caplog.text + + +async def test_command_template(hass: HomeAssistant) -> None: + """Test the command line output using template as command.""" + + with tempfile.TemporaryDirectory() as tempdirname: + filename = os.path.join(tempdirname, "message.txt") + message = "one, two, testing, testing" + hass.states.async_set("sensor.test_state", filename) + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "cat > {{ states.sensor.test_state.state }}", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True + ) + assert message == await hass.async_add_executor_job(Path(filename).read_text) + + +async def test_command_incorrect_template( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the command line output using template as command which isn't working.""" + + message = "one, two, testing, testing" + await setup.async_setup_component( + hass, + DOMAIN, + { + "command_line": [ + { + "notify": { + "command": "cat > {{ this template doesn't parse ", + "name": "Test3", + } + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(NOTIFY_DOMAIN, "test3") + + await hass.services.async_call( + NOTIFY_DOMAIN, "test3", {"message": message}, blocking=True + ) + + assert ( + "Error rendering command template: TemplateSyntaxError: expected token" + in caplog.text + ) + + @pytest.mark.parametrize( "get_config", [ From 49cf66269ce8008b357cf3222eaca0687efadc2d Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Mon, 26 May 2025 21:06:07 +0800 Subject: [PATCH 0899/1175] =?UTF-8?q?Set=20quality=20scale=20to=20?= =?UTF-8?q?=F0=9F=A5=87=20gold=20for=20switchbot=20integration=20(#144608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update quality scale * update to gold --- homeassistant/components/switchbot/manifest.json | 1 + homeassistant/components/switchbot/quality_scale.yaml | 6 ++---- script/hassfest/quality_scale.py | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dfbfd9335a5..eadd3ad2a2d 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -40,5 +40,6 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], + "quality_scale": "gold", "requirements": ["PySwitchbot==0.64.1"] } diff --git a/homeassistant/components/switchbot/quality_scale.yaml b/homeassistant/components/switchbot/quality_scale.yaml index b8db573f405..5226016c527 100644 --- a/homeassistant/components/switchbot/quality_scale.yaml +++ b/homeassistant/components/switchbot/quality_scale.yaml @@ -40,13 +40,11 @@ rules: Once a cryptographic key is successfully obtained for SwitchBot devices, it will be granted perpetual validity with no expiration constraints. test-coverage: - status: todo - comment: | - Consider using snapshots for fixating all the entities a device creates. + status: done # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: status: exempt comment: | diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index 11d3af590a0..f27106570bd 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -2030,7 +2030,6 @@ INTEGRATIONS_WITHOUT_SCALE = [ "swisscom", "switch_as_x", "switchbee", - "switchbot", "switchbot_cloud", "switcher_kis", "switchmate", From 2d5867cab6fcd2673b2df41ccec9987c0f5f3e44 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Mon, 26 May 2025 21:06:33 +0800 Subject: [PATCH 0900/1175] Add switchbot air purifier support (#144552) * add support for air purifier * add unit tests for air purifier * fix aqi translation * fix aqi translation * add air purifier table * fix air purifier * remove init and add options for aqi level --- .../components/switchbot/__init__.py | 4 + homeassistant/components/switchbot/const.py | 8 + homeassistant/components/switchbot/fan.py | 71 ++++++++- homeassistant/components/switchbot/icons.json | 18 +++ homeassistant/components/switchbot/sensor.py | 8 + .../components/switchbot/strings.json | 29 ++++ tests/components/switchbot/__init__.py | 100 +++++++++++++ tests/components/switchbot/test_fan.py | 140 +++++++++++++++++- 8 files changed, 374 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index ee7d0b7e658..af4001f0d9a 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -90,6 +90,8 @@ PLATFORMS_BY_TYPE = { Platform.LOCK, Platform.SENSOR, ], + SupportedModels.AIR_PURIFIER.value: [Platform.FAN, Platform.SENSOR], + SupportedModels.AIR_PURIFIER_TABLE.value: [Platform.FAN, Platform.SENSOR], } CLASS_BY_DEVICE = { SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight, @@ -113,6 +115,8 @@ CLASS_BY_DEVICE = { SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum, SupportedModels.LOCK_LITE.value: switchbot.SwitchbotLock, SupportedModels.LOCK_ULTRA.value: switchbot.SwitchbotLock, + SupportedModels.AIR_PURIFIER.value: switchbot.SwitchbotAirPurifier, + SupportedModels.AIR_PURIFIER_TABLE.value: switchbot.SwitchbotAirPurifier, } diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index aae189be2e1..f6536ca3ff3 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -46,6 +46,8 @@ class SupportedModels(StrEnum): HUB3 = "hub3" LOCK_LITE = "lock_lite" LOCK_ULTRA = "lock_ultra" + AIR_PURIFIER = "air_purifier" + AIR_PURIFIER_TABLE = "air_purifier_table" CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -71,6 +73,8 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM, SwitchbotModel.LOCK_LITE: SupportedModels.LOCK_LITE, SwitchbotModel.LOCK_ULTRA: SupportedModels.LOCK_ULTRA, + SwitchbotModel.AIR_PURIFIER: SupportedModels.AIR_PURIFIER, + SwitchbotModel.AIR_PURIFIER_TABLE: SupportedModels.AIR_PURIFIER_TABLE, } NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = { @@ -97,6 +101,8 @@ ENCRYPTED_MODELS = { SwitchbotModel.LOCK_PRO, SwitchbotModel.LOCK_LITE, SwitchbotModel.LOCK_ULTRA, + SwitchbotModel.AIR_PURIFIER, + SwitchbotModel.AIR_PURIFIER_TABLE, } ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ @@ -108,6 +114,8 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ SwitchbotModel.RELAY_SWITCH_1: switchbot.SwitchbotRelaySwitch, SwitchbotModel.LOCK_LITE: switchbot.SwitchbotLock, SwitchbotModel.LOCK_ULTRA: switchbot.SwitchbotLock, + SwitchbotModel.AIR_PURIFIER: switchbot.SwitchbotAirPurifier, + SwitchbotModel.AIR_PURIFIER_TABLE: switchbot.SwitchbotAirPurifier, } HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = { diff --git a/homeassistant/components/switchbot/fan.py b/homeassistant/components/switchbot/fan.py index f704af309bf..9a7260f5925 100644 --- a/homeassistant/components/switchbot/fan.py +++ b/homeassistant/components/switchbot/fan.py @@ -6,7 +6,7 @@ import logging from typing import Any import switchbot -from switchbot import FanMode +from switchbot import AirPurifierMode, FanMode from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator -from .entity import SwitchbotEntity +from .entity import SwitchbotEntity, exception_handler _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 @@ -27,7 +27,10 @@ async def async_setup_entry( ) -> None: """Set up Switchbot fan based on a config entry.""" coordinator = entry.runtime_data - async_add_entities([SwitchBotFanEntity(coordinator)]) + if isinstance(coordinator.device, switchbot.SwitchbotAirPurifier): + async_add_entities([SwitchBotAirPurifierEntity(coordinator)]) + else: + async_add_entities([SwitchBotFanEntity(coordinator)]) class SwitchBotFanEntity(SwitchbotEntity, FanEntity, RestoreEntity): @@ -120,3 +123,65 @@ class SwitchBotFanEntity(SwitchbotEntity, FanEntity, RestoreEntity): _LOGGER.debug("Switchbot fan to set turn off %s", self._address) self._last_run_success = bool(await self._device.turn_off()) self.async_write_ha_state() + + +class SwitchBotAirPurifierEntity(SwitchbotEntity, FanEntity): + """Representation of a Switchbot air purifier.""" + + _device: switchbot.SwitchbotAirPurifier + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + _attr_preset_modes = AirPurifierMode.get_modes() + _attr_translation_key = "air_purifier" + _attr_name = None + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + return self._device.is_on() + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self._device.get_current_mode() + + @exception_handler + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set preset mode %s %s", + preset_mode, + self._address, + ) + self._last_run_success = bool(await self._device.set_preset_mode(preset_mode)) + self.async_write_ha_state() + + @exception_handler + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set turn on %s %s %s", + percentage, + preset_mode, + self._address, + ) + self._last_run_success = bool(await self._device.turn_on()) + self.async_write_ha_state() + + @exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the air purifier.""" + + _LOGGER.debug("Switchbot air purifier to set turn off %s", self._address) + self._last_run_success = bool(await self._device.turn_off()) + self.async_write_ha_state() diff --git a/homeassistant/components/switchbot/icons.json b/homeassistant/components/switchbot/icons.json index a1c1682d255..9dd46e0717a 100644 --- a/homeassistant/components/switchbot/icons.json +++ b/homeassistant/components/switchbot/icons.json @@ -12,6 +12,24 @@ } } } + }, + "air_purifier": { + "default": "mdi:air-purifier", + "state": { + "off": "mdi:air-purifier-off" + }, + "state_attributes": { + "preset_mode": { + "state": { + "level_1": "mdi:fan-speed-1", + "level_2": "mdi:fan-speed-2", + "level_3": "mdi:fan-speed-3", + "auto": "mdi:auto-mode", + "pet": "mdi:paw", + "sleep": "mdi:power-sleep" + } + } + } } } } diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index d68c913db15..75ac0f7bc74 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations +from switchbot.const.air_purifier import AirQualityLevel + from homeassistant.components.bluetooth import async_last_service_info from homeassistant.components.sensor import ( SensorDeviceClass, @@ -102,6 +104,12 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.VOLTAGE, ), + "aqi_level": SensorEntityDescription( + key="aqi_level", + translation_key="aqi_quality_level", + device_class=SensorDeviceClass.ENUM, + options=[member.name.lower() for member in AirQualityLevel], + ), } diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index a5f502a261b..c758ae645ae 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -105,6 +105,15 @@ }, "light_level": { "name": "Light level" + }, + "aqi_quality_level": { + "name": "Air quality level", + "state": { + "excellent": "Excellent", + "good": "Good", + "moderate": "Moderate", + "unhealthy": "Unhealthy" + } } }, "cover": { @@ -179,6 +188,26 @@ } } } + }, + "air_purifier": { + "state_attributes": { + "last_run_success": { + "state": { + "true": "[%key:component::binary_sensor::entity_component::problem::state::off%]", + "false": "[%key:component::binary_sensor::entity_component::problem::state::on%]" + } + }, + "preset_mode": { + "state": { + "level_1": "Level 1", + "level_2": "Level 2", + "level_3": "Level 3", + "auto": "[%key:common::state::auto%]", + "pet": "Pet", + "sleep": "Sleep" + } + } + } } }, "vacuum": { diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 1e90b0bf1fe..5dca8167e05 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -759,3 +759,103 @@ LOCK_ULTRA_SERVICE_INFO = BluetoothServiceInfoBleak( connectable=True, tx_power=-127, ) + + +AIR_PURIFIER_TBALE_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier Table PM25", + manufacturer_data={ + 2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"7\x00\x00\x95-\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Air Purifier Table PM25", + manufacturer_data={ + 2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"7\x00\x00\x95-\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table PM25"), + time=0, + connectable=True, + tx_power=-127, +) + + +AIR_PURIFIER_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier PM25", + manufacturer_data={ + 2409: b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00', + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"*\x00\x00\x15\x04\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Air Purifier PM25", + manufacturer_data={ + 2409: b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00', + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"*\x00\x00\x15\x04\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier PM25"), + time=0, + connectable=True, + tx_power=-127, +) + + +AIR_PURIFIER_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier VOC", + manufacturer_data={ + 2409: b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"+\x00\x00\x15\x04\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Air Purifier VOC", + manufacturer_data={ + 2409: b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"+\x00\x00\x15\x04\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier VOC"), + time=0, + connectable=True, + tx_power=-127, +) + + +AIR_PURIFIER_TABLE_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier Table VOC", + manufacturer_data={ + 2409: b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"8\x00\x00\x95-\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Air Purifier Table VOC", + manufacturer_data={ + 2409: b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00", + }, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"8\x00\x00\x95-\x00"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table VOC"), + time=0, + connectable=True, + tx_power=-127, +) diff --git a/tests/components/switchbot/test_fan.py b/tests/components/switchbot/test_fan.py index 815d3aceda3..bd0306a133c 100644 --- a/tests/components/switchbot/test_fan.py +++ b/tests/components/switchbot/test_fan.py @@ -4,7 +4,9 @@ from collections.abc import Callable from unittest.mock import AsyncMock, patch import pytest +from switchbot.devices.device import SwitchbotOperationError +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, @@ -16,8 +18,15 @@ from homeassistant.components.fan import ( ) 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 CIRCULATOR_FAN_SERVICE_INFO +from . import ( + AIR_PURIFIER_PM25_SERVICE_INFO, + AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, + AIR_PURIFIER_TBALE_PM25_SERVICE_INFO, + AIR_PURIFIER_VOC_SERVICE_INFO, + CIRCULATOR_FAN_SERVICE_INFO, +) from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -89,3 +98,132 @@ async def test_circulator_fan_controlling( ) mocked_instance.assert_awaited_once() + + +@pytest.mark.parametrize( + ("service_info", "sensor_type"), + [ + (AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier"), + (AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table"), + (AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier"), + (AIR_PURIFIER_TBALE_PM25_SERVICE_INFO, "air_purifier_table"), + ], +) +@pytest.mark.parametrize( + ("service", "service_data", "mock_method"), + [ + ( + SERVICE_SET_PRESET_MODE, + {ATTR_PRESET_MODE: "sleep"}, + "set_preset_mode", + ), + ( + SERVICE_TURN_OFF, + {}, + "turn_off", + ), + ( + SERVICE_TURN_ON, + {}, + "turn_on", + ), + ], +) +async def test_air_purifier_controlling( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service_info: BluetoothServiceInfoBleak, + sensor_type: str, + service: str, + service_data: dict, + mock_method: str, +) -> None: + """Test controlling the air purifier with different services.""" + inject_bluetooth_service_info(hass, service_info) + + entry = mock_entry_encrypted_factory(sensor_type) + entity_id = "fan.test_name" + entry.add_to_hass(hass) + + mocked_instance = AsyncMock(return_value=True) + mcoked_none_instance = AsyncMock(return_value=None) + with patch.multiple( + "homeassistant.components.switchbot.fan.switchbot.SwitchbotAirPurifier", + get_basic_info=mcoked_none_instance, + update=mcoked_none_instance, + **{mock_method: mocked_instance}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + FAN_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + mocked_instance.assert_awaited_once() + + +@pytest.mark.parametrize( + ("service_info", "sensor_type"), + [ + (AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier"), + (AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table"), + (AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier"), + (AIR_PURIFIER_TBALE_PM25_SERVICE_INFO, "air_purifier_table"), + ], +) +@pytest.mark.parametrize( + ("service", "service_data", "mock_method"), + [ + (SERVICE_SET_PRESET_MODE, {ATTR_PRESET_MODE: "sleep"}, "set_preset_mode"), + (SERVICE_TURN_OFF, {}, "turn_off"), + (SERVICE_TURN_ON, {}, "turn_on"), + ], +) +@pytest.mark.parametrize( + ("exception", "error_message"), + [ + ( + SwitchbotOperationError("Operation failed"), + "An error occurred while performing the action: Operation failed", + ), + ], +) +async def test_exception_handling_air_purifier_service( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], + service_info: BluetoothServiceInfoBleak, + sensor_type: str, + service: str, + service_data: dict, + mock_method: str, + exception: Exception, + error_message: str, +) -> None: + """Test exception handling for air purifier service with exception.""" + inject_bluetooth_service_info(hass, service_info) + + entry = mock_entry_encrypted_factory(sensor_type) + entry.add_to_hass(hass) + entity_id = "fan.test_name" + + mcoked_none_instance = AsyncMock(return_value=None) + with patch.multiple( + "homeassistant.components.switchbot.fan.switchbot.SwitchbotAirPurifier", + get_basic_info=mcoked_none_instance, + update=mcoked_none_instance, + **{mock_method: AsyncMock(side_effect=exception)}, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match=error_message): + await hass.services.async_call( + FAN_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) From d3275c383344c586029a9cc9703ce330a26776fd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 15:07:05 +0200 Subject: [PATCH 0901/1175] Use shorthand attributes in xiaomi_miio (#145614) --- .../components/xiaomi_miio/air_quality.py | 46 ++++------- .../components/xiaomi_miio/entity.py | 39 ++-------- homeassistant/components/xiaomi_miio/light.py | 78 +++++++------------ .../components/xiaomi_miio/remote.py | 14 +--- .../components/xiaomi_miio/sensor.py | 28 +++---- .../components/xiaomi_miio/switch.py | 44 ++++------- 6 files changed, 78 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index c96a29a423c..9e52abb1c85 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -45,6 +45,8 @@ PROP_TO_ATTR = { class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" + _attr_icon = "mdi:cloud" + def __init__( self, name: str, @@ -55,7 +57,6 @@ class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._icon = "mdi:cloud" self._air_quality_index = None self._carbon_dioxide = None self._carbon_dioxide_equivalent = None @@ -74,21 +75,11 @@ class AirMonitorB1(XiaomiMiioEntity, AirQualityEntity): self._total_volatile_organic_compounds = round(state.tvoc, 3) self._temperature = round(state.temperature, 2) self._humidity = round(state.humidity, 2) - self._available = True + self._attr_available = True except DeviceException as ex: - self._available = False + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def available(self): - """Return true when state is known.""" - return self._available - @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" @@ -149,10 +140,10 @@ class AirMonitorS1(AirMonitorB1): self._total_volatile_organic_compounds = state.tvoc self._temperature = state.temperature self._humidity = state.humidity - self._available = True + self._attr_available = True except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @@ -165,10 +156,10 @@ class AirMonitorV1(AirMonitorB1): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._air_quality_index = state.aqi - self._available = True + self._attr_available = True except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @property @@ -180,6 +171,8 @@ class AirMonitorV1(AirMonitorB1): class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): """Air Quality class for cgllc.airm.cgdn1 device.""" + _attr_icon = "mdi:cloud" + def __init__( self, name: str, @@ -190,7 +183,6 @@ class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._icon = "mdi:cloud" self._carbon_dioxide = None self._particulate_matter_2_5 = None self._particulate_matter_10 = None @@ -203,21 +195,11 @@ class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): self._carbon_dioxide = state.co2 self._particulate_matter_2_5 = round(state.pm25, 1) self._particulate_matter_10 = round(state.pm10, 1) - self._available = True + self._attr_available = True except DeviceException as ex: - self._available = False + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def available(self): - """Return true when state is known.""" - return self._available - @property def carbon_dioxide(self): """Return the CO2 (carbon dioxide) level.""" diff --git a/homeassistant/components/xiaomi_miio/entity.py b/homeassistant/components/xiaomi_miio/entity.py index bb4e68f9f71..f5da22265c4 100644 --- a/homeassistant/components/xiaomi_miio/entity.py +++ b/homeassistant/components/xiaomi_miio/entity.py @@ -39,19 +39,9 @@ class XiaomiMiioEntity(Entity): self._model = entry.data[CONF_MODEL] self._mac = entry.data[CONF_MAC] self._device_id = entry.unique_id - self._unique_id = unique_id - self._name = name - self._available = False - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_available = False @property def device_info(self) -> DeviceInfo: @@ -62,7 +52,7 @@ class XiaomiMiioEntity(Entity): identifiers={(DOMAIN, self._device_id)}, manufacturer="Xiaomi", model=self._model, - name=self._name, + name=self._attr_name, ) if self._mac is not None: @@ -92,12 +82,7 @@ class XiaomiCoordinatedMiioEntity[_T: DataUpdateCoordinator[Any]]( self._mac = entry.data[CONF_MAC] self._device_id = entry.unique_id self._device_name = entry.title - self._unique_id = unique_id - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id + self._attr_unique_id = unique_id @property def device_info(self) -> DeviceInfo: @@ -183,18 +168,8 @@ class XiaomiGatewayDevice( super().__init__(coordinator) self._sub_device = sub_device self._entry = entry - self._unique_id = sub_device.sid - self._name = f"{sub_device.name} ({sub_device.sid})" - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name + self._attr_unique_id = sub_device.sid + self._attr_name = f"{sub_device.name} ({sub_device.sid})" @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index f452c704db2..6f4978b163e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -275,11 +275,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): self._state = None self._state_attrs: dict[str, Any] = {} - @property - def available(self) -> bool: - """Return true when state is known.""" - return self._available - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -302,9 +297,9 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): partial(func, *args, **kwargs) ) except DeviceException as exc: - if self._available: + if self._attr_available: _LOGGER.error(mask_error, exc) - self._available = False + self._attr_available = False return False @@ -339,14 +334,14 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -373,14 +368,14 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -556,14 +551,14 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -627,14 +622,14 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -685,14 +680,14 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -836,14 +831,14 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.ambient self._brightness = ceil((255 / 100.0) * state.ambient_brightness) @@ -1007,14 +1002,14 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): try: state = await self.hass.async_add_executor_job(self._device.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -1051,20 +1046,15 @@ class XiaomiGatewayLight(LightEntity): def __init__(self, gateway_device, gateway_name, gateway_device_id): """Initialize the XiaomiGatewayLight.""" self._gateway = gateway_device - self._name = f"{gateway_name} Light" + self._attr_name = f"{gateway_name} Light" self._gateway_device_id = gateway_device_id - self._unique_id = gateway_device_id - self._available = False + self._attr_unique_id = gateway_device_id + self._attr_available = False self._is_on = None self._brightness_pct = 100 self._rgb = (255, 255, 255) self._hs = (0, 0) - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - @property def device_info(self) -> DeviceInfo: """Return the device info of the gateway.""" @@ -1072,16 +1062,6 @@ class XiaomiGatewayLight(LightEntity): identifiers={(DOMAIN, self._gateway_device_id)}, ) - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - - @property - def available(self) -> bool: - """Return true when state is known.""" - return self._available - @property def is_on(self): """Return true if it is on.""" @@ -1125,14 +1105,14 @@ class XiaomiGatewayLight(LightEntity): self._gateway.light.rgb_status ) except GatewayException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error( "Got exception while fetching the gateway light state: %s", ex ) return - self._available = True + self._attr_available = True self._is_on = state_dict["is_on"] if self._is_on: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 9c83f3f4674..b5c7fa8710a 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -187,24 +187,14 @@ class XiaomiMiioRemote(RemoteEntity): def __init__(self, friendly_name, device, unique_id, slot, timeout, commands): """Initialize the remote.""" - self._name = friendly_name + self._attr_name = friendly_name self._device = device - self._unique_id = unique_id + self._attr_unique_id = unique_id self._slot = slot self._timeout = timeout self._state = False self._commands = commands - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the remote.""" - return self._name - @property def device(self): """Return the remote object.""" diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 9088dbb3a06..da4552cc63e 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -951,11 +951,6 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): } self.entity_description = description - @property - def available(self) -> bool: - """Return true when state is known.""" - return self._available - @property def native_value(self): """Return the state of the device.""" @@ -972,7 +967,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.aqi self._state_attrs.update( { @@ -988,8 +983,8 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): ) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @@ -1005,8 +1000,8 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): ) -> None: """Initialize the XiaomiSensor.""" super().__init__(coordinator, sub_device, entry) - self._unique_id = f"{sub_device.sid}-{description.key}" - self._name = f"{description.key} ({sub_device.sid})".capitalize() + self._attr_unique_id = f"{sub_device.sid}-{description.key}" + self._attr_name = f"{description.key} ({sub_device.sid})".capitalize() self.entity_description = description @property @@ -1027,14 +1022,9 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): ) self._gateway = gateway_device self.entity_description = description - self._available = False + self._attr_available = False self._state = None - @property - def available(self) -> bool: - """Return true when state is known.""" - return self._available - @property def native_value(self): """Return the state of the device.""" @@ -1046,10 +1036,10 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): self._state = await self.hass.async_add_executor_job( self._gateway.get_illumination ) - self._available = True + self._attr_available = True except GatewayException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error( "Got exception while fetching the gateway illuminance state: %s", ex ) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 2bd9e406a14..508a6e1a227 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -779,8 +779,8 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): super().__init__(coordinator, sub_device, entry) self._channel = GATEWAY_SWITCH_VARS[variable][KEY_CHANNEL] self._data_key = f"status_ch{self._channel}" - self._unique_id = f"{sub_device.sid}-ch{self._channel}" - self._name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})" + self._attr_unique_id = f"{sub_device.sid}-ch{self._channel}" + self._attr_name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})" @property def is_on(self): @@ -803,6 +803,7 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" + _attr_icon = "mdi:power-socket" _device: AirConditioningCompanionV3 | ChuangmiPlug | PowerStrip def __init__( @@ -815,22 +816,11 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) - self._icon = "mdi:power-socket" self._state: bool | None = None self._state_attrs = {ATTR_TEMPERATURE: None, ATTR_MODEL: self._model} self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def available(self) -> bool: - """Return true when state is known.""" - return self._available - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -848,9 +838,9 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): partial(func, *args, **kwargs) ) except DeviceException as exc: - if self._available: + if self._attr_available: _LOGGER.error(mask_error, exc) - self._available = False + self._attr_available = False return False @@ -891,13 +881,13 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._state_attrs[ATTR_TEMPERATURE] = state.temperature except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) async def async_set_wifi_led_on(self): @@ -972,7 +962,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.is_on self._state_attrs.update( {ATTR_TEMPERATURE: state.temperature, ATTR_LOAD_POWER: state.load_power} @@ -991,8 +981,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): self._state_attrs[ATTR_POWER_PRICE] = state.power_price except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) async def async_set_power_mode(self, mode: str): @@ -1079,7 +1069,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True if self._channel_usb: self._state = state.usb_power else: @@ -1094,8 +1084,8 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @@ -1149,11 +1139,11 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True self._state = state.power_socket == "on" self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) From 68a4e1a112d34d3eee00dcce2f75f69e78cd6e8b Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 26 May 2025 09:10:30 -0400 Subject: [PATCH 0902/1175] Add reconfigure config flow to APCUPSD (#143801) * Add reconfigure config flow * Add reconfigure config flow * Add more subtests for wrong device * Reduce the patch scopes * Address comments * Fix --------- Co-authored-by: Joostlek --- .../components/apcupsd/config_flow.py | 31 +++- homeassistant/components/apcupsd/strings.json | 4 +- tests/components/apcupsd/test_config_flow.py | 133 +++++++++++++++++- 3 files changed, 158 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py index b65c9c33265..bd26aa0a2d4 100644 --- a/homeassistant/components/apcupsd/config_flow.py +++ b/homeassistant/components/apcupsd/config_flow.py @@ -46,11 +46,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=_SCHEMA) host, port = user_input[CONF_HOST], user_input[CONF_PORT] - - # Abort if an entry with same host and port is present. self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) - - # Test the connection to the host and get the current status for serial number. try: async with asyncio.timeout(CONNECTION_TIMEOUT): data = APCUPSdData(await aioapcaccess.request_status(host, port)) @@ -67,3 +63,30 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): title = data.name or data.model or data.serial_no or "APC UPS" return self.async_create_entry(title=title, data=user_input) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of an existing entry.""" + + if user_input is None: + return self.async_show_form(step_id="reconfigure", data_schema=_SCHEMA) + + host, port = user_input[CONF_HOST], user_input[CONF_PORT] + self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port}) + try: + async with asyncio.timeout(CONNECTION_TIMEOUT): + data = APCUPSdData(await aioapcaccess.request_status(host, port)) + except (OSError, asyncio.IncompleteReadError, TimeoutError): + errors = {"base": "cannot_connect"} + return self.async_show_form( + step_id="reconfigure", data_schema=_SCHEMA, errors=errors + ) + + await self.async_set_unique_id(data.serial_no) + self._abort_if_unique_id_mismatch(reason="wrong_apcupsd_daemon") + + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), + data_updates=user_input, + ) diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json index 27a620491d1..d821b66ef67 100644 --- a/homeassistant/components/apcupsd/strings.json +++ b/homeassistant/components/apcupsd/strings.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "wrong_apcupsd_daemon": "The reconfigured APC UPS Daemon is not the same as the one already configured.", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index 0b8386dbb5a..e635b7d6681 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -1,5 +1,7 @@ """Test APCUPSd config flow setup process.""" +from __future__ import annotations + from copy import copy from unittest.mock import patch @@ -25,7 +27,9 @@ def _patch_setup(): async def test_config_flow_cannot_connect(hass: HomeAssistant) -> None: """Test config flow setup with connection error.""" - with patch("aioapcaccess.request_status") as mock_get: + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_get: mock_get.side_effect = OSError() result = await hass.config_entries.flow.async_init( @@ -51,7 +55,9 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: mock_entry.add_to_hass(hass) with ( - patch("aioapcaccess.request_status") as mock_request_status, + patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_request_status, _patch_setup(), ): mock_request_status.return_value = MOCK_STATUS @@ -98,7 +104,10 @@ async def test_config_flow_duplicate(hass: HomeAssistant) -> None: async def test_flow_works(hass: HomeAssistant) -> None: """Test successful creation of config entries via user configuration.""" with ( - patch("aioapcaccess.request_status", return_value=MOCK_STATUS), + patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=MOCK_STATUS, + ), _patch_setup() as mock_setup, ): result = await hass.config_entries.flow.async_init( @@ -111,7 +120,6 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONF_DATA ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_STATUS["UPSNAME"] assert result["data"] == CONF_DATA @@ -139,7 +147,9 @@ async def test_flow_minimal_status( integration will vary. """ with ( - patch("aioapcaccess.request_status") as mock_request_status, + patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" + ) as mock_request_status, _patch_setup() as mock_setup, ): status = MOCK_MINIMAL_STATUS | extra_status @@ -153,3 +163,116 @@ async def test_flow_minimal_status( assert result["data"] == CONF_DATA assert result["title"] == expected_title mock_setup.assert_called_once() + + +async def test_reconfigure_flow_works(hass: HomeAssistant) -> None: + """Test successful reconfiguration of an existing entry.""" + mock_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + unique_id=MOCK_STATUS["SERIALNO"], + source=SOURCE_USER, + ) + mock_entry.add_to_hass(hass) + + result = await mock_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # New configuration data with different host/port. + new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} + + with ( + patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=MOCK_STATUS, + ), + _patch_setup() as mock_setup, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) + await hass.async_block_till_done() + mock_setup.assert_called_once() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + # Check that the entry was updated with the new configuration. + assert mock_entry.data[CONF_HOST] == new_conf_data[CONF_HOST] + assert mock_entry.data[CONF_PORT] == new_conf_data[CONF_PORT] + + +async def test_reconfigure_flow_cannot_connect(hass: HomeAssistant) -> None: + """Test reconfiguration with connection error.""" + mock_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + unique_id=MOCK_STATUS["SERIALNO"], + source=SOURCE_USER, + ) + mock_entry.add_to_hass(hass) + + result = await mock_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # New configuration data with different host/port. + new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + side_effect=OSError(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == "cannot_connect" + + +@pytest.mark.parametrize( + ("unique_id_before", "unique_id_after"), + [ + (None, MOCK_STATUS["SERIALNO"]), + (MOCK_STATUS["SERIALNO"], "Blank"), + (MOCK_STATUS["SERIALNO"], MOCK_STATUS["SERIALNO"] + "ZZZ"), + ], +) +async def test_reconfigure_flow_wrong_device( + hass: HomeAssistant, unique_id_before: str | None, unique_id_after: str | None +) -> None: + """Test reconfiguration with a different device (wrong serial number).""" + mock_entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + unique_id=unique_id_before, + source=SOURCE_USER, + ) + mock_entry.add_to_hass(hass) + + result = await mock_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # New configuration data with different host/port. + new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} + # Make a copy of the status and modify the serial number if needed. + mock_status = {k: v for k, v in MOCK_STATUS.items() if k != "SERIALNO"} + mock_status["SERIALNO"] = unique_id_after + with patch( + "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", + return_value=mock_status, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_apcupsd_daemon" From dafda420e57ca1c7064d6918a736b4f531e264cb Mon Sep 17 00:00:00 2001 From: Robin Lintermann Date: Mon, 26 May 2025 15:21:23 +0200 Subject: [PATCH 0903/1175] Add smarla integration (#143081) * Added smarla integration * Apply suggested changes * Bump pysmarlaapi version and reevaluate quality scale * Focus on switch platform * Bump pysmarlaapi version * Change default name of device * Code refactoring * Removed obsolete reload function * Code refactoring and clean up * Bump pysmarlaapi version * Refactoring and changed access token format * Fix tests for smarla config_flow * Update quality_scale * Major rework of tests and refactoring * Bump pysmarlaapi version * Use object equality operator when applicable * Refactoring * Patch both connection objects * Refactor tests * Fix leaking tests * Implemented full test coverage * Bump pysmarlaapi version * Fix tests * Improve tests --------- Co-authored-by: Joostlek --- CODEOWNERS | 2 + homeassistant/components/smarla/__init__.py | 39 +++++++ .../components/smarla/config_flow.py | 62 +++++++++++ homeassistant/components/smarla/const.py | 12 ++ homeassistant/components/smarla/entity.py | 41 +++++++ homeassistant/components/smarla/icons.json | 9 ++ homeassistant/components/smarla/manifest.json | 12 ++ .../components/smarla/quality_scale.yaml | 60 ++++++++++ homeassistant/components/smarla/strings.json | 28 +++++ homeassistant/components/smarla/switch.py | 80 ++++++++++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/smarla/__init__.py | 22 ++++ tests/components/smarla/conftest.py | 63 +++++++++++ tests/components/smarla/const.py | 20 ++++ .../smarla/snapshots/test_switch.ambr | 95 ++++++++++++++++ tests/components/smarla/test_config_flow.py | 102 +++++++++++++++++ tests/components/smarla/test_init.py | 21 ++++ tests/components/smarla/test_switch.py | 103 ++++++++++++++++++ 21 files changed, 784 insertions(+) create mode 100644 homeassistant/components/smarla/__init__.py create mode 100644 homeassistant/components/smarla/config_flow.py create mode 100644 homeassistant/components/smarla/const.py create mode 100644 homeassistant/components/smarla/entity.py create mode 100644 homeassistant/components/smarla/icons.json create mode 100644 homeassistant/components/smarla/manifest.json create mode 100644 homeassistant/components/smarla/quality_scale.yaml create mode 100644 homeassistant/components/smarla/strings.json create mode 100644 homeassistant/components/smarla/switch.py create mode 100644 tests/components/smarla/__init__.py create mode 100644 tests/components/smarla/conftest.py create mode 100644 tests/components/smarla/const.py create mode 100644 tests/components/smarla/snapshots/test_switch.ambr create mode 100644 tests/components/smarla/test_config_flow.py create mode 100644 tests/components/smarla/test_init.py create mode 100644 tests/components/smarla/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 25c842cc6fa..45070195112 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1419,6 +1419,8 @@ build.json @home-assistant/supervisor /tests/components/sma/ @kellerza @rklomp @erwindouna /homeassistant/components/smappee/ @bsmappee /tests/components/smappee/ @bsmappee +/homeassistant/components/smarla/ @explicatis @rlint-explicatis +/tests/components/smarla/ @explicatis @rlint-explicatis /homeassistant/components/smart_meter_texas/ @grahamwetzler /tests/components/smart_meter_texas/ @grahamwetzler /homeassistant/components/smartthings/ @joostlek diff --git a/homeassistant/components/smarla/__init__.py b/homeassistant/components/smarla/__init__.py new file mode 100644 index 00000000000..c55b1067735 --- /dev/null +++ b/homeassistant/components/smarla/__init__.py @@ -0,0 +1,39 @@ +"""The Swing2Sleep Smarla integration.""" + +from pysmarlaapi import Connection, Federwiege + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed + +from .const import HOST, PLATFORMS + +type FederwiegeConfigEntry = ConfigEntry[Federwiege] + + +async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool: + """Set up this integration using UI.""" + connection = Connection(HOST, token_b64=entry.data[CONF_ACCESS_TOKEN]) + + # Check if token still has access + if not await connection.refresh_token(): + raise ConfigEntryAuthFailed("Invalid authentication") + + federwiege = Federwiege(hass.loop, connection) + federwiege.register() + federwiege.connect() + + entry.runtime_data = federwiege + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + entry.runtime_data.disconnect() + + return unload_ok diff --git a/homeassistant/components/smarla/config_flow.py b/homeassistant/components/smarla/config_flow.py new file mode 100644 index 00000000000..816adc85d1a --- /dev/null +++ b/homeassistant/components/smarla/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Swing2Sleep Smarla integration.""" + +from __future__ import annotations + +from typing import Any + +from pysmarlaapi import Connection +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN + +from .const import DOMAIN, HOST + +STEP_USER_DATA_SCHEMA = vol.Schema({CONF_ACCESS_TOKEN: str}) + + +class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Swing2Sleep Smarla.""" + + VERSION = 1 + + async def _handle_token(self, token: str) -> tuple[dict[str, str], str | None]: + """Handle the token input.""" + errors: dict[str, str] = {} + + try: + conn = Connection(url=HOST, token_b64=token) + except ValueError: + errors["base"] = "malformed_token" + return errors, None + + if not await conn.refresh_token(): + errors["base"] = "invalid_auth" + return errors, None + + return errors, conn.token.serialNumber + + 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: + raw_token = user_input[CONF_ACCESS_TOKEN] + errors, serial_number = await self._handle_token(token=raw_token) + + if not errors and serial_number is not None: + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=serial_number, + data={CONF_ACCESS_TOKEN: raw_token}, + ) + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/smarla/const.py b/homeassistant/components/smarla/const.py new file mode 100644 index 00000000000..7125e3f7270 --- /dev/null +++ b/homeassistant/components/smarla/const.py @@ -0,0 +1,12 @@ +"""Constants for the Swing2Sleep Smarla integration.""" + +from homeassistant.const import Platform + +DOMAIN = "smarla" + +HOST = "https://devices.swing2sleep.de" + +PLATFORMS = [Platform.SWITCH] + +DEVICE_MODEL_NAME = "Smarla" +MANUFACTURER_NAME = "Swing2Sleep" diff --git a/homeassistant/components/smarla/entity.py b/homeassistant/components/smarla/entity.py new file mode 100644 index 00000000000..a0ca052219c --- /dev/null +++ b/homeassistant/components/smarla/entity.py @@ -0,0 +1,41 @@ +"""Common base for entities.""" + +from typing import Any + +from pysmarlaapi import Federwiege +from pysmarlaapi.federwiege.classes import Property + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import Entity + +from .const import DEVICE_MODEL_NAME, DOMAIN, MANUFACTURER_NAME + + +class SmarlaBaseEntity(Entity): + """Common Base Entity class for defining Smarla device.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__(self, federwiege: Federwiege, prop: Property) -> None: + """Initialise the entity.""" + self._property = prop + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, federwiege.serial_number)}, + name=DEVICE_MODEL_NAME, + model=DEVICE_MODEL_NAME, + manufacturer=MANUFACTURER_NAME, + serial_number=federwiege.serial_number, + ) + + async def on_change(self, value: Any): + """Notify ha when state changes.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + await self._property.add_listener(self.on_change) + + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + await self._property.remove_listener(self.on_change) diff --git a/homeassistant/components/smarla/icons.json b/homeassistant/components/smarla/icons.json new file mode 100644 index 00000000000..5a31ec88822 --- /dev/null +++ b/homeassistant/components/smarla/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "switch": { + "smart_mode": { + "default": "mdi:refresh-auto" + } + } + } +} diff --git a/homeassistant/components/smarla/manifest.json b/homeassistant/components/smarla/manifest.json new file mode 100644 index 00000000000..5e572c78536 --- /dev/null +++ b/homeassistant/components/smarla/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "smarla", + "name": "Swing2Sleep Smarla", + "codeowners": ["@explicatis", "@rlint-explicatis"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/smarla", + "integration_type": "device", + "iot_class": "cloud_push", + "loggers": ["pysmarlaapi", "pysignalr"], + "quality_scale": "bronze", + "requirements": ["pysmarlaapi==0.8.2"] +} diff --git a/homeassistant/components/smarla/quality_scale.yaml b/homeassistant/components/smarla/quality_scale.yaml new file mode 100644 index 00000000000..99b6e0c608c --- /dev/null +++ b/homeassistant/components/smarla/quality_scale.yaml @@ -0,0 +1,60 @@ +rules: + # Bronze + action-setup: done + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: done + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: done + docs-installation-parameters: done + entity-unavailable: todo + integration-owner: done + log-when-unavailable: todo + parallel-updates: todo + reauthentication-flow: todo + test-coverage: done + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: todo + entity-device-class: done + entity-disabled-by-default: done + entity-translations: done + exception-translations: done + icon-translations: done + reconfiguration-flow: todo + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: done + inject-websession: todo + strict-typing: todo diff --git a/homeassistant/components/smarla/strings.json b/homeassistant/components/smarla/strings.json new file mode 100644 index 00000000000..8426bc30566 --- /dev/null +++ b/homeassistant/components/smarla/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "malformed_token": "Malformed access token" + }, + "step": { + "user": { + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + }, + "data_description": { + "access_token": "The access token generated by the Swing2Sleep app." + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "switch": { + "smart_mode": { + "name": "Smart Mode" + } + } + } +} diff --git a/homeassistant/components/smarla/switch.py b/homeassistant/components/smarla/switch.py new file mode 100644 index 00000000000..49bcce23b24 --- /dev/null +++ b/homeassistant/components/smarla/switch.py @@ -0,0 +1,80 @@ +"""Support for the Swing2Sleep Smarla switch entities.""" + +from dataclasses import dataclass +from typing import Any + +from pysmarlaapi import Federwiege +from pysmarlaapi.federwiege.classes import Property + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import FederwiegeConfigEntry +from .entity import SmarlaBaseEntity + + +@dataclass(frozen=True, kw_only=True) +class SmarlaSwitchEntityDescription(SwitchEntityDescription): + """Class describing Swing2Sleep Smarla switch entity.""" + + service: str + property: str + + +SWITCHES: list[SmarlaSwitchEntityDescription] = [ + SmarlaSwitchEntityDescription( + key="swing_active", + name=None, + service="babywiege", + property="swing_active", + ), + SmarlaSwitchEntityDescription( + key="smart_mode", + translation_key="smart_mode", + service="babywiege", + property="smart_mode", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: FederwiegeConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Smarla switches from config entry.""" + federwiege = config_entry.runtime_data + async_add_entities(SmarlaSwitch(federwiege, desc) for desc in SWITCHES) + + +class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity): + """Representation of Smarla switch.""" + + entity_description: SmarlaSwitchEntityDescription + + _property: Property[bool] + + def __init__( + self, + federwiege: Federwiege, + desc: SmarlaSwitchEntityDescription, + ) -> None: + """Initialize a Smarla switch.""" + prop = federwiege.get_property(desc.service, desc.property) + super().__init__(federwiege, prop) + self.entity_description = desc + self._attr_unique_id = f"{federwiege.serial_number}-{desc.key}" + + @property + def is_on(self) -> bool: + """Return the entity value to represent the entity state.""" + return self._property.get() + + def turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + self._property.set(True) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + self._property.set(False) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1cba78af0b0..44a9b19e8c2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -578,6 +578,7 @@ FLOWS = { "slimproto", "sma", "smappee", + "smarla", "smart_meter_texas", "smartthings", "smarttub", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 66693d41396..4ae336f3c61 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6028,6 +6028,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "smarla": { + "name": "Swing2Sleep Smarla", + "integration_type": "device", + "config_flow": true, + "iot_class": "cloud_push" + }, "smart_blinds": { "name": "Smartblinds", "integration_type": "virtual", diff --git a/requirements_all.txt b/requirements_all.txt index 67cfc2c49c7..7cb0a029ce8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2337,6 +2337,9 @@ pysma==0.7.5 # homeassistant.components.smappee pysmappee==0.2.29 +# homeassistant.components.smarla +pysmarlaapi==0.8.2 + # homeassistant.components.smartthings pysmartthings==3.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b51c8823c02..ecd2a1d2b31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1910,6 +1910,9 @@ pysma==0.7.5 # homeassistant.components.smappee pysmappee==0.2.29 +# homeassistant.components.smarla +pysmarlaapi==0.8.2 + # homeassistant.components.smartthings pysmartthings==3.2.3 diff --git a/tests/components/smarla/__init__.py b/tests/components/smarla/__init__.py new file mode 100644 index 00000000000..df4a735c0ca --- /dev/null +++ b/tests/components/smarla/__init__.py @@ -0,0 +1,22 @@ +"""Tests for the Smarla integration.""" + +from typing import Any +from unittest.mock import AsyncMock + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> bool: + """Set up the component.""" + config_entry.add_to_hass(hass) + if success := await hass.config_entries.async_setup(config_entry.entry_id): + await hass.async_block_till_done() + return success + + +async def update_property_listeners(mock: AsyncMock, value: Any = None) -> None: + """Update the property listeners for the mock object.""" + for call in mock.add_listener.call_args_list: + await call[0][0](value) diff --git a/tests/components/smarla/conftest.py b/tests/components/smarla/conftest.py new file mode 100644 index 00000000000..a188924415a --- /dev/null +++ b/tests/components/smarla/conftest.py @@ -0,0 +1,63 @@ +"""Configuration for smarla tests.""" + +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from pysmarlaapi.classes import AuthToken +import pytest + +from homeassistant.components.smarla.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER + +from .const import MOCK_ACCESS_TOKEN_JSON, MOCK_SERIAL_NUMBER, MOCK_USER_INPUT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL_NUMBER, + source=SOURCE_USER, + data=MOCK_USER_INPUT, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator: + """Override async_setup_entry.""" + with patch("homeassistant.components.smarla.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_connection() -> Generator[MagicMock]: + """Patch Connection object.""" + with ( + patch( + "homeassistant.components.smarla.config_flow.Connection", autospec=True + ) as mock_connection, + patch( + "homeassistant.components.smarla.Connection", + mock_connection, + ), + ): + connection = mock_connection.return_value + connection.token = AuthToken.from_json(MOCK_ACCESS_TOKEN_JSON) + connection.refresh_token.return_value = True + yield connection + + +@pytest.fixture +def mock_federwiege(mock_connection: MagicMock) -> Generator[MagicMock]: + """Mock the Federwiege instance.""" + with patch( + "homeassistant.components.smarla.Federwiege", autospec=True + ) as mock_federwiege: + federwiege = mock_federwiege.return_value + federwiege.serial_number = MOCK_SERIAL_NUMBER + yield federwiege diff --git a/tests/components/smarla/const.py b/tests/components/smarla/const.py new file mode 100644 index 00000000000..33cb51c63d1 --- /dev/null +++ b/tests/components/smarla/const.py @@ -0,0 +1,20 @@ +"""Constants for the Smarla integration tests.""" + +import base64 +import json + +from homeassistant.const import CONF_ACCESS_TOKEN + +MOCK_ACCESS_TOKEN_JSON = { + "refreshToken": "test", + "appIdentifier": "HA-test", + "serialNumber": "ABCD", +} + +MOCK_SERIAL_NUMBER = MOCK_ACCESS_TOKEN_JSON["serialNumber"] + +MOCK_ACCESS_TOKEN = base64.b64encode( + json.dumps(MOCK_ACCESS_TOKEN_JSON).encode() +).decode() + +MOCK_USER_INPUT = {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN} diff --git a/tests/components/smarla/snapshots/test_switch.ambr b/tests/components/smarla/snapshots/test_switch.ambr new file mode 100644 index 00000000000..bd713c209c1 --- /dev/null +++ b/tests/components/smarla/snapshots/test_switch.ambr @@ -0,0 +1,95 @@ +# serializer version: 1 +# name: test_entities[switch.smarla-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.smarla', + 'has_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': 'smarla', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ABCD-swing_active', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.smarla-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla', + }), + 'context': , + 'entity_id': 'switch.smarla', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_entities[switch.smarla_smart_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.smarla_smart_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': 'Smart Mode', + 'platform': 'smarla', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_mode', + 'unique_id': 'ABCD-smart_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[switch.smarla_smart_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smarla Smart Mode', + }), + 'context': , + 'entity_id': 'switch.smarla_smart_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/smarla/test_config_flow.py b/tests/components/smarla/test_config_flow.py new file mode 100644 index 00000000000..a2bd5b36fc0 --- /dev/null +++ b/tests/components/smarla/test_config_flow.py @@ -0,0 +1,102 @@ +"""Test config flow for Swing2Sleep Smarla integration.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.smarla.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import MOCK_SERIAL_NUMBER, MOCK_USER_INPUT + +from tests.common import MockConfigEntry + + +async def test_config_flow( + hass: HomeAssistant, mock_setup_entry, mock_connection: MagicMock +) -> None: + """Test creating a config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == MOCK_SERIAL_NUMBER + assert result["data"] == MOCK_USER_INPUT + assert result["result"].unique_id == MOCK_SERIAL_NUMBER + + +async def test_malformed_token( + hass: HomeAssistant, mock_setup_entry, mock_connection: MagicMock +) -> None: + """Test we show user form on malformed token input.""" + with patch( + "homeassistant.components.smarla.config_flow.Connection", side_effect=ValueError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "malformed_token"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_invalid_auth( + hass: HomeAssistant, mock_setup_entry, mock_connection: MagicMock +) -> None: + """Test we show user form on invalid auth.""" + with patch.object( + mock_connection, "refresh_token", new=AsyncMock(return_value=False) + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_device_exists_abort( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection: MagicMock +) -> None: + """Test we abort config flow if Smarla device already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smarla/test_init.py b/tests/components/smarla/test_init.py new file mode 100644 index 00000000000..b9d291f582d --- /dev/null +++ b/tests/components/smarla/test_init.py @@ -0,0 +1,21 @@ +"""Test switch platform for Swing2Sleep Smarla integration.""" + +from unittest.mock import MagicMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_init_invalid_auth( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_connection: MagicMock +) -> None: + """Test init invalid authentication behavior.""" + mock_connection.refresh_token.return_value = False + + assert not await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/smarla/test_switch.py b/tests/components/smarla/test_switch.py new file mode 100644 index 00000000000..24a645dac9f --- /dev/null +++ b/tests/components/smarla/test_switch.py @@ -0,0 +1,103 @@ +"""Test switch platform for Swing2Sleep Smarla integration.""" + +from unittest.mock import MagicMock, patch + +from pysmarlaapi.federwiege.classes import Property +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration, update_property_listeners + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture +def mock_switch_property() -> MagicMock: + """Mock a switch property.""" + mock = MagicMock(spec=Property) + mock.get.return_value = False + return mock + + +async def test_entities( + hass: HomeAssistant, + mock_federwiege: MagicMock, + mock_switch_property: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Smarla entities.""" + mock_federwiege.get_property.return_value = mock_switch_property + + with ( + patch("homeassistant.components.smarla.PLATFORMS", [Platform.SWITCH]), + ): + assert await setup_integration(hass, mock_config_entry) + + await snapshot_platform( + hass, entity_registry, snapshot, mock_config_entry.entry_id + ) + + +@pytest.mark.parametrize( + ("service", "parameter"), + [ + (SERVICE_TURN_ON, True), + (SERVICE_TURN_OFF, False), + ], +) +async def test_switch_action( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_federwiege: MagicMock, + mock_switch_property: MagicMock, + service: str, + parameter: bool, +) -> None: + """Test Smarla Switch on/off behavior.""" + mock_federwiege.get_property.return_value = mock_switch_property + + assert await setup_integration(hass, mock_config_entry) + + # Turn on + await hass.services.async_call( + SWITCH_DOMAIN, + service, + {ATTR_ENTITY_ID: "switch.smarla"}, + blocking=True, + ) + mock_switch_property.set.assert_called_once_with(parameter) + + +async def test_switch_state_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_federwiege: MagicMock, + mock_switch_property: MagicMock, +) -> None: + """Test Smarla Switch callback.""" + mock_federwiege.get_property.return_value = mock_switch_property + + assert await setup_integration(hass, mock_config_entry) + + assert hass.states.get("switch.smarla").state == STATE_OFF + + mock_switch_property.get.return_value = True + + await update_property_listeners(mock_switch_property) + await hass.async_block_till_done() + + assert hass.states.get("switch.smarla").state == STATE_ON From a3b7cd7b4d582c78f1eb6b01d2d8003e652b34ce Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 26 May 2025 15:23:11 +0200 Subject: [PATCH 0904/1175] Implement NVR download for Reolink recordings (#144121) --- .../components/reolink/media_source.py | 23 +++++++++++++----- tests/components/reolink/test_media_source.py | 24 +++++++++---------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 092f0d4ddca..49257128a2d 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -27,6 +27,8 @@ from .views import async_generate_playback_proxy_url _LOGGER = logging.getLogger(__name__) +VOD_SPLIT_TIME = dt.timedelta(minutes=5) + async def async_get_media_source(hass: HomeAssistant) -> ReolinkVODMediaSource: """Set up camera media source.""" @@ -60,11 +62,13 @@ class ReolinkVODMediaSource(MediaSource): """Resolve media to a url.""" identifier = ["UNKNOWN"] if item.identifier is not None: - identifier = item.identifier.split("|", 5) + identifier = item.identifier.split("|", 6) if identifier[0] != "FILE": raise Unresolvable(f"Unknown media item '{item.identifier}'.") - _, config_entry_id, channel_str, stream_res, filename = identifier + _, config_entry_id, channel_str, stream_res, filename, start_time, end_time = ( + identifier + ) channel = int(channel_str) host = get_host(self.hass, config_entry_id) @@ -75,12 +79,19 @@ class ReolinkVODMediaSource(MediaSource): return VodRequestType.DOWNLOAD return VodRequestType.PLAYBACK if host.api.is_nvr: - return VodRequestType.FLV + return VodRequestType.NVR_DOWNLOAD return VodRequestType.RTMP vod_type = get_vod_type() - if vod_type in [VodRequestType.DOWNLOAD, VodRequestType.PLAYBACK]: + if vod_type == VodRequestType.NVR_DOWNLOAD: + filename = f"{start_time}_{end_time}" + + if vod_type in { + VodRequestType.DOWNLOAD, + VodRequestType.NVR_DOWNLOAD, + VodRequestType.PLAYBACK, + }: proxy_url = async_generate_playback_proxy_url( config_entry_id, channel, filename, stream_res, vod_type.value ) @@ -358,7 +369,7 @@ class ReolinkVODMediaSource(MediaSource): day, ) _, vod_files = await host.api.request_vod_files( - channel, start, end, stream=stream + channel, start, end, stream=stream, split_time=VOD_SPLIT_TIME ) for file in vod_files: file_name = f"{file.start_time.time()} {file.duration}" @@ -372,7 +383,7 @@ class ReolinkVODMediaSource(MediaSource): children.append( BrowseMediaSource( domain=DOMAIN, - identifier=f"FILE|{config_entry_id}|{channel}|{stream}|{file.file_name}", + identifier=f"FILE|{config_entry_id}|{channel}|{stream}|{file.file_name}|{file.start_time_id}|{file.end_time_id}", media_class=MediaClass.VIDEO, media_content_type=MediaType.VIDEO, title=file_name, diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 7044ea53671..59868514226 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -51,8 +51,10 @@ TEST_DAY = 14 TEST_DAY2 = 15 TEST_HOUR = 13 TEST_MINUTE = 12 -TEST_FILE_NAME = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00" -TEST_FILE_NAME_MP4 = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}00.mp4" +TEST_START = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}" +TEST_END = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE + 5}" +TEST_FILE_NAME = f"{TEST_START}00" +TEST_FILE_NAME_MP4 = f"{TEST_START}00.mp4" TEST_STREAM = "main" TEST_CHANNEL = "0" TEST_CAM_NAME = "Cam new name" @@ -92,17 +94,15 @@ async def test_resolve( await hass.async_block_till_done() caplog.set_level(logging.DEBUG) - file_id = ( - f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" - ) - reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) + file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}" + reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) play_media = await async_resolve_media( hass, f"{URI_SCHEME}{DOMAIN}/{file_id}", None ) - assert play_media.mime_type == TEST_MIME_TYPE + assert play_media.mime_type == TEST_MIME_TYPE_MP4 - file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME_MP4}" + file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME_MP4}|{TEST_START}|{TEST_END}" reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL2) play_media = await async_resolve_media( @@ -117,9 +117,7 @@ async def test_resolve( ) assert play_media.mime_type == TEST_MIME_TYPE_MP4 - file_id = ( - f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" - ) + file_id = f"FILE|{config_entry.entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}" reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE, TEST_URL) play_media = await async_resolve_media( @@ -217,6 +215,8 @@ async def test_browsing( mock_vod_file.start_time = datetime( TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE ) + mock_vod_file.start_time_id = TEST_START + mock_vod_file.end_time_id = TEST_END mock_vod_file.duration = timedelta(minutes=15) mock_vod_file.file_name = TEST_FILE_NAME reolink_connect.request_vod_files.return_value = ([mock_status], [mock_vod_file]) @@ -224,7 +224,7 @@ async def test_browsing( browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_day_0_id}") browse_files_id = f"FILES|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}" - browse_file_id = f"FILE|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}" + browse_file_id = f"FILE|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_FILE_NAME}|{TEST_START}|{TEST_END}" assert browse.domain == DOMAIN assert ( browse.title From 54dce536280f83ab6b7851e1536dea166c94ac38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 26 May 2025 14:28:30 +0100 Subject: [PATCH 0905/1175] Add sensor tests for device class enums (#145523) --- tests/components/sensor/test_init.py | 53 +++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e8daff09b7c..e0fe1713b82 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -24,7 +24,7 @@ from homeassistant.components.sensor import ( async_rounded_state, async_update_suggested_units, ) -from homeassistant.components.sensor.const import STATE_CLASS_UNITS +from homeassistant.components.sensor.const import STATE_CLASS_UNITS, UNIT_CONVERTERS from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -2812,3 +2812,54 @@ async def test_suggested_unit_guard_valid_unit( assert entry.options == { "sensor.private": {"suggested_unit_of_measurement": suggested_unit}, } + + +def test_device_class_units_are_complete() -> None: + """Test that the device class units enum is complete.""" + no_unit_device_classes = { + SensorDeviceClass.DATE, + SensorDeviceClass.ENUM, + SensorDeviceClass.MONETARY, + SensorDeviceClass.TIMESTAMP, + } + unit_device_classes = { + device_class.value for device_class in SensorDeviceClass + } - no_unit_device_classes + assert set(DEVICE_CLASS_UNITS.keys()) == unit_device_classes + + +def test_device_class_converters_are_complete() -> None: + """Test that the device class converters enum is complete.""" + no_converter_device_classes = { + SensorDeviceClass.APPARENT_POWER, + SensorDeviceClass.AQI, + SensorDeviceClass.BATTERY, + SensorDeviceClass.CO, + SensorDeviceClass.CO2, + SensorDeviceClass.DATE, + SensorDeviceClass.ENUM, + SensorDeviceClass.FREQUENCY, + SensorDeviceClass.HUMIDITY, + SensorDeviceClass.ILLUMINANCE, + SensorDeviceClass.IRRADIANCE, + SensorDeviceClass.MOISTURE, + SensorDeviceClass.MONETARY, + SensorDeviceClass.NITROGEN_DIOXIDE, + SensorDeviceClass.NITROGEN_MONOXIDE, + SensorDeviceClass.NITROUS_OXIDE, + SensorDeviceClass.OZONE, + SensorDeviceClass.PH, + SensorDeviceClass.PM1, + SensorDeviceClass.PM10, + SensorDeviceClass.PM25, + SensorDeviceClass.REACTIVE_POWER, + SensorDeviceClass.SIGNAL_STRENGTH, + SensorDeviceClass.SOUND_PRESSURE, + SensorDeviceClass.SULPHUR_DIOXIDE, + SensorDeviceClass.TIMESTAMP, + SensorDeviceClass.WIND_DIRECTION, + } + converter_device_classes = { + device_class.value for device_class in SensorDeviceClass + } - no_converter_device_classes + assert set(UNIT_CONVERTERS.keys()) == converter_device_classes From 486535c1892f7d220a807f36218189a241555e93 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Mon, 26 May 2025 15:37:07 +0200 Subject: [PATCH 0906/1175] Add scene platform to Qbus integration (#144032) * Add scene platform * Remove updating last_activated * Simplify device info * Move _attr_name to specific classes * Refactor device info --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/qbus/climate.py | 1 + homeassistant/components/qbus/const.py | 1 + homeassistant/components/qbus/coordinator.py | 1 + homeassistant/components/qbus/entity.py | 19 ++++-- homeassistant/components/qbus/light.py | 1 + homeassistant/components/qbus/scene.py | 66 +++++++++++++++++++ homeassistant/components/qbus/switch.py | 1 + .../qbus/fixtures/payload_config.json | 13 ++++ tests/components/qbus/test_scene.py | 45 +++++++++++++ 9 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/qbus/scene.py create mode 100644 tests/components/qbus/test_scene.py diff --git a/homeassistant/components/qbus/climate.py b/homeassistant/components/qbus/climate.py index 57d97c046b7..c6f234a14b7 100644 --- a/homeassistant/components/qbus/climate.py +++ b/homeassistant/components/qbus/climate.py @@ -57,6 +57,7 @@ async def async_setup_entry( class QbusClimate(QbusEntity, ClimateEntity): """Representation of a Qbus climate entity.""" + _attr_name = None _attr_hvac_modes = [HVACMode.HEAT] _attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/qbus/const.py b/homeassistant/components/qbus/const.py index 767a41f48cc..e679c4b9927 100644 --- a/homeassistant/components/qbus/const.py +++ b/homeassistant/components/qbus/const.py @@ -8,6 +8,7 @@ DOMAIN: Final = "qbus" PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.LIGHT, + Platform.SCENE, Platform.SWITCH, ] diff --git a/homeassistant/components/qbus/coordinator.py b/homeassistant/components/qbus/coordinator.py index dd57a98787b..42e226c8e6a 100644 --- a/homeassistant/components/qbus/coordinator.py +++ b/homeassistant/components/qbus/coordinator.py @@ -105,6 +105,7 @@ class QbusControllerCoordinator(DataUpdateCoordinator[list[QbusMqttOutput]]): device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, self._controller.mac)}, identifiers={(DOMAIN, format_mac(self._controller.mac))}, manufacturer=MANUFACTURER, model="CTD3.x", diff --git a/homeassistant/components/qbus/entity.py b/homeassistant/components/qbus/entity.py index 4ab1913c4dc..70d469f9c93 100644 --- a/homeassistant/components/qbus/entity.py +++ b/homeassistant/components/qbus/entity.py @@ -54,34 +54,39 @@ def format_ref_id(ref_id: str) -> str | None: return None +def create_main_device_identifier(mqtt_output: QbusMqttOutput) -> tuple[str, str]: + """Create the identifier referring to the main device this output belongs to.""" + return (DOMAIN, format_mac(mqtt_output.device.mac)) + + class QbusEntity(Entity, ABC): """Representation of a Qbus entity.""" _attr_has_entity_name = True - _attr_name = None _attr_should_poll = False def __init__(self, mqtt_output: QbusMqttOutput) -> None: """Initialize the Qbus entity.""" + self._mqtt_output = mqtt_output + self._topic_factory = QbusMqttTopicFactory() self._message_factory = QbusMqttMessageFactory() + self._state_topic = self._topic_factory.get_output_state_topic( + mqtt_output.device.id, mqtt_output.id + ) ref_id = format_ref_id(mqtt_output.ref_id) self._attr_unique_id = f"ctd_{mqtt_output.device.serial_number}_{ref_id}" + # Create linked device self._attr_device_info = DeviceInfo( name=mqtt_output.name.title(), manufacturer=MANUFACTURER, identifiers={(DOMAIN, f"{mqtt_output.device.serial_number}_{ref_id}")}, suggested_area=mqtt_output.location.title(), - via_device=(DOMAIN, format_mac(mqtt_output.device.mac)), - ) - - self._mqtt_output = mqtt_output - self._state_topic = self._topic_factory.get_output_state_topic( - mqtt_output.device.id, mqtt_output.id + via_device=create_main_device_identifier(mqtt_output), ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/qbus/light.py b/homeassistant/components/qbus/light.py index 3d2c763b8e3..654aab80ac7 100644 --- a/homeassistant/components/qbus/light.py +++ b/homeassistant/components/qbus/light.py @@ -43,6 +43,7 @@ async def async_setup_entry( class QbusLight(QbusEntity, LightEntity): """Representation of a Qbus light entity.""" + _attr_name = None _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/qbus/scene.py b/homeassistant/components/qbus/scene.py new file mode 100644 index 00000000000..9a9a1e2df83 --- /dev/null +++ b/homeassistant/components/qbus/scene.py @@ -0,0 +1,66 @@ +"""Support for Qbus scene.""" + +from typing import Any + +from qbusmqttapi.discovery import QbusMqttOutput +from qbusmqttapi.state import QbusMqttState, StateAction, StateType + +from homeassistant.components.mqtt import ReceiveMessage +from homeassistant.components.scene import Scene +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import QbusConfigEntry +from .entity import QbusEntity, add_new_outputs, create_main_device_identifier + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: QbusConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up scene entities.""" + + coordinator = entry.runtime_data + added_outputs: list[QbusMqttOutput] = [] + + def _check_outputs() -> None: + add_new_outputs( + coordinator, + added_outputs, + lambda output: output.type == "scene", + QbusScene, + async_add_entities, + ) + + _check_outputs() + entry.async_on_unload(coordinator.async_add_listener(_check_outputs)) + + +class QbusScene(QbusEntity, Scene): + """Representation of a Qbus scene entity.""" + + def __init__(self, mqtt_output: QbusMqttOutput) -> None: + """Initialize scene entity.""" + + super().__init__(mqtt_output) + + # Add to main controller device + self._attr_device_info = DeviceInfo( + identifiers={create_main_device_identifier(mqtt_output)} + ) + self._attr_name = mqtt_output.name.title() + + async def async_activate(self, **kwargs: Any) -> None: + """Activate scene.""" + state = QbusMqttState( + id=self._mqtt_output.id, type=StateType.ACTION, action=StateAction.ACTIVE + ) + await self._async_publish_output_state(state) + + async def _state_received(self, msg: ReceiveMessage) -> None: + # Nothing to do + pass diff --git a/homeassistant/components/qbus/switch.py b/homeassistant/components/qbus/switch.py index e1feccf4450..c0e2b112bc5 100644 --- a/homeassistant/components/qbus/switch.py +++ b/homeassistant/components/qbus/switch.py @@ -42,6 +42,7 @@ async def async_setup_entry( class QbusSwitch(QbusEntity, SwitchEntity): """Representation of a Qbus switch entity.""" + _attr_name = None _attr_device_class = SwitchDeviceClass.SWITCH def __init__(self, mqtt_output: QbusMqttOutput) -> None: diff --git a/tests/components/qbus/fixtures/payload_config.json b/tests/components/qbus/fixtures/payload_config.json index fc204c975ad..3a9e845bc26 100644 --- a/tests/components/qbus/fixtures/payload_config.json +++ b/tests/components/qbus/fixtures/payload_config.json @@ -99,6 +99,19 @@ "write": true } } + }, + { + "id": "UL25", + "location": "Living", + "locationId": 0, + "name": "Watching TV", + "originalName": "Watching TV", + "refId": "000001/105/3", + "type": "scene", + "actions": { + "active": null + }, + "properties": {} } ] } diff --git a/tests/components/qbus/test_scene.py b/tests/components/qbus/test_scene.py new file mode 100644 index 00000000000..8fdf60ec502 --- /dev/null +++ b/tests/components/qbus/test_scene.py @@ -0,0 +1,45 @@ +"""Test Qbus scene entities.""" + +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from tests.common import async_fire_mqtt_message +from tests.typing import MqttMockHAClient + +_PAYLOAD_SCENE_STATE = '{"id":"UL25","properties":{"value":true},"type":"state"}' +_PAYLOAD_SCENE_ACTIVATE = '{"id": "UL25", "type": "action", "action": "active"}' + +_TOPIC_SCENE_STATE = "cloudapp/QBUSMQTTGW/UL1/UL25/state" +_TOPIC_SCENE_SET_STATE = "cloudapp/QBUSMQTTGW/UL1/UL25/setState" + +_SCENE_ENTITY_ID = "scene.ctd_000001_watching_tv" + + +async def test_scene( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + setup_integration: None, +) -> None: + """Test scene.""" + + assert hass.states.get(_SCENE_ENTITY_ID).state == STATE_UNKNOWN + + # Activate scene + mqtt_mock.reset_mock() + await hass.services.async_call( + SCENE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: _SCENE_ENTITY_ID}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + _TOPIC_SCENE_SET_STATE, _PAYLOAD_SCENE_ACTIVATE, 0, False + ) + + # Simulate response + async_fire_mqtt_message(hass, _TOPIC_SCENE_STATE, _PAYLOAD_SCENE_STATE) + await hass.async_block_till_done() + + assert hass.states.get(_SCENE_ENTITY_ID).state != STATE_UNKNOWN From 14cd00a116f3b06f9e6fe5932fa14676dc0309c7 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 26 May 2025 15:40:15 +0200 Subject: [PATCH 0907/1175] Add user picture to fyta (#140934) * Add user picture * FYTA integration: Add separate entities for both default and user plant images (#12) * Refactor FYTA integration to provide both default and user plant images as separate entities * Refactor FYTA tests by removing unused CONF_USER_IMAGE option and related test cases * Update FytaPlantImageEntity to set entity name based on image type * Refactor FYTA image tests to accommodate separate plant and user image entities, updating assertions and snapshots accordingly. * Enhance FYTA image handling by introducing FytaImageEntityDescription for better separation of plant and user images, and update image URL retrieval logic. Additionally, add localized strings for image entities in strings.json. * Correct typo * Update FYTA image snapshots to reflect changes in translation keys for plant and user images. * Update homeassistant/components/fyta/image.py * Update homeassistant/components/fyta/image.py --------- Co-authored-by: dontinelli <73341522+dontinelli@users.noreply.github.com> * Update QS + ruff * Revert MINOR_VERSION increase and remove obsolete migration test * Update snapshot * Resolve comments * Update snapshot * Fix --------- Co-authored-by: Alexander --- homeassistant/components/fyta/__init__.py | 5 +- homeassistant/components/fyta/image.py | 84 +++++++++-- homeassistant/components/fyta/strings.json | 8 + .../fyta/fixtures/plant_status1_update.json | 2 +- .../components/fyta/snapshots/test_image.ambr | 138 +++++++++++++++--- tests/components/fyta/test_image.py | 94 +++++++++++- 6 files changed, 293 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index 1b00afc9c80..2264f341bad 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -84,7 +84,10 @@ async def async_migrate_entry( new[CONF_EXPIRATION] = credentials.expiration.isoformat() hass.config_entries.async_update_entry( - config_entry, data=new, minor_version=2, version=1 + config_entry, + data=new, + minor_version=2, + version=1, ) _LOGGER.debug( diff --git a/homeassistant/components/fyta/image.py b/homeassistant/components/fyta/image.py index 326f2ddf570..891c0bf53eb 100644 --- a/homeassistant/components/fyta/image.py +++ b/homeassistant/components/fyta/image.py @@ -2,9 +2,20 @@ from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime +import logging +from typing import Final -from homeassistant.components.image import ImageEntity, ImageEntityDescription +from fyta_cli.fyta_models import Plant + +from homeassistant.components.image import ( + Image, + ImageEntity, + ImageEntityDescription, + valid_image_content_type, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -12,6 +23,30 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import FytaConfigEntry, FytaCoordinator from .entity import FytaPlantEntity +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class FytaImageEntityDescription(ImageEntityDescription): + """Describes Fyta image entity.""" + + url_fn: Callable[[Plant], str] + name_key: str | None = None + + +IMAGES: Final[list[FytaImageEntityDescription]] = [ + FytaImageEntityDescription( + key="plant_image", + translation_key="plant_image", + url_fn=lambda plant: plant.plant_origin_path, + ), + FytaImageEntityDescription( + key="plant_image_user", + translation_key="plant_image_user", + url_fn=lambda plant: plant.user_picture_path, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -21,17 +56,17 @@ async def async_setup_entry( """Set up the FYTA plant images.""" coordinator = entry.runtime_data - description = ImageEntityDescription(key="plant_image") - async_add_entities( FytaPlantImageEntity(coordinator, entry, description, plant_id) for plant_id in coordinator.fyta.plant_list if plant_id in coordinator.data + for description in IMAGES ) def _async_add_new_device(plant_id: int) -> None: async_add_entities( - [FytaPlantImageEntity(coordinator, entry, description, plant_id)] + FytaPlantImageEntity(coordinator, entry, description, plant_id) + for description in IMAGES ) coordinator.new_device_callbacks.append(_async_add_new_device) @@ -40,26 +75,49 @@ async def async_setup_entry( class FytaPlantImageEntity(FytaPlantEntity, ImageEntity): """Represents a Fyta image.""" - entity_description: ImageEntityDescription + entity_description: FytaImageEntityDescription def __init__( self, coordinator: FytaCoordinator, entry: ConfigEntry, - description: ImageEntityDescription, + description: FytaImageEntityDescription, plant_id: int, ) -> None: - """Initiatlize Fyta Image entity.""" + """Initialize Fyta Image entity.""" super().__init__(coordinator, entry, description, plant_id) ImageEntity.__init__(self, coordinator.hass) - self._attr_name = None + async def async_image(self) -> bytes | None: + """Return bytes of image.""" + if self.entity_description.key == "plant_image_user": + if self._cached_image is None: + response = await self.coordinator.fyta.get_plant_image( + self.plant.user_picture_path + ) + _LOGGER.debug("Response of downloading user image: %s", response) + if response is None: + _LOGGER.debug( + "%s: Error getting new image from %s", + self.entity_id, + self.plant.user_picture_path, + ) + return None + + content_type, raw_image = response + self._cached_image = Image( + valid_image_content_type(content_type), raw_image + ) + + return self._cached_image.content + return await ImageEntity.async_image(self) @property def image_url(self) -> str: - """Return the image_url for this sensor.""" - image = self.plant.plant_origin_path - if image != self._attr_image_url: - self._attr_image_last_updated = datetime.now() + """Return the image_url for this plant.""" + url = self.entity_description.url_fn(self.plant) - return image + if url != self._attr_image_url: + self._cached_image = None + self._attr_image_last_updated = datetime.now() + return url diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index a10fa5bfc47..67bb991a437 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -61,6 +61,14 @@ "name": "Sensor update available" } }, + "image": { + "plant_image": { + "name": "Plant image" + }, + "plant_image_user": { + "name": "User image" + } + }, "sensor": { "scientific_name": { "name": "Scientific name" diff --git a/tests/components/fyta/fixtures/plant_status1_update.json b/tests/components/fyta/fixtures/plant_status1_update.json index 5363c5bd290..85f77a014a7 100644 --- a/tests/components/fyta/fixtures/plant_status1_update.json +++ b/tests/components/fyta/fixtures/plant_status1_update.json @@ -25,7 +25,7 @@ "sw_version": "1.0", "status": 1, "online": true, - "origin_path": "http://www.plant_picture.com/user_picture", + "origin_path": "http://www.plant_picture.com/user_picture1", "ph": null, "plant_id": 0, "plant_origin_path": "http://www.plant_picture.com/picture1", diff --git a/tests/components/fyta/snapshots/test_image.ambr b/tests/components/fyta/snapshots/test_image.ambr index cb39efb4500..d36472f91b9 100644 --- a/tests/components/fyta/snapshots/test_image.ambr +++ b/tests/components/fyta/snapshots/test_image.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[image.gummibaum-entry] +# name: test_all_entities[image.gummibaum_plant_image-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'image', 'entity_category': None, - 'entity_id': 'image.gummibaum', + 'entity_id': 'image.gummibaum_plant_image', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -24,31 +24,31 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'Plant image', 'platform': 'fyta', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, + 'translation_key': 'plant_image', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-plant_image', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[image.gummibaum-state] +# name: test_all_entities[image.gummibaum_plant_image-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'access_token': '1', - 'entity_picture': '/api/image_proxy/image.gummibaum?token=1', - 'friendly_name': 'Gummibaum', + 'entity_picture': '/api/image_proxy/image.gummibaum_plant_image?token=1', + 'friendly_name': 'Gummibaum Plant image', }), 'context': , - 'entity_id': 'image.gummibaum', + 'entity_id': 'image.gummibaum_plant_image', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- -# name: test_all_entities[image.kakaobaum-entry] +# name: test_all_entities[image.gummibaum_user_image-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'image', 'entity_category': None, - 'entity_id': 'image.kakaobaum', + 'entity_id': 'image.gummibaum_user_image', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -73,27 +73,131 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': None, + 'original_name': 'User image', 'platform': 'fyta', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': None, - 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image', + 'translation_key': 'plant_image_user', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-plant_image_user', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[image.kakaobaum-state] +# name: test_all_entities[image.gummibaum_user_image-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'access_token': '1', - 'entity_picture': '/api/image_proxy/image.kakaobaum?token=1', - 'friendly_name': 'Kakaobaum', + 'entity_picture': '/api/image_proxy/image.gummibaum_user_image?token=1', + 'friendly_name': 'Gummibaum User image', }), 'context': , - 'entity_id': 'image.kakaobaum', + 'entity_id': 'image.gummibaum_user_image', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'unknown', }) # --- +# name: test_all_entities[image.kakaobaum_plant_image-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'image', + 'entity_category': None, + 'entity_id': 'image.kakaobaum_plant_image', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Plant image', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plant_image', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[image.kakaobaum_plant_image-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'entity_picture': '/api/image_proxy/image.kakaobaum_plant_image?token=1', + 'friendly_name': 'Kakaobaum Plant image', + }), + 'context': , + 'entity_id': 'image.kakaobaum_plant_image', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[image.kakaobaum_user_image-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'image', + 'entity_category': None, + 'entity_id': 'image.kakaobaum_user_image', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'User image', + 'platform': 'fyta', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'plant_image_user', + 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image_user', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[image.kakaobaum_user_image-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'access_token': '1', + 'entity_picture': '/api/image_proxy/image.kakaobaum_user_image?token=1', + 'friendly_name': 'Kakaobaum User image', + }), + 'context': , + 'entity_id': 'image.kakaobaum_user_image', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_update_user_image + None +# --- +# name: test_update_user_image.1 + b'd' +# --- diff --git a/tests/components/fyta/test_image.py b/tests/components/fyta/test_image.py index 93cca1a1c09..2a0c71d68cc 100644 --- a/tests/components/fyta/test_image.py +++ b/tests/components/fyta/test_image.py @@ -1,6 +1,7 @@ """Test the Home Assistant fyta sensor module.""" from datetime import timedelta +from http import HTTPStatus from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory @@ -23,6 +24,7 @@ from tests.common import ( load_json_object_fixture, snapshot_platform, ) +from tests.typing import ClientSessionGenerator async def test_all_entities( @@ -37,7 +39,7 @@ async def test_all_entities( await setup_platform(hass, mock_config_entry, [Platform.IMAGE]) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - assert len(hass.states.async_all("image")) == 2 + assert len(hass.states.async_all("image")) == 4 @pytest.mark.parametrize( @@ -63,7 +65,8 @@ async def test_connection_error( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("image.gummibaum").state == STATE_UNAVAILABLE + assert hass.states.get("image.gummibaum_plant_image").state == STATE_UNAVAILABLE + assert hass.states.get("image.gummibaum_user_image").state == STATE_UNAVAILABLE async def test_add_remove_entities( @@ -76,7 +79,8 @@ async def test_add_remove_entities( await setup_platform(hass, mock_config_entry, [Platform.IMAGE]) - assert hass.states.get("image.gummibaum") is not None + assert hass.states.get("image.gummibaum_plant_image") is not None + assert hass.states.get("image.gummibaum_user_image") is not None plants: dict[int, Plant] = { 0: Plant.from_dict(load_json_object_fixture("plant_status1.json", FYTA_DOMAIN)), @@ -92,8 +96,10 @@ async def test_add_remove_entities( async_fire_time_changed(hass) await hass.async_block_till_done() - assert hass.states.get("image.kakaobaum") is None - assert hass.states.get("image.tomatenpflanze") is not None + assert hass.states.get("image.kakaobaum_plant_image") is None + assert hass.states.get("image.kakaobaum_user_image") is None + assert hass.states.get("image.tomatenpflanze_plant_image") is not None + assert hass.states.get("image.tomatenpflanze_user_image") is not None async def test_update_image( @@ -106,7 +112,10 @@ async def test_update_image( await setup_platform(hass, mock_config_entry, [Platform.IMAGE]) - image_entity: ImageEntity = hass.data["domain_entities"]["image"]["image.gummibaum"] + image_entity: ImageEntity = hass.data["domain_entities"]["image"][ + "image.gummibaum_plant_image" + ] + image_state_1 = hass.states.get("image.gummibaum_plant_image") assert image_entity.image_url == "http://www.plant_picture.com/picture" @@ -126,4 +135,77 @@ async def test_update_image( async_fire_time_changed(hass) await hass.async_block_till_done() + image_state_2 = hass.states.get("image.gummibaum_plant_image") + assert image_entity.image_url == "http://www.plant_picture.com/picture1" + assert image_state_1 != image_state_2 + + +async def test_update_user_image_error( + freezer: FrozenDateTimeFactory, + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_fyta_connector: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test error during user picture update.""" + + mock_fyta_connector.get_plant_image.return_value = AsyncMock(return_value=None) + + await setup_platform(hass, mock_config_entry, [Platform.IMAGE]) + + mock_fyta_connector.get_plant_image.return_value = None + + freezer.tick(delta=timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + image_entity: ImageEntity = hass.data["domain_entities"]["image"][ + "image.gummibaum_user_image" + ] + + assert image_entity.image_url == "http://www.plant_picture.com/user_picture" + assert image_entity._cached_image is None + + # Validate no image is available + client = await hass_client() + resp = await client.get("/api/image_proxy/image.gummibaum_user_image?token=1") + assert resp.status == 500 + + +async def test_update_user_image( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_fyta_connector: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test if entity user picture is updated.""" + + await setup_platform(hass, mock_config_entry, [Platform.IMAGE]) + + mock_fyta_connector.get_plant_image.return_value = ( + "image/png", + bytes([100]), + ) + + freezer.tick(delta=timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + image_entity: ImageEntity = hass.data["domain_entities"]["image"][ + "image.gummibaum_user_image" + ] + + assert image_entity.image_url == "http://www.plant_picture.com/user_picture" + image = image_entity._cached_image + assert image == snapshot + + # Validate image + client = await hass_client() + resp = await client.get("/api/image_proxy/image.gummibaum_user_image?token=1") + assert resp.status == HTTPStatus.OK + body = await resp.read() + assert body == snapshot From a14f3ab6b1bb4398c1ec81f959055b1ef5b8da3d Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Mon, 26 May 2025 14:43:28 +0100 Subject: [PATCH 0908/1175] Fix clear night weather condition for metoffice (#145470) --- homeassistant/components/metoffice/sensor.py | 2 +- homeassistant/components/metoffice/weather.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index b707bf604e6..c6b9f96514b 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -241,7 +241,7 @@ class MetOfficeCurrentSensor( if ( self.entity_description.native_attr_name == "significantWeatherCode" - and value + and value is not None ): value = CONDITION_MAP.get(value) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index c7ce0db6c50..3496e88c046 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -180,7 +180,7 @@ class MetOfficeWeather( weather_now = self.coordinator.data.now() value = get_attribute(weather_now, "significantWeatherCode") - if value: + if value is not None: return CONDITION_MAP.get(value) return None From c346b932f095cda9dc79876384ec5e7fa218ecea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 15:57:01 +0200 Subject: [PATCH 0909/1175] Use shorthand attributes in xiaomi_miio (part 2) (#145616) * Use shorthand attributes in xiaomi_miio (part 2) * Brightness * Is_on --- homeassistant/components/xiaomi_miio/fan.py | 52 +++++++-------- homeassistant/components/xiaomi_miio/light.py | 66 +++++++------------ .../components/xiaomi_miio/sensor.py | 16 +---- .../components/xiaomi_miio/switch.py | 30 ++++----- .../components/xiaomi_miio/vacuum.py | 19 ++---- 5 files changed, 65 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index aa7069f1e92..4bb922383dc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -313,7 +313,6 @@ class XiaomiGenericDevice( super().__init__(device, entry, unique_id, coordinator) self._available_attributes: dict[str, Any] = {} - self._state: bool | None = None self._mode: str | None = None self._fan_level: int | None = None self._state_attrs: dict[str, Any] = {} @@ -340,11 +339,6 @@ class XiaomiGenericDevice( """Return the state attributes of the device.""" return self._state_attrs - @property - def is_on(self) -> bool | None: - """Return true if device is on.""" - return self._state - async def async_turn_on( self, percentage: int | None = None, @@ -364,7 +358,7 @@ class XiaomiGenericDevice( await self.async_set_preset_mode(preset_mode) if result: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -375,7 +369,7 @@ class XiaomiGenericDevice( ) if result: - self._state = False + self._attr_is_on = False self.async_write_ha_state() @@ -402,7 +396,7 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): @property def preset_mode(self) -> str | None: """Get the active preset mode.""" - if self._state: + if self._attr_is_on: preset_mode = self.operation_mode_class(self._mode).name return preset_mode if preset_mode in self._preset_modes else None @@ -411,7 +405,7 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._state_attrs.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) @@ -510,7 +504,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON ) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._state_attrs.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) @@ -528,7 +522,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): @property def percentage(self) -> int | None: """Return the current percentage based speed.""" - if self._state: + if self._attr_is_on: mode = self.operation_mode_class(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( @@ -604,7 +598,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): """Return the current percentage based speed.""" if self._fan_level is None: return None - if self._state: + if self._attr_is_on: return ranged_value_to_percentage((1, 3), self._fan_level) return None @@ -652,7 +646,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): | FanEntityFeature.TURN_ON ) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._mode = self.coordinator.data.mode.value self._favorite_rpm: int | None = None self._speed_range = (300, 2200) @@ -671,7 +665,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): return ranged_value_to_percentage(self._speed_range, self._motor_speed) if self._favorite_rpm is None: return None - if self._state: + if self._attr_is_on: return ranged_value_to_percentage(self._speed_range, self._favorite_rpm) return None @@ -698,7 +692,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - if not self._state: + if not self._attr_is_on: await self.async_turn_on() if await self._try_command( @@ -712,7 +706,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._mode = self.coordinator.data.mode.value self._favorite_rpm = getattr(self.coordinator.data, ATTR_FAVORITE_RPM, None) self._motor_speed = min( @@ -763,7 +757,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): | FanEntityFeature.TURN_ON ) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._state_attrs.update( { key: getattr(self.coordinator.data, value) @@ -780,7 +774,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): @property def percentage(self) -> int | None: """Return the current percentage based speed.""" - if self._state: + if self._attr_is_on: mode = AirfreshOperationMode(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( @@ -865,7 +859,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): | FanEntityFeature.TURN_ON ) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._mode = self.coordinator.data.mode.value self._speed_range = (60, 150) @@ -879,7 +873,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Return the current percentage based speed.""" if self._favorite_speed is None: return None - if self._state: + if self._attr_is_on: return ranged_value_to_percentage(self._speed_range, self._favorite_speed) return None @@ -918,7 +912,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._mode = self.coordinator.data.mode.value self._favorite_speed = getattr(self.coordinator.data, ATTR_FAVORITE_SPEED, None) self.async_write_ha_state() @@ -993,7 +987,7 @@ class XiaomiGenericFan(XiaomiGenericDevice): @property def percentage(self) -> int | None: """Return the current speed as a percentage.""" - if self._state: + if self._attr_is_on: return self._percentage return None @@ -1038,7 +1032,7 @@ class XiaomiFan(XiaomiGenericFan): """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._oscillating = self.coordinator.data.oscillate self._nature_mode = self.coordinator.data.natural_speed != 0 if self._nature_mode: @@ -1063,7 +1057,7 @@ class XiaomiFan(XiaomiGenericFan): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._oscillating = self.coordinator.data.oscillate self._nature_mode = self.coordinator.data.natural_speed != 0 if self._nature_mode: @@ -1131,7 +1125,7 @@ class XiaomiFanP5(XiaomiGenericFan): """Initialize the fan.""" super().__init__(device, entry, unique_id, coordinator) - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate self._percentage = self.coordinator.data.speed @@ -1144,7 +1138,7 @@ class XiaomiFanP5(XiaomiGenericFan): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate self._percentage = self.coordinator.data.speed @@ -1197,7 +1191,7 @@ class XiaomiFanMiot(XiaomiGenericFan): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: @@ -1264,7 +1258,7 @@ class XiaomiFan1C(XiaomiFanMiot): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._state = self.coordinator.data.is_on + self._attr_is_on = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 6f4978b163e..4271894ba17 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -271,8 +271,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._brightness = None - self._state = None self._state_attrs: dict[str, Any] = {} @property @@ -280,16 +278,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Return the state attributes of the device.""" return self._state_attrs - @property - def is_on(self): - """Return true if light is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" try: @@ -321,7 +309,7 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): ) if result: - self._brightness = brightness + self._attr_brightness = brightness else: await self._try_command("Turning the light on failed.", self._device.on) @@ -342,8 +330,8 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @@ -376,8 +364,8 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, @@ -510,7 +498,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): if result: self._color_temp = color_temp - self._brightness = brightness + self._attr_brightness = brightness elif ATTR_COLOR_TEMP_KELVIN in kwargs: _LOGGER.debug( @@ -541,7 +529,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): ) if result: - self._brightness = brightness + self._attr_brightness = brightness else: await self._try_command("Turning the light on failed.", self._device.on) @@ -559,8 +547,8 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( state.color_temperature, CCT_MIN, @@ -630,8 +618,8 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( state.color_temperature, CCT_MIN, @@ -688,8 +676,8 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, @@ -814,7 +802,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): ) if result: - self._brightness = brightness + self._attr_brightness = brightness else: await self._try_command( "Turning the ambient light on failed.", self._device.ambient_on @@ -839,8 +827,8 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.ambient - self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + self._attr_is_on = state.ambient + self._attr_brightness = ceil((255 / 100.0) * state.ambient_brightness) class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): @@ -928,7 +916,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): if result: self._hs_color = hs_color - self._brightness = brightness + self._attr_brightness = brightness elif ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP_KELVIN in kwargs: _LOGGER.debug( @@ -951,7 +939,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): if result: self._color_temp = color_temp - self._brightness = brightness + self._attr_brightness = brightness elif ATTR_HS_COLOR in kwargs: _LOGGER.debug("Setting color: %s", rgb) @@ -992,7 +980,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ) if result: - self._brightness = brightness + self._attr_brightness = brightness else: await self._try_command("Turning the light on failed.", self._device.on) @@ -1010,8 +998,8 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on - self._brightness = ceil((255 / 100.0) * state.brightness) + self._attr_is_on = state.is_on + self._attr_brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( state.color_temperature, CCT_MIN, @@ -1050,7 +1038,6 @@ class XiaomiGatewayLight(LightEntity): self._gateway_device_id = gateway_device_id self._attr_unique_id = gateway_device_id self._attr_available = False - self._is_on = None self._brightness_pct = 100 self._rgb = (255, 255, 255) self._hs = (0, 0) @@ -1062,11 +1049,6 @@ class XiaomiGatewayLight(LightEntity): identifiers={(DOMAIN, self._gateway_device_id)}, ) - @property - def is_on(self): - """Return true if it is on.""" - return self._is_on - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -1113,9 +1095,9 @@ class XiaomiGatewayLight(LightEntity): return self._attr_available = True - self._is_on = state_dict["is_on"] + self._attr_is_on = state_dict["is_on"] - if self._is_on: + if self._attr_is_on: self._brightness_pct = state_dict["brightness"] self._rgb = state_dict["rgb"] self._hs = color_util.color_RGB_to_hs(*self._rgb) @@ -1139,7 +1121,7 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): return self._sub_device.status["color_temp"] @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self._sub_device.status["status"] == "on" diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index da4552cc63e..e7f652d1de2 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -938,7 +938,6 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._state = None self._state_attrs = { ATTR_POWER: None, ATTR_BATTERY_LEVEL: None, @@ -951,11 +950,6 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): } self.entity_description = description - @property - def native_value(self): - """Return the state of the device.""" - return self._state - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -968,7 +962,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.aqi + self._attr_native_value = state.aqi self._state_attrs.update( { ATTR_POWER: state.power, @@ -1023,17 +1017,11 @@ class XiaomiGatewayIlluminanceSensor(SensorEntity): self._gateway = gateway_device self.entity_description = description self._attr_available = False - self._state = None - - @property - def native_value(self): - """Return the state of the device.""" - return self._state async def async_update(self) -> None: """Fetch state from the device.""" try: - self._state = await self.hass.async_add_executor_job( + self._attr_native_value = await self.hass.async_add_executor_job( self._gateway.get_illumination ) self._attr_available = True diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 508a6e1a227..ff6387bc7c1 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -783,7 +783,7 @@ class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): self._attr_name = f"{sub_device.name} ch{self._channel} ({sub_device.sid})" @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self._sub_device.status[self._data_key] == "on" @@ -816,7 +816,6 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) - self._state: bool | None = None self._state_attrs = {ATTR_TEMPERATURE: None, ATTR_MODEL: self._model} self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @@ -826,11 +825,6 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Return the state attributes of the device.""" return self._state_attrs - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" try: @@ -857,7 +851,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): result = await self._try_command("Turning the plug on failed", self._device.on) if result: - self._state = True + self._attr_is_on = True self._skip_update = True async def async_turn_off(self, **kwargs: Any) -> None: @@ -867,7 +861,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): ) if result: - self._state = False + self._attr_is_on = False self._skip_update = True async def async_update(self) -> None: @@ -882,7 +876,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on + self._attr_is_on = state.is_on self._state_attrs[ATTR_TEMPERATURE] = state.temperature except DeviceException as ex: @@ -963,7 +957,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.is_on + self._attr_is_on = state.is_on self._state_attrs.update( {ATTR_TEMPERATURE: state.temperature, ATTR_LOAD_POWER: state.load_power} ) @@ -1039,7 +1033,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): ) if result: - self._state = True + self._attr_is_on = True self._skip_update = True async def async_turn_off(self, **kwargs: Any) -> None: @@ -1055,7 +1049,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): ) if result: - self._state = False + self._attr_is_on = False self._skip_update = True async def async_update(self) -> None: @@ -1071,9 +1065,9 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._attr_available = True if self._channel_usb: - self._state = state.usb_power + self._attr_is_on = state.usb_power else: - self._state = state.is_on + self._attr_is_on = state.is_on self._state_attrs[ATTR_TEMPERATURE] = state.temperature @@ -1114,7 +1108,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): ) if result: - self._state = True + self._attr_is_on = True self._skip_update = True async def async_turn_off(self, **kwargs: Any) -> None: @@ -1125,7 +1119,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): ) if result: - self._state = False + self._attr_is_on = False self._skip_update = True async def async_update(self) -> None: @@ -1140,7 +1134,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): _LOGGER.debug("Got new state: %s", state) self._attr_available = True - self._state = state.power_socket == "on" + self._attr_is_on = state.power_socket == "on" self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index ca6ab084324..3b397e9ccfd 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -6,7 +6,7 @@ from functools import partial import logging from typing import Any -from miio import Device as MiioDevice, DeviceException +from miio import DeviceException import voluptuous as vol from homeassistant.components.vacuum import ( @@ -194,17 +194,6 @@ class MiroboVacuum( | VacuumEntityFeature.START ) - def __init__( - self, - device: MiioDevice, - entry: XiaomiMiioConfigEntry, - unique_id: str | None, - coordinator: DataUpdateCoordinator[VacuumCoordinatorData], - ) -> None: - """Initialize the Xiaomi vacuum cleaner robot handler.""" - super().__init__(device, entry, unique_id, coordinator) - self._state: VacuumActivity | None = None - async def async_added_to_hass(self) -> None: """Run when entity is about to be added to hass.""" await super().async_added_to_hass() @@ -218,7 +207,7 @@ class MiroboVacuum( if self.coordinator.data.status.got_error: return VacuumActivity.ERROR - return self._state + return super().activity @property def battery_level(self) -> int: @@ -435,8 +424,8 @@ class MiroboVacuum( self.coordinator.data.status.state, self.coordinator.data.status.state_code, ) - self._state = None + self._attr_activity = None else: - self._state = STATE_CODE_TO_STATE[state_code] + self._attr_activity = STATE_CODE_TO_STATE[state_code] super()._handle_coordinator_update() From 0802fc8a210a7e950d8312dbd5a3cf4ccabd5122 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 17:01:11 +0300 Subject: [PATCH 0910/1175] Add switch platform to Amazon Devices (#145588) * Add switch platform to Amazon Devices * apply review comment * make logic generic * test cleanup --- .../components/amazon_devices/__init__.py | 5 +- .../components/amazon_devices/strings.json | 5 + .../components/amazon_devices/switch.py | 84 +++++++++++++++++ .../amazon_devices/snapshots/test_switch.ambr | 48 ++++++++++ .../components/amazon_devices/test_switch.py | 91 +++++++++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/amazon_devices/switch.py create mode 100644 tests/components/amazon_devices/snapshots/test_switch.ambr create mode 100644 tests/components/amazon_devices/test_switch.py diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/amazon_devices/__init__.py index a7318824b4c..c63c8ab7664 100644 --- a/homeassistant/components/amazon_devices/__init__.py +++ b/homeassistant/components/amazon_devices/__init__.py @@ -5,7 +5,10 @@ from homeassistant.core import HomeAssistant from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json index edc10aa9d40..a3219eaa449 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/amazon_devices/strings.json @@ -42,6 +42,11 @@ "bluetooth": { "name": "Bluetooth" } + }, + "switch": { + "do_not_disturb": { + "name": "Do not disturb" + } } } } diff --git a/homeassistant/components/amazon_devices/switch.py b/homeassistant/components/amazon_devices/switch.py new file mode 100644 index 00000000000..428ef3e3b45 --- /dev/null +++ b/homeassistant/components/amazon_devices/switch.py @@ -0,0 +1,84 @@ +"""Support for switches.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Final + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AmazonConfigEntry +from .entity import AmazonEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class AmazonSwitchEntityDescription(SwitchEntityDescription): + """Amazon Devices switch entity description.""" + + is_on_fn: Callable[[AmazonDevice], bool] + subkey: str + method: str + + +SWITCHES: Final = ( + AmazonSwitchEntityDescription( + key="do_not_disturb", + subkey="AUDIO_PLAYER", + translation_key="do_not_disturb", + is_on_fn=lambda _device: _device.do_not_disturb, + method="set_do_not_disturb", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Amazon Devices switches based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + AmazonSwitchEntity(coordinator, serial_num, switch_desc) + for switch_desc in SWITCHES + for serial_num in coordinator.data + if switch_desc.subkey in coordinator.data[serial_num].capabilities + ) + + +class AmazonSwitchEntity(AmazonEntity, SwitchEntity): + """Switch device.""" + + entity_description: AmazonSwitchEntityDescription + + async def _switch_set_state(self, state: bool) -> None: + """Set desired switch state.""" + method = getattr(self.coordinator.api, self.entity_description.method) + + if TYPE_CHECKING: + assert method is not None + + await method(self.device, state) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self._switch_set_state(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self._switch_set_state(False) + + @property + def is_on(self) -> bool: + """Return True if switch is on.""" + return self.entity_description.is_on_fn(self.device) diff --git a/tests/components/amazon_devices/snapshots/test_switch.ambr b/tests/components/amazon_devices/snapshots/test_switch.ambr new file mode 100644 index 00000000000..b6b1d0579d2 --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_switch.ambr @@ -0,0 +1,48 @@ +# serializer version: 1 +# name: test_all_entities[switch.echo_test_do_not_disturb-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.echo_test_do_not_disturb', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Do not disturb', + 'platform': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'do_not_disturb', + 'unique_id': 'echo_test_serial_number-do_not_disturb', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[switch.echo_test_do_not_disturb-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Echo Test Do not disturb', + }), + 'context': , + 'entity_id': 'switch.echo_test_do_not_disturb', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/amazon_devices/test_switch.py new file mode 100644 index 00000000000..004d6cce842 --- /dev/null +++ b/tests/components/amazon_devices/test_switch.py @@ -0,0 +1,91 @@ +"""Tests for the Amazon Devices switch platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .conftest import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.SWITCH]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_switch_dnd( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test switching DND.""" + await setup_integration(hass, mock_config_entry) + + entity_id = "switch.echo_test_do_not_disturb" + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert mock_amazon_devices_client.set_do_not_disturb.call_count == 1 + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].do_not_disturb = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].do_not_disturb = False + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_amazon_devices_client.set_do_not_disturb.call_count == 2 + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_OFF From 0260a034474dff341c05819ae25452a0f6e4255d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 26 May 2025 16:07:33 +0200 Subject: [PATCH 0911/1175] Store information about add-ons and folders which could not be backed up (#145367) * Store information about add-ons and folders which could not be backed up * Address review comments --- homeassistant/components/backup/manager.py | 64 +++++- homeassistant/components/backup/models.py | 4 +- homeassistant/components/backup/store.py | 7 +- homeassistant/components/hassio/backup.py | 13 +- tests/components/aws_s3/test_backup.py | 36 +-- tests/components/azure_storage/test_backup.py | 16 +- .../backup/snapshots/test_backup.ambr | 8 + .../backup/snapshots/test_onboarding.ambr | 8 + .../backup/snapshots/test_store.ambr | 209 +++++++++++++++++- .../backup/snapshots/test_websocket.ambr | 152 +++++++++++-- tests/components/backup/test_manager.py | 41 +++- tests/components/backup/test_onboarding.py | 12 +- tests/components/backup/test_store.py | 59 +++++ tests/components/backup/test_websocket.py | 14 +- tests/components/cloud/test_backup.py | 18 +- tests/components/google_drive/test_backup.py | 4 +- tests/components/hassio/test_backup.py | 8 +- tests/components/kitchen_sink/test_backup.py | 4 + tests/components/onedrive/test_backup.py | 12 +- tests/components/synology_dsm/test_backup.py | 12 +- tests/components/webdav/test_backup.py | 12 +- 21 files changed, 621 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index f51c2a14b47..8dbce1b455c 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -62,6 +62,7 @@ from .const import ( LOGGER, ) from .models import ( + AddonInfo, AgentBackup, BackupError, BackupManagerError, @@ -102,7 +103,9 @@ class ManagerBackup(BaseBackup): """Backup class.""" agents: dict[str, AgentBackupStatus] + failed_addons: list[AddonInfo] failed_agent_ids: list[str] + failed_folders: list[Folder] with_automatic_settings: bool | None @@ -110,7 +113,7 @@ class ManagerBackup(BaseBackup): class AddonErrorData: """Addon error class.""" - name: str + addon: AddonInfo errors: list[tuple[str, str]] @@ -646,9 +649,13 @@ class BackupManager: for agent_backup in result: if (backup_id := agent_backup.backup_id) not in backups: if known_backup := self.known_backups.get(backup_id): + failed_addons = known_backup.failed_addons failed_agent_ids = known_backup.failed_agent_ids + failed_folders = known_backup.failed_folders else: + failed_addons = [] failed_agent_ids = [] + failed_folders = [] with_automatic_settings = self.is_our_automatic_backup( agent_backup, await instance_id.async_get(self.hass) ) @@ -659,7 +666,9 @@ class BackupManager: date=agent_backup.date, database_included=agent_backup.database_included, extra_metadata=agent_backup.extra_metadata, + failed_addons=failed_addons, failed_agent_ids=failed_agent_ids, + failed_folders=failed_folders, folders=agent_backup.folders, homeassistant_included=agent_backup.homeassistant_included, homeassistant_version=agent_backup.homeassistant_version, @@ -714,9 +723,13 @@ class BackupManager: continue if backup is None: if known_backup := self.known_backups.get(backup_id): + failed_addons = known_backup.failed_addons failed_agent_ids = known_backup.failed_agent_ids + failed_folders = known_backup.failed_folders else: + failed_addons = [] failed_agent_ids = [] + failed_folders = [] with_automatic_settings = self.is_our_automatic_backup( result, await instance_id.async_get(self.hass) ) @@ -727,7 +740,9 @@ class BackupManager: date=result.date, database_included=result.database_included, extra_metadata=result.extra_metadata, + failed_addons=failed_addons, failed_agent_ids=failed_agent_ids, + failed_folders=failed_folders, folders=result.folders, homeassistant_included=result.homeassistant_included, homeassistant_version=result.homeassistant_version, @@ -970,7 +985,7 @@ class BackupManager: password=None, ) await written_backup.release_stream() - self.known_backups.add(written_backup.backup, agent_errors, []) + self.known_backups.add(written_backup.backup, agent_errors, {}, {}, []) return written_backup.backup.backup_id async def async_create_backup( @@ -1208,7 +1223,11 @@ class BackupManager: finally: await written_backup.release_stream() self.known_backups.add( - written_backup.backup, agent_errors, unavailable_agents + written_backup.backup, + agent_errors, + written_backup.addon_errors, + written_backup.folder_errors, + unavailable_agents, ) if not agent_errors: if with_automatic_settings: @@ -1416,7 +1435,12 @@ class BackupManager: # No issues with agents or folders, but issues with add-ons self._create_automatic_backup_failed_issue( "automatic_backup_failed_addons", - {"failed_addons": ", ".join(val.name for val in addon_errors.values())}, + { + "failed_addons": ", ".join( + val.addon.name or val.addon.slug + for val in addon_errors.values() + ) + }, ) elif folder_errors and not (failed_agents or addon_errors): # No issues with agents or add-ons, but issues with folders @@ -1431,7 +1455,11 @@ class BackupManager: { "failed_agents": ", ".join(failed_agents) or "-", "failed_addons": ( - ", ".join(val.name for val in addon_errors.values()) or "-" + ", ".join( + val.addon.name or val.addon.slug + for val in addon_errors.values() + ) + or "-" ), "failed_folders": ", ".join(f for f in folder_errors) or "-", }, @@ -1501,7 +1529,12 @@ class KnownBackups: self._backups = { backup["backup_id"]: KnownBackup( backup_id=backup["backup_id"], + failed_addons=[ + AddonInfo(name=a["name"], slug=a["slug"], version=a["version"]) + for a in backup["failed_addons"] + ], failed_agent_ids=backup["failed_agent_ids"], + failed_folders=[Folder(f) for f in backup["failed_folders"]], ) for backup in stored_backups } @@ -1514,12 +1547,16 @@ class KnownBackups: self, backup: AgentBackup, agent_errors: dict[str, Exception], + failed_addons: dict[str, AddonErrorData], + failed_folders: dict[Folder, list[tuple[str, str]]], unavailable_agents: list[str], ) -> None: """Add a backup.""" self._backups[backup.backup_id] = KnownBackup( backup_id=backup.backup_id, + failed_addons=[val.addon for val in failed_addons.values()], failed_agent_ids=list(chain(agent_errors, unavailable_agents)), + failed_folders=list(failed_folders), ) self._manager.store.save() @@ -1540,21 +1577,38 @@ class KnownBackup: """Persistent backup data.""" backup_id: str + failed_addons: list[AddonInfo] failed_agent_ids: list[str] + failed_folders: list[Folder] def to_dict(self) -> StoredKnownBackup: """Convert known backup to a dict.""" return { "backup_id": self.backup_id, + "failed_addons": [ + {"name": a.name, "slug": a.slug, "version": a.version} + for a in self.failed_addons + ], "failed_agent_ids": self.failed_agent_ids, + "failed_folders": [f.value for f in self.failed_folders], } +class StoredAddonInfo(TypedDict): + """Stored add-on info.""" + + name: str | None + slug: str + version: str | None + + class StoredKnownBackup(TypedDict): """Stored persistent backup data.""" backup_id: str + failed_addons: list[StoredAddonInfo] failed_agent_ids: list[str] + failed_folders: list[str] class CoreBackupReaderWriter(BackupReaderWriter): diff --git a/homeassistant/components/backup/models.py b/homeassistant/components/backup/models.py index 95c5ef9809d..d927cd0bac5 100644 --- a/homeassistant/components/backup/models.py +++ b/homeassistant/components/backup/models.py @@ -13,9 +13,9 @@ from homeassistant.exceptions import HomeAssistantError class AddonInfo: """Addon information.""" - name: str + name: str | None slug: str - version: str + version: str | None class Folder(StrEnum): diff --git a/homeassistant/components/backup/store.py b/homeassistant/components/backup/store.py index c220ab0731e..17ef1d3a8fb 100644 --- a/homeassistant/components/backup/store.py +++ b/homeassistant/components/backup/store.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: STORE_DELAY_SAVE = 30 STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -STORAGE_VERSION_MINOR = 6 +STORAGE_VERSION_MINOR = 7 class StoredBackupData(TypedDict): @@ -76,6 +76,11 @@ class _BackupStore(Store[StoredBackupData]): # Version 1.6 adds agent retention settings for agent in data["config"]["agents"]: data["config"]["agents"][agent]["retention"] = None + if old_minor_version < 7: + # Version 1.7 adds failing addons and folders + for backup in data["backups"]: + backup["failed_addons"] = [] + backup["failed_folders"] = [] # Note: We allow reading data with major version 2 in which the unused key # data["config"]["schedule"]["state"] will be removed. The bump to 2 is diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index 46e3d0d3c98..7f7bf077e21 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -429,10 +429,19 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): for slug, errors in _addon_errors.items(): try: addon_info = await self._client.addons.addon_info(slug) - addon_errors[slug] = AddonErrorData(name=addon_info.name, errors=errors) + addon_errors[slug] = AddonErrorData( + addon=AddonInfo( + name=addon_info.name, + slug=addon_info.slug, + version=addon_info.version, + ), + errors=errors, + ) except SupervisorError as err: _LOGGER.debug("Error getting addon %s: %s", slug, err) - addon_errors[slug] = AddonErrorData(name=slug, errors=errors) + addon_errors[slug] = AddonErrorData( + addon=AddonInfo(name=None, slug=slug, version=None), errors=errors + ) _folder_errors = _collect_errors( full_status, "backup_store_folders", "backup_folder_save" diff --git a/tests/components/aws_s3/test_backup.py b/tests/components/aws_s3/test_backup.py index a8b24ec1ab4..bf5baf2044b 100644 --- a/tests/components/aws_s3/test_backup.py +++ b/tests/components/aws_s3/test_backup.py @@ -114,21 +114,23 @@ async def test_agents_list_backups( assert response["result"]["backups"] == [ { "addons": test_backup.addons, - "backup_id": test_backup.backup_id, - "date": test_backup.date, - "database_included": test_backup.database_included, - "folders": test_backup.folders, - "homeassistant_included": test_backup.homeassistant_included, - "homeassistant_version": test_backup.homeassistant_version, - "name": test_backup.name, - "extra_metadata": test_backup.extra_metadata, "agents": { f"{DOMAIN}.{mock_config_entry.entry_id}": { "protected": test_backup.protected, "size": test_backup.size, } }, + "backup_id": test_backup.backup_id, + "database_included": test_backup.database_included, + "date": test_backup.date, + "extra_metadata": test_backup.extra_metadata, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], + "folders": test_backup.folders, + "homeassistant_included": test_backup.homeassistant_included, + "homeassistant_version": test_backup.homeassistant_version, + "name": test_backup.name, "with_automatic_settings": None, } ] @@ -152,21 +154,23 @@ async def test_agents_get_backup( assert response["result"]["agent_errors"] == {} assert response["result"]["backup"] == { "addons": test_backup.addons, - "backup_id": test_backup.backup_id, - "date": test_backup.date, - "database_included": test_backup.database_included, - "folders": test_backup.folders, - "homeassistant_included": test_backup.homeassistant_included, - "homeassistant_version": test_backup.homeassistant_version, - "name": test_backup.name, - "extra_metadata": test_backup.extra_metadata, "agents": { f"{DOMAIN}.{mock_config_entry.entry_id}": { "protected": test_backup.protected, "size": test_backup.size, } }, + "backup_id": test_backup.backup_id, + "database_included": test_backup.database_included, + "date": test_backup.date, + "extra_metadata": test_backup.extra_metadata, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], + "folders": test_backup.folders, + "homeassistant_included": test_backup.homeassistant_included, + "homeassistant_version": test_backup.homeassistant_version, + "name": test_backup.name, "with_automatic_settings": None, } diff --git a/tests/components/azure_storage/test_backup.py b/tests/components/azure_storage/test_backup.py index ebb491c2b7c..8fb81e7dbc4 100644 --- a/tests/components/azure_storage/test_backup.py +++ b/tests/components/azure_storage/test_backup.py @@ -93,14 +93,16 @@ async def test_agents_list_backups( } }, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", + "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], - "extra_metadata": {}, "with_automatic_settings": None, } ] @@ -129,14 +131,16 @@ async def test_agents_get_backup( } }, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", + "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", - "extra_metadata": {}, "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, } diff --git a/tests/components/backup/snapshots/test_backup.ambr b/tests/components/backup/snapshots/test_backup.ambr index 7cbbb9ddbce..bf6305e8479 100644 --- a/tests/components/backup/snapshots/test_backup.ambr +++ b/tests/components/backup/snapshots/test_backup.ambr @@ -75,8 +75,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -102,8 +106,12 @@ 'instance_id': 'unknown_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', diff --git a/tests/components/backup/snapshots/test_onboarding.ambr b/tests/components/backup/snapshots/test_onboarding.ambr index 48ddf30d1f2..975406fc265 100644 --- a/tests/components/backup/snapshots/test_onboarding.ambr +++ b/tests/components/backup/snapshots/test_onboarding.ambr @@ -23,8 +23,12 @@ 'instance_id': 'abc123', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -50,8 +54,12 @@ 'instance_id': 'unknown_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', diff --git a/tests/components/backup/snapshots/test_store.ambr b/tests/components/backup/snapshots/test_store.ambr index 6f1bce8d5e4..aa9ccde4b8a 100644 --- a/tests/components/backup/snapshots/test_store.ambr +++ b/tests/components/backup/snapshots/test_store.ambr @@ -5,9 +5,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -40,7 +44,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -50,9 +54,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -86,7 +94,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -96,9 +104,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -131,7 +143,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -141,9 +153,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -177,7 +193,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -187,9 +203,19 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + 'ssl', + ]), }), ]), 'config': dict({ @@ -226,7 +252,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -236,9 +262,19 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + 'ssl', + ]), }), ]), 'config': dict({ @@ -276,7 +312,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -286,9 +322,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -325,7 +365,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -335,9 +375,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -375,7 +419,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -385,9 +429,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -424,7 +472,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -434,9 +482,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -474,7 +526,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -484,9 +536,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -526,7 +582,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -536,9 +592,13 @@ 'backups': list([ dict({ 'backup_id': 'abc123', + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + ]), }), ]), 'config': dict({ @@ -579,7 +639,132 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data6] + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 7, + 'version': 1, + }) +# --- +# name: test_store_migration[store_data6].1 + dict({ + 'data': dict({ + 'backups': list([ + dict({ + 'backup_id': 'abc123', + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), + 'failed_agent_ids': list([ + 'test.remote', + ]), + 'failed_folders': list([ + 'ssl', + ]), + }), + ]), + 'config': dict({ + 'agents': dict({ + 'test.remote': dict({ + 'protected': True, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + }), + }), + 'automatic_backups_configured': True, + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': True, + 'include_folders': None, + 'name': None, + 'password': 'hunter2', + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'state': 'never', + 'time': None, + }), + }), + }), + 'key': 'backup', + 'minor_version': 7, 'version': 1, }) # --- diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 7528785ab0d..1ce16b2c7d3 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -1312,7 +1312,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -1429,7 +1429,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -1546,7 +1546,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -1677,7 +1677,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -1955,7 +1955,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2070,7 +2070,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2185,7 +2185,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2302,7 +2302,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2421,7 +2421,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2538,7 +2538,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2659,7 +2659,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2784,7 +2784,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -2901,7 +2901,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -3018,7 +3018,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -3135,7 +3135,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -3252,7 +3252,7 @@ }), }), 'key': 'backup', - 'minor_version': 6, + 'minor_version': 7, 'version': 1, }) # --- @@ -4397,8 +4397,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4478,8 +4482,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4540,8 +4548,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4586,8 +4598,12 @@ 'instance_id': 'unknown_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4643,8 +4659,12 @@ 'instance_id': 'unknown_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4698,8 +4718,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4760,8 +4784,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4823,8 +4851,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -4886,9 +4918,19 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + 'ssl', + ]), 'folders': list([ 'media', 'share', @@ -4949,8 +4991,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5011,8 +5057,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5074,8 +5124,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5137,9 +5191,19 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + dict({ + 'name': 'Test add-on', + 'slug': 'test_addon', + 'version': '1.0.0', + }), + ]), 'failed_agent_ids': list([ 'test.remote', ]), + 'failed_folders': list([ + 'ssl', + ]), 'folders': list([ 'media', 'share', @@ -5200,8 +5264,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5243,8 +5311,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5302,8 +5374,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5358,8 +5434,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5402,8 +5482,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5446,8 +5530,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5715,8 +5803,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5766,8 +5858,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5821,8 +5917,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5867,8 +5967,12 @@ 'instance_id': 'unknown_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5899,8 +6003,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -5951,8 +6059,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -6003,8 +6115,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', @@ -6055,8 +6171,12 @@ 'instance_id': 'our_uuid', 'with_automatic_settings': True, }), + 'failed_addons': list([ + ]), 'failed_agent_ids': list([ ]), + 'failed_folders': list([ + ]), 'folders': list([ 'media', 'share', diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 24eead134cf..59c1bf24b21 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -36,6 +36,7 @@ from homeassistant.components.backup.agent import BackupAgentError from homeassistant.components.backup.const import DATA_MANAGER from homeassistant.components.backup.manager import ( AddonErrorData, + AddonInfo, BackupManagerError, BackupManagerExceptionGroup, BackupManagerState, @@ -653,7 +654,9 @@ async def test_initiate_backup( "database_included": include_database, "date": ANY, "extra_metadata": {"instance_id": "our_uuid", "with_automatic_settings": False}, + "failed_addons": [], "failed_agent_ids": expected_failed_agent_ids, + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.1.0", @@ -706,7 +709,9 @@ async def test_initiate_backup_with_agent_error( "instance_id": "our_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -726,7 +731,9 @@ async def test_initiate_backup_with_agent_error( "instance_id": "unknown_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -752,7 +759,9 @@ async def test_initiate_backup_with_agent_error( "instance_id": "our_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -857,7 +866,9 @@ async def test_initiate_backup_with_agent_error( "database_included": True, "date": ANY, "extra_metadata": {"instance_id": "our_uuid", "with_automatic_settings": False}, + "failed_addons": [], "failed_agent_ids": ["test.remote"], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.1.0", @@ -890,7 +901,9 @@ async def test_initiate_backup_with_agent_error( assert hass_storage[DOMAIN]["data"]["backups"] == [ { "backup_id": "abc123", + "failed_addons": [], "failed_agent_ids": ["test.remote"], + "failed_folders": [], } ] @@ -1121,7 +1134,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate", "agent_ids": ["test.remote"]}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {}, @@ -1135,7 +1149,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate_with_automatic_settings"}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {}, @@ -1181,7 +1196,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate", "agent_ids": ["test.remote"]}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {Folder.MEDIA: [("test_error", "Boom!")]}, @@ -1195,7 +1211,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate_with_automatic_settings"}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {Folder.MEDIA: [("test_error", "Boom!")]}, @@ -1219,7 +1236,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate", "agent_ids": ["test.remote"]}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {Folder.MEDIA: [("test_error", "Boom!")]}, @@ -1241,7 +1259,8 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]: {"type": "backup/generate_with_automatic_settings"}, { "test_addon": AddonErrorData( - name="Test Add-on", errors=[("test_error", "Boom!")] + addon=AddonInfo(name="Test Add-on", slug="test", version="0.0"), + errors=[("test_error", "Boom!")], ) }, {Folder.MEDIA: [("test_error", "Boom!")]}, @@ -2080,7 +2099,9 @@ async def test_receive_backup_agent_error( "instance_id": "our_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -2100,7 +2121,9 @@ async def test_receive_backup_agent_error( "instance_id": "unknown_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -2126,7 +2149,9 @@ async def test_receive_backup_agent_error( "instance_id": "our_uuid", "with_automatic_settings": True, }, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [ "media", "share", @@ -2256,7 +2281,9 @@ async def test_receive_backup_agent_error( assert hass_storage[DOMAIN]["data"]["backups"] == [ { "backup_id": "abc123", + "failed_addons": [], "failed_agent_ids": ["test.remote"], + "failed_folders": [], } ] @@ -3571,7 +3598,9 @@ async def test_initiate_backup_per_agent_encryption( "database_included": True, "date": ANY, "extra_metadata": {"instance_id": "our_uuid", "with_automatic_settings": False}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.1.0", diff --git a/tests/components/backup/test_onboarding.py b/tests/components/backup/test_onboarding.py index 48e7252289a..51d704b8ba5 100644 --- a/tests/components/backup/test_onboarding.py +++ b/tests/components/backup/test_onboarding.py @@ -124,14 +124,16 @@ async def test_onboarding_backup_info( "backup.local": backup.manager.AgentBackupStatus(protected=True, size=0) }, backup_id="abc123", - date="1970-01-01T00:00:00.000Z", database_included=True, + date="1970-01-01T00:00:00.000Z", extra_metadata={"instance_id": "abc123", "with_automatic_settings": True}, + failed_addons=[], + failed_agent_ids=[], + failed_folders=[], folders=[backup.Folder.MEDIA, backup.Folder.SHARE], homeassistant_included=True, homeassistant_version="2024.12.0", name="Test", - failed_agent_ids=[], with_automatic_settings=True, ), "def456": backup.ManagerBackup( @@ -140,17 +142,19 @@ async def test_onboarding_backup_info( "test.remote": backup.manager.AgentBackupStatus(protected=True, size=0) }, backup_id="def456", - date="1980-01-01T00:00:00.000Z", database_included=False, + date="1980-01-01T00:00:00.000Z", extra_metadata={ "instance_id": "unknown_uuid", "with_automatic_settings": True, }, + failed_addons=[], + failed_agent_ids=[], + failed_folders=[], folders=[backup.Folder.MEDIA, backup.Folder.SHARE], homeassistant_included=True, homeassistant_version="2024.12.0", name="Test 2", - failed_agent_ids=[], with_automatic_settings=None, ), } diff --git a/tests/components/backup/test_store.py b/tests/components/backup/test_store.py index 97f6a4102f7..a016ab36f3d 100644 --- a/tests/components/backup/test_store.py +++ b/tests/components/backup/test_store.py @@ -94,7 +94,15 @@ def mock_delay_save() -> Generator[None]: "backups": [ { "backup_id": "abc123", + "failed_addons": [ + { + "name": "Test add-on", + "slug": "test_addon", + "version": "1.0.0", + } + ], "failed_agent_ids": ["test.remote"], + "failed_folders": ["ssl"], } ], "config": { @@ -243,6 +251,57 @@ def mock_delay_save() -> Generator[None]: "minor_version": 6, "version": 1, }, + { + "data": { + "backups": [ + { + "backup_id": "abc123", + "failed_addons": [ + { + "name": "Test add-on", + "slug": "test_addon", + "version": "1.0.0", + } + ], + "failed_agent_ids": ["test.remote"], + "failed_folders": ["ssl"], + } + ], + "config": { + "agents": { + "test.remote": { + "protected": True, + "retention": {"copies": None, "days": None}, + } + }, + "automatic_backups_configured": True, + "create_backup": { + "agent_ids": [], + "include_addons": None, + "include_all_addons": False, + "include_database": True, + "include_folders": None, + "name": None, + "password": "hunter2", + }, + "last_attempted_automatic_backup": None, + "last_completed_automatic_backup": None, + "retention": { + "copies": None, + "days": None, + }, + "schedule": { + "days": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "minor_version": 7, + "version": 1, + }, ], ) async def test_store_migration( diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 2115533452e..34e562ecfd6 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -87,14 +87,16 @@ TEST_MANAGER_BACKUP = ManagerBackup( addons=[AddonInfo(name="Test", slug="test", version="1.0.0")], agents={"test.test-agent": AgentBackupStatus(protected=True, size=0)}, backup_id="backup-1", - date="1970-01-01T00:00:00.000Z", database_included=True, + date="1970-01-01T00:00:00.000Z", extra_metadata={"instance_id": "abc123", "with_automatic_settings": True}, + failed_addons=[], + failed_agent_ids=[], + failed_folders=[], folders=[Folder.MEDIA, Folder.SHARE], homeassistant_included=True, homeassistant_version="2024.12.0", name="Test", - failed_agent_ids=[], with_automatic_settings=True, ) @@ -326,7 +328,15 @@ async def test_delete( "backups": [ { "backup_id": "abc123", + "failed_addons": [ + { + "name": "Test add-on", + "slug": "test_addon", + "version": "1.0.0", + } + ], "failed_agent_ids": ["test.remote"], + "failed_folders": ["ssl"], } ] }, diff --git a/tests/components/cloud/test_backup.py b/tests/components/cloud/test_backup.py index e75cf72332c..c9e0f37829a 100644 --- a/tests/components/cloud/test_backup.py +++ b/tests/components/cloud/test_backup.py @@ -152,28 +152,32 @@ async def test_agents_list_backups( "addons": [], "agents": {"cloud.cloud": {"protected": False, "size": 34519040}}, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, }, { "addons": [], "agents": {"cloud.cloud": {"protected": False, "size": 34519040}}, "backup_id": "23e64aed", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, }, ] @@ -216,14 +220,16 @@ async def test_agents_list_backups_fail_cloud( "addons": [], "agents": {"cloud.cloud": {"protected": False, "size": 34519040}}, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, }, ), diff --git a/tests/components/google_drive/test_backup.py b/tests/components/google_drive/test_backup.py index 9cf86a280bd..b8e37d0f3b8 100644 --- a/tests/components/google_drive/test_backup.py +++ b/tests/components/google_drive/test_backup.py @@ -49,11 +49,13 @@ TEST_AGENT_BACKUP_RESULT = { "database_included": True, "date": "2025-01-01T01:23:45.678Z", "extra_metadata": {"with_automatic_settings": False}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0", "name": "Test", - "failed_agent_ids": [], "with_automatic_settings": None, } diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index e232a57d4e4..4bf420e6b0d 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -497,7 +497,9 @@ async def test_agent_info( "database_included": True, "date": "1970-01-01T00:00:00+00:00", "extra_metadata": {}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": ["share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", @@ -517,7 +519,9 @@ async def test_agent_info( "database_included": False, "date": "1970-01-01T00:00:00+00:00", "extra_metadata": {}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": ["share"], "homeassistant_included": False, "homeassistant_version": None, @@ -653,7 +657,9 @@ async def test_agent_get_backup( "database_included": True, "date": "1970-01-01T00:00:00+00:00", "extra_metadata": {}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": ["share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", @@ -992,7 +998,7 @@ async def test_reader_writer_create( @pytest.mark.parametrize( "addon_info_side_effect", # Getting info fails for one of the addons, should fall back to slug - [[Mock(), SupervisorError("Boom")]], + [[Mock(slug="core_ssh", version="0.0.0"), SupervisorError("Boom")]], ) async def test_reader_writer_create_addon_folder_error( hass: HomeAssistant, diff --git a/tests/components/kitchen_sink/test_backup.py b/tests/components/kitchen_sink/test_backup.py index 933979ee913..02ad346cd58 100644 --- a/tests/components/kitchen_sink/test_backup.py +++ b/tests/components/kitchen_sink/test_backup.py @@ -109,7 +109,9 @@ async def test_agents_list_backups( "database_included": False, "date": "1970-01-01T00:00:00Z", "extra_metadata": {}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": ["media", "share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", @@ -191,7 +193,9 @@ async def test_agents_upload( "database_included": True, "date": "1970-01-01T00:00:00.000Z", "extra_metadata": {"instance_id": ANY, "with_automatic_settings": False}, + "failed_addons": [], "failed_agent_ids": [], + "failed_folders": [], "folders": ["media", "share"], "homeassistant_included": True, "homeassistant_version": "2024.12.0", diff --git a/tests/components/onedrive/test_backup.py b/tests/components/onedrive/test_backup.py index f3f2fbdad40..4d0abd5a602 100644 --- a/tests/components/onedrive/test_backup.py +++ b/tests/components/onedrive/test_backup.py @@ -91,14 +91,16 @@ async def test_agents_list_backups( "onedrive.mock_drive_id": {"protected": False, "size": 34519040} }, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, } ] @@ -143,14 +145,16 @@ async def test_agents_get_backup( } }, "backup_id": "23e64aec", - "date": "2024-11-22T11:48:48.727189+01:00", "database_included": True, + "date": "2024-11-22T11:48:48.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2024.12.0.dev0", "name": "Core 2024.12.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, } diff --git a/tests/components/synology_dsm/test_backup.py b/tests/components/synology_dsm/test_backup.py index 5d54377c202..0a887bbcae3 100644 --- a/tests/components/synology_dsm/test_backup.py +++ b/tests/components/synology_dsm/test_backup.py @@ -342,14 +342,16 @@ async def test_agents_list_backups( } }, "backup_id": "abcd12ef", - "date": "2025-01-09T20:14:35.457323+01:00", "database_included": True, + "date": "2025-01-09T20:14:35.457323+01:00", "extra_metadata": {"instance_id": ANY, "with_automatic_settings": True}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.0.dev0", "name": "Automatic backup 2025.2.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, } ] @@ -413,14 +415,16 @@ async def test_agents_list_backups_disabled_filestation( } }, "backup_id": "abcd12ef", - "date": "2025-01-09T20:14:35.457323+01:00", "database_included": True, + "date": "2025-01-09T20:14:35.457323+01:00", "extra_metadata": {"instance_id": ANY, "with_automatic_settings": True}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.0.dev0", "name": "Automatic backup 2025.2.0.dev0", - "failed_agent_ids": [], "with_automatic_settings": None, }, ), diff --git a/tests/components/webdav/test_backup.py b/tests/components/webdav/test_backup.py index ca20467484f..65badabe593 100644 --- a/tests/components/webdav/test_backup.py +++ b/tests/components/webdav/test_backup.py @@ -86,14 +86,16 @@ async def test_agents_list_backups( } }, "backup_id": "23e64aec", - "date": "2025-02-10T17:47:22.727189+01:00", "database_included": True, + "date": "2025-02-10T17:47:22.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.1", "name": "Automatic backup 2025.2.1", - "failed_agent_ids": [], "with_automatic_settings": None, } ] @@ -122,14 +124,16 @@ async def test_agents_get_backup( } }, "backup_id": "23e64aec", - "date": "2025-02-10T17:47:22.727189+01:00", "database_included": True, + "date": "2025-02-10T17:47:22.727189+01:00", "extra_metadata": {}, + "failed_addons": [], + "failed_agent_ids": [], + "failed_folders": [], "folders": [], "homeassistant_included": True, "homeassistant_version": "2025.2.1", "name": "Automatic backup 2025.2.1", - "failed_agent_ids": [], "with_automatic_settings": None, } From 109bcf362aef283233d429fada5e1598c507395d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 16:16:18 +0200 Subject: [PATCH 0912/1175] Use shorthand attributes in xiaomi_miio (part 3) (#145617) --- homeassistant/components/xiaomi_miio/fan.py | 149 ++++++------------ homeassistant/components/xiaomi_miio/light.py | 49 +++--- .../components/xiaomi_miio/sensor.py | 9 +- .../components/xiaomi_miio/switch.py | 44 +++--- 4 files changed, 96 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 4bb922383dc..c69bd150226 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -301,6 +301,7 @@ class XiaomiGenericDevice( """Representation of a generic Xiaomi device.""" _attr_name = None + _attr_preset_modes: list[str] def __init__( self, @@ -315,30 +316,20 @@ class XiaomiGenericDevice( self._available_attributes: dict[str, Any] = {} self._mode: str | None = None self._fan_level: int | None = None - self._state_attrs: dict[str, Any] = {} + self._attr_extra_state_attributes = {} self._device_features = 0 - self._preset_modes: list[str] = [] + self._attr_preset_modes = [] @property @abstractmethod def operation_mode_class(self): """Hold operation mode class.""" - @property - def preset_modes(self) -> list[str]: - """Get the list of available preset modes.""" - return self._preset_modes - @property def percentage(self) -> int | None: """Return the percentage based speed of the fan.""" return None - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the device.""" - return self._state_attrs - async def async_turn_on( self, percentage: int | None = None, @@ -376,29 +367,12 @@ class XiaomiGenericDevice( class XiaomiGenericAirPurifier(XiaomiGenericDevice): """Representation of a generic AirPurifier device.""" - def __init__( - self, - device: MiioDevice, - entry: XiaomiMiioConfigEntry, - unique_id: str | None, - coordinator: DataUpdateCoordinator[Any], - ) -> None: - """Initialize the generic AirPurifier device.""" - super().__init__(device, entry, unique_id, coordinator) - - self._speed_count = 100 - - @property - def speed_count(self) -> int: - """Return the number of speeds of the fan supported.""" - return self._speed_count - @property def preset_mode(self) -> str | None: """Get the active preset mode.""" if self._attr_is_on: preset_mode = self.operation_mode_class(self._mode).name - return preset_mode if preset_mode in self._preset_modes else None + return preset_mode if preset_mode in self._attr_preset_modes else None return None @@ -406,7 +380,7 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): def _handle_coordinator_update(self): """Fetch state from the device.""" self._attr_is_on = self.coordinator.data.is_on - self._state_attrs.update( + self._attr_extra_state_attributes.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) for key, value in self._available_attributes.items() @@ -442,70 +416,70 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO - self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_PRO self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 elif self._model in [MODEL_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_4 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT - self._preset_modes = PRESET_MODES_AIRPURIFIER_MIOT + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_MIOT self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE ) - self._speed_count = 3 + self._attr_speed_count = 3 elif self._model in [ MODEL_AIRPURIFIER_4_LITE_RMA1, MODEL_AIRPURIFIER_4_LITE_RMB1, ]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_4_LITE self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT - self._preset_modes = PRESET_MODES_AIRPURIFIER_4_LITE + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_4_LITE self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 - self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7 + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_PRO_V7 self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 elif self._model in [MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2H]: self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON - self._preset_modes = PRESET_MODES_AIRPURIFIER_2S + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_2S self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 elif self._model == MODEL_AIRPURIFIER_ZA1: self._device_features = FEATURE_FLAGS_AIRPURIFIER_ZA1 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT - self._preset_modes = PRESET_MODES_AIRPURIFIER_ZA1 + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_ZA1 self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 elif self._model in MODELS_PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_MIOT self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT - self._preset_modes = PRESET_MODES_AIRPURIFIER_MIOT + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_MIOT self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE ) - self._speed_count = 3 + self._attr_speed_count = 3 elif self._model == MODEL_AIRPURIFIER_V3: self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 - self._preset_modes = PRESET_MODES_AIRPURIFIER_V3 + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_V3 self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 else: self._device_features = FEATURE_FLAGS_AIRPURIFIER_MIIO self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER - self._preset_modes = PRESET_MODES_AIRPURIFIER + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER self._attr_supported_features = FanEntityFeature.PRESET_MODE - self._speed_count = 1 + self._attr_speed_count = 1 self._attr_supported_features |= ( FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON ) self._attr_is_on = self.coordinator.data.is_on - self._state_attrs.update( + self._attr_extra_state_attributes.update( { key: self._extract_value_from_attribute(self.coordinator.data, value) for key, value in self._available_attributes.items() @@ -526,7 +500,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): mode = self.operation_mode_class(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( - (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] + (1, self.speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] ) return None @@ -541,7 +515,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): return speed_mode = math.ceil( - percentage_to_ranged_value((1, self._speed_count), percentage) + percentage_to_ranged_value((1, self.speed_count), percentage) ) if speed_mode: await self._try_command( @@ -638,7 +612,7 @@ class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C - self._preset_modes = PRESET_MODES_AIRPURIFIER_3C + self._attr_preset_modes = PRESET_MODES_AIRPURIFIER_3C self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -748,8 +722,8 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH - self._speed_count = 4 - self._preset_modes = PRESET_MODES_AIRFRESH + self._attr_speed_count = 4 + self._attr_preset_modes = PRESET_MODES_AIRFRESH self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -758,7 +732,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): ) self._attr_is_on = self.coordinator.data.is_on - self._state_attrs.update( + self._attr_extra_state_attributes.update( { key: getattr(self.coordinator.data, value) for key, value in self._available_attributes.items() @@ -778,7 +752,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): mode = AirfreshOperationMode(self._mode) if mode in self.REVERSE_SPEED_MODE_MAPPING: return ranged_value_to_percentage( - (1, self._speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] + (1, self.speed_count), self.REVERSE_SPEED_MODE_MAPPING[mode] ) return None @@ -789,7 +763,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): This method is a coroutine. """ speed_mode = math.ceil( - percentage_to_ranged_value((1, self._speed_count), percentage) + percentage_to_ranged_value((1, self.speed_count), percentage) ) if speed_mode: if await self._try_command( @@ -851,7 +825,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): super().__init__(device, entry, unique_id, coordinator) self._favorite_speed: int | None = None self._device_features = FEATURE_FLAGS_AIRFRESH_A1 - self._preset_modes = PRESET_MODES_AIRFRESH_A1 + self._attr_preset_modes = PRESET_MODES_AIRFRESH_A1 self._attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -970,15 +944,8 @@ class XiaomiGenericFan(XiaomiGenericDevice): ) if self._model != MODEL_FAN_1C: self._attr_supported_features |= FanEntityFeature.DIRECTION - self._preset_mode: str | None = None - self._oscillating: bool | None = None self._percentage: int | None = None - @property - def preset_mode(self) -> str | None: - """Get the active preset mode.""" - return self._preset_mode - @property def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" @@ -992,11 +959,6 @@ class XiaomiGenericFan(XiaomiGenericDevice): return None - @property - def oscillating(self) -> bool | None: - """Return whether or not the fan is currently oscillating.""" - return self._oscillating - async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" await self._try_command( @@ -1004,12 +966,12 @@ class XiaomiGenericFan(XiaomiGenericDevice): self._device.set_oscillate, # type: ignore[attr-defined] oscillating, ) - self._oscillating = oscillating + self._attr_oscillating = oscillating self.async_write_ha_state() async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - if self._oscillating: + if self._attr_oscillating: await self.async_oscillate(oscillating=False) await self._try_command( @@ -1033,7 +995,7 @@ class XiaomiFan(XiaomiGenericFan): super().__init__(device, entry, unique_id, coordinator) self._attr_is_on = self.coordinator.data.is_on - self._oscillating = self.coordinator.data.oscillate + self._attr_oscillating = self.coordinator.data.oscillate self._nature_mode = self.coordinator.data.natural_speed != 0 if self._nature_mode: self._percentage = self.coordinator.data.natural_speed @@ -1058,7 +1020,7 @@ class XiaomiFan(XiaomiGenericFan): def _handle_coordinator_update(self): """Fetch state from the device.""" self._attr_is_on = self.coordinator.data.is_on - self._oscillating = self.coordinator.data.oscillate + self._attr_oscillating = self.coordinator.data.oscillate self._nature_mode = self.coordinator.data.natural_speed != 0 if self._nature_mode: self._percentage = self.coordinator.data.natural_speed @@ -1082,7 +1044,7 @@ class XiaomiFan(XiaomiGenericFan): self._percentage, ) - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: @@ -1126,8 +1088,8 @@ class XiaomiFanP5(XiaomiGenericFan): super().__init__(device, entry, unique_id, coordinator) self._attr_is_on = self.coordinator.data.is_on - self._preset_mode = self.coordinator.data.mode.name - self._oscillating = self.coordinator.data.oscillate + self._attr_preset_mode = self.coordinator.data.mode.name + self._attr_oscillating = self.coordinator.data.oscillate self._percentage = self.coordinator.data.speed @property @@ -1139,8 +1101,8 @@ class XiaomiFanP5(XiaomiGenericFan): def _handle_coordinator_update(self): """Fetch state from the device.""" self._attr_is_on = self.coordinator.data.is_on - self._preset_mode = self.coordinator.data.mode.name - self._oscillating = self.coordinator.data.oscillate + self._attr_preset_mode = self.coordinator.data.mode.name + self._attr_oscillating = self.coordinator.data.oscillate self._percentage = self.coordinator.data.speed self.async_write_ha_state() @@ -1152,7 +1114,7 @@ class XiaomiFanP5(XiaomiGenericFan): self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ) - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: @@ -1183,17 +1145,12 @@ class XiaomiFanMiot(XiaomiGenericFan): """Hold operation mode class.""" return FanOperationMode - @property - def preset_mode(self) -> str | None: - """Get the active preset mode.""" - return self._preset_mode - @callback def _handle_coordinator_update(self): """Fetch state from the device.""" self._attr_is_on = self.coordinator.data.is_on - self._preset_mode = self.coordinator.data.mode.name - self._oscillating = self.coordinator.data.oscillate + self._attr_preset_mode = self.coordinator.data.mode.name + self._attr_oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: self._percentage = self.coordinator.data.speed else: @@ -1208,7 +1165,7 @@ class XiaomiFanMiot(XiaomiGenericFan): self._device.set_mode, # type: ignore[attr-defined] self.operation_mode_class[preset_mode], ) - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: @@ -1253,17 +1210,17 @@ class XiaomiFan1C(XiaomiFanMiot): ) -> None: """Initialize MIOT fan with speed count.""" super().__init__(device, entry, unique_id, coordinator) - self._speed_count = 3 + self._attr_speed_count = 3 @callback def _handle_coordinator_update(self): """Fetch state from the device.""" self._attr_is_on = self.coordinator.data.is_on - self._preset_mode = self.coordinator.data.mode.name - self._oscillating = self.coordinator.data.oscillate + self._attr_preset_mode = self.coordinator.data.mode.name + self._attr_oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: self._percentage = ranged_value_to_percentage( - (1, self._speed_count), self.coordinator.data.speed + (1, self.speed_count), self.coordinator.data.speed ) else: self._percentage = 0 @@ -1277,9 +1234,7 @@ class XiaomiFan1C(XiaomiFanMiot): await self.async_turn_off() return - speed = math.ceil( - percentage_to_ranged_value((1, self._speed_count), percentage) - ) + speed = math.ceil(percentage_to_ranged_value((1, self.speed_count), percentage)) # if the fan is not on, we have to turn it on first if not self.is_on: @@ -1292,5 +1247,5 @@ class XiaomiFan1C(XiaomiFanMiot): ) if result: - self._percentage = ranged_value_to_percentage((1, self._speed_count), speed) + self._percentage = ranged_value_to_percentage((1, self.speed_count), speed) self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 4271894ba17..0ff6df93d3e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -271,12 +271,7 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._state_attrs: dict[str, Any] = {} - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._state_attrs + self._attr_extra_state_attributes = {} async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" @@ -349,7 +344,9 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None}) + self._attr_extra_state_attributes.update( + {ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None} + ) async def async_update(self) -> None: """Fetch state from the device.""" @@ -370,10 +367,10 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, dt_util.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF], + self._attr_extra_state_attributes[ATTR_DELAYED_TURN_OFF], ) - self._state_attrs.update( + self._attr_extra_state_attributes.update( {ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off} ) @@ -560,10 +557,10 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, dt_util.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF], + self._attr_extra_state_attributes[ATTR_DELAYED_TURN_OFF], ) - self._state_attrs.update( + self._attr_extra_state_attributes.update( {ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off} ) @@ -591,7 +588,7 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._state_attrs.update( + self._attr_extra_state_attributes.update( {ATTR_NIGHT_LIGHT_MODE: None, ATTR_AUTOMATIC_COLOR_TEMPERATURE: None} ) @@ -631,10 +628,10 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, dt_util.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF], + self._attr_extra_state_attributes[ATTR_DELAYED_TURN_OFF], ) - self._state_attrs.update( + self._attr_extra_state_attributes.update( { ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off, @@ -659,7 +656,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._state_attrs.update( + self._attr_extra_state_attributes.update( {ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None} ) @@ -682,10 +679,10 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): delayed_turn_off = self.delayed_turn_off_timestamp( state.delay_off_countdown, dt_util.utcnow(), - self._state_attrs[ATTR_DELAYED_TURN_OFF], + self._attr_extra_state_attributes[ATTR_DELAYED_TURN_OFF], ) - self._state_attrs.update( + self._attr_extra_state_attributes.update( { ATTR_SCENE: state.scene, ATTR_DELAYED_TURN_OFF: delayed_turn_off, @@ -847,9 +844,8 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) - self._hs_color: tuple[float, float] | None = None - self._state_attrs.pop(ATTR_DELAYED_TURN_OFF) - self._state_attrs.update( + self._attr_extra_state_attributes.pop(ATTR_DELAYED_TURN_OFF) + self._attr_extra_state_attributes.update( { ATTR_SLEEP_ASSISTANT: None, ATTR_SLEEP_OFF_TIME: None, @@ -869,11 +865,6 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): """Return the warmest color_temp that this light supports.""" return 588 - @property - def hs_color(self) -> tuple[float, float] | None: - """Return the hs color value.""" - return self._hs_color - @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" @@ -915,7 +906,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ) if result: - self._hs_color = hs_color + self._attr_hs_color = hs_color self._attr_brightness = brightness elif ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP_KELVIN in kwargs: @@ -949,7 +940,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ) if result: - self._hs_color = hs_color + self._attr_hs_color = hs_color elif ATTR_COLOR_TEMP_KELVIN in kwargs: _LOGGER.debug( @@ -1007,9 +998,9 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): self._max_mireds, self._min_mireds, ) - self._hs_color = color_util.color_RGB_to_hs(*state.rgb) + self._attr_hs_color = color_util.color_RGB_to_hs(*state.rgb) - self._state_attrs.update( + self._attr_extra_state_attributes.update( { ATTR_SCENE: state.scene, ATTR_SLEEP_ASSISTANT: state.sleep_assistant, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index e7f652d1de2..eb630e6d28f 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -938,7 +938,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._state_attrs = { + self._attr_extra_state_attributes = { ATTR_POWER: None, ATTR_BATTERY_LEVEL: None, ATTR_CHARGING: None, @@ -950,11 +950,6 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): } self.entity_description = description - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._state_attrs - async def async_update(self) -> None: """Fetch state from the miio device.""" try: @@ -963,7 +958,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): self._attr_available = True self._attr_native_value = state.aqi - self._state_attrs.update( + self._attr_extra_state_attributes.update( { ATTR_POWER: state.power, ATTR_CHARGING: state.usb_power, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ff6387bc7c1..0f78e67d30c 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -816,15 +816,13 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) - self._state_attrs = {ATTR_TEMPERATURE: None, ATTR_MODEL: self._model} + self._attr_extra_state_attributes = { + ATTR_TEMPERATURE: None, + ATTR_MODEL: self._model, + } self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._state_attrs - async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" try: @@ -877,7 +875,7 @@ class XiaomiPlugGenericSwitch(XiaomiMiioEntity, SwitchEntity): self._attr_available = True self._attr_is_on = state.is_on - self._state_attrs[ATTR_TEMPERATURE] = state.temperature + self._attr_extra_state_attributes[ATTR_TEMPERATURE] = state.temperature except DeviceException as ex: if self._attr_available: @@ -934,16 +932,16 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): else: self._device_features = FEATURE_FLAGS_POWER_STRIP_V1 - self._state_attrs[ATTR_LOAD_POWER] = None + self._attr_extra_state_attributes[ATTR_LOAD_POWER] = None if self._device_features & FEATURE_SET_POWER_MODE == 1: - self._state_attrs[ATTR_POWER_MODE] = None + self._attr_extra_state_attributes[ATTR_POWER_MODE] = None if self._device_features & FEATURE_SET_WIFI_LED == 1: - self._state_attrs[ATTR_WIFI_LED] = None + self._attr_extra_state_attributes[ATTR_WIFI_LED] = None if self._device_features & FEATURE_SET_POWER_PRICE == 1: - self._state_attrs[ATTR_POWER_PRICE] = None + self._attr_extra_state_attributes[ATTR_POWER_PRICE] = None async def async_update(self) -> None: """Fetch state from the device.""" @@ -958,21 +956,21 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): self._attr_available = True self._attr_is_on = state.is_on - self._state_attrs.update( + self._attr_extra_state_attributes.update( {ATTR_TEMPERATURE: state.temperature, ATTR_LOAD_POWER: state.load_power} ) if self._device_features & FEATURE_SET_POWER_MODE == 1 and state.mode: - self._state_attrs[ATTR_POWER_MODE] = state.mode.value + self._attr_extra_state_attributes[ATTR_POWER_MODE] = state.mode.value if self._device_features & FEATURE_SET_WIFI_LED == 1 and state.wifi_led: - self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + self._attr_extra_state_attributes[ATTR_WIFI_LED] = state.wifi_led if ( self._device_features & FEATURE_SET_POWER_PRICE == 1 and state.power_price ): - self._state_attrs[ATTR_POWER_PRICE] = state.power_price + self._attr_extra_state_attributes[ATTR_POWER_PRICE] = state.power_price except DeviceException as ex: if self._attr_available: @@ -1015,9 +1013,9 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): if self._model == MODEL_PLUG_V3: self._device_features = FEATURE_FLAGS_PLUG_V3 - self._state_attrs[ATTR_WIFI_LED] = None + self._attr_extra_state_attributes[ATTR_WIFI_LED] = None if self._channel_usb is False: - self._state_attrs[ATTR_LOAD_POWER] = None + self._attr_extra_state_attributes[ATTR_LOAD_POWER] = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn a channel on.""" @@ -1069,13 +1067,13 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): else: self._attr_is_on = state.is_on - self._state_attrs[ATTR_TEMPERATURE] = state.temperature + self._attr_extra_state_attributes[ATTR_TEMPERATURE] = state.temperature if state.wifi_led: - self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + self._attr_extra_state_attributes[ATTR_WIFI_LED] = state.wifi_led if self._channel_usb is False and state.load_power: - self._state_attrs[ATTR_LOAD_POWER] = state.load_power + self._attr_extra_state_attributes[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: if self._attr_available: @@ -1098,7 +1096,9 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): """Initialize the acpartner switch.""" super().__init__(name, plug, entry, unique_id) - self._state_attrs.update({ATTR_TEMPERATURE: None, ATTR_LOAD_POWER: None}) + self._attr_extra_state_attributes.update( + {ATTR_TEMPERATURE: None, ATTR_LOAD_POWER: None} + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the socket on.""" @@ -1135,7 +1135,7 @@ class XiaomiAirConditioningCompanionSwitch(XiaomiPlugGenericSwitch): self._attr_available = True self._attr_is_on = state.power_socket == "on" - self._state_attrs[ATTR_LOAD_POWER] = state.load_power + self._attr_extra_state_attributes[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: if self._attr_available: From 0d816946409f4de0c7ba66bada905216a80c9552 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 26 May 2025 16:20:55 +0200 Subject: [PATCH 0913/1175] Add event browsing to Reolink recordings (#144259) --- .../components/reolink/media_source.py | 51 ++++++++++++++++++- tests/components/reolink/test_media_source.py | 48 +++++++++++++++-- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 49257128a2d..36a2f3c5489 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -7,6 +7,7 @@ import logging from reolink_aio.api import DUAL_LENS_MODELS from reolink_aio.enums import VodRequestType +from reolink_aio.typings import VOD_trigger from homeassistant.components.camera import DOMAIN as CAM_DOMAIN, DynamicStreamSettings from homeassistant.components.media_player import MediaClass, MediaType @@ -152,6 +153,26 @@ class ReolinkVODMediaSource(MediaSource): int(month_str), int(day_str), ) + if item_type == "EVE": + ( + _, + config_entry_id, + channel_str, + stream, + year_str, + month_str, + day_str, + event, + ) = identifier + return await self._async_generate_camera_files( + config_entry_id, + int(channel_str), + stream, + int(year_str), + int(month_str), + int(day_str), + event, + ) raise Unresolvable(f"Unknown media item '{item.identifier}' during browsing.") @@ -352,6 +373,7 @@ class ReolinkVODMediaSource(MediaSource): year: int, month: int, day: int, + event: str | None = None, ) -> BrowseMediaSource: """Return all recording files on a specific day of a Reolink camera.""" host = get_host(self.hass, config_entry_id) @@ -368,9 +390,34 @@ class ReolinkVODMediaSource(MediaSource): month, day, ) + event_trigger = VOD_trigger[event] if event is not None else None _, vod_files = await host.api.request_vod_files( - channel, start, end, stream=stream, split_time=VOD_SPLIT_TIME + channel, + start, + end, + stream=stream, + split_time=VOD_SPLIT_TIME, + trigger=event_trigger, ) + + if event is None and host.api.is_nvr and not host.api.is_hub: + triggers = VOD_trigger.NONE + for file in vod_files: + triggers |= file.triggers + + children.extend( + BrowseMediaSource( + domain=DOMAIN, + identifier=f"EVE|{config_entry_id}|{channel}|{stream}|{year}|{month}|{day}|{trigger.name}", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.PLAYLIST, + title=str(trigger.name).title(), + can_play=False, + can_expand=True, + ) + for trigger in triggers + ) + for file in vod_files: file_name = f"{file.start_time.time()} {file.duration}" if file.triggers != file.triggers.NONE: @@ -397,6 +444,8 @@ class ReolinkVODMediaSource(MediaSource): ) if host.api.model in DUAL_LENS_MODELS: title = f"{host.api.camera_name(channel)} lens {channel} {res_name(stream)} {year}/{month}/{day}" + if event: + title = f"{title} {event.title()}" return BrowseMediaSource( domain=DOMAIN, diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 59868514226..126d445ca01 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from reolink_aio.exceptions import ReolinkError +from reolink_aio.typings import VOD_trigger from homeassistant.components.media_source import ( DOMAIN as MEDIA_SOURCE_DOMAIN, @@ -16,6 +17,7 @@ from homeassistant.components.media_source import ( ) from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.components.reolink.const import CONF_BC_PORT, CONF_USE_HTTPS, DOMAIN +from homeassistant.components.reolink.media_source import VOD_SPLIT_TIME from homeassistant.components.stream import DOMAIN as MEDIA_STREAM_DOMAIN from homeassistant.const import ( CONF_HOST, @@ -53,6 +55,8 @@ TEST_HOUR = 13 TEST_MINUTE = 12 TEST_START = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE}" TEST_END = f"{TEST_YEAR}{TEST_MONTH}{TEST_DAY}{TEST_HOUR}{TEST_MINUTE + 5}" +TEST_START_TIME = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, 0, 0) +TEST_END_TIME = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, 23, 59, 59) TEST_FILE_NAME = f"{TEST_START}00" TEST_FILE_NAME_MP4 = f"{TEST_START}00.mp4" TEST_STREAM = "main" @@ -212,13 +216,12 @@ async def test_browsing( # browse camera recording files on day mock_vod_file = MagicMock() - mock_vod_file.start_time = datetime( - TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE - ) + mock_vod_file.start_time = TEST_START_TIME mock_vod_file.start_time_id = TEST_START mock_vod_file.end_time_id = TEST_END - mock_vod_file.duration = timedelta(minutes=15) + mock_vod_file.duration = timedelta(minutes=5) mock_vod_file.file_name = TEST_FILE_NAME + mock_vod_file.triggers = VOD_trigger.PERSON reolink_connect.request_vod_files.return_value = ([mock_status], [mock_vod_file]) browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_day_0_id}") @@ -232,9 +235,46 @@ async def test_browsing( ) assert browse.identifier == browse_files_id assert browse.children[0].identifier == browse_file_id + reolink_connect.request_vod_files.assert_called_with( + int(TEST_CHANNEL), + TEST_START_TIME, + TEST_END_TIME, + stream=TEST_STREAM, + split_time=VOD_SPLIT_TIME, + trigger=None, + ) reolink_connect.model = TEST_HOST_MODEL + # browse event trigger person on a NVR + reolink_connect.is_nvr = True + browse_event_person_id = f"EVE|{entry_id}|{TEST_CHANNEL}|{TEST_STREAM}|{TEST_YEAR}|{TEST_MONTH}|{TEST_DAY}|{VOD_trigger.PERSON.name}" + + browse = await async_browse_media(hass, f"{URI_SCHEME}{DOMAIN}/{browse_day_0_id}") + assert browse.children[0].identifier == browse_event_person_id + + browse = await async_browse_media( + hass, f"{URI_SCHEME}{DOMAIN}/{browse_event_person_id}" + ) + + assert browse.domain == DOMAIN + assert ( + browse.title + == f"{TEST_NVR_NAME} High res. {TEST_YEAR}/{TEST_MONTH}/{TEST_DAY} Person" + ) + assert browse.identifier == browse_files_id + assert browse.children[0].identifier == browse_file_id + reolink_connect.request_vod_files.assert_called_with( + int(TEST_CHANNEL), + TEST_START_TIME, + TEST_END_TIME, + stream=TEST_STREAM, + split_time=VOD_SPLIT_TIME, + trigger=VOD_trigger.PERSON, + ) + + reolink_connect.is_nvr = False + async def test_browsing_h265_encoding( hass: HomeAssistant, From 6f9a39ab89313ffd5601b4fe8ef5389419bbfdf8 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 26 May 2025 16:28:18 +0200 Subject: [PATCH 0914/1175] Add select source action to Music Assistant (#145619) --- .../music_assistant/media_player.py | 11 +++++++ .../music_assistant/fixtures/players.json | 30 +++++++++++++++++-- .../snapshots/test_media_player.ambr | 14 +++++++-- .../music_assistant/test_media_player.py | 28 +++++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 5dc8ab2ec00..91c9d5ffd90 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -292,6 +292,10 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): self._attr_state = MediaPlayerState(player.state.value) else: self._attr_state = MediaPlayerState(STATE_OFF) + self._attr_source = player.active_source + self._attr_source_list = [ + source.name for source in player.source_list if not source.passive + ] group_members: list[str] = [] if player.group_childs: @@ -459,6 +463,11 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): """Remove this player from any group.""" await self.mass.players.player_command_ungroup(self.player_id) + @catch_musicassistant_error + async def async_select_source(self, source: str) -> None: + """Select input source.""" + await self.mass.players.player_command_select_source(self.player_id, source) + @catch_musicassistant_error async def _async_handle_play_media( self, @@ -735,4 +744,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): if self.player.power_control != PLAYER_CONTROL_NONE: supported_features |= MediaPlayerEntityFeature.TURN_ON supported_features |= MediaPlayerEntityFeature.TURN_OFF + if PlayerFeature.SELECT_SOURCE in self.player.supported_features: + supported_features |= MediaPlayerEntityFeature.SELECT_SOURCE self._attr_supported_features = supported_features diff --git a/tests/components/music_assistant/fixtures/players.json b/tests/components/music_assistant/fixtures/players.json index e8978f17f86..58ce20da824 100644 --- a/tests/components/music_assistant/fixtures/players.json +++ b/tests/components/music_assistant/fixtures/players.json @@ -18,7 +18,8 @@ "pause", "set_members", "power", - "enqueue" + "enqueue", + "select_source" ], "elapsed_time": null, "elapsed_time_last_updated": 0, @@ -43,7 +44,32 @@ "hide_player_in_ui": ["when_unavailable"], "expose_to_ha": true, "can_group_with": ["00:00:00:00:00:02"], - "source_list": [] + "source_list": [ + { + "id": "00:00:00:00:00:01", + "name": "Music Assistant Queue", + "passive": false, + "can_play_pause": true, + "can_seek": true, + "can_next_previous": true + }, + { + "id": "spotify", + "name": "Spotify Connect", + "passive": true, + "can_play_pause": true, + "can_seek": true, + "can_next_previous": true + }, + { + "id": "linein", + "name": "Line-In", + "passive": false, + "can_play_pause": false, + "can_seek": false, + "can_next_previous": false + } + ] }, { "player_id": "00:00:00:00:00:02", diff --git a/tests/components/music_assistant/snapshots/test_media_player.ambr b/tests/components/music_assistant/snapshots/test_media_player.ambr index f561a5c3afb..5782156e722 100644 --- a/tests/components/music_assistant/snapshots/test_media_player.ambr +++ b/tests/components/music_assistant/snapshots/test_media_player.ambr @@ -54,6 +54,7 @@ 'media_duration': 300, 'media_position': 0, 'media_title': 'Test Track', + 'source': 'spotify', 'supported_features': , 'volume_level': 0.2, }), @@ -125,6 +126,7 @@ 'media_title': 'November Rain', 'repeat': 'all', 'shuffle': True, + 'source': 'test_group_player_1', 'supported_features': , 'volume_level': 0.06, }), @@ -142,6 +144,10 @@ }), 'area_id': None, 'capabilities': dict({ + 'source_list': list([ + 'Music Assistant Queue', + 'Line-In', + ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -165,7 +171,7 @@ 'original_name': None, 'platform': 'music_assistant', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:01', 'unit_of_measurement': None, @@ -181,7 +187,11 @@ ]), 'icon': 'mdi:speaker', 'mass_player_type': 'player', - 'supported_features': , + 'source_list': list([ + 'Music Assistant Queue', + 'Line-In', + ]), + 'supported_features': , }), 'context': , 'entity_id': 'media_player.test_player_1', diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 288d49092e5..e2b45db45e4 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -16,6 +16,7 @@ from syrupy.filters import paths from homeassistant.components.media_player import ( ATTR_GROUP_MEMBERS, + ATTR_INPUT_SOURCE, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_REPEAT, ATTR_MEDIA_SEEK_POSITION, @@ -25,6 +26,7 @@ from homeassistant.components.media_player import ( DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, SERVICE_JOIN, + SERVICE_SELECT_SOURCE, SERVICE_UNJOIN, MediaPlayerEntityFeature, ) @@ -620,6 +622,31 @@ async def test_media_player_get_queue_action( assert response == snapshot(exclude=paths(f"{entity_id}.elapsed_time")) +async def test_media_player_select_source_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, +) -> None: + """Test media_player entity select source action.""" + await setup_integration_from_fixtures(hass, music_assistant_client) + entity_id = "media_player.test_player_1" + mass_player_id = "00:00:00:00:00:01" + state = hass.states.get(entity_id) + assert state + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOURCE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_INPUT_SOURCE: "linein", + }, + blocking=True, + ) + assert music_assistant_client.send_command.call_count == 1 + assert music_assistant_client.send_command.call_args == call( + "players/cmd/select_source", player_id=mass_player_id, source="linein" + ) + + async def test_media_player_supported_features( hass: HomeAssistant, music_assistant_client: MagicMock, @@ -652,6 +679,7 @@ async def test_media_player_supported_features( | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SEARCH_MEDIA + | MediaPlayerEntityFeature.SELECT_SOURCE ) assert state.attributes["supported_features"] == expected_features # remove power control capability from player, trigger subscription callback From 42cacd28e78b6ecf25794c324d5eb4a75900af21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Mon, 26 May 2025 16:38:41 +0200 Subject: [PATCH 0915/1175] Add tests to miele fan entity and api push data pathway (#144481) * Use device class transation * WIP * Test api push * Use constants * Use callbacks registered with mock * Add comment * Adress review comments * Empty commit * Fix tests * Updates after review --- homeassistant/components/miele/climate.py | 10 +- tests/components/miele/__init__.py | 13 + tests/components/miele/conftest.py | 19 + .../components/miele/fixtures/4_actions.json | 86 ++ .../miele/fixtures/action_push_vacuum.json | 17 + .../fixtures/action_washing_machine.json | 2 +- .../miele/snapshots/test_binary_sensor.ambr | 1092 ++++++++++++++++ .../miele/snapshots/test_button.ambr | 188 +++ .../miele/snapshots/test_climate.ambr | 126 ++ .../miele/snapshots/test_diagnostics.ambr | 10 +- .../components/miele/snapshots/test_fan.ambr | 54 + .../miele/snapshots/test_light.ambr | 112 ++ .../miele/snapshots/test_sensor.ambr | 1140 +++++++++++++++++ .../miele/snapshots/test_switch.ambr | 188 +++ .../miele/snapshots/test_vacuum.ambr | 62 + tests/components/miele/test_binary_sensor.py | 15 + tests/components/miele/test_button.py | 18 +- tests/components/miele/test_climate.py | 18 +- tests/components/miele/test_fan.py | 60 +- tests/components/miele/test_light.py | 18 +- tests/components/miele/test_sensor.py | 17 +- tests/components/miele/test_switch.py | 16 +- tests/components/miele/test_vacuum.py | 34 +- 23 files changed, 3294 insertions(+), 21 deletions(-) create mode 100644 tests/components/miele/fixtures/4_actions.json create mode 100644 tests/components/miele/fixtures/action_push_vacuum.json diff --git a/homeassistant/components/miele/climate.py b/homeassistant/components/miele/climate.py index 85235322616..24d020823c8 100644 --- a/homeassistant/components/miele/climate.py +++ b/homeassistant/components/miele/climate.py @@ -197,13 +197,13 @@ class MieleClimate(MieleEntity, ClimateEntity): self._attr_name = None if description.zone == 2: + t_key = "zone_2" if self.device.device_type in ( MieleAppliance.FRIDGE_FREEZER, MieleAppliance.WINE_CABINET_FREEZER, ): t_key = DEVICE_TYPE_TAGS[MieleAppliance.FREEZER] - else: - t_key = "zone_2" + elif description.zone == 3: t_key = "zone_3" @@ -234,11 +234,11 @@ class MieleClimate(MieleEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: - return try: await self.api.set_target_temperature( - self._device_id, temperature, self.entity_description.zone + self._device_id, + cast(float, kwargs.get(ATTR_TEMPERATURE)), + self.entity_description.zone, ) except aiohttp.ClientError as err: raise HomeAssistantError( diff --git a/tests/components/miele/__init__.py b/tests/components/miele/__init__.py index b0278defa8e..2e75470c4a4 100644 --- a/tests/components/miele/__init__.py +++ b/tests/components/miele/__init__.py @@ -1,5 +1,8 @@ """Tests for the Miele integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import AsyncMock + from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,3 +14,13 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +def get_data_callback(mock: AsyncMock) -> Callable[[int], Awaitable[None]]: + """Get registered callback for api data push.""" + return mock.listen_events.call_args_list[0].kwargs.get("data_callback") + + +def get_actions_callback(mock: AsyncMock) -> Callable[[int], Awaitable[None]]: + """Get registered callback for api data push.""" + return mock.listen_events.call_args_list[0].kwargs.get("actions_callback") diff --git a/tests/components/miele/conftest.py b/tests/components/miele/conftest.py index 8e3b5628ed4..211c1d27814 100644 --- a/tests/components/miele/conftest.py +++ b/tests/components/miele/conftest.py @@ -15,6 +15,7 @@ from homeassistant.components.miele.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import get_actions_callback, get_data_callback from .const import CLIENT_ID, CLIENT_SECRET from tests.common import MockConfigEntry, load_fixture, load_json_object_fixture @@ -157,3 +158,21 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.miele.async_setup_entry", return_value=True ) as mock_setup_entry: yield mock_setup_entry + + +@pytest.fixture +async def push_data_and_actions( + hass: HomeAssistant, + mock_miele_client: MagicMock, + device_fixture: MieleDevices, +) -> None: + """Fixture to push data and actions through mock.""" + + data_callback = get_data_callback(mock_miele_client) + await data_callback(device_fixture) + await hass.async_block_till_done() + + act_file = load_json_object_fixture("4_actions.json", DOMAIN) + action_callback = get_actions_callback(mock_miele_client) + await action_callback(act_file) + await hass.async_block_till_done() diff --git a/tests/components/miele/fixtures/4_actions.json b/tests/components/miele/fixtures/4_actions.json new file mode 100644 index 00000000000..6a89fb4604a --- /dev/null +++ b/tests/components/miele/fixtures/4_actions.json @@ -0,0 +1,86 @@ +{ + "Dummy_Appliance_1": { + "processAction": [4], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": -27, + "max": -13 + } + ], + "deviceName": true, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [1], + "runOnTime": [] + }, + "Dummy_Appliance_2": { + "processAction": [6], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + } + ], + "deviceName": true, + "powerOn": false, + "powerOff": false, + "colors": [], + "modes": [1], + "runOnTime": [] + }, + "Dummy_Appliance_3": { + "processAction": [1, 2, 3], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + } + ], + "deviceName": true, + "powerOn": true, + "powerOff": false, + "colors": [], + "modes": [], + "runOnTime": [] + }, + "DummyAppliance_18": { + "processAction": [], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [], + "targetTemperature": [ + { + "zone": 1, + "min": 1, + "max": 9 + } + ], + "deviceName": true, + "powerOn": true, + "powerOff": false, + "colors": [], + "modes": [], + "runOnTime": [] + } +} diff --git a/tests/components/miele/fixtures/action_push_vacuum.json b/tests/components/miele/fixtures/action_push_vacuum.json new file mode 100644 index 00000000000..f760d7e5e82 --- /dev/null +++ b/tests/components/miele/fixtures/action_push_vacuum.json @@ -0,0 +1,17 @@ +{ + "Dummy_Vacuum_1": { + "processAction": [], + "light": [], + "ambientLight": [], + "startTime": [], + "ventilationStep": [], + "programId": [3], + "targetTemperature": [], + "deviceName": true, + "powerOn": true, + "powerOff": false, + "colors": [], + "modes": [], + "runOnTime": [] + } +} diff --git a/tests/components/miele/fixtures/action_washing_machine.json b/tests/components/miele/fixtures/action_washing_machine.json index 363d3ae6c63..c9b656363c8 100644 --- a/tests/components/miele/fixtures/action_washing_machine.json +++ b/tests/components/miele/fixtures/action_washing_machine.json @@ -9,7 +9,7 @@ { "zone": 1, "min": -28, - "max": -14 + "max": 28 } ], "deviceName": true, diff --git a/tests/components/miele/snapshots/test_binary_sensor.ambr b/tests/components/miele/snapshots/test_binary_sensor.ambr index 9f5b886b0ba..423a4639ffb 100644 --- a/tests/components/miele/snapshots/test_binary_sensor.ambr +++ b/tests/components/miele/snapshots/test_binary_sensor.ambr @@ -1091,3 +1091,1095 @@ 'state': 'off', }) # --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.freezer_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_1-state_signal_door', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Freezer Door', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_mobile_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.freezer_mobile_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mobile start', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mobile_start', + 'unique_id': 'Dummy_Appliance_1-state_mobile_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_mobile_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Freezer Mobile start', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_mobile_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_notification_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.freezer_notification_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Notification active', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'notification_active', + 'unique_id': 'Dummy_Appliance_1-state_signal_info', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_notification_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Freezer Notification active', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_notification_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.freezer_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_1-state_signal_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Freezer Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.freezer_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'Dummy_Appliance_1-state_full_remote_control', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Freezer Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_smart_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.freezer_smart_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart grid', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_grid', + 'unique_id': 'Dummy_Appliance_1-state_smart_grid', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.freezer_smart_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Freezer Smart grid', + }), + 'context': , + 'entity_id': 'binary_sensor.freezer_smart_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_mobile_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.hood_mobile_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mobile start', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mobile_start', + 'unique_id': 'DummyAppliance_18-state_mobile_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_mobile_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Mobile start', + }), + 'context': , + 'entity_id': 'binary_sensor.hood_mobile_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_notification_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.hood_notification_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Notification active', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'notification_active', + 'unique_id': 'DummyAppliance_18-state_signal_info', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_notification_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Hood Notification active', + }), + 'context': , + 'entity_id': 'binary_sensor.hood_notification_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.hood_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'DummyAppliance_18-state_signal_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Hood Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.hood_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.hood_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'DummyAppliance_18-state_full_remote_control', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.hood_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_smart_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.hood_smart_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart grid', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_grid', + 'unique_id': 'DummyAppliance_18-state_smart_grid', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.hood_smart_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Smart grid', + }), + 'context': , + 'entity_id': 'binary_sensor.hood_smart_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.refrigerator_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_2-state_signal_door', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Refrigerator Door', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_mobile_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_mobile_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mobile start', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mobile_start', + 'unique_id': 'Dummy_Appliance_2-state_mobile_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_mobile_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Mobile start', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_mobile_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_notification_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_notification_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Notification active', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'notification_active', + 'unique_id': 'Dummy_Appliance_2-state_signal_info', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_notification_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Refrigerator Notification active', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_notification_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_2-state_signal_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Refrigerator Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'Dummy_Appliance_2-state_full_remote_control', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_smart_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.refrigerator_smart_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart grid', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_grid', + 'unique_id': 'Dummy_Appliance_2-state_smart_grid', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.refrigerator_smart_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Smart grid', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_smart_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.washing_machine_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_3-state_signal_door', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'door', + 'friendly_name': 'Washing machine Door', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_mobile_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.washing_machine_mobile_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Mobile start', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mobile_start', + 'unique_id': 'Dummy_Appliance_3-state_mobile_start', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_mobile_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Mobile start', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_mobile_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_notification_active-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.washing_machine_notification_active', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Notification active', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'notification_active', + 'unique_id': 'Dummy_Appliance_3-state_signal_info', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_notification_active-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Washing machine Notification active', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_notification_active', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.washing_machine_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_3-state_signal_failure', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Washing machine Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_remote_control-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.washing_machine_remote_control', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Remote control', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remote_control', + 'unique_id': 'Dummy_Appliance_3-state_full_remote_control', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_remote_control-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Remote control', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_remote_control', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_smart_grid-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.washing_machine_smart_grid', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Smart grid', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'smart_grid', + 'unique_id': 'Dummy_Appliance_3-state_smart_grid', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensor_states_api_push[platforms0][binary_sensor.washing_machine_smart_grid-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Smart grid', + }), + 'context': , + 'entity_id': 'binary_sensor.washing_machine_smart_grid', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/miele/snapshots/test_button.ambr b/tests/components/miele/snapshots/test_button.ambr index b4f5ea5685a..a7683caac24 100644 --- a/tests/components/miele/snapshots/test_button.ambr +++ b/tests/components/miele/snapshots/test_button.ambr @@ -187,3 +187,191 @@ 'state': 'unknown', }) # --- +# name: test_button_states_api_push[platforms0][button.hood_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.hood_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': 'DummyAppliance_18-stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_states_api_push[platforms0][button.hood_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Stop', + }), + 'context': , + 'entity_id': 'button.hood_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_pause-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.washing_machine_pause', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Pause', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pause', + 'unique_id': 'Dummy_Appliance_3-pause', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_pause-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Pause', + }), + 'context': , + 'entity_id': 'button.washing_machine_pause', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_start-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.washing_machine_start', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Start', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start', + 'unique_id': 'Dummy_Appliance_3-start', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_start-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Start', + }), + 'context': , + 'entity_id': 'button.washing_machine_start', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_stop-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': None, + 'entity_id': 'button.washing_machine_stop', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Stop', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'stop', + 'unique_id': 'Dummy_Appliance_3-stop', + 'unit_of_measurement': None, + }) +# --- +# name: test_button_states_api_push[platforms0][button.washing_machine_stop-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Stop', + }), + 'context': , + 'entity_id': 'button.washing_machine_stop', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 85f7bf212f5..5739f853d94 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -125,3 +125,129 @@ 'state': 'cool', }) # --- +# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': -13, + 'min_temp': -27, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.freezer', + 'has_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': 'miele', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'freezer', + 'unique_id': 'Dummy_Appliance_1-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_api_push[platforms0-freezer][climate.freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': -18, + 'friendly_name': 'Freezer', + 'hvac_modes': list([ + , + ]), + 'max_temp': -13, + 'min_temp': -27, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': -18, + }), + 'context': , + 'entity_id': 'climate.freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- +# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'target_temp_step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.refrigerator', + 'has_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': 'miele', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'refrigerator', + 'unique_id': 'Dummy_Appliance_2-thermostat-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_climate_states_api_push[platforms0-freezer][climate.refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 4, + 'friendly_name': 'Refrigerator', + 'hvac_modes': list([ + , + ]), + 'max_temp': 9, + 'min_temp': 1, + 'supported_features': , + 'target_temp_step': 1.0, + 'temperature': 4, + }), + 'context': , + 'entity_id': 'climate.refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cool', + }) +# --- diff --git a/tests/components/miele/snapshots/test_diagnostics.ambr b/tests/components/miele/snapshots/test_diagnostics.ambr index 92e312f8d73..8fa40755888 100644 --- a/tests/components/miele/snapshots/test_diagnostics.ambr +++ b/tests/components/miele/snapshots/test_diagnostics.ambr @@ -37,7 +37,7 @@ ]), 'targetTemperature': list([ dict({ - 'max': -14, + 'max': 28, 'min': -28, 'zone': 1, }), @@ -70,7 +70,7 @@ ]), 'targetTemperature': list([ dict({ - 'max': -14, + 'max': 28, 'min': -28, 'zone': 1, }), @@ -103,7 +103,7 @@ ]), 'targetTemperature': list([ dict({ - 'max': -14, + 'max': 28, 'min': -28, 'zone': 1, }), @@ -136,7 +136,7 @@ ]), 'targetTemperature': list([ dict({ - 'max': -14, + 'max': 28, 'min': -28, 'zone': 1, }), @@ -710,7 +710,7 @@ ]), 'targetTemperature': list([ dict({ - 'max': -14, + 'max': 28, 'min': -28, 'zone': 1, }), diff --git a/tests/components/miele/snapshots/test_fan.ambr b/tests/components/miele/snapshots/test_fan.ambr index 595d4463462..8f30b785bc9 100644 --- a/tests/components/miele/snapshots/test_fan.ambr +++ b/tests/components/miele/snapshots/test_fan.ambr @@ -151,3 +151,57 @@ 'state': 'off', }) # --- +# name: test_fan_states_api_push[platforms0][fan.hood_fan-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': None, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.hood_fan', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fan', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'fan', + 'unique_id': 'DummyAppliance_18-fan', + 'unit_of_measurement': None, + }) +# --- +# name: test_fan_states_api_push[platforms0][fan.hood_fan-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Fan', + 'percentage': 0, + 'percentage_step': 25.0, + 'preset_mode': None, + 'preset_modes': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.hood_fan', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/miele/snapshots/test_light.ambr b/tests/components/miele/snapshots/test_light.ambr index 128b642d7a0..9cfc228873f 100644 --- a/tests/components/miele/snapshots/test_light.ambr +++ b/tests/components/miele/snapshots/test_light.ambr @@ -111,3 +111,115 @@ 'state': 'on', }) # --- +# name: test_light_states_api_push[platforms0][light.hood_ambient_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hood_ambient_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Ambient light', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'ambient_light', + 'unique_id': 'DummyAppliance_18-ambient_light', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_states_api_push[platforms0][light.hood_ambient_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': None, + 'friendly_name': 'Hood Ambient light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hood_ambient_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_light_states_api_push[platforms0][light.hood_light-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.hood_light', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Light', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'light', + 'unique_id': 'DummyAppliance_18-light', + 'unit_of_measurement': None, + }) +# --- +# name: test_light_states_api_push[platforms0][light.hood_light-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'Hood Light', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.hood_light', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index aadcdb1118d..2c3c4dfd506 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -1734,3 +1734,1143 @@ 'state': '0.0', }) # --- +# name: test_sensor_states_api_push[platforms0][sensor.freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.freezer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:fridge-industrial-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'Dummy_Appliance_1-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Freezer', + 'icon': 'mdi:fridge-industrial-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.freezer_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.freezer_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': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_1-state_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.freezer_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Freezer Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.freezer_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-18.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.hood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hood', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:turbine', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'DummyAppliance_18-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.hood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Hood', + 'icon': 'mdi:turbine', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.hood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.refrigerator-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:fridge-industrial-outline', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'Dummy_Appliance_2-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.refrigerator-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Refrigerator', + 'icon': 'mdi:fridge-industrial-outline', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.refrigerator', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'in_use', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.refrigerator_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_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': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'Dummy_Appliance_2-state_temperature_1', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.refrigerator_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Refrigerator Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.refrigerator_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washing_machine', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:washing-machine', + 'original_name': None, + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'status', + 'unique_id': 'Dummy_Appliance_3-state_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washing machine', + 'icon': 'mdi:washing-machine', + 'options': list([ + 'autocleaning', + 'failure', + 'idle', + 'in_use', + 'not_connected', + 'off', + 'on', + 'pause', + 'program_ended', + 'program_interrupted', + 'programmed', + 'rinse_hold', + 'service', + 'supercooling', + 'supercooling_superfreezing', + 'superfreezing', + 'superheating', + 'waiting_to_start', + ]), + }), + 'context': , + 'entity_id': 'sensor.washing_machine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_elapsed_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_elapsed_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Elapsed time', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'elapsed_time', + 'unique_id': 'Dummy_Appliance_3-state_elapsed_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_elapsed_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Washing machine Elapsed time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.washing_machine_elapsed_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_energy_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy consumption', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_consumption', + 'unique_id': 'Dummy_Appliance_3-current_energy_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Washing machine Energy consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.washing_machine_energy_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_forecast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_energy_forecast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Energy forecast', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_forecast', + 'unique_id': 'Dummy_Appliance_3-energy_forecast', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_forecast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Energy forecast', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.washing_machine_energy_forecast', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'automatic_plus', + 'clean_machine', + 'cool_air', + 'cottons', + 'cottons_eco', + 'cottons_hygiene', + 'curtains', + 'dark_garments', + 'delicates', + 'denim', + 'down_duvets', + 'down_filled_items', + 'drain_spin', + 'eco_40_60', + 'express_20', + 'first_wash', + 'freshen_up', + 'minimum_iron', + 'no_program', + 'outerwear', + 'pillows', + 'proofing', + 'quick_power_wash', + 'rinse', + 'rinse_out_lint', + 'separate_rinse_starch', + 'shirts', + 'silks', + 'sportswear', + 'starch', + 'steam_care', + 'trainers', + 'warm_air', + 'woollens', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washing_machine_program', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'program_id', + 'unique_id': 'Dummy_Appliance_3-state_program_id', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washing machine Program', + 'options': list([ + 'automatic_plus', + 'clean_machine', + 'cool_air', + 'cottons', + 'cottons_eco', + 'cottons_hygiene', + 'curtains', + 'dark_garments', + 'delicates', + 'denim', + 'down_duvets', + 'down_filled_items', + 'drain_spin', + 'eco_40_60', + 'express_20', + 'first_wash', + 'freshen_up', + 'minimum_iron', + 'no_program', + 'outerwear', + 'pillows', + 'proofing', + 'quick_power_wash', + 'rinse', + 'rinse_out_lint', + 'separate_rinse_starch', + 'shirts', + 'silks', + 'sportswear', + 'starch', + 'steam_care', + 'trainers', + 'warm_air', + 'woollens', + ]), + }), + 'context': , + 'entity_id': 'sensor.washing_machine_program', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_program', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program_phase-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'anti_crease', + 'cleaning', + 'cooling_down', + 'disinfecting', + 'drain', + 'drying', + 'finished', + 'freshen_up_and_moisten', + 'hygiene', + 'main_wash', + 'not_running', + 'pre_wash', + 'rinse', + 'rinse_hold', + 'soak', + 'spin', + 'starch_stop', + 'steam_smoothing', + 'venting', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.washing_machine_program_phase', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program phase', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'program_phase', + 'unique_id': 'Dummy_Appliance_3-state_program_phase', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program_phase-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washing machine Program phase', + 'options': list([ + 'anti_crease', + 'cleaning', + 'cooling_down', + 'disinfecting', + 'drain', + 'drying', + 'finished', + 'freshen_up_and_moisten', + 'hygiene', + 'main_wash', + 'not_running', + 'pre_wash', + 'rinse', + 'rinse_hold', + 'soak', + 'spin', + 'starch_stop', + 'steam_smoothing', + 'venting', + ]), + }), + 'context': , + 'entity_id': 'sensor.washing_machine_program_phase', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'not_running', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program_type-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'automatic_program', + 'cleaning_care_program', + 'maintenance_program', + 'normal_operation_mode', + 'own_program', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_program_type', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Program type', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'program_type', + 'unique_id': 'Dummy_Appliance_3-state_program_type', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_program_type-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Washing machine Program type', + 'options': list([ + 'automatic_program', + 'cleaning_care_program', + 'maintenance_program', + 'normal_operation_mode', + 'own_program', + ]), + }), + 'context': , + 'entity_id': 'sensor.washing_machine_program_type', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'normal_operation_mode', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_remaining_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_remaining_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Remaining time', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'remaining_time', + 'unique_id': 'Dummy_Appliance_3-state_remaining_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_remaining_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Washing machine Remaining time', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.washing_machine_remaining_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_spin_speed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_spin_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': 'Spin speed', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'spin_speed', + 'unique_id': 'Dummy_Appliance_3-state_spinning_speed', + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_spin_speed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Spin speed', + 'unit_of_measurement': 'rpm', + }), + 'context': , + 'entity_id': 'sensor.washing_machine_spin_speed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_start_in-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_start_in', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Start in', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'start_time', + 'unique_id': 'Dummy_Appliance_3-state_start_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_start_in-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Washing machine Start in', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.washing_machine_start_in', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_water_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water consumption', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_consumption', + 'unique_id': 'Dummy_Appliance_3-current_water_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Washing machine Water consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.washing_machine_water_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_forecast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.washing_machine_water_forecast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Water forecast', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_forecast', + 'unique_id': 'Dummy_Appliance_3-water_forecast', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_water_forecast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Water forecast', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.washing_machine_water_forecast', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- diff --git a/tests/components/miele/snapshots/test_switch.ambr b/tests/components/miele/snapshots/test_switch.ambr index b7f49f84eed..24166e379e7 100644 --- a/tests/components/miele/snapshots/test_switch.ambr +++ b/tests/components/miele/snapshots/test_switch.ambr @@ -187,3 +187,191 @@ 'state': 'off', }) # --- +# name: test_switch_states_api_push[platforms0][switch.freezer_superfreezing-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.freezer_superfreezing', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Superfreezing', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'superfreezing', + 'unique_id': 'Dummy_Appliance_1-superfreezing', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.freezer_superfreezing-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Freezer Superfreezing', + }), + 'context': , + 'entity_id': 'switch.freezer_superfreezing', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.hood_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.hood_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'DummyAppliance_18-poweronoff', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.hood_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hood Power', + }), + 'context': , + 'entity_id': 'switch.hood_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.refrigerator_supercooling-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.refrigerator_supercooling', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Supercooling', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'supercooling', + 'unique_id': 'Dummy_Appliance_2-supercooling', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.refrigerator_supercooling-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Supercooling', + }), + 'context': , + 'entity_id': 'switch.refrigerator_supercooling', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.washing_machine_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.washing_machine_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'miele', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'Dummy_Appliance_3-poweronoff', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_states_api_push[platforms0][switch.washing_machine_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Washing machine Power', + }), + 'context': , + 'entity_id': 'switch.washing_machine_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/miele/snapshots/test_vacuum.ambr b/tests/components/miele/snapshots/test_vacuum.ambr index 8147b56282d..c99a6f9b39f 100644 --- a/tests/components/miele/snapshots/test_vacuum.ambr +++ b/tests/components/miele/snapshots/test_vacuum.ambr @@ -61,3 +61,65 @@ 'state': 'cleaning', }) # --- +# name: test_vacuum_states_api_push[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'fan_speed_list': list([ + 'normal', + 'turbo', + 'silent', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'vacuum', + 'entity_category': None, + 'entity_id': 'vacuum.robot_vacuum_cleaner', + 'has_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': 'miele', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'vacuum', + 'unique_id': 'Dummy_Vacuum_1-vacuum', + 'unit_of_measurement': None, + }) +# --- +# name: test_vacuum_states_api_push[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'battery_icon': 'mdi:battery-60', + 'battery_level': 65, + 'fan_speed': 'normal', + 'fan_speed_list': list([ + 'normal', + 'turbo', + 'silent', + ]), + 'friendly_name': 'Robot vacuum cleaner', + 'supported_features': , + }), + 'context': , + 'entity_id': 'vacuum.robot_vacuum_cleaner', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cleaning', + }) +# --- diff --git a/tests/components/miele/test_binary_sensor.py b/tests/components/miele/test_binary_sensor.py index db44ea554a4..02cdd7eafe1 100644 --- a/tests/components/miele/test_binary_sensor.py +++ b/tests/components/miele/test_binary_sensor.py @@ -24,3 +24,18 @@ async def test_binary_sensor_states( """Test binary sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("platforms", [(BINARY_SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_binary_sensor_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test binary sensor state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/miele/test_button.py b/tests/components/miele/test_button.py index d3cfb2af999..e4841707a18 100644 --- a/tests/components/miele/test_button.py +++ b/tests/components/miele/test_button.py @@ -33,6 +33,20 @@ async def test_button_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_button_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test binary sensor state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_button_press( hass: HomeAssistant, @@ -58,7 +72,9 @@ async def test_api_failure( """Test handling of exception from API.""" mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" + ): await hass.services.async_call( TEST_PLATFORM, SERVICE_PRESS, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True ) diff --git a/tests/components/miele/test_climate.py b/tests/components/miele/test_climate.py index bff55311f4b..c4966430a9d 100644 --- a/tests/components/miele/test_climate.py +++ b/tests/components/miele/test_climate.py @@ -42,6 +42,20 @@ async def test_climate_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_climate_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test climate state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + async def test_set_target( hass: HomeAssistant, mock_miele_client: MagicMock, @@ -68,7 +82,9 @@ async def test_api_failure( """Test handling of exception from API.""" mock_miele_client.set_target_temperature.side_effect = ClientError - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" + ): await hass.services.async_call( TEST_PLATFORM, SERVICE_SET_TEMPERATURE, diff --git a/tests/components/miele/test_fan.py b/tests/components/miele/test_fan.py index 47c7c4fb8ec..557458e08dc 100644 --- a/tests/components/miele/test_fan.py +++ b/tests/components/miele/test_fan.py @@ -7,7 +7,11 @@ from aiohttp import ClientResponseError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.fan import ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, +) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -34,6 +38,20 @@ async def test_fan_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_fan_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test fan state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.parametrize("load_device_file", ["fan_devices.json"]) @pytest.mark.parametrize( ("service", "expected_argument"), @@ -78,7 +96,7 @@ async def test_fan_set_speed( percentage: int, expected_argument: dict[str, Any], ) -> None: - """Test the fan can be turned on/off.""" + """Test the fan can set percentage.""" await hass.services.async_call( TEST_PLATFORM, @@ -91,6 +109,24 @@ async def test_fan_set_speed( ) +async def test_fan_turn_on_w_percentage( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, +) -> None: + """Test the fan can turn on with percentage.""" + + await hass.services.async_call( + TEST_PLATFORM, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50}, + blocking=True, + ) + mock_miele_client.send_action.assert_called_with( + "DummyAppliance_18", {"ventilationStep": 2} + ) + + @pytest.mark.parametrize( ("service"), [ @@ -112,3 +148,23 @@ async def test_api_failure( TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True ) mock_miele_client.send_action.assert_called_once() + + +async def test_set_percentage( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, +) -> None: + """Test handling of exception at set_percentage.""" + mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") + + with pytest.raises( + HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" + ): + await hass.services.async_call( + TEST_PLATFORM, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50}, + blocking=True, + ) + mock_miele_client.send_action.assert_called_once() diff --git a/tests/components/miele/test_light.py b/tests/components/miele/test_light.py index c0cae688c1c..85f1fcd8d04 100644 --- a/tests/components/miele/test_light.py +++ b/tests/components/miele/test_light.py @@ -32,6 +32,20 @@ async def test_light_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_light_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test light state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.parametrize( ("service", "light_state"), [ @@ -72,7 +86,9 @@ async def test_api_failure( """Test handling of exception from API.""" mock_miele_client.send_action.side_effect = ClientError - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" + ): await hass.services.async_call( TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True ) diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index 7beb2fec8f1..47e101c6636 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -21,7 +21,22 @@ async def test_sensor_states( entity_registry: er.EntityRegistry, setup_platform: MockConfigEntry, ) -> None: - """Test sensor state.""" + """Test sensor state after polling the API for data.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test sensor state when the API pushes data via SSE.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) diff --git a/tests/components/miele/test_switch.py b/tests/components/miele/test_switch.py index d60708c24e1..7115432cfba 100644 --- a/tests/components/miele/test_switch.py +++ b/tests/components/miele/test_switch.py @@ -32,6 +32,20 @@ async def test_switch_states( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_switch_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + push_data_and_actions: None, +) -> None: + """Test switch state when the API pushes data via SSE.""" + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.parametrize( ("entity"), [ @@ -87,7 +101,7 @@ async def test_api_failure( """Test handling of exception from API.""" mock_miele_client.send_action.side_effect = ClientError - with pytest.raises(HomeAssistantError): + with pytest.raises(HomeAssistantError, match=f"Failed to set state for {entity}"): await hass.services.async_call( TEST_PLATFORM, service, {ATTR_ENTITY_ID: entity}, blocking=True ) diff --git a/tests/components/miele/test_vacuum.py b/tests/components/miele/test_vacuum.py index f1f0ae22930..6dc5b45f187 100644 --- a/tests/components/miele/test_vacuum.py +++ b/tests/components/miele/test_vacuum.py @@ -3,10 +3,11 @@ from unittest.mock import MagicMock from aiohttp import ClientResponseError +from pymiele import MieleDevices import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.miele.const import PROCESS_ACTION, PROGRAM_ID +from homeassistant.components.miele.const import DOMAIN, PROCESS_ACTION, PROGRAM_ID from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, DOMAIN as VACUUM_DOMAIN, @@ -21,7 +22,9 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry, snapshot_platform +from . import get_actions_callback, get_data_callback + +from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform TEST_PLATFORM = VACUUM_DOMAIN ENTITY_ID = "vacuum.robot_vacuum_cleaner" @@ -46,6 +49,29 @@ async def test_sensor_states( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_vacuum_states_api_push( + hass: HomeAssistant, + mock_miele_client: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, + device_fixture: MieleDevices, +) -> None: + """Test vacuum state when the API pushes data via SSE.""" + + data_callback = get_data_callback(mock_miele_client) + await data_callback(device_fixture) + await hass.async_block_till_done() + + act_file = load_json_object_fixture("action_push_vacuum.json", DOMAIN) + action_callback = get_actions_callback(mock_miele_client) + await action_callback(act_file) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + @pytest.mark.parametrize( ("service", "action_command", "vacuum_power"), [ @@ -112,7 +138,9 @@ async def test_api_failure( """Test handling of exception from API.""" mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test") - with pytest.raises(HomeAssistantError): + with pytest.raises( + HomeAssistantError, match=f"Failed to set state for {ENTITY_ID}" + ): await hass.services.async_call( TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True ) From 49f91666469ee0ae5d058f4d557ce4f4587e4891 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 26 May 2025 16:48:41 +0200 Subject: [PATCH 0916/1175] Deprecate cups integration (#145615) --- CODEOWNERS | 1 + homeassistant/components/cups/__init__.py | 3 ++ homeassistant/components/cups/sensor.py | 21 +++++++++- .../components/homeassistant/strings.json | 4 ++ requirements_test_all.txt | 3 ++ tests/components/cups/__init__.py | 1 + tests/components/cups/test_sensor.py | 40 +++++++++++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/components/cups/__init__.py create mode 100644 tests/components/cups/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 45070195112..3f3ce07ce84 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -305,6 +305,7 @@ build.json @home-assistant/supervisor /homeassistant/components/crownstone/ @Crownstone @RicArch97 /tests/components/crownstone/ @Crownstone @RicArch97 /homeassistant/components/cups/ @fabaff +/tests/components/cups/ @fabaff /homeassistant/components/daikin/ @fredrike /tests/components/daikin/ @fredrike /homeassistant/components/date/ @home-assistant/core diff --git a/homeassistant/components/cups/__init__.py b/homeassistant/components/cups/__init__.py index 7cd5ce4ca0a..92679aec079 100644 --- a/homeassistant/components/cups/__init__.py +++ b/homeassistant/components/cups/__init__.py @@ -1 +1,4 @@ """The cups component.""" + +DOMAIN = "cups" +CONF_PRINTERS = "printers" diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 701bad3f104..671c8c87a8c 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -14,12 +14,15 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import CONF_PRINTERS, DOMAIN + _LOGGER = logging.getLogger(__name__) ATTR_MARKER_TYPE = "marker_type" @@ -36,7 +39,6 @@ ATTR_PRINTER_STATE_REASON = "printer_state_reason" ATTR_PRINTER_TYPE = "printer_type" ATTR_PRINTER_URI_SUPPORTED = "printer_uri_supported" -CONF_PRINTERS = "printers" CONF_IS_CUPS_SERVER = "is_cups_server" DEFAULT_HOST = "127.0.0.1" @@ -72,6 +74,21 @@ def setup_platform( printers: list[str] = config[CONF_PRINTERS] is_cups: bool = config[CONF_IS_CUPS_SERVER] + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "CUPS", + }, + ) + if is_cups: data = CupsData(host, port, None) data.update() diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index b8b5f77cf52..0987461b4dc 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -18,6 +18,10 @@ "title": "The {integration_title} YAML configuration is being removed", "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." }, + "deprecated_system_packages_yaml_integration": { + "title": "The {integration_title} integration is being removed", + "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, "historic_currency": { "title": "The configured currency is no longer in use", "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration." diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecd2a1d2b31..2b156e3ca2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1560,6 +1560,9 @@ pycountry==24.6.1 # homeassistant.components.microsoft pycsspeechtts==1.0.8 +# homeassistant.components.cups +# pycups==2.0.4 + # homeassistant.components.daikin pydaikin==2.15.0 diff --git a/tests/components/cups/__init__.py b/tests/components/cups/__init__.py new file mode 100644 index 00000000000..c96e2d7c7dc --- /dev/null +++ b/tests/components/cups/__init__.py @@ -0,0 +1 @@ +"""CUPS tests.""" diff --git a/tests/components/cups/test_sensor.py b/tests/components/cups/test_sensor.py new file mode 100644 index 00000000000..60e7ce5fd44 --- /dev/null +++ b/tests/components/cups/test_sensor.py @@ -0,0 +1,40 @@ +"""Tests for the CUPS sensor platform.""" + +from unittest.mock import patch + +from homeassistant.components.cups import CONF_PRINTERS, DOMAIN as CUPS_DOMAIN +from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + with patch( + "homeassistant.components.cups.sensor.CupsData", autospec=True + ) as cups_data: + cups_data.available = True + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: [ + { + CONF_PLATFORM: CUPS_DOMAIN, + CONF_PRINTERS: [ + "printer1", + ], + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{CUPS_DOMAIN}", + ) in issue_registry.issues From acbfe54c7b0d94aa290928870eded5d1452b4590 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 16:49:42 +0200 Subject: [PATCH 0917/1175] Drop obsolete IGNORE_PIN in gen_requirements_all.py (#145487) Drop IGNORE_PIN in gen_requirements_all.py --- 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 87f7edaa892..082062c53a0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -94,8 +94,6 @@ OVERRIDDEN_REQUIREMENTS_ACTIONS = { }, } -IGNORE_PIN = ("colorlog>2.1,<3", "urllib3") - URL_PIN = ( "https://developers.home-assistant.io/docs/" "creating_platform_code_review.html#1-requirements" @@ -425,7 +423,7 @@ def process_requirements( for req in module_requirements: if "://" in req: errors.append(f"{package}[Only pypi dependencies are allowed: {req}]") - if req.partition("==")[1] == "" and req not in IGNORE_PIN: + if req.partition("==")[1] == "": errors.append(f"{package}[Please pin requirement {req}, see {URL_PIN}]") reqs.setdefault(req, []).append(package) From ca50fca7382d58c5ea9a59bf0b1713b74977391e Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Mon, 26 May 2025 15:56:15 +0100 Subject: [PATCH 0918/1175] Add twice-daily forecasts to MetOffice (#145472) --- .../components/metoffice/__init__.py | 16 + homeassistant/components/metoffice/const.py | 26 ++ homeassistant/components/metoffice/weather.py | 38 +- .../metoffice/snapshots/test_weather.ambr | 378 +++++++++++++++++- tests/components/metoffice/test_weather.py | 4 +- 5 files changed, 458 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 6977974c2e5..913d87fe3d7 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -29,6 +29,7 @@ from .const import ( METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, + METOFFICE_TWICE_DAILY_COORDINATOR, ) from .helpers import fetch_data @@ -59,6 +60,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: fetch_data, connection, latitude, longitude, "daily" ) + async def async_update_twice_daily() -> datapoint.Forecast: + return await hass.async_add_executor_job( + fetch_data, connection, latitude, longitude, "twice-daily" + ) + metoffice_hourly_coordinator = TimestampDataUpdateCoordinator( hass, _LOGGER, @@ -77,10 +83,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=DEFAULT_SCAN_INTERVAL, ) + metoffice_twice_daily_coordinator = TimestampDataUpdateCoordinator( + hass, + _LOGGER, + config_entry=entry, + name=f"MetOffice Twice Daily Coordinator for {site_name}", + update_method=async_update_twice_daily, + update_interval=DEFAULT_SCAN_INTERVAL, + ) + metoffice_hass_data = hass.data.setdefault(DOMAIN, {}) metoffice_hass_data[entry.entry_id] = { METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator, METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator, + METOFFICE_TWICE_DAILY_COORDINATOR: metoffice_twice_daily_coordinator, METOFFICE_NAME: site_name, METOFFICE_COORDINATES: coordinates, } diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 68c94f3d7a5..e5ba50f2a90 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -41,6 +41,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=15) METOFFICE_COORDINATES = "metoffice_coordinates" METOFFICE_HOURLY_COORDINATOR = "metoffice_hourly_coordinator" METOFFICE_DAILY_COORDINATOR = "metoffice_daily_coordinator" +METOFFICE_TWICE_DAILY_COORDINATOR = "metoffice_twice_daily_coordinator" METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_NAME = "metoffice_name" @@ -92,3 +93,28 @@ DAILY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = { ATTR_FORECAST_NATIVE_WIND_SPEED: "midday10MWindSpeed", ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust", } + +DAY_FORECAST_ATTRIBUTE_MAP: dict[str, str] = { + ATTR_FORECAST_CONDITION: "daySignificantWeatherCode", + ATTR_FORECAST_NATIVE_APPARENT_TEMP: "dayMaxFeelsLikeTemp", + ATTR_FORECAST_NATIVE_PRESSURE: "middayMslp", + ATTR_FORECAST_NATIVE_TEMP: "dayUpperBoundMaxTemp", + ATTR_FORECAST_NATIVE_TEMP_LOW: "dayLowerBoundMaxTemp", + ATTR_FORECAST_PRECIPITATION_PROBABILITY: "dayProbabilityOfPrecipitation", + ATTR_FORECAST_UV_INDEX: "maxUvIndex", + ATTR_FORECAST_WIND_BEARING: "midday10MWindDirection", + ATTR_FORECAST_NATIVE_WIND_SPEED: "midday10MWindSpeed", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midday10MWindGust", +} + +NIGHT_FORECAST_ATTRIBUTE_MAP: dict[str, str] = { + ATTR_FORECAST_CONDITION: "nightSignificantWeatherCode", + ATTR_FORECAST_NATIVE_APPARENT_TEMP: "nightMinFeelsLikeTemp", + ATTR_FORECAST_NATIVE_PRESSURE: "midnightMslp", + ATTR_FORECAST_NATIVE_TEMP: "nightUpperBoundMinTemp", + ATTR_FORECAST_NATIVE_TEMP_LOW: "nightLowerBoundMinTemp", + ATTR_FORECAST_PRECIPITATION_PROBABILITY: "nightProbabilityOfPrecipitation", + ATTR_FORECAST_WIND_BEARING: "midnight10MWindDirection", + ATTR_FORECAST_NATIVE_WIND_SPEED: "midnight10MWindSpeed", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "midnight10MWindGust", +} diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 3496e88c046..90fbc36f8fb 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -9,6 +9,7 @@ from datapoint.Forecast import Forecast as ForecastData from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_IS_DAYTIME, ATTR_FORECAST_NATIVE_APPARENT_TEMP, ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, @@ -41,12 +42,15 @@ from .const import ( ATTRIBUTION, CONDITION_MAP, DAILY_FORECAST_ATTRIBUTE_MAP, + DAY_FORECAST_ATTRIBUTE_MAP, DOMAIN, HOURLY_FORECAST_ATTRIBUTE_MAP, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, + METOFFICE_TWICE_DAILY_COORDINATOR, + NIGHT_FORECAST_ATTRIBUTE_MAP, ) from .helpers import get_attribute @@ -73,6 +77,7 @@ async def async_setup_entry( MetOfficeWeather( hass_data[METOFFICE_DAILY_COORDINATOR], hass_data[METOFFICE_HOURLY_COORDINATOR], + hass_data[METOFFICE_TWICE_DAILY_COORDINATOR], hass_data, ) ], @@ -92,6 +97,19 @@ def _build_daily_forecast_data(timestep: dict[str, Any]) -> Forecast: return data +def _build_twice_daily_forecast_data(timestep: dict[str, Any]) -> Forecast: + data = Forecast(datetime=timestep["time"].isoformat()) + + # day and night forecasts have slightly different format + if "daySignificantWeatherCode" in timestep: + data[ATTR_FORECAST_IS_DAYTIME] = True + _populate_forecast_data(data, timestep, DAY_FORECAST_ATTRIBUTE_MAP) + else: + data[ATTR_FORECAST_IS_DAYTIME] = False + _populate_forecast_data(data, timestep, NIGHT_FORECAST_ATTRIBUTE_MAP) + return data + + def _populate_forecast_data( forecast: Forecast, timestep: dict[str, Any], mapping: dict[str, str] ) -> None: @@ -152,13 +170,16 @@ class MetOfficeWeather( _attr_native_visibility_unit = UnitOfLength.METERS _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND _attr_supported_features = ( - WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY + WeatherEntityFeature.FORECAST_HOURLY + | WeatherEntityFeature.FORECAST_TWICE_DAILY + | WeatherEntityFeature.FORECAST_DAILY ) def __init__( self, coordinator_daily: TimestampDataUpdateCoordinator[ForecastData], coordinator_hourly: TimestampDataUpdateCoordinator[ForecastData], + coordinator_twice_daily: TimestampDataUpdateCoordinator[ForecastData], hass_data: dict[str, Any], ) -> None: """Initialise the platform with a data instance.""" @@ -167,6 +188,7 @@ class MetOfficeWeather( observation_coordinator, daily_coordinator=coordinator_daily, hourly_coordinator=coordinator_hourly, + twice_daily_coordinator=coordinator_twice_daily, ) self._attr_device_info = get_device_info( @@ -268,3 +290,17 @@ class MetOfficeWeather( for timestep in timesteps if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo) ] + + @callback + def _async_forecast_twice_daily(self) -> list[Forecast] | None: + """Return the twice daily forecast in native units.""" + coordinator = cast( + TimestampDataUpdateCoordinator[ForecastData], + self.forecast_coordinators["twice_daily"], + ) + timesteps = coordinator.data.timesteps + return [ + _build_twice_daily_forecast_data(timestep) + for timestep in timesteps + if timestep["time"] > datetime.now(tz=timesteps[0]["time"].tzinfo) + ] diff --git a/tests/components/metoffice/snapshots/test_weather.ambr b/tests/components/metoffice/snapshots/test_weather.ambr index a567f9bde74..74b54d1bc2f 100644 --- a/tests/components/metoffice/snapshots/test_weather.ambr +++ b/tests/components/metoffice/snapshots/test_weather.ambr @@ -724,6 +724,194 @@ }) # --- # name: test_forecast_service[get_forecasts].2 + dict({ + 'weather.met_office_wavertree': dict({ + 'forecast': list([ + dict({ + 'apparent_temperature': 4.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.7, + 'templow': 7.0, + 'uv_index': None, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 26, + 'pressure': 987.48, + 'temperature': 15.2, + 'templow': 11.9, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 4.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1004.81, + 'temperature': 9.3, + 'templow': 4.4, + 'uv_index': None, + 'wind_bearing': 262, + 'wind_gust_speed': 47.99, + 'wind_speed': 29.23, + }), + dict({ + 'apparent_temperature': 5.3, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 5, + 'pressure': 994.88, + 'temperature': 11.0, + 'templow': 8.4, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, + }), + dict({ + 'apparent_temperature': 1.3, + 'condition': 'cloudy', + 'datetime': '2024-11-26T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 44, + 'pressure': 1013.9, + 'temperature': 7.5, + 'templow': -0.4, + 'uv_index': None, + 'wind_bearing': 74, + 'wind_gust_speed': 19.51, + 'wind_speed': 11.41, + }), + dict({ + 'apparent_temperature': 5.9, + 'condition': 'partlycloudy', + 'datetime': '2024-11-26T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1012.93, + 'temperature': 10.1, + 'templow': 6.5, + 'uv_index': 1, + 'wind_bearing': 265, + 'wind_gust_speed': 34.49, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 0.2, + 'condition': 'clear-night', + 'datetime': '2024-11-27T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1021.75, + 'temperature': 7.2, + 'templow': -3.0, + 'uv_index': None, + 'wind_bearing': 31, + 'wind_gust_speed': 19.94, + 'wind_speed': 11.84, + }), + dict({ + 'apparent_temperature': 3.3, + 'condition': 'rainy', + 'datetime': '2024-11-27T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 43, + 'pressure': 1014.39, + 'temperature': 11.1, + 'templow': 3.0, + 'uv_index': 1, + 'wind_bearing': 8, + 'wind_gust_speed': 32.18, + 'wind_speed': 18.54, + }), + dict({ + 'apparent_temperature': 1.6, + 'condition': 'cloudy', + 'datetime': '2024-11-28T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1023.82, + 'temperature': 8.2, + 'templow': -1.9, + 'uv_index': None, + 'wind_bearing': 131, + 'wind_gust_speed': 33.16, + 'wind_speed': 20.05, + }), + dict({ + 'apparent_temperature': 3.0, + 'condition': 'cloudy', + 'datetime': '2024-11-28T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1025.12, + 'temperature': 9.4, + 'templow': 1.3, + 'uv_index': 1, + 'wind_bearing': 104, + 'wind_gust_speed': 22.36, + 'wind_speed': 12.64, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'cloudy', + 'datetime': '2024-11-29T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 13, + 'pressure': 1016.88, + 'temperature': 10.8, + 'templow': -1.9, + 'uv_index': None, + 'wind_bearing': 151, + 'wind_gust_speed': 33.16, + 'wind_speed': 20.12, + }), + dict({ + 'apparent_temperature': 4.9, + 'condition': 'cloudy', + 'datetime': '2024-11-29T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 11, + 'pressure': 1019.85, + 'temperature': 12.6, + 'templow': 4.2, + 'uv_index': 1, + 'wind_bearing': 137, + 'wind_gust_speed': 38.59, + 'wind_speed': 23.0, + }), + ]), + }), + }) +# --- +# name: test_forecast_service[get_forecasts].3 dict({ 'weather.met_office_wavertree': dict({ 'forecast': list([ @@ -815,7 +1003,7 @@ }), }) # --- -# name: test_forecast_service[get_forecasts].3 +# name: test_forecast_service[get_forecasts].4 dict({ 'weather.met_office_wavertree': dict({ 'forecast': list([ @@ -1447,6 +1635,194 @@ }), }) # --- +# name: test_forecast_service[get_forecasts].5 + dict({ + 'weather.met_office_wavertree': dict({ + 'forecast': list([ + dict({ + 'apparent_temperature': 4.8, + 'condition': 'cloudy', + 'datetime': '2024-11-24T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 23, + 'pressure': 987.12, + 'temperature': 10.7, + 'templow': 7.0, + 'uv_index': None, + 'wind_bearing': 211, + 'wind_gust_speed': 47.2, + 'wind_speed': 26.39, + }), + dict({ + 'apparent_temperature': 9.2, + 'condition': 'cloudy', + 'datetime': '2024-11-24T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 26, + 'pressure': 987.48, + 'temperature': 15.2, + 'templow': 11.9, + 'uv_index': 1, + 'wind_bearing': 203, + 'wind_gust_speed': 42.66, + 'wind_speed': 23.94, + }), + dict({ + 'apparent_temperature': 4.2, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1004.81, + 'temperature': 9.3, + 'templow': 4.4, + 'uv_index': None, + 'wind_bearing': 262, + 'wind_gust_speed': 47.99, + 'wind_speed': 29.23, + }), + dict({ + 'apparent_temperature': 5.3, + 'condition': 'partlycloudy', + 'datetime': '2024-11-25T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 5, + 'pressure': 994.88, + 'temperature': 11.0, + 'templow': 8.4, + 'uv_index': 1, + 'wind_bearing': 251, + 'wind_gust_speed': 52.16, + 'wind_speed': 30.67, + }), + dict({ + 'apparent_temperature': 1.3, + 'condition': 'cloudy', + 'datetime': '2024-11-26T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 44, + 'pressure': 1013.9, + 'temperature': 7.5, + 'templow': -0.4, + 'uv_index': None, + 'wind_bearing': 74, + 'wind_gust_speed': 19.51, + 'wind_speed': 11.41, + }), + dict({ + 'apparent_temperature': 5.9, + 'condition': 'partlycloudy', + 'datetime': '2024-11-26T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 6, + 'pressure': 1012.93, + 'temperature': 10.1, + 'templow': 6.5, + 'uv_index': 1, + 'wind_bearing': 265, + 'wind_gust_speed': 34.49, + 'wind_speed': 20.45, + }), + dict({ + 'apparent_temperature': 0.2, + 'condition': 'clear-night', + 'datetime': '2024-11-27T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1021.75, + 'temperature': 7.2, + 'templow': -3.0, + 'uv_index': None, + 'wind_bearing': 31, + 'wind_gust_speed': 19.94, + 'wind_speed': 11.84, + }), + dict({ + 'apparent_temperature': 3.3, + 'condition': 'rainy', + 'datetime': '2024-11-27T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 43, + 'pressure': 1014.39, + 'temperature': 11.1, + 'templow': 3.0, + 'uv_index': 1, + 'wind_bearing': 8, + 'wind_gust_speed': 32.18, + 'wind_speed': 18.54, + }), + dict({ + 'apparent_temperature': 1.6, + 'condition': 'cloudy', + 'datetime': '2024-11-28T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1023.82, + 'temperature': 8.2, + 'templow': -1.9, + 'uv_index': None, + 'wind_bearing': 131, + 'wind_gust_speed': 33.16, + 'wind_speed': 20.05, + }), + dict({ + 'apparent_temperature': 3.0, + 'condition': 'cloudy', + 'datetime': '2024-11-28T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 9, + 'pressure': 1025.12, + 'temperature': 9.4, + 'templow': 1.3, + 'uv_index': 1, + 'wind_bearing': 104, + 'wind_gust_speed': 22.36, + 'wind_speed': 12.64, + }), + dict({ + 'apparent_temperature': 5.0, + 'condition': 'cloudy', + 'datetime': '2024-11-29T00:00:00+00:00', + 'is_daytime': False, + 'precipitation': None, + 'precipitation_probability': 13, + 'pressure': 1016.88, + 'temperature': 10.8, + 'templow': -1.9, + 'uv_index': None, + 'wind_bearing': 151, + 'wind_gust_speed': 33.16, + 'wind_speed': 20.12, + }), + dict({ + 'apparent_temperature': 4.9, + 'condition': 'cloudy', + 'datetime': '2024-11-29T12:00:00+00:00', + 'is_daytime': True, + 'precipitation': None, + 'precipitation_probability': 11, + 'pressure': 1019.85, + 'temperature': 12.6, + 'templow': 4.2, + 'uv_index': 1, + 'wind_bearing': 137, + 'wind_gust_speed': 38.59, + 'wind_speed': 23.0, + }), + ]), + }), + }) +# --- # name: test_forecast_subscription list([ dict({ diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index f248ead3173..48e7626a97f 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -301,7 +301,7 @@ async def test_forecast_service( assert wavertree_data["wavertree_daily_mock"].call_count == 1 assert wavertree_data["wavertree_hourly_mock"].call_count == 1 - for forecast_type in ("daily", "hourly"): + for forecast_type in ("daily", "hourly", "twice_daily"): response = await hass.services.async_call( WEATHER_DOMAIN, service, @@ -319,7 +319,7 @@ async def test_forecast_service( async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - for forecast_type in ("daily", "hourly"): + for forecast_type in ("daily", "hourly", "twice_daily"): response = await hass.services.async_call( WEATHER_DOMAIN, service, From 13d7234f977e60c86e8543a15a710651355509d7 Mon Sep 17 00:00:00 2001 From: David Poll Date: Mon, 26 May 2025 08:00:07 -0700 Subject: [PATCH 0919/1175] Add apply to make macros/callables useful in filters and tests (#144227) --- homeassistant/helpers/template.py | 7 ++++ tests/helpers/test_template.py | 56 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 408e88ef8b3..e3267d2933b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2019,6 +2019,11 @@ def add(value, amount, default=_SENTINEL): return default +def apply(value, fn, *args, **kwargs): + """Call the given callable with the provided arguments and keyword arguments.""" + return fn(value, *args, **kwargs) + + def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" try: @@ -3117,6 +3122,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["acos"] = arc_cosine self.filters["add"] = add + self.filters["apply"] = apply self.filters["as_datetime"] = as_datetime self.filters["as_local"] = dt_util.as_local self.filters["as_timedelta"] = as_timedelta @@ -3177,6 +3183,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["unpack"] = struct_unpack self.filters["version"] = version + self.tests["apply"] = apply self.tests["contains"] = contains self.tests["datetime"] = _is_datetime self.tests["is_number"] = is_number diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6c41b7970da..8d2f8c7cc60 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -772,6 +772,62 @@ def test_add(hass: HomeAssistant) -> None: assert render(hass, "{{ 'no_number' | add(10, default=1) }}") == 1 +def test_apply(hass: HomeAssistant) -> None: + """Test apply.""" + assert template.Template( + """ + {%- macro add_foo(arg) -%} + {{arg}}foo + {%- endmacro -%} + {{ ["a", "b", "c"] | map('apply', add_foo) | list }} + """, + hass, + ).async_render() == ["afoo", "bfoo", "cfoo"] + + assert template.Template( + """ + {{ ['1', '2', '3', '4', '5'] | map('apply', int) | list }} + """, + hass, + ).async_render() == [1, 2, 3, 4, 5] + + +def test_apply_macro_with_arguments(hass: HomeAssistant) -> None: + """Test apply macro with positional, named, and mixed arguments.""" + # Test macro with positional arguments + assert template.Template( + """ + {%- macro greet(name, greeting) -%} + {{ greeting }}, {{ name }}! + {%- endmacro %} + {{ ["Alice", "Bob"] | map('apply', greet, "Hello") | list }} + """, + hass, + ).async_render() == ["Hello, Alice!", "Hello, Bob!"] + + # Test macro with named arguments + assert template.Template( + """ + {%- macro greet(name, greeting="Hi") -%} + {{ greeting }}, {{ name }}! + {%- endmacro %} + {{ ["Alice", "Bob"] | map('apply', greet, greeting="Hello") | list }} + """, + hass, + ).async_render() == ["Hello, Alice!", "Hello, Bob!"] + + # Test macro with mixed positional and named arguments + assert template.Template( + """ + {%- macro greet(name, separator, greeting="Hi") -%} + {{ greeting }}{{separator}} {{ name }}! + {%- endmacro %} + {{ ["Alice", "Bob"] | map('apply', greet, "," , greeting="Hey") | list }} + """, + hass, + ).async_render() == ["Hey, Alice!", "Hey, Bob!"] + + def test_logarithm(hass: HomeAssistant) -> None: """Test logarithm.""" tests = [ From c2a5e1aaf92ee4a2b3c5494478a9e3f7c51dd9a2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 26 May 2025 17:07:05 +0200 Subject: [PATCH 0920/1175] Prefer source name in Music Assistant integration (#145622) Co-authored-by: Joost Lekkerkerker --- .../music_assistant/media_player.py | 28 +++++++++++++++---- .../snapshots/test_media_player.ambr | 3 +- .../music_assistant/test_media_player.py | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 91c9d5ffd90..a11e334824a 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -42,7 +42,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.const import ATTR_NAME, STATE_OFF from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import ( AddConfigEntryEntitiesCallback, @@ -227,6 +227,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): self._set_supported_features() self._attr_device_class = MediaPlayerDeviceClass.SPEAKER self._prev_time: float = 0 + self._source_list_mapping: dict[str, str] = {} async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -292,10 +293,20 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): self._attr_state = MediaPlayerState(player.state.value) else: self._attr_state = MediaPlayerState(STATE_OFF) - self._attr_source = player.active_source - self._attr_source_list = [ - source.name for source in player.source_list if not source.passive - ] + # active source and source list (translate to HA source names) + source_mappings: dict[str, str] = {} + active_source_name: str | None = None + for source in player.source_list: + if source.id == player.active_source: + active_source_name = source.name + if source.passive: + # ignore passive sources because HA does not differentiate between + # active and passive sources + continue + source_mappings[source.name] = source.id + self._attr_source_list = list(source_mappings.keys()) + self._source_list_mapping = source_mappings + self._attr_source = active_source_name group_members: list[str] = [] if player.group_childs: @@ -466,7 +477,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): @catch_musicassistant_error async def async_select_source(self, source: str) -> None: """Select input source.""" - await self.mass.players.player_command_select_source(self.player_id, source) + source_id = self._source_list_mapping.get(source) + if source_id is None: + raise ServiceValidationError( + f"Source '{source}' not found for player {self.name}" + ) + await self.mass.players.player_command_select_source(self.player_id, source_id) @catch_musicassistant_error async def _async_handle_play_media( diff --git a/tests/components/music_assistant/snapshots/test_media_player.ambr b/tests/components/music_assistant/snapshots/test_media_player.ambr index 5782156e722..e7c2eec6f4b 100644 --- a/tests/components/music_assistant/snapshots/test_media_player.ambr +++ b/tests/components/music_assistant/snapshots/test_media_player.ambr @@ -54,7 +54,7 @@ 'media_duration': 300, 'media_position': 0, 'media_title': 'Test Track', - 'source': 'spotify', + 'source': 'Spotify Connect', 'supported_features': , 'volume_level': 0.2, }), @@ -126,7 +126,6 @@ 'media_title': 'November Rain', 'repeat': 'all', 'shuffle': True, - 'source': 'test_group_player_1', 'supported_features': , 'volume_level': 0.06, }), diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index e2b45db45e4..eb1e64485c4 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -637,7 +637,7 @@ async def test_media_player_select_source_action( SERVICE_SELECT_SOURCE, { ATTR_ENTITY_ID: entity_id, - ATTR_INPUT_SOURCE: "linein", + ATTR_INPUT_SOURCE: "Line-In", }, blocking=True, ) From b7ce0f63a954c0f5062d530ab8a01816a3b559dc Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 18:17:32 +0300 Subject: [PATCH 0921/1175] Add notify platform to Amazon Devices (#145589) * Add notify platform to Amazon Devices * apply review comment * leftover * tests leftovers * remove sound notification * missing await --- .../components/amazon_devices/__init__.py | 1 + .../components/amazon_devices/notify.py | 74 ++++++++++++++ .../components/amazon_devices/strings.json | 8 ++ .../amazon_devices/snapshots/test_notify.ambr | 97 +++++++++++++++++++ .../components/amazon_devices/test_notify.py | 70 +++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 homeassistant/components/amazon_devices/notify.py create mode 100644 tests/components/amazon_devices/snapshots/test_notify.ambr create mode 100644 tests/components/amazon_devices/test_notify.py diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/amazon_devices/__init__.py index c63c8ab7664..1db41d335ef 100644 --- a/homeassistant/components/amazon_devices/__init__.py +++ b/homeassistant/components/amazon_devices/__init__.py @@ -7,6 +7,7 @@ from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.NOTIFY, Platform.SWITCH, ] diff --git a/homeassistant/components/amazon_devices/notify.py b/homeassistant/components/amazon_devices/notify.py new file mode 100644 index 00000000000..3762a7a3264 --- /dev/null +++ b/homeassistant/components/amazon_devices/notify.py @@ -0,0 +1,74 @@ +"""Support for notification entity.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Final + +from aioamazondevices.api import AmazonDevice, AmazonEchoApi + +from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import AmazonConfigEntry +from .entity import AmazonEntity + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class AmazonNotifyEntityDescription(NotifyEntityDescription): + """Amazon Devices notify entity description.""" + + method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]] + subkey: str + + +NOTIFY: Final = ( + AmazonNotifyEntityDescription( + key="speak", + translation_key="speak", + subkey="AUDIO_PLAYER", + method=lambda api, device, message: api.call_alexa_speak(device, message), + ), + AmazonNotifyEntityDescription( + key="announce", + translation_key="announce", + subkey="AUDIO_PLAYER", + method=lambda api, device, message: api.call_alexa_announcement( + device, message + ), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Amazon Devices notification entity based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + AmazonNotifyEntity(coordinator, serial_num, sensor_desc) + for sensor_desc in NOTIFY + for serial_num in coordinator.data + if sensor_desc.subkey in coordinator.data[serial_num].capabilities + ) + + +class AmazonNotifyEntity(AmazonEntity, NotifyEntity): + """Binary sensor notify platform.""" + + entity_description: AmazonNotifyEntityDescription + + async def async_send_message( + self, message: str, title: str | None = None, **kwargs: Any + ) -> None: + """Send a message.""" + + await self.entity_description.method(self.coordinator.api, self.device, message) diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json index a3219eaa449..8db249b44ed 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/amazon_devices/strings.json @@ -43,6 +43,14 @@ "name": "Bluetooth" } }, + "notify": { + "speak": { + "name": "Speak" + }, + "announce": { + "name": "Announce" + } + }, "switch": { "do_not_disturb": { "name": "Do not disturb" diff --git a/tests/components/amazon_devices/snapshots/test_notify.ambr b/tests/components/amazon_devices/snapshots/test_notify.ambr new file mode 100644 index 00000000000..47983abd269 --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_notify.ambr @@ -0,0 +1,97 @@ +# serializer version: 1 +# name: test_all_entities[notify.echo_test_announce-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'notify', + 'entity_category': None, + 'entity_id': 'notify.echo_test_announce', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Announce', + 'platform': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'announce', + 'unique_id': 'echo_test_serial_number-announce', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[notify.echo_test_announce-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Echo Test Announce', + 'supported_features': , + }), + 'context': , + 'entity_id': 'notify.echo_test_announce', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[notify.echo_test_speak-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'notify', + 'entity_category': None, + 'entity_id': 'notify.echo_test_speak', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Speak', + 'platform': 'amazon_devices', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'speak', + 'unique_id': 'echo_test_serial_number-speak', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[notify.echo_test_speak-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Echo Test Speak', + 'supported_features': , + }), + 'context': , + 'entity_id': 'notify.echo_test_speak', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/amazon_devices/test_notify.py b/tests/components/amazon_devices/test_notify.py new file mode 100644 index 00000000000..c1147af94c7 --- /dev/null +++ b/tests/components/amazon_devices/test_notify.py @@ -0,0 +1,70 @@ +"""Tests for the Amazon Devices notify platform.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.notify import ( + ATTR_MESSAGE, + DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +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 + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.NOTIFY]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "mode", + ["speak", "announce"], +) +async def test_notify_send_message( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mode: str, +) -> None: + """Test notify send message.""" + await setup_integration(hass, mock_config_entry) + + entity_id = f"notify.echo_test_{mode}" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + assert now + + freezer.move_to(now) + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_MESSAGE: "Test Message", + }, + blocking=True, + ) + + assert (state := hass.states.get(entity_id)) + assert state.state == now.isoformat() From c14d17f88c4b8f9e3d65302994b584f2bf7d5ea4 Mon Sep 17 00:00:00 2001 From: Florian von Garrel Date: Mon, 26 May 2025 17:24:23 +0200 Subject: [PATCH 0922/1175] Add status sensors to paperless (#145591) * Add first status sensor and coordinator * New snapshot * Add comment * Add test for forbidden status endpoint * Changed comment * Fixed translation * Minor changes and code optimization * Add common translation; minor tweaks * Moved translation from common to integration --- .../components/paperless_ngx/__init__.py | 95 +++- .../components/paperless_ngx/coordinator.py | 138 +++--- .../components/paperless_ngx/diagnostics.py | 7 +- .../components/paperless_ngx/entity.py | 8 +- .../components/paperless_ngx/icons.json | 61 ++- .../components/paperless_ngx/sensor.py | 215 +++++++- .../components/paperless_ngx/strings.json | 54 +++ tests/components/paperless_ngx/conftest.py | 21 +- .../fixtures/test_data_status.json | 36 ++ .../snapshots/test_diagnostics.ambr | 97 +++- .../paperless_ngx/snapshots/test_sensor.ambr | 458 ++++++++++++++++++ tests/components/paperless_ngx/test_init.py | 20 +- tests/components/paperless_ngx/test_sensor.py | 12 +- 13 files changed, 1106 insertions(+), 116 deletions(-) create mode 100644 tests/components/paperless_ngx/fixtures/test_data_status.json diff --git a/homeassistant/components/paperless_ngx/__init__.py b/homeassistant/components/paperless_ngx/__init__.py index 145f3ec2caf..22c05d798e8 100644 --- a/homeassistant/components/paperless_ngx/__init__.py +++ b/homeassistant/components/paperless_ngx/__init__.py @@ -1,9 +1,30 @@ """The Paperless-ngx integration.""" -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from pypaperless import Paperless +from pypaperless.exceptions import ( + InitializationError, + PaperlessConnectionError, + PaperlessForbiddenError, + PaperlessInactiveOrDeletedError, + PaperlessInvalidTokenError, +) -from .coordinator import PaperlessConfigEntry, PaperlessCoordinator +from homeassistant.const import CONF_API_KEY, CONF_URL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, LOGGER +from .coordinator import ( + PaperlessConfigEntry, + PaperlessData, + PaperlessStatisticCoordinator, + PaperlessStatusCoordinator, +) PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -11,10 +32,28 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool: """Set up Paperless-ngx from a config entry.""" - coordinator = PaperlessCoordinator(hass, entry) - await coordinator.async_config_entry_first_refresh() + api = await _get_paperless_api(hass, entry) - entry.runtime_data = coordinator + statistics_coordinator = PaperlessStatisticCoordinator(hass, entry, api) + status_coordinator = PaperlessStatusCoordinator(hass, entry, api) + + await statistics_coordinator.async_config_entry_first_refresh() + + try: + await status_coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady as err: + # Catch the error so the integration doesn't fail just because status coordinator fails. + LOGGER.warning("Could not initialize status coordinator: %s", err) + + entry.runtime_data = PaperlessData( + status=status_coordinator, + statistics=statistics_coordinator, + ) + + entry.runtime_data = PaperlessData( + status=status_coordinator, + statistics=statistics_coordinator, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -24,3 +63,47 @@ async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> bool: """Unload paperless-ngx config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def _get_paperless_api( + hass: HomeAssistant, + entry: PaperlessConfigEntry, +) -> Paperless: + """Create and initialize paperless-ngx API.""" + + api = Paperless( + entry.data[CONF_URL], + entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) + + try: + await api.initialize() + await api.statistics() # test permissions on api + except PaperlessConnectionError as err: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + except PaperlessInvalidTokenError as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_api_key", + ) from err + except PaperlessInactiveOrDeletedError as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="user_inactive_or_deleted", + ) from err + except PaperlessForbiddenError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="forbidden", + ) from err + except InitializationError as err: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="cannot_connect", + ) from err + else: + return api diff --git a/homeassistant/components/paperless_ngx/coordinator.py b/homeassistant/components/paperless_ngx/coordinator.py index a8296bbda89..d5960bed49b 100644 --- a/homeassistant/components/paperless_ngx/coordinator.py +++ b/homeassistant/components/paperless_ngx/coordinator.py @@ -2,38 +2,45 @@ from __future__ import annotations +from abc import abstractmethod +from dataclasses import dataclass from datetime import timedelta +from typing import TypeVar from pypaperless import Paperless from pypaperless.exceptions import ( - InitializationError, PaperlessConnectionError, PaperlessForbiddenError, PaperlessInactiveOrDeletedError, PaperlessInvalidTokenError, ) -from pypaperless.models import Statistic +from pypaperless.models import Statistic, Status from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryError, - ConfigEntryNotReady, -) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER -type PaperlessConfigEntry = ConfigEntry[PaperlessCoordinator] +type PaperlessConfigEntry = ConfigEntry[PaperlessData] -UPDATE_INTERVAL = 120 +TData = TypeVar("TData") + +UPDATE_INTERVAL_STATISTICS = timedelta(seconds=120) +UPDATE_INTERVAL_STATUS = timedelta(seconds=300) -class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): - """Coordinator to manage Paperless-ngx statistic updates.""" +@dataclass +class PaperlessData: + """Data for the Paperless-ngx integration.""" + + statistics: PaperlessStatisticCoordinator + status: PaperlessStatusCoordinator + + +class PaperlessCoordinator(DataUpdateCoordinator[TData]): + """Coordinator to manage fetching Paperless-ngx API.""" config_entry: PaperlessConfigEntry @@ -41,28 +48,27 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): self, hass: HomeAssistant, entry: PaperlessConfigEntry, + api: Paperless, + name: str, + update_interval: timedelta, ) -> None: - """Initialize my coordinator.""" + """Initialize Paperless-ngx statistics coordinator.""" + self.api = api + super().__init__( hass, LOGGER, config_entry=entry, - name="Paperless-ngx Coordinator", - update_interval=timedelta(seconds=UPDATE_INTERVAL), + name=name, + update_interval=update_interval, ) - self.api = Paperless( - entry.data[CONF_URL], - entry.data[CONF_API_KEY], - session=async_get_clientsession(self.hass), - ) - - async def _async_setup(self) -> None: + async def _async_update_data(self) -> TData: + """Update data via internal method.""" try: - await self.api.initialize() - await self.api.statistics() # test permissions on api + return await self._async_update_data_internal() except PaperlessConnectionError as err: - raise ConfigEntryNotReady( + raise UpdateFailed( translation_domain=DOMAIN, translation_key="cannot_connect", ) from err @@ -77,37 +83,57 @@ class PaperlessCoordinator(DataUpdateCoordinator[Statistic]): translation_key="user_inactive_or_deleted", ) from err except PaperlessForbiddenError as err: - raise ConfigEntryError( + raise UpdateFailed( translation_domain=DOMAIN, translation_key="forbidden", ) from err - except InitializationError as err: - raise ConfigEntryError( - translation_domain=DOMAIN, - translation_key="cannot_connect", - ) from err - async def _async_update_data(self) -> Statistic: - """Fetch data from API endpoint.""" - try: - return await self.api.statistics() - except PaperlessConnectionError as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="cannot_connect", - ) from err - except PaperlessForbiddenError as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="forbidden", - ) from err - except PaperlessInvalidTokenError as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="invalid_api_key", - ) from err - except PaperlessInactiveOrDeletedError as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="user_inactive_or_deleted", - ) from err + @abstractmethod + async def _async_update_data_internal(self) -> TData: + """Update data via paperless-ngx API.""" + + +class PaperlessStatisticCoordinator(PaperlessCoordinator[Statistic]): + """Coordinator to manage Paperless-ngx statistic updates.""" + + def __init__( + self, + hass: HomeAssistant, + entry: PaperlessConfigEntry, + api: Paperless, + ) -> None: + """Initialize Paperless-ngx status coordinator.""" + super().__init__( + hass, + entry, + api, + name="Statistics Coordinator", + update_interval=UPDATE_INTERVAL_STATISTICS, + ) + + async def _async_update_data_internal(self) -> Statistic: + """Fetch statistics data from API endpoint.""" + return await self.api.statistics() + + +class PaperlessStatusCoordinator(PaperlessCoordinator[Status]): + """Coordinator to manage Paperless-ngx status updates.""" + + def __init__( + self, + hass: HomeAssistant, + entry: PaperlessConfigEntry, + api: Paperless, + ) -> None: + """Initialize Paperless-ngx status coordinator.""" + super().__init__( + hass, + entry, + api, + name="Status Coordinator", + update_interval=UPDATE_INTERVAL_STATUS, + ) + + async def _async_update_data_internal(self) -> Status: + """Fetch status data from API endpoint.""" + return await self.api.status() diff --git a/homeassistant/components/paperless_ngx/diagnostics.py b/homeassistant/components/paperless_ngx/diagnostics.py index 3f8351c6dca..3222295d055 100644 --- a/homeassistant/components/paperless_ngx/diagnostics.py +++ b/homeassistant/components/paperless_ngx/diagnostics.py @@ -15,4 +15,9 @@ async def async_get_config_entry_diagnostics( entry: PaperlessConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - return {"data": asdict(entry.runtime_data.data)} + return { + "data": { + "statistics": asdict(entry.runtime_data.statistics.data), + "status": asdict(entry.runtime_data.status.data), + }, + } diff --git a/homeassistant/components/paperless_ngx/entity.py b/homeassistant/components/paperless_ngx/entity.py index 934f460af8d..e7eb0f0edcf 100644 --- a/homeassistant/components/paperless_ngx/entity.py +++ b/homeassistant/components/paperless_ngx/entity.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Generic, TypeVar + from homeassistant.components.sensor import EntityDescription from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -9,15 +11,17 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import PaperlessCoordinator +TCoordinator = TypeVar("TCoordinator", bound=PaperlessCoordinator) -class PaperlessEntity(CoordinatorEntity[PaperlessCoordinator]): + +class PaperlessEntity(CoordinatorEntity[TCoordinator], Generic[TCoordinator]): """Defines a base Paperless-ngx entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: PaperlessCoordinator, + coordinator: TCoordinator, description: EntityDescription, ) -> None: """Initialize the Paperless-ngx entity.""" diff --git a/homeassistant/components/paperless_ngx/icons.json b/homeassistant/components/paperless_ngx/icons.json index 5d5db9a6b51..1df7a7d701c 100644 --- a/homeassistant/components/paperless_ngx/icons.json +++ b/homeassistant/components/paperless_ngx/icons.json @@ -16,8 +16,65 @@ "correspondent_count": { "default": "mdi:account-group" }, - "document_type_count": { - "default": "mdi:format-list-bulleted-type" + "storage_total": { + "default": "mdi:harddisk" + }, + "storage_available": { + "default": "mdi:harddisk" + }, + "database_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } + }, + "index_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } + }, + "classifier_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } + }, + "celery_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } + }, + "redis_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } + }, + "sanity_check_status": { + "default": "mdi:check-circle", + "state": { + "ok": "mdi:check-circle", + "warning": "mdi:alert", + "error": "mdi:alert-circle", + "unknown": "mdi:help-circle" + } } } } diff --git a/homeassistant/components/paperless_ngx/sensor.py b/homeassistant/components/paperless_ngx/sensor.py index 4c358933ae7..e3f601b68e6 100644 --- a/homeassistant/components/paperless_ngx/sensor.py +++ b/homeassistant/components/paperless_ngx/sensor.py @@ -4,62 +4,73 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Generic -from pypaperless.models import Statistic +from pypaperless.models import Statistic, Status +from pypaperless.models.common import StatusType from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) +from homeassistant.const import EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util.unit_conversion import InformationConverter -from .coordinator import PaperlessConfigEntry -from .entity import PaperlessEntity +from .coordinator import ( + PaperlessConfigEntry, + PaperlessStatisticCoordinator, + PaperlessStatusCoordinator, + TData, +) +from .entity import PaperlessEntity, TCoordinator PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) -class PaperlessEntityDescription(SensorEntityDescription): +class PaperlessEntityDescription(SensorEntityDescription, Generic[TData]): """Describes Paperless-ngx sensor entity.""" - value_fn: Callable[[Statistic], int | None] + value_fn: Callable[[TData], StateType] -SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = ( - PaperlessEntityDescription( +SENSOR_STATISTICS: tuple[PaperlessEntityDescription, ...] = ( + PaperlessEntityDescription[Statistic]( key="documents_total", translation_key="documents_total", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.documents_total, ), - PaperlessEntityDescription( + PaperlessEntityDescription[Statistic]( key="documents_inbox", translation_key="documents_inbox", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.documents_inbox, ), - PaperlessEntityDescription( + PaperlessEntityDescription[Statistic]( key="characters_count", translation_key="characters_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.character_count, ), - PaperlessEntityDescription( + PaperlessEntityDescription[Statistic]( key="tag_count", translation_key="tag_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.tag_count, ), - PaperlessEntityDescription( + PaperlessEntityDescription[Statistic]( key="correspondent_count", translation_key="correspondent_count", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.correspondent_count, ), - PaperlessEntityDescription( + PaperlessEntityDescription[Statistic]( key="document_type_count", translation_key="document_type_count", state_class=SensorStateClass.MEASUREMENT, @@ -67,6 +78,157 @@ SENSOR_DESCRIPTIONS: tuple[PaperlessEntityDescription, ...] = ( ), ) +SENSOR_STATUS: tuple[PaperlessEntityDescription, ...] = ( + PaperlessEntityDescription[Status]( + key="storage_total", + translation_key="storage_total", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.GIGABYTES, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=( + lambda data: round( + InformationConverter().convert( + data.storage.total, + UnitOfInformation.BYTES, + UnitOfInformation.GIGABYTES, + ), + 2, + ) + if data.storage is not None and data.storage.total is not None + else None + ), + ), + PaperlessEntityDescription[Status]( + key="storage_available", + translation_key="storage_available", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DATA_SIZE, + native_unit_of_measurement=UnitOfInformation.GIGABYTES, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=( + lambda data: round( + InformationConverter().convert( + data.storage.available, + UnitOfInformation.BYTES, + UnitOfInformation.GIGABYTES, + ), + 2, + ) + if data.storage is not None and data.storage.available is not None + else None + ), + ), + PaperlessEntityDescription[Status]( + key="database_status", + translation_key="database_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.database.status.value.lower() + if ( + data.database is not None + and data.database.status is not None + and data.database.status != StatusType.UNKNOWN + ) + else None + ), + ), + PaperlessEntityDescription[Status]( + key="index_status", + translation_key="index_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.tasks.index_status.value.lower() + if ( + data.tasks is not None + and data.tasks.index_status is not None + and data.tasks.index_status != StatusType.UNKNOWN + ) + else None + ), + ), + PaperlessEntityDescription[Status]( + key="classifier_status", + translation_key="classifier_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.tasks.classifier_status.value.lower() + if ( + data.tasks is not None + and data.tasks.classifier_status is not None + and data.tasks.classifier_status != StatusType.UNKNOWN + ) + else None + ), + ), + PaperlessEntityDescription[Status]( + key="celery_status", + translation_key="celery_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.tasks.celery_status.value.lower() + if ( + data.tasks is not None + and data.tasks.celery_status is not None + and data.tasks.celery_status != StatusType.UNKNOWN + ) + else None + ), + ), + PaperlessEntityDescription[Status]( + key="redis_status", + translation_key="redis_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.tasks.redis_status.value.lower() + if ( + data.tasks is not None + and data.tasks.redis_status is not None + and data.tasks.redis_status != StatusType.UNKNOWN + ) + else None + ), + ), + PaperlessEntityDescription[Status]( + key="sanity_check_status", + translation_key="sanity_check_status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + options=[ + item.value.lower() for item in StatusType if item != StatusType.UNKNOWN + ], + value_fn=( + lambda data: data.tasks.sanity_check_status.value.lower() + if ( + data.tasks is not None + and data.tasks.sanity_check_status is not None + and data.tasks.sanity_check_status != StatusType.UNKNOWN + ) + else None + ), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -74,21 +236,34 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Paperless-ngx sensors.""" - async_add_entities( - PaperlessSensor( - coordinator=entry.runtime_data, - description=sensor_description, + + entities: list[PaperlessSensor] = [] + + entities += [ + PaperlessSensor[PaperlessStatisticCoordinator]( + coordinator=entry.runtime_data.statistics, + description=description, ) - for sensor_description in SENSOR_DESCRIPTIONS - ) + for description in SENSOR_STATISTICS + ] + + entities += [ + PaperlessSensor[PaperlessStatusCoordinator]( + coordinator=entry.runtime_data.status, + description=description, + ) + for description in SENSOR_STATUS + ] + + async_add_entities(entities) -class PaperlessSensor(PaperlessEntity, SensorEntity): +class PaperlessSensor(PaperlessEntity[TCoordinator], SensorEntity): """Defines a Paperless-ngx sensor entity.""" entity_description: PaperlessEntityDescription @property - def native_value(self) -> int | None: + def native_value(self) -> StateType: """Return the current value of the sensor.""" return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/paperless_ngx/strings.json b/homeassistant/components/paperless_ngx/strings.json index dbcd3cf37e1..4cceeb37a5a 100644 --- a/homeassistant/components/paperless_ngx/strings.json +++ b/homeassistant/components/paperless_ngx/strings.json @@ -71,6 +71,60 @@ "document_type_count": { "name": "Document types", "unit_of_measurement": "document types" + }, + "storage_total": { + "name": "Total storage" + }, + "storage_available": { + "name": "Available storage" + }, + "database_status": { + "name": "Status database", + "state": { + "ok": "OK", + "warning": "Warning", + "error": "[%key:common::state::error%]" + } + }, + "index_status": { + "name": "Status index", + "state": { + "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", + "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", + "error": "[%key:common::state::error%]" + } + }, + "classifier_status": { + "name": "Status classifier", + "state": { + "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", + "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", + "error": "[%key:common::state::error%]" + } + }, + "celery_status": { + "name": "Status celery", + "state": { + "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", + "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", + "error": "[%key:common::state::error%]" + } + }, + "redis_status": { + "name": "Status redis", + "state": { + "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", + "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", + "error": "[%key:common::state::error%]" + } + }, + "sanity_check_status": { + "name": "Status sanity", + "state": { + "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", + "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", + "error": "[%key:common::state::error%]" + } } } }, diff --git a/tests/components/paperless_ngx/conftest.py b/tests/components/paperless_ngx/conftest.py index a96a0b115e1..c57246eecf0 100644 --- a/tests/components/paperless_ngx/conftest.py +++ b/tests/components/paperless_ngx/conftest.py @@ -4,7 +4,7 @@ from collections.abc import Generator import json from unittest.mock import AsyncMock, MagicMock, patch -from pypaperless.models import Statistic +from pypaperless.models import Statistic, Status import pytest from homeassistant.components.paperless_ngx.const import DOMAIN @@ -16,6 +16,12 @@ from .const import USER_INPUT_ONE from tests.common import MockConfigEntry, load_fixture +@pytest.fixture +def mock_status_data() -> Generator[MagicMock]: + """Return test status data.""" + return json.loads(load_fixture("test_data_status.json", DOMAIN)) + + @pytest.fixture def mock_statistic_data() -> Generator[MagicMock]: """Return test statistic data.""" @@ -29,7 +35,9 @@ def mock_statistic_data_update() -> Generator[MagicMock]: @pytest.fixture(autouse=True) -def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]: +def mock_paperless( + mock_statistic_data: MagicMock, mock_status_data: MagicMock +) -> Generator[AsyncMock]: """Mock the pypaperless.Paperless client.""" with ( patch( @@ -40,6 +48,10 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]: "homeassistant.components.paperless_ngx.config_flow.Paperless", new=paperless_mock, ), + patch( + "homeassistant.components.paperless_ngx.Paperless", + new=paperless_mock, + ), ): paperless = paperless_mock.return_value @@ -51,6 +63,11 @@ def mock_paperless(mock_statistic_data: MagicMock) -> Generator[AsyncMock]: paperless, data=mock_statistic_data, fetched=True ) ) + paperless.status = AsyncMock( + return_value=Status.create_with_data( + paperless, data=mock_status_data, fetched=True + ) + ) yield paperless diff --git a/tests/components/paperless_ngx/fixtures/test_data_status.json b/tests/components/paperless_ngx/fixtures/test_data_status.json new file mode 100644 index 00000000000..9a4ffc25cd0 --- /dev/null +++ b/tests/components/paperless_ngx/fixtures/test_data_status.json @@ -0,0 +1,36 @@ +{ + "pngx_version": "2.15.3", + "server_os": "Linux-6.6.74-haos-raspi-aarch64-with-glibc2.36", + "install_type": "docker", + "storage": { + "total": 62101651456, + "available": 25376927744 + }, + "database": { + "type": "sqlite", + "url": "/config/data/db.sqlite3", + "status": "OK", + "error": null, + "migration_status": { + "latest_migration": "paperless_mail.0029_mailrule_pdf_layout", + "unapplied_migrations": [] + } + }, + "tasks": { + "redis_url": "redis://localhost:6379", + "redis_status": "OK", + "redis_error": null, + "celery_status": "OK", + "celery_url": "celery@ca5234a0-paperless-ngx", + "celery_error": null, + "index_status": "OK", + "index_last_modified": "2025-05-25T00:00:27.053090+02:00", + "index_error": null, + "classifier_status": "OK", + "classifier_last_trained": "2025-05-25T15:05:15.824671Z", + "classifier_error": null, + "sanity_check_status": "OK", + "sanity_check_last_run": "2025-05-24T22:30:21.005536Z", + "sanity_check_error": null + } +} diff --git a/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr b/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr index 77adafd31f6..778d10d3d1b 100644 --- a/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr +++ b/tests/components/paperless_ngx/snapshots/test_diagnostics.ambr @@ -2,28 +2,85 @@ # name: test_config_entry_diagnostics dict({ 'data': dict({ - 'character_count': 99999, - 'correspondent_count': 99, - 'current_asn': 99, - 'document_file_type_counts': list([ - dict({ - 'mime_type': 'application/pdf', - 'mime_type_count': 998, + 'statistics': dict({ + 'character_count': 99999, + 'correspondent_count': 99, + 'current_asn': 99, + 'document_file_type_counts': list([ + dict({ + 'mime_type': 'application/pdf', + 'mime_type_count': 998, + }), + dict({ + 'mime_type': 'image/png', + 'mime_type_count': 1, + }), + ]), + 'document_type_count': 99, + 'documents_inbox': 9, + 'documents_total': 999, + 'inbox_tag': 9, + 'inbox_tags': list([ + 9, + ]), + 'storage_path_count': 9, + 'tag_count': 99, + }), + 'status': dict({ + 'database': dict({ + 'error': None, + 'migration_status': dict({ + 'latest_migration': 'paperless_mail.0029_mailrule_pdf_layout', + 'unapplied_migrations': list([ + ]), + }), + 'status': dict({ + '__type': "", + 'repr': "", + }), + 'type': 'sqlite', + 'url': '/config/data/db.sqlite3', }), - dict({ - 'mime_type': 'image/png', - 'mime_type_count': 1, + 'install_type': 'docker', + 'pngx_version': '2.15.3', + 'server_os': 'Linux-6.6.74-haos-raspi-aarch64-with-glibc2.36', + 'storage': dict({ + 'available': 25376927744, + 'total': 62101651456, }), - ]), - 'document_type_count': 99, - 'documents_inbox': 9, - 'documents_total': 999, - 'inbox_tag': 9, - 'inbox_tags': list([ - 9, - ]), - 'storage_path_count': 9, - 'tag_count': 99, + 'tasks': dict({ + 'celery_error': None, + 'celery_status': dict({ + '__type': "", + 'repr': "", + }), + 'celery_url': 'celery@ca5234a0-paperless-ngx', + 'classifier_error': None, + 'classifier_last_trained': '2025-05-25T15:05:15.824671+00:00', + 'classifier_status': dict({ + '__type': "", + 'repr': "", + }), + 'index_error': None, + 'index_last_modified': '2025-05-25T00:00:27.053090+02:00', + 'index_status': dict({ + '__type': "", + 'repr': "", + }), + 'redis_error': None, + 'redis_status': dict({ + '__type': "", + 'repr': "", + }), + 'redis_url': 'redis://localhost:6379', + 'sanity_check_error': None, + 'sanity_check_last_run': '2025-05-24T22:30:21.005536+00:00', + 'sanity_check_status': dict({ + '__type': "", + 'repr': "", + }), + }), + }), }), }) # --- diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index cc197e23ff5..1f7c7b09d9c 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -1,4 +1,56 @@ # serializer version: 1 +# name: test_sensor_platform[sensor.paperless_ngx_available_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_available_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Available storage', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_available', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_available', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_available_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Paperless-ngx Available storage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_available_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.38', + }) +# --- # name: test_sensor_platform[sensor.paperless_ngx_correspondents-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -152,6 +204,360 @@ 'state': '9', }) # --- +# name: test_sensor_platform[sensor.paperless_ngx_status_celery-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_celery', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status celery', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'celery_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_celery_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_celery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status celery', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_celery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_classifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_classifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status classifier', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'classifier_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_classifier_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_classifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status classifier', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_classifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_database-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_database', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status database', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'database_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_database_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_database-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status database', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_database', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_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': 'Status index', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'index_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_index_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status index', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_redis-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_redis', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status redis', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'redis_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_redis_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_redis-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status redis', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_redis', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_sanity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_status_sanity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Status sanity', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'sanity_check_status', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_sanity_check_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_status_sanity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Paperless-ngx Status sanity', + 'options': list([ + 'ok', + 'error', + 'warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_status_sanity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'ok', + }) +# --- # name: test_sensor_platform[sensor.paperless_ngx_tags-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -305,3 +711,55 @@ 'state': '999', }) # --- +# name: test_sensor_platform[sensor.paperless_ngx_total_storage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.paperless_ngx_total_storage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total storage', + 'platform': 'paperless_ngx', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'storage_total', + 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_total', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_platform[sensor.paperless_ngx_total_storage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'data_size', + 'friendly_name': 'Paperless-ngx Total storage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.paperless_ngx_total_storage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '62.1', + }) +# --- diff --git a/tests/components/paperless_ngx/test_init.py b/tests/components/paperless_ngx/test_init.py index 9a132cf7eff..fd459213ea0 100644 --- a/tests/components/paperless_ngx/test_init.py +++ b/tests/components/paperless_ngx/test_init.py @@ -34,10 +34,28 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.NOT_LOADED +async def test_load_config_status_forbidden( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_paperless: AsyncMock, +) -> None: + """Test loading and unloading the integration.""" + mock_paperless.status.side_effect = PaperlessForbiddenError + + 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 + + @pytest.mark.parametrize( ("side_effect", "expected_state", "expected_error_key"), [ - (PaperlessConnectionError(), ConfigEntryState.SETUP_RETRY, None), + (PaperlessConnectionError(), ConfigEntryState.SETUP_RETRY, "cannot_connect"), (PaperlessInvalidTokenError(), ConfigEntryState.SETUP_ERROR, "invalid_api_key"), ( PaperlessInactiveOrDeletedError(), diff --git a/tests/components/paperless_ngx/test_sensor.py b/tests/components/paperless_ngx/test_sensor.py index 33610d9b6d6..d2233a64ee2 100644 --- a/tests/components/paperless_ngx/test_sensor.py +++ b/tests/components/paperless_ngx/test_sensor.py @@ -1,7 +1,5 @@ """Tests for Paperless-ngx sensor platform.""" -from datetime import timedelta - from freezegun.api import FrozenDateTimeFactory from pypaperless.exceptions import ( PaperlessConnectionError, @@ -12,7 +10,9 @@ from pypaperless.exceptions import ( from pypaperless.models import Statistic import pytest -from homeassistant.components.paperless_ngx.coordinator import UPDATE_INTERVAL +from homeassistant.components.paperless_ngx.coordinator import ( + UPDATE_INTERVAL_STATISTICS, +) from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -61,7 +61,7 @@ async def test_statistic_sensor_state( ) ) - freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + freezer.tick(UPDATE_INTERVAL_STATISTICS) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -91,7 +91,7 @@ async def test__statistic_sensor_state_on_error( # simulate error mock_paperless.statistics.side_effect = error_cls - freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + freezer.tick(UPDATE_INTERVAL_STATISTICS) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -105,7 +105,7 @@ async def test__statistic_sensor_state_on_error( ) ) - freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) + freezer.tick(UPDATE_INTERVAL_STATISTICS) async_fire_time_changed(hass) await hass.async_block_till_done() From 3dc7b75e4b7c5b740ea3257b170255c8bd47447b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Mon, 26 May 2025 17:34:13 +0200 Subject: [PATCH 0923/1175] Allow nested schema validation in event automation trigger (#126771) * Allow nested schema validation in event automation trigger * Fix rfxtrx device trigger * Don't create nested voluptuous schemas * Fix editing mistake * Fix format --------- Co-authored-by: Erik Montnemery Co-authored-by: Martin Hjelmare --- .../homeassistant/triggers/event.py | 14 +++--- .../components/rfxtrx/device_trigger.py | 2 +- .../homeassistant/triggers/test_event.py | 45 +++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 985e4819b24..8065c23c5c1 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -75,14 +75,18 @@ async def async_attach_trigger( event_data.update( template.render_complex(config[CONF_EVENT_DATA], variables, limited=True) ) - # Build the schema or a an items view if the schema is simple - # and does not contain sub-dicts. We explicitly do not check for - # list like the context data below since lists are a special case - # only for context data. (see test test_event_data_with_list) + + # For performance reasons, we want to avoid using a voluptuous schema here + # unless required. Thus, if possible, we try to use a simple items comparison + # For that, we explicitly do not check for list like the context data below + # since lists are a special case only used for context data, see test + # test_event_data_with_list. Otherwise, we build a volutupus schema, see test + # test_event_data_with_list_nested if any(isinstance(value, dict) for value in event_data.values()): event_data_schema = vol.Schema( - {vol.Required(key): value for key, value in event_data.items()}, + event_data, extra=vol.ALLOW_EXTRA, + required=True, ) else: # Use a simple items comparison if possible diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index 35c1944948b..fe9e0da0d52 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -97,7 +97,7 @@ async def async_attach_trigger( if config[CONF_TYPE] == CONF_TYPE_COMMAND: event_data["values"] = {"Command": config[CONF_SUBTYPE]} elif config[CONF_TYPE] == CONF_TYPE_STATUS: - event_data["values"] = {"Status": config[CONF_SUBTYPE]} + event_data["values"] = {"Sensor Status": config[CONF_SUBTYPE]} event_config = event_trigger.TRIGGER_SCHEMA( { diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 293a9007175..5536db1eb5e 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -517,6 +517,51 @@ async def test_event_data_with_list( await hass.async_block_till_done() assert len(service_calls) == 1 + # don't match if property doesn't exist at all + hass.bus.async_fire("test_event", {"other_attr": [1, 2]}) + await hass.async_block_till_done() + assert len(service_calls) == 1 + + +async def test_event_data_with_list_nested( + hass: HomeAssistant, service_calls: list[ServiceCall] +) -> None: + """Test the (non)firing of event when the data schema has nested lists.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "event", + "event_type": "test_event", + "event_data": {"service_data": {"some_attr": [1, 2]}}, + "context": {}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.bus.async_fire("test_event", {"service_data": {"some_attr": [1, 2]}}) + await hass.async_block_till_done() + assert len(service_calls) == 1 + + # don't match a single value + hass.bus.async_fire("test_event", {"service_data": {"some_attr": 1}}) + await hass.async_block_till_done() + assert len(service_calls) == 1 + + # don't match a containing list + hass.bus.async_fire("test_event", {"service_data": {"some_attr": [1, 2, 3]}}) + await hass.async_block_till_done() + assert len(service_calls) == 1 + + # don't match if property doesn't exist at all + hass.bus.async_fire("test_event", {"service_data": {"other_attr": [1, 2]}}) + await hass.async_block_till_done() + assert len(service_calls) == 1 + @pytest.mark.parametrize( "event_type", ["state_reported", ["test_event", "state_reported"]] From 8623d96deb5cf275eff0556a3dff21063dde4392 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Mon, 26 May 2025 16:41:28 +0100 Subject: [PATCH 0924/1175] Squeezebox add alarms support - switch platform. Part 1 (#141055) * initial * remove dupe name definition * snapshot update * name def updates * test update for new entity name * remove attributes * icon translations * merge fixes * Snapshot update post merge * update to class initialisation * move entity delete to coordinator * remove some comments * move known_alarms to coordinator * test_switch update for syrupy change * listener and sets * check self.available * remove refresh from conftest * test update * test tweak * move listener to switch platform * updates revew * SWITCH_DOMAIN --- .../components/squeezebox/__init__.py | 1 + homeassistant/components/squeezebox/const.py | 8 + .../components/squeezebox/coordinator.py | 15 +- .../components/squeezebox/icons.json | 16 ++ .../components/squeezebox/strings.json | 8 + homeassistant/components/squeezebox/switch.py | 185 ++++++++++++++++++ tests/components/squeezebox/conftest.py | 44 ++++- .../squeezebox/snapshots/test_switch.ambr | 96 +++++++++ tests/components/squeezebox/test_switch.py | 135 +++++++++++++ 9 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/squeezebox/switch.py create mode 100644 tests/components/squeezebox/snapshots/test_switch.ambr create mode 100644 tests/components/squeezebox/test_switch.py diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 18acd74efd7..596a44c498c 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -61,6 +61,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, + Platform.SWITCH, Platform.UPDATE, ] diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py index 3f355951acf..92eb3736341 100644 --- a/homeassistant/components/squeezebox/const.py +++ b/homeassistant/components/squeezebox/const.py @@ -44,5 +44,13 @@ DEFAULT_VOLUME_STEP = 5 ATTR_ANNOUNCE_VOLUME = "announce_volume" ATTR_ANNOUNCE_TIMEOUT = "announce_timeout" UNPLAYABLE_TYPES = ("text", "actions") +ATTR_ALARM_ID = "alarm_id" +ATTR_DAYS_OF_WEEK = "dow" +ATTR_ENABLED = "enabled" +ATTR_REPEAT = "repeat" +ATTR_SCHEDULED_TODAY = "scheduled_today" +ATTR_TIME = "time" +ATTR_VOLUME = "volume" +ATTR_URL = "url" UPDATE_PLUGINS_RELEASE_SUMMARY = "update_plugins_release_summary" UPDATE_RELEASE_SUMMARY = "update_release_summary" diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py index 9c7d00eae58..7792ec96e0d 100644 --- a/homeassistant/components/squeezebox/coordinator.py +++ b/homeassistant/components/squeezebox/coordinator.py @@ -9,8 +9,10 @@ import logging from typing import TYPE_CHECKING, Any from pysqueezebox import Player, Server +from pysqueezebox.player import Alarm from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -98,11 +100,13 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) self.player = player self.available = True + self.known_alarms: set[str] = set() self._remove_dispatcher: Callable | None = None + self.player_uuid = format_mac(player.player_id) self.server_uuid = server_uuid async def _async_update_data(self) -> dict[str, Any]: - """Update Player if available, or listen for rediscovery if not.""" + """Update the Player() object if available, or listen for rediscovery if not.""" if self.available: # Only update players available at last update, unavailable players are rediscovered instead await self.player.async_update() @@ -115,7 +119,14 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self._remove_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered ) - return {} + + alarm_dict: dict[str, Alarm] = ( + {alarm["id"]: alarm for alarm in self.player.alarms} + if self.player.alarms + else {} + ) + + return {"alarms": alarm_dict} @callback def rediscovered(self, unique_id: str, connected: bool) -> None: diff --git a/homeassistant/components/squeezebox/icons.json b/homeassistant/components/squeezebox/icons.json index 29911ddad77..06779ea5e60 100644 --- a/homeassistant/components/squeezebox/icons.json +++ b/homeassistant/components/squeezebox/icons.json @@ -19,6 +19,22 @@ "other_player_count": { "default": "mdi:folder-play-outline" } + }, + "switch": { + "alarms_enabled": { + "default": "mdi:alarm-check", + "state": { + "on": "mdi:alarm-check", + "off": "mdi:alarm-off" + } + }, + "alarm": { + "default": "mdi:alarm", + "state": { + "on": "mdi:alarm", + "off": "mdi:alarm-off" + } + } } }, "services": { diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index a8c0b4bb0ae..59d426047de 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -133,6 +133,14 @@ "unit_of_measurement": "[%key:component::squeezebox::entity::sensor::player_count::unit_of_measurement%]" } }, + "switch": { + "alarm": { + "name": "Alarm ({alarm_id})" + }, + "alarms_enabled": { + "name": "Alarms enabled" + } + }, "update": { "newversion": { "name": "Lyrion Music Server" diff --git a/homeassistant/components/squeezebox/switch.py b/homeassistant/components/squeezebox/switch.py new file mode 100644 index 00000000000..33926c53e64 --- /dev/null +++ b/homeassistant/components/squeezebox/switch.py @@ -0,0 +1,185 @@ +"""Switch entity representing a Squeezebox alarm.""" + +import datetime +import logging +from typing import Any, cast + +from pysqueezebox.player import Alarm + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.event import async_track_time_change + +from .const import ATTR_ALARM_ID, DOMAIN, SIGNAL_PLAYER_DISCOVERED +from .coordinator import SqueezeBoxPlayerUpdateCoordinator +from .entity import SqueezeboxEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Squeezebox alarm switch.""" + + async def _player_discovered( + coordinator: SqueezeBoxPlayerUpdateCoordinator, + ) -> None: + def _async_listener() -> None: + """Handle alarm creation and deletion after coordinator data update.""" + new_alarms: set[str] = set() + received_alarms: set[str] = set() + + if coordinator.data["alarms"] and coordinator.available: + received_alarms = set(coordinator.data["alarms"]) + new_alarms = received_alarms - coordinator.known_alarms + removed_alarms = coordinator.known_alarms - received_alarms + + if new_alarms: + for new_alarm in new_alarms: + coordinator.known_alarms.add(new_alarm) + _LOGGER.debug( + "Setting up alarm entity for alarm %s on player %s", + new_alarm, + coordinator.player, + ) + async_add_entities([SqueezeBoxAlarmEntity(coordinator, new_alarm)]) + + if removed_alarms and coordinator.available: + for removed_alarm in removed_alarms: + _uid = f"{coordinator.player_uuid}_alarm_{removed_alarm}" + _LOGGER.debug( + "Alarm %s with unique_id %s needs to be deleted", + removed_alarm, + _uid, + ) + + entity_registry = er.async_get(hass) + _entity_id = entity_registry.async_get_entity_id( + Platform.SWITCH, + DOMAIN, + _uid, + ) + if _entity_id: + entity_registry.async_remove(_entity_id) + coordinator.known_alarms.remove(removed_alarm) + + _LOGGER.debug( + "Setting up alarm enabled entity for player %s", coordinator.player + ) + # Add listener first for future coordinator refresh + coordinator.async_add_listener(_async_listener) + + # If coordinator already has alarm data from the initial refresh, + # call the listener immediately to process existing alarms and create alarm entities. + if coordinator.data["alarms"]: + _LOGGER.debug( + "Coordinator has alarm data, calling _async_listener immediately for player %s", + coordinator.player, + ) + _async_listener() + async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)]) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered) + ) + + +class SqueezeBoxAlarmEntity(SqueezeboxEntity, SwitchEntity): + """Representation of a Squeezebox alarm switch.""" + + _attr_translation_key = "alarm" + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, coordinator: SqueezeBoxPlayerUpdateCoordinator, alarm_id: str + ) -> None: + """Initialize the Squeezebox alarm switch.""" + super().__init__(coordinator) + self._alarm_id = alarm_id + self._attr_translation_placeholders = {"alarm_id": self._alarm_id} + self._attr_unique_id: str = ( + f"{format_mac(self._player.player_id)}_alarm_{self._alarm_id}" + ) + + async def async_added_to_hass(self) -> None: + """Set up alarm switch when added to hass.""" + await super().async_added_to_hass() + + async def async_write_state_daily(now: datetime.datetime) -> None: + """Update alarm state attributes each calendar day.""" + _LOGGER.debug("Updating state attributes for %s", self.name) + self.async_write_ha_state() + + self.async_on_remove( + async_track_time_change( + self.hass, async_write_state_daily, hour=0, minute=0, second=0 + ) + ) + + @property + def alarm(self) -> Alarm: + """Return the alarm object.""" + return self.coordinator.data["alarms"][self._alarm_id] + + @property + def available(self) -> bool: + """Return whether the alarm is available.""" + return super().available and self._alarm_id in self.coordinator.data["alarms"] + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return attributes of Squeezebox alarm switch.""" + return {ATTR_ALARM_ID: str(self._alarm_id)} + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return cast(bool, self.alarm["enabled"]) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.coordinator.player.async_update_alarm(self._alarm_id, enabled=False) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.coordinator.player.async_update_alarm(self._alarm_id, enabled=True) + await self.coordinator.async_request_refresh() + + +class SqueezeBoxAlarmsEnabledEntity(SqueezeboxEntity, SwitchEntity): + """Representation of a Squeezebox players alarms enabled master switch.""" + + _attr_translation_key = "alarms_enabled" + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None: + """Initialize the Squeezebox alarm switch.""" + super().__init__(coordinator) + self._attr_unique_id: str = ( + f"{format_mac(self._player.player_id)}_alarms_enabled" + ) + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return cast(bool, self.coordinator.player.alarms_enabled) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.coordinator.player.async_set_alarms_enabled(False) + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.coordinator.player.async_set_alarms_enabled(True) + await self.coordinator.async_request_refresh() diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 2cbc1305bcb..a3adf05f5f0 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -32,7 +32,6 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac -# from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry CONF_VOLUME_STEP = "volume_step" @@ -48,6 +47,7 @@ SERVER_UUIDS = [ TEST_MAC = ["aa:bb:cc:dd:ee:ff", "ff:ee:dd:cc:bb:aa"] TEST_PLAYER_NAME = "Test Player" TEST_SERVER_NAME = "Test Server" +TEST_ALARM_ID = "1" FAKE_VALID_ITEM_ID = "1234" FAKE_INVALID_ITEM_ID = "4321" @@ -294,6 +294,7 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock: mock_player.image_url = None mock_player.model = "SqueezeLite" mock_player.creator = "Ralph Irving & Adrian Smith" + mock_player.alarms_enabled = True return mock_player @@ -363,6 +364,47 @@ async def configure_squeezebox_media_player_button_platform( await hass.async_block_till_done(wait_background_tasks=True) +async def configure_squeezebox_switch_platform( + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms: MagicMock, +) -> None: + """Configure a squeezebox config entry with appropriate mocks for switch.""" + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.SWITCH], + ), + patch("homeassistant.components.squeezebox.Server", return_value=lms), + ): + # Set up the switch platform. + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + +@pytest.fixture +async def mock_alarms_player( + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms: MagicMock, +) -> MagicMock: + """Mock the alarms of a configured player.""" + players = await lms.async_get_players() + players[0].alarms = [ + { + "id": TEST_ALARM_ID, + "enabled": True, + "time": "07:00", + "dow": [0, 1, 2, 3, 4, 5, 6], + "repeat": False, + "url": "CURRENT_PLAYLIST", + "volume": 50, + }, + ] + await configure_squeezebox_switch_platform(hass, config_entry, lms) + return players[0] + + @pytest.fixture async def configured_player( hass: HomeAssistant, config_entry: MockConfigEntry, lms: MagicMock diff --git a/tests/components/squeezebox/snapshots/test_switch.ambr b/tests/components/squeezebox/snapshots/test_switch.ambr new file mode 100644 index 00000000000..b084e3a583d --- /dev/null +++ b/tests/components/squeezebox/snapshots/test_switch.ambr @@ -0,0 +1,96 @@ +# serializer version: 1 +# name: test_entity_registry[switch.test_player_alarm_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_player_alarm_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': 'Alarm (1)', + 'platform': 'squeezebox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarm', + 'unique_id': 'aa:bb:cc:dd:ee:ff_alarm_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[switch.test_player_alarm_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'alarm_id': '1', + 'friendly_name': 'Test Player Alarm (1)', + }), + 'context': , + 'entity_id': 'switch.test_player_alarm_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_entity_registry[switch.test_player_alarms_enabled-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.test_player_alarms_enabled', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Alarms enabled', + 'platform': 'squeezebox', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'alarms_enabled', + 'unique_id': 'aa:bb:cc:dd:ee:ff_alarms_enabled', + 'unit_of_measurement': None, + }) +# --- +# name: test_entity_registry[switch.test_player_alarms_enabled-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Test Player Alarms enabled', + }), + 'context': , + 'entity_id': 'switch.test_player_alarms_enabled', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/squeezebox/test_switch.py b/tests/components/squeezebox/test_switch.py new file mode 100644 index 00000000000..e4c8c3b5e4d --- /dev/null +++ b/tests/components/squeezebox/test_switch.py @@ -0,0 +1,135 @@ +"""Tests for the Squeezebox alarm switch platform.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.squeezebox.const import PLAYER_UPDATE_INTERVAL +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry + +from .conftest import TEST_ALARM_ID + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_entity_registry( + hass: HomeAssistant, + entity_registry: EntityRegistry, + mock_alarms_player: MagicMock, + snapshot: SnapshotAssertion, + config_entry: MockConfigEntry, +) -> None: + """Test squeezebox media_player entity registered in the entity registry.""" + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +async def test_switch_state( + hass: HomeAssistant, + mock_alarms_player: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the state of the switch.""" + assert hass.states.get(f"switch.test_player_alarm_{TEST_ALARM_ID}").state == "on" + + mock_alarms_player.alarms[0]["enabled"] = False + freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(f"switch.test_player_alarm_{TEST_ALARM_ID}").state == "off" + + +async def test_switch_deleted( + hass: HomeAssistant, + mock_alarms_player: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test detecting switch deleted.""" + assert hass.states.get(f"switch.test_player_alarm_{TEST_ALARM_ID}").state == "on" + + mock_alarms_player.alarms = [] + freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(f"switch.test_player_alarm_{TEST_ALARM_ID}") is None + + +async def test_turn_on( + hass: HomeAssistant, + mock_alarms_player: MagicMock, +) -> None: + """Test turning on the switch.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: f"switch.test_player_alarm_{TEST_ALARM_ID}"}, + blocking=True, + ) + mock_alarms_player.async_update_alarm.assert_called_once_with( + TEST_ALARM_ID, enabled=True + ) + + +async def test_turn_off( + hass: HomeAssistant, + mock_alarms_player: MagicMock, +) -> None: + """Test turning on the switch.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: f"switch.test_player_alarm_{TEST_ALARM_ID}"}, + blocking=True, + ) + mock_alarms_player.async_update_alarm.assert_called_once_with( + TEST_ALARM_ID, enabled=False + ) + + +async def test_alarms_enabled_state( + hass: HomeAssistant, + mock_alarms_player: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the alarms enabled switch.""" + + assert hass.states.get("switch.test_player_alarms_enabled").state == "on" + + mock_alarms_player.alarms_enabled = False + freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_player_alarms_enabled").state == "off" + + +async def test_alarms_enabled_turn_on( + hass: HomeAssistant, + mock_alarms_player: MagicMock, +) -> None: + """Test turning on the alarms enabled switch.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.test_player_alarms_enabled"}, + blocking=True, + ) + mock_alarms_player.async_set_alarms_enabled.assert_called_once_with(True) + + +async def test_alarms_enabled_turn_off( + hass: HomeAssistant, + mock_alarms_player: MagicMock, +) -> None: + """Test turning off the alarms enabled switch.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.test_player_alarms_enabled"}, + blocking=True, + ) + mock_alarms_player.async_set_alarms_enabled.assert_called_once_with(False) From 51562e5ab4c1d67be157e69a5298fe382635ee71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 May 2025 11:05:26 -0500 Subject: [PATCH 0925/1175] Bump aiohttp to 3.12.1rc0 (#145624) --- 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 075d6a7f502..1d89321bacf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.0 +aiohttp==3.12.1rc0 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 30862625712..011f79c60e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.0", + "aiohttp==3.12.1rc0", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 53502f0d8df..85cdc5f5715 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.0 +aiohttp==3.12.1rc0 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 15a7d1376883a6c17e3741618b38c1ae13aaded7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 19:24:23 +0300 Subject: [PATCH 0926/1175] Use model details data from library for Amazon Devices (#145601) * Log warning for unknown models for Amazon Devices * use method from library * apply review comment * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/amazon_devices/entity.py | 8 ++------ homeassistant/components/amazon_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/amazon_devices/conftest.py | 4 ++++ 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py index 2ac90410bec..825a63db476 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/amazon_devices/entity.py @@ -1,9 +1,7 @@ """Defines a base Amazon Devices entity.""" -from typing import cast - from aioamazondevices.api import AmazonDevice -from aioamazondevices.const import DEVICE_TYPE_TO_MODEL, SPEAKER_GROUP_MODEL +from aioamazondevices.const import SPEAKER_GROUP_MODEL from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription @@ -27,9 +25,7 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): """Initialize the entity.""" super().__init__(coordinator) self._serial_num = serial_num - model_details: dict[str, str] = cast( - "dict", DEVICE_TYPE_TO_MODEL.get(self.device.device_type) - ) + model_details = coordinator.api.get_model_details(self.device) model = model_details["model"] if model_details else None self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_num)}, diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index f20c226230d..606dec83150 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -29,5 +29,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==2.0.1"] + "requirements": ["aioamazondevices==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 7cb0a029ce8..488b7011a06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.0.1 +aioamazondevices==2.1.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b156e3ca2a..07d96dd55b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.0.1 +aioamazondevices==2.1.1 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/tests/components/amazon_devices/conftest.py b/tests/components/amazon_devices/conftest.py index 5978faa0b31..f0ee29d44e5 100644 --- a/tests/components/amazon_devices/conftest.py +++ b/tests/components/amazon_devices/conftest.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch from aioamazondevices.api import AmazonDevice +from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN @@ -57,6 +58,9 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: bluetooth_state=True, ) } + client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( + device.device_type + ) yield client From 8fb4f1f7f99b2b8722f1c7902a3deb4ac8da6059 Mon Sep 17 00:00:00 2001 From: Perchun Pak Date: Mon, 26 May 2025 18:39:13 +0200 Subject: [PATCH 0927/1175] Update mcstatus to 12.0.1 in Minecraft Server (#144704) Update mcstatus to 12.0.1 --- homeassistant/components/minecraft_server/api.py | 2 +- homeassistant/components/minecraft_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/minecraft_server/const.py | 3 ++- tests/components/minecraft_server/test_binary_sensor.py | 2 +- tests/components/minecraft_server/test_diagnostics.py | 2 +- tests/components/minecraft_server/test_sensor.py | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/minecraft_server/api.py b/homeassistant/components/minecraft_server/api.py index 3155d83a736..8eb556319f9 100644 --- a/homeassistant/components/minecraft_server/api.py +++ b/homeassistant/components/minecraft_server/api.py @@ -6,7 +6,7 @@ import logging from dns.resolver import LifetimeTimeout from mcstatus import BedrockServer, JavaServer -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index be399a3c8dc..f68586f1992 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["dnspython", "mcstatus"], "quality_scale": "silver", - "requirements": ["mcstatus==11.1.1"] + "requirements": ["mcstatus==12.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 488b7011a06..d0069cc4b8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1397,7 +1397,7 @@ mbddns==0.1.2 mcp==1.5.0 # homeassistant.components.minecraft_server -mcstatus==11.1.1 +mcstatus==12.0.1 # homeassistant.components.meater meater-python==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07d96dd55b2..00fd5b080bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,7 +1176,7 @@ mbddns==0.1.2 mcp==1.5.0 # homeassistant.components.minecraft_server -mcstatus==11.1.1 +mcstatus==12.0.1 # homeassistant.components.meater meater-python==0.0.8 diff --git a/tests/components/minecraft_server/const.py b/tests/components/minecraft_server/const.py index 6914d36ba5b..2c577e45d21 100644 --- a/tests/components/minecraft_server/const.py +++ b/tests/components/minecraft_server/const.py @@ -1,7 +1,7 @@ """Constants for Minecraft Server integration tests.""" from mcstatus.motd import Motd -from mcstatus.status_response import ( +from mcstatus.responses import ( BedrockStatusPlayers, BedrockStatusResponse, BedrockStatusVersion, @@ -44,6 +44,7 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse( icon=None, enforces_secure_chat=False, latency=5, + forge_data=None, ) TEST_JAVA_DATA = MinecraftServerData( diff --git a/tests/components/minecraft_server/test_binary_sensor.py b/tests/components/minecraft_server/test_binary_sensor.py index c87644961f2..a3b71b2442f 100644 --- a/tests/components/minecraft_server/test_binary_sensor.py +++ b/tests/components/minecraft_server/test_binary_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from mcstatus import BedrockServer, JavaServer -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/minecraft_server/test_diagnostics.py b/tests/components/minecraft_server/test_diagnostics.py index 800af79e51c..d576b31ca5d 100644 --- a/tests/components/minecraft_server/test_diagnostics.py +++ b/tests/components/minecraft_server/test_diagnostics.py @@ -3,7 +3,7 @@ from unittest.mock import patch from mcstatus import BedrockServer, JavaServer -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse import pytest from syrupy.assertion import SnapshotAssertion diff --git a/tests/components/minecraft_server/test_sensor.py b/tests/components/minecraft_server/test_sensor.py index 3502184df86..daa20d16a66 100644 --- a/tests/components/minecraft_server/test_sensor.py +++ b/tests/components/minecraft_server/test_sensor.py @@ -5,7 +5,7 @@ from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory from mcstatus import BedrockServer, JavaServer -from mcstatus.status_response import BedrockStatusResponse, JavaStatusResponse +from mcstatus.responses import BedrockStatusResponse, JavaStatusResponse import pytest from syrupy.assertion import SnapshotAssertion From 039383ab22d96a8f50901425981e9ebe1b406975 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 18:40:13 +0200 Subject: [PATCH 0928/1175] Add pyserial-asyncio to forbidden packages (#145625) --- script/hassfest/requirements.py | 76 ++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 8c1892f20a7..944724fb2cb 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -41,7 +41,19 @@ PACKAGE_REGEX = re.compile( PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -FORBIDDEN_PACKAGES = {"codecov", "pytest", "setuptools", "wheel"} +FORBIDDEN_PACKAGES = { + # Only needed for tests + "codecov": "not be a runtime dependency", + # Does blocking I/O and should be replaced by pyserial-asyncio-fast + # See https://github.com/home-assistant/core/pull/116635 + "pyserial-asyncio": "be replaced by pyserial-asyncio-fast", + # Only needed for tests + "pytest": "not be a runtime dependency", + # Only needed for build + "setuptools": "not be a runtime dependency", + # Only needed for build + "wheel": "not be a runtime dependency", +} FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # In the form dict("domain": {"package": {"reason1", "reason2"}}) # - domain is the integration domain @@ -52,6 +64,11 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # aioazuredevops > incremental > setuptools "incremental": {"setuptools"} }, + "blackbird": { + # https://github.com/koolsb/pyblackbird/issues/12 + # pyblackbird > pyserial-asyncio + "pyblackbird": {"pyserial-asyncio"} + }, "cmus": { # https://github.com/mtreinish/pycmus/issues/4 # pycmus > pbr > setuptools @@ -62,12 +79,22 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # concord232 > stevedore > pbr > setuptools "pbr": {"setuptools"} }, + "edl21": { + # https://github.com/mtdcr/pysml/issues/21 + # pysml > pyserial-asyncio + "pysml": {"pyserial-asyncio"} + }, "efergy": { # https://github.com/tkdrob/pyefergy/issues/46 # pyefergy > codecov # pyefergy > types-pytz "pyefergy": {"codecov", "types-pytz"} }, + "epson": { + # https://github.com/pszafer/epson_projector/pull/22 + # epson-projector > pyserial-asyncio + "epson-projector": {"pyserial-asyncio"} + }, "fitbit": { # https://github.com/orcasgit/python-fitbit/pull/178 # but project seems unmaintained @@ -79,16 +106,31 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # aioguardian > asyncio-dgram > setuptools "asyncio-dgram": {"setuptools"} }, + "heatmiser": { + # https://github.com/andylockran/heatmiserV3/issues/96 + # heatmiserV3 > pyserial-asyncio + "heatmiserv3": {"pyserial-asyncio"} + }, "hive": { # https://github.com/Pyhass/Pyhiveapi/pull/88 # pyhive-integration > unasync > setuptools "unasync": {"setuptools"} }, + "homeassistant_hardware": { + # https://github.com/zigpy/zigpy/issues/1604 + # universal-silabs-flasher > zigpy > pyserial-asyncio + "zigpy": {"pyserial-asyncio"}, + }, "influxdb": { # https://github.com/influxdata/influxdb-client-python/issues/695 # influxdb-client > setuptools "influxdb-client": {"setuptools"} }, + "insteon": { + # https://github.com/pyinsteon/pyinsteon/issues/430 + # pyinsteon > pyserial-asyncio + "pyinsteon": {"pyserial-asyncio"} + }, "keba": { # https://github.com/jsbronder/asyncio-dgram/issues/20 # keba-kecontact > asyncio-dgram > setuptools @@ -114,11 +156,26 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # pymochad > pbr > setuptools "pbr": {"setuptools"} }, + "monoprice": { + # https://github.com/etsinko/pymonoprice/issues/9 + # pymonoprice > pyserial-asyncio + "pymonoprice": {"pyserial-asyncio"} + }, + "mysensors": { + # https://github.com/theolind/pymysensors/issues/818 + # pymysensors > pyserial-asyncio + "pymysensors": {"pyserial-asyncio"} + }, "mystrom": { # https://github.com/home-assistant-ecosystem/python-mystrom/issues/55 # python-mystrom > setuptools "python-mystrom": {"setuptools"} }, + "ness_alarm": { + # https://github.com/nickw444/nessclient/issues/73 + # nessclient > pyserial-asyncio + "nessclient": {"pyserial-asyncio"} + }, "nx584": { # https://bugs.launchpad.net/python-stevedore/+bug/2111694 # pynx584 > stevedore > pbr > setuptools @@ -149,6 +206,11 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # gpiozero > colorzero > setuptools "colorzero": {"setuptools"} }, + "rflink": { + # https://github.com/aequitas/python-rflink/issues/78 + # rflink > pyserial-asyncio + "rflink": {"pyserial-asyncio"} + }, "system_bridge": { # https://github.com/timmo001/system-bridge-connector/pull/78 # systembridgeconnector > incremental > setuptools @@ -165,7 +227,10 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { "zha": { # https://github.com/waveform80/colorzero/issues/9 # zha > zigpy-zigate > gpiozero > colorzero > setuptools - "colorzero": {"setuptools"} + "colorzero": {"setuptools"}, + # https://github.com/zigpy/zigpy/issues/1604 + # zha > zigpy > pyserial-asyncio + "zigpy": {"pyserial-asyncio"}, }, } @@ -343,8 +408,6 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: all_requirements.add(package) item = deptree.get(package) - if forbidden_package_exceptions: - print(f"Integration {integration.domain}: {item}") if item is None: # Only warn if direct dependencies could not be resolved @@ -358,16 +421,17 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: package_exceptions = forbidden_package_exceptions.get(package, set()) for pkg, version in dependencies.items(): if pkg.startswith("types-") or pkg in FORBIDDEN_PACKAGES: + reason = FORBIDDEN_PACKAGES.get(pkg, "not be a runtime dependency") needs_forbidden_package_exceptions = True if pkg in package_exceptions: integration.add_warning( "requirements", - f"Package {pkg} should not be a runtime dependency in {package}", + f"Package {pkg} should {reason} in {package}", ) else: integration.add_error( "requirements", - f"Package {pkg} should not be a runtime dependency in {package}", + f"Package {pkg} should {reason} in {package}", ) check_dependency_version_range(integration, package, pkg, version) From 01ea58eb9ba5763d6032637509aabfcbc161f43d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 May 2025 11:54:00 -0500 Subject: [PATCH 0929/1175] Bump aiohttp to 3.12.1 (#145627) changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.1rc0...v3.12.1 No changes since 3.12.1rc0, only the version number --- 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 1d89321bacf..98349ca1d66 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.1rc0 +aiohttp==3.12.1 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 011f79c60e4..1fc4a28b9da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.1rc0", + "aiohttp==3.12.1", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 85cdc5f5715..b89c164188e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.1rc0 +aiohttp==3.12.1 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 99ebac5452f1e502bb93076fb5c89db32fd9e213 Mon Sep 17 00:00:00 2001 From: Adrian Freund Date: Mon, 26 May 2025 19:02:52 +0200 Subject: [PATCH 0930/1175] Add translation keys for zha fan states (#145629) --- homeassistant/components/zha/strings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index a330fa6b0ee..33158dacf70 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -659,7 +659,15 @@ }, "fan": { "fan": { - "name": "[%key:component::fan::title%]" + "name": "[%key:component::fan::title%]", + "state_attributes": { + "preset_mode": { + "state": { + "auto": "[%key:common::state::auto%]", + "smart": "Smart" + } + } + } }, "fan_group": { "name": "Fan group" From 03a26836ede59596b5289fe95c8c5f8af5dc0831 Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 26 May 2025 19:13:20 +0200 Subject: [PATCH 0931/1175] Refactor eheimdigital tests to use fixtures (#145428) --- .../components/eheimdigital/light.py | 4 - tests/components/eheimdigital/conftest.py | 96 ++++++++----------- .../fixtures/classic_led_ctrl/acclimate.json | 9 ++ .../fixtures/classic_led_ctrl/ccv.json | 1 + .../fixtures/classic_led_ctrl/clock.json | 13 +++ .../fixtures/classic_led_ctrl/cloud.json | 12 +++ .../fixtures/classic_led_ctrl/moon.json | 8 ++ .../fixtures/classic_led_ctrl/usrdta.json | 35 +++++++ .../classic_vario/classic_vario_data.json | 22 +++++ .../fixtures/classic_vario/usrdta.json | 34 +++++++ .../fixtures/heater/heater_data.json | 20 ++++ .../eheimdigital/fixtures/heater/usrdta.json | 34 +++++++ .../eheimdigital/snapshots/test_light.ambr | 16 ++-- tests/components/eheimdigital/test_climate.py | 38 ++++---- tests/components/eheimdigital/test_light.py | 53 ++++++---- tests/components/eheimdigital/test_number.py | 74 ++++++++------ tests/components/eheimdigital/test_select.py | 22 +++-- tests/components/eheimdigital/test_sensor.py | 63 ++++++++---- tests/components/eheimdigital/test_switch.py | 55 ++++++++--- tests/components/eheimdigital/test_time.py | 50 ++++++---- 20 files changed, 462 insertions(+), 197 deletions(-) create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/acclimate.json create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/ccv.json create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/clock.json create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/cloud.json create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/moon.json create mode 100644 tests/components/eheimdigital/fixtures/classic_led_ctrl/usrdta.json create mode 100644 tests/components/eheimdigital/fixtures/classic_vario/classic_vario_data.json create mode 100644 tests/components/eheimdigital/fixtures/classic_vario/usrdta.json create mode 100644 tests/components/eheimdigital/fixtures/heater/heater_data.json create mode 100644 tests/components/eheimdigital/fixtures/heater/usrdta.json diff --git a/homeassistant/components/eheimdigital/light.py b/homeassistant/components/eheimdigital/light.py index 7960e956859..4e148ee5204 100644 --- a/homeassistant/components/eheimdigital/light.py +++ b/homeassistant/components/eheimdigital/light.py @@ -94,8 +94,6 @@ class EheimDigitalClassicLEDControlLight( await self._device.set_light_mode(EFFECT_TO_LIGHT_MODE[kwargs[ATTR_EFFECT]]) return if ATTR_BRIGHTNESS in kwargs: - if self._device.light_mode == LightMode.DAYCL_MODE: - await self._device.set_light_mode(LightMode.MAN_MODE) await self._device.turn_on( int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])), self._channel, @@ -104,8 +102,6 @@ class EheimDigitalClassicLEDControlLight( @exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - if self._device.light_mode == LightMode.DAYCL_MODE: - await self._device.set_light_mode(LightMode.MAN_MODE) await self._device.turn_off(self._channel) def _async_update_attrs(self) -> None: diff --git a/tests/components/eheimdigital/conftest.py b/tests/components/eheimdigital/conftest.py index 5b828f830a4..c05e95701e1 100644 --- a/tests/components/eheimdigital/conftest.py +++ b/tests/components/eheimdigital/conftest.py @@ -1,7 +1,6 @@ """Configurations for the EHEIM Digital tests.""" from collections.abc import Generator -from datetime import time, timedelta, timezone from unittest.mock import AsyncMock, MagicMock, patch from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl @@ -9,12 +8,13 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario from eheimdigital.heater import EheimDigitalHeater from eheimdigital.hub import EheimDigitalHub from eheimdigital.types import ( - EheimDeviceType, - FilterErrorCode, - FilterMode, - HeaterMode, - HeaterUnit, - LightMode, + AcclimatePacket, + CCVPacket, + ClassicVarioDataPacket, + ClockPacket, + CloudPacket, + MoonPacket, + UsrDtaPacket, ) import pytest @@ -22,7 +22,7 @@ from homeassistant.components.eheimdigital.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_object_fixture @pytest.fixture @@ -36,66 +36,50 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture def classic_led_ctrl_mock(): """Mock a classicLEDcontrol device.""" - classic_led_ctrl_mock = MagicMock(spec=EheimDigitalClassicLEDControl) - classic_led_ctrl_mock.tankconfig = [["CLASSIC_DAYLIGHT"], []] - classic_led_ctrl_mock.mac_address = "00:00:00:00:00:01" - classic_led_ctrl_mock.device_type = ( - EheimDeviceType.VERSION_EHEIM_CLASSIC_LED_CTRL_PLUS_E + classic_led_ctrl = EheimDigitalClassicLEDControl( + MagicMock(spec=EheimDigitalHub), + UsrDtaPacket(load_json_object_fixture("classic_led_ctrl/usrdta.json", DOMAIN)), ) - classic_led_ctrl_mock.name = "Mock classicLEDcontrol+e" - classic_led_ctrl_mock.aquarium_name = "Mock Aquarium" - classic_led_ctrl_mock.sw_version = "1.0.0_1.0.0" - classic_led_ctrl_mock.light_mode = LightMode.DAYCL_MODE - classic_led_ctrl_mock.light_level = (10, 39) - classic_led_ctrl_mock.sys_led = 20 - return classic_led_ctrl_mock + classic_led_ctrl.ccv = CCVPacket( + load_json_object_fixture("classic_led_ctrl/ccv.json", DOMAIN) + ) + classic_led_ctrl.moon = MoonPacket( + load_json_object_fixture("classic_led_ctrl/moon.json", DOMAIN) + ) + classic_led_ctrl.acclimate = AcclimatePacket( + load_json_object_fixture("classic_led_ctrl/acclimate.json", DOMAIN) + ) + classic_led_ctrl.cloud = CloudPacket( + load_json_object_fixture("classic_led_ctrl/cloud.json", DOMAIN) + ) + classic_led_ctrl.clock = ClockPacket( + load_json_object_fixture("classic_led_ctrl/clock.json", DOMAIN) + ) + return classic_led_ctrl @pytest.fixture def heater_mock(): """Mock a Heater device.""" - heater_mock = MagicMock(spec=EheimDigitalHeater) - heater_mock.mac_address = "00:00:00:00:00:02" - heater_mock.device_type = EheimDeviceType.VERSION_EHEIM_EXT_HEATER - heater_mock.name = "Mock Heater" - heater_mock.aquarium_name = "Mock Aquarium" - heater_mock.sw_version = "1.0.0_1.0.0" - heater_mock.temperature_unit = HeaterUnit.CELSIUS - heater_mock.current_temperature = 24.2 - heater_mock.target_temperature = 25.5 - heater_mock.temperature_offset = 0.1 - heater_mock.night_temperature_offset = -0.2 - heater_mock.is_heating = True - heater_mock.is_active = True - heater_mock.operation_mode = HeaterMode.MANUAL - heater_mock.day_start_time = time(8, 0, tzinfo=timezone(timedelta(hours=1))) - heater_mock.night_start_time = time(20, 0, tzinfo=timezone(timedelta(hours=1))) - heater_mock.sys_led = 20 - return heater_mock + heater = EheimDigitalHeater( + MagicMock(spec=EheimDigitalHub), + load_json_object_fixture("heater/usrdta.json", DOMAIN), + ) + heater.heater_data = load_json_object_fixture("heater/heater_data.json", DOMAIN) + return heater @pytest.fixture def classic_vario_mock(): """Mock a classicVARIO device.""" - classic_vario_mock = MagicMock(spec=EheimDigitalClassicVario) - classic_vario_mock.mac_address = "00:00:00:00:00:03" - classic_vario_mock.device_type = EheimDeviceType.VERSION_EHEIM_CLASSIC_VARIO - classic_vario_mock.name = "Mock classicVARIO" - classic_vario_mock.aquarium_name = "Mock Aquarium" - classic_vario_mock.sw_version = "1.0.0_1.0.0" - classic_vario_mock.current_speed = 75 - classic_vario_mock.manual_speed = 75 - classic_vario_mock.day_speed = 80 - classic_vario_mock.day_start_time = time(8, 0, tzinfo=timezone(timedelta(hours=1))) - classic_vario_mock.night_start_time = time( - 20, 0, tzinfo=timezone(timedelta(hours=1)) + classic_vario = EheimDigitalClassicVario( + MagicMock(spec=EheimDigitalHub), + UsrDtaPacket(load_json_object_fixture("classic_vario/usrdta.json", DOMAIN)), ) - classic_vario_mock.night_speed = 20 - classic_vario_mock.is_active = True - classic_vario_mock.filter_mode = FilterMode.MANUAL - classic_vario_mock.error_code = FilterErrorCode.NO_ERROR - classic_vario_mock.service_hours = 360 - return classic_vario_mock + classic_vario.classic_vario_data = ClassicVarioDataPacket( + load_json_object_fixture("classic_vario/classic_vario_data.json", DOMAIN) + ) + return classic_vario @pytest.fixture diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/acclimate.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/acclimate.json new file mode 100644 index 00000000000..43159de0488 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/acclimate.json @@ -0,0 +1,9 @@ +{ + "title": "ACCLIMATE", + "from": "00:00:00:00:00:01", + "duration": 30, + "intensityReduction": 99, + "currentAcclDay": 0, + "acclActive": 0, + "pause": 0 +} diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/ccv.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/ccv.json new file mode 100644 index 00000000000..68f07d97d64 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/ccv.json @@ -0,0 +1 @@ +{ "title": "CCV", "from": "00:00:00:00:00:01", "currentValues": [10, 39] } diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/clock.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/clock.json new file mode 100644 index 00000000000..0606e0154b6 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/clock.json @@ -0,0 +1,13 @@ +{ + "title": "CLOCK", + "from": "00:00:00:00:00:01", + "year": 2025, + "month": 5, + "day": 22, + "hour": 5, + "min": 53, + "sec": 22, + "mode": "DAYCL_MODE", + "valid": 1, + "to": "USER" +} diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/cloud.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/cloud.json new file mode 100644 index 00000000000..d7e18e75943 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/cloud.json @@ -0,0 +1,12 @@ +{ + "title": "CLOUD", + "from": "00:00:00:00:00:01", + "probability": 50, + "maxAmount": 90, + "minIntensity": 60, + "maxIntensity": 100, + "minDuration": 600, + "maxDuration": 1500, + "cloudActive": 1, + "mode": 2 +} diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/moon.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/moon.json new file mode 100644 index 00000000000..6a8ba896902 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/moon.json @@ -0,0 +1,8 @@ +{ + "title": "MOON", + "from": "00:00:00:00:00:01", + "maxmoonlight": 18, + "minmoonlight": 4, + "moonlightActive": 1, + "moonlightCycle": 1 +} diff --git a/tests/components/eheimdigital/fixtures/classic_led_ctrl/usrdta.json b/tests/components/eheimdigital/fixtures/classic_led_ctrl/usrdta.json new file mode 100644 index 00000000000..332e72faabd --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_led_ctrl/usrdta.json @@ -0,0 +1,35 @@ +{ + "title": "USRDTA", + "from": "00:00:00:00:00:01", + "name": "Mock classicLEDcontrol+e", + "aqName": "Mock Aquarium", + "mode": "DAYCL_MODE", + "version": 17, + "language": "EN", + "timezone": 60, + "tID": 30, + "dst": 1, + "tankconfig": "[[],[\"CLASSIC_DAYLIGHT\"]]", + "power": "[[],[14]]", + "netmode": "ST", + "host": "eheimdigital", + "groupID": 0, + "meshing": 1, + "firstStart": 0, + "revision": [2034, 2034], + "build": ["1722600896000", "1722596503307"], + "latestAvailableRevision": [-1, -1, -1, -1], + "firmwareAvailable": 0, + "softChange": 0, + "emailAddr": "", + "stMail": 0, + "stMailMode": 0, + "fstTime": 0, + "sstTime": 0, + "liveTime": 832140, + "usrName": "", + "unit": 0, + "demoUse": 0, + "sysLED": 20, + "to": "USER" +} diff --git a/tests/components/eheimdigital/fixtures/classic_vario/classic_vario_data.json b/tests/components/eheimdigital/fixtures/classic_vario/classic_vario_data.json new file mode 100644 index 00000000000..4065818483c --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_vario/classic_vario_data.json @@ -0,0 +1,22 @@ +{ + "title": "CLASSIC_VARIO_DATA", + "from": "00:00:00:00:00:03", + "rel_speed": 75, + "pumpMode": 16, + "filterActive": 1, + "turnOffTime": 0, + "serviceHour": 360, + "rel_manual_motor_speed": 75, + "rel_motor_speed_day": 80, + "rel_motor_speed_night": 20, + "startTime_day": 480, + "startTime_night": 1200, + "pulse_motorSpeed_High": 100, + "pulse_motorSpeed_Low": 20, + "pulse_Time_High": 100, + "pulse_Time_Low": 50, + "turnTimeFeeding": 0, + "errorCode": 0, + "version": 0, + "to": "USER" +} diff --git a/tests/components/eheimdigital/fixtures/classic_vario/usrdta.json b/tests/components/eheimdigital/fixtures/classic_vario/usrdta.json new file mode 100644 index 00000000000..9c3535e9494 --- /dev/null +++ b/tests/components/eheimdigital/fixtures/classic_vario/usrdta.json @@ -0,0 +1,34 @@ +{ + "title": "USRDTA", + "from": "00:00:00:00:00:03", + "name": "Mock classicVARIO", + "aqName": "Mock Aquarium", + "version": 18, + "language": "EN", + "timezone": 60, + "tID": 30, + "dst": 1, + "tankconfig": "CLASSIC-VARIO", + "power": "9", + "netmode": "ST", + "host": "eheimdigital", + "groupID": 0, + "meshing": 1, + "firstStart": 0, + "revision": [2034, 2034], + "build": ["1722600896000", "1722596503307"], + "latestAvailableRevision": [1024, 1028, 2036, 2036], + "firmwareAvailable": 1, + "softChange": 0, + "emailAddr": "", + "stMail": 0, + "stMailMode": 0, + "fstTime": 720, + "sstTime": 0, + "liveTime": 444600, + "usrName": "", + "unit": 0, + "demoUse": 0, + "sysLED": 100, + "to": "USER" +} diff --git a/tests/components/eheimdigital/fixtures/heater/heater_data.json b/tests/components/eheimdigital/fixtures/heater/heater_data.json new file mode 100644 index 00000000000..ad8ef1be17d --- /dev/null +++ b/tests/components/eheimdigital/fixtures/heater/heater_data.json @@ -0,0 +1,20 @@ +{ + "title": "HEATER_DATA", + "from": "00:00:00:00:00:02", + "mUnit": 0, + "sollTemp": 255, + "isTemp": 242, + "hystLow": 5, + "hystHigh": 5, + "offset": 1, + "active": 1, + "isHeating": 1, + "mode": 0, + "sync": "", + "partnerName": "", + "dayStartT": 480, + "nightStartT": 1200, + "nReduce": -2, + "alertState": 0, + "to": "USER" +} diff --git a/tests/components/eheimdigital/fixtures/heater/usrdta.json b/tests/components/eheimdigital/fixtures/heater/usrdta.json new file mode 100644 index 00000000000..c243ebb03bd --- /dev/null +++ b/tests/components/eheimdigital/fixtures/heater/usrdta.json @@ -0,0 +1,34 @@ +{ + "title": "USRDTA", + "from": "00:00:00:00:00:02", + "name": "Mock Heater", + "aqName": "Mock Aquarium", + "version": 5, + "language": "EN", + "timezone": 60, + "tID": 30, + "dst": 1, + "tankconfig": "HEAT400", + "power": "9", + "netmode": "ST", + "host": "eheimdigital", + "groupID": 0, + "meshing": 1, + "firstStart": 0, + "remote": 0, + "revision": [1021, 1024], + "build": ["1718889198000", "1718868200327"], + "latestAvailableRevision": [-1, -1, -1, -1], + "firmwareAvailable": 0, + "emailAddr": "", + "stMail": 0, + "stMailMode": 0, + "fstTime": 0, + "sstTime": 0, + "liveTime": 302580, + "usrName": "", + "unit": 0, + "demoUse": 0, + "sysLED": 20, + "to": "USER" +} diff --git a/tests/components/eheimdigital/snapshots/test_light.ambr b/tests/components/eheimdigital/snapshots/test_light.ambr index a8b454f416e..b2398a6a419 100644 --- a/tests/components/eheimdigital/snapshots/test_light.ambr +++ b/tests/components/eheimdigital/snapshots/test_light.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_dynamic_new_devices[light.mock_classicledcontrol_e_channel_0-entry] +# name: test_dynamic_new_devices[light.mock_classicledcontrol_e_channel_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -19,7 +19,7 @@ 'disabled_by': None, 'domain': 'light', 'entity_category': None, - 'entity_id': 'light.mock_classicledcontrol_e_channel_0', + 'entity_id': 'light.mock_classicledcontrol_e_channel_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -31,32 +31,32 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Channel 0', + 'original_name': 'Channel 1', 'platform': 'eheimdigital', 'previous_unique_id': None, 'supported_features': , 'translation_key': 'channel', - 'unique_id': '00:00:00:00:00:01_0', + 'unique_id': '00:00:00:00:00:01_1', 'unit_of_measurement': None, }) # --- -# name: test_dynamic_new_devices[light.mock_classicledcontrol_e_channel_0-state] +# name: test_dynamic_new_devices[light.mock_classicledcontrol_e_channel_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'brightness': 26, + 'brightness': 99, 'color_mode': , 'effect': 'daycl_mode', 'effect_list': list([ 'daycl_mode', ]), - 'friendly_name': 'Mock classicLEDcontrol+e Channel 0', + 'friendly_name': 'Mock classicLEDcontrol+e Channel 1', 'supported_color_modes': list([ , ]), 'supported_features': , }), 'context': , - 'entity_id': 'light.mock_classicledcontrol_e_channel_0', + 'entity_id': 'light.mock_classicledcontrol_e_channel_1', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/eheimdigital/test_climate.py b/tests/components/eheimdigital/test_climate.py index 4abc33e449e..492d001953c 100644 --- a/tests/components/eheimdigital/test_climate.py +++ b/tests/components/eheimdigital/test_climate.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch +from eheimdigital.heater import EheimDigitalHeater from eheimdigital.types import ( EheimDeviceType, EheimDigitalClientError, @@ -67,7 +68,7 @@ async def test_setup_heater( async def test_dynamic_new_devices( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, - heater_mock: MagicMock, + heater_mock: EheimDigitalHeater, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, mock_config_entry: MockConfigEntry, @@ -116,7 +117,7 @@ async def test_dynamic_new_devices( async def test_set_preset_mode( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, - heater_mock: MagicMock, + heater_mock: EheimDigitalHeater, mock_config_entry: MockConfigEntry, preset_mode: str, heater_mode: HeaterMode, @@ -129,7 +130,7 @@ async def test_set_preset_mode( ) await hass.async_block_till_done() - heater_mock.set_operation_mode.side_effect = EheimDigitalClientError + heater_mock.hub.send_packet.side_effect = EheimDigitalClientError with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -139,7 +140,7 @@ async def test_set_preset_mode( blocking=True, ) - heater_mock.set_operation_mode.side_effect = None + heater_mock.hub.send_packet.side_effect = None await hass.services.async_call( CLIMATE_DOMAIN, @@ -148,7 +149,8 @@ async def test_set_preset_mode( blocking=True, ) - heater_mock.set_operation_mode.assert_awaited_with(heater_mode) + calls = [call for call in heater_mock.hub.mock_calls if call[0] == "send_packet"] + assert len(calls) == 2 and calls[1][1][0]["mode"] == int(heater_mode) async def test_set_temperature( @@ -165,7 +167,7 @@ async def test_set_temperature( ) await hass.async_block_till_done() - heater_mock.set_target_temperature.side_effect = EheimDigitalClientError + heater_mock.hub.send_packet.side_effect = EheimDigitalClientError with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -175,7 +177,7 @@ async def test_set_temperature( blocking=True, ) - heater_mock.set_target_temperature.side_effect = None + heater_mock.hub.send_packet.side_effect = None await hass.services.async_call( CLIMATE_DOMAIN, @@ -184,7 +186,8 @@ async def test_set_temperature( blocking=True, ) - heater_mock.set_target_temperature.assert_awaited_with(26.0) + calls = [call for call in heater_mock.hub.mock_calls if call[0] == "send_packet"] + assert len(calls) == 2 and calls[1][1][0]["sollTemp"] == 260 @pytest.mark.parametrize( @@ -206,7 +209,7 @@ async def test_set_hvac_mode( ) await hass.async_block_till_done() - heater_mock.set_active.side_effect = EheimDigitalClientError + heater_mock.hub.send_packet.side_effect = EheimDigitalClientError with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -216,7 +219,7 @@ async def test_set_hvac_mode( blocking=True, ) - heater_mock.set_active.side_effect = None + heater_mock.hub.send_packet.side_effect = None await hass.services.async_call( CLIMATE_DOMAIN, @@ -225,19 +228,20 @@ async def test_set_hvac_mode( blocking=True, ) - heater_mock.set_active.assert_awaited_with(active=active) + calls = [call for call in heater_mock.hub.mock_calls if call[0] == "send_packet"] + assert len(calls) == 2 and calls[1][1][0]["active"] == int(active) async def test_state_update( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - heater_mock: MagicMock, + heater_mock: EheimDigitalHeater, ) -> None: """Test the climate state update.""" - heater_mock.temperature_unit = HeaterUnit.FAHRENHEIT - heater_mock.is_heating = False - heater_mock.operation_mode = HeaterMode.BIO + heater_mock.heater_data["mUnit"] = int(HeaterUnit.FAHRENHEIT) + heater_mock.heater_data["isHeating"] = int(False) + heater_mock.heater_data["mode"] = int(HeaterMode.BIO) await init_integration(hass, mock_config_entry) @@ -251,8 +255,8 @@ async def test_state_update( assert state.attributes["hvac_action"] == HVACAction.IDLE assert state.attributes["preset_mode"] == HEATER_BIO_MODE - heater_mock.is_active = False - heater_mock.operation_mode = HeaterMode.SMART + heater_mock.heater_data["active"] = int(False) + heater_mock.heater_data["mode"] = int(HeaterMode.SMART) await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() diff --git a/tests/components/eheimdigital/test_light.py b/tests/components/eheimdigital/test_light.py index 81b63218085..c6b2063ec0c 100644 --- a/tests/components/eheimdigital/test_light.py +++ b/tests/components/eheimdigital/test_light.py @@ -4,7 +4,8 @@ from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientError -from eheimdigital.types import EheimDeviceType, LightMode +from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl +from eheimdigital.types import EheimDeviceType from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -117,7 +118,7 @@ async def test_dynamic_new_devices( async def test_turn_off( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - classic_led_ctrl_mock: MagicMock, + classic_led_ctrl_mock: EheimDigitalClassicLEDControl, ) -> None: """Test turning off the light.""" await init_integration(hass, mock_config_entry) @@ -130,12 +131,18 @@ async def test_turn_off( await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_0"}, + {ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_1"}, blocking=True, ) - classic_led_ctrl_mock.set_light_mode.assert_awaited_once_with(LightMode.MAN_MODE) - classic_led_ctrl_mock.turn_off.assert_awaited_once_with(0) + calls = [ + call + for call in classic_led_ctrl_mock.hub.mock_calls + if call[0] == "send_packet" + ] + assert len(calls) == 2 + assert calls[0][1][0].get("title") == "MAN_MODE" + assert calls[1][1][0]["currentValues"][1] == 0 @pytest.mark.parametrize( @@ -150,7 +157,7 @@ async def test_turn_on_brightness( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - classic_led_ctrl_mock: MagicMock, + classic_led_ctrl_mock: EheimDigitalClassicLEDControl, dim_input: int, expected_dim_value: int, ) -> None: @@ -166,24 +173,30 @@ async def test_turn_on_brightness( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_0", + ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_1", ATTR_BRIGHTNESS: dim_input, }, blocking=True, ) - classic_led_ctrl_mock.set_light_mode.assert_awaited_once_with(LightMode.MAN_MODE) - classic_led_ctrl_mock.turn_on.assert_awaited_once_with(expected_dim_value, 0) + calls = [ + call + for call in classic_led_ctrl_mock.hub.mock_calls + if call[0] == "send_packet" + ] + assert len(calls) == 2 + assert calls[0][1][0].get("title") == "MAN_MODE" + assert calls[1][1][0]["currentValues"][1] == expected_dim_value async def test_turn_on_effect( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - classic_led_ctrl_mock: MagicMock, + classic_led_ctrl_mock: EheimDigitalClassicLEDControl, ) -> None: """Test turning on the light with an effect value.""" - classic_led_ctrl_mock.light_mode = LightMode.MAN_MODE + classic_led_ctrl_mock.clock["mode"] = "MAN_MODE" await init_integration(hass, mock_config_entry) @@ -196,20 +209,26 @@ async def test_turn_on_effect( LIGHT_DOMAIN, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_0", + ATTR_ENTITY_ID: "light.mock_classicledcontrol_e_channel_1", ATTR_EFFECT: EFFECT_DAYCL_MODE, }, blocking=True, ) - classic_led_ctrl_mock.set_light_mode.assert_awaited_once_with(LightMode.DAYCL_MODE) + calls = [ + call + for call in classic_led_ctrl_mock.hub.mock_calls + if call[0] == "send_packet" + ] + assert len(calls) == 1 + assert calls[0][1][0].get("title") == "DAYCL_MODE" async def test_state_update( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - classic_led_ctrl_mock: MagicMock, + classic_led_ctrl_mock: EheimDigitalClassicLEDControl, ) -> None: """Test the light state update.""" await init_integration(hass, mock_config_entry) @@ -219,11 +238,11 @@ async def test_state_update( ) await hass.async_block_till_done() - classic_led_ctrl_mock.light_level = (20, 30) + classic_led_ctrl_mock.ccv["currentValues"] = [30, 20] await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() - assert (state := hass.states.get("light.mock_classicledcontrol_e_channel_0")) + assert (state := hass.states.get("light.mock_classicledcontrol_e_channel_1")) assert state.attributes["brightness"] == value_to_brightness((1, 100), 20) @@ -248,6 +267,6 @@ async def test_update_failed( await hass.async_block_till_done() assert ( - hass.states.get("light.mock_classicledcontrol_e_channel_0").state + hass.states.get("light.mock_classicledcontrol_e_channel_1").state == STATE_UNAVAILABLE ) diff --git a/tests/components/eheimdigital/test_number.py b/tests/components/eheimdigital/test_number.py index a23f461744a..5235dfcdb75 100644 --- a/tests/components/eheimdigital/test_number.py +++ b/tests/components/eheimdigital/test_number.py @@ -58,20 +58,20 @@ async def test_setup( ( "number.mock_heater_temperature_offset", 0.4, - "set_temperature_offset", - (0.4,), + "offset", + 4, ), ( "number.mock_heater_night_temperature_offset", 0.4, - "set_night_temperature_offset", - (0.4,), + "nReduce", + 4, ), ( "number.mock_heater_system_led_brightness", 20, - "set_sys_led", - (20,), + "sysLED", + 20, ), ], ), @@ -81,26 +81,26 @@ async def test_setup( ( "number.mock_classicvario_manual_speed", 72.1, - "set_manual_speed", - (int(72.1),), + "rel_manual_motor_speed", + int(72.1), ), ( "number.mock_classicvario_day_speed", 72.1, - "set_day_speed", - (int(72.1),), + "rel_motor_speed_day", + int(72.1), ), ( "number.mock_classicvario_night_speed", 72.1, - "set_night_speed", - (int(72.1),), + "rel_motor_speed_night", + int(72.1), ), ( "number.mock_classicvario_system_led_brightness", 20, - "set_sys_led", - (20,), + "sysLED", + 20, ), ], ), @@ -131,8 +131,8 @@ async def test_set_value( {ATTR_ENTITY_ID: item[0], ATTR_VALUE: item[1]}, blocking=True, ) - calls = [call for call in device.mock_calls if call[0] == item[2]] - assert len(calls) == 1 and calls[0][1] == item[3] + calls = [call for call in device.hub.mock_calls if call[0] == "send_packet"] + assert calls[-1][1][0][item[2]] == item[3] @pytest.mark.usefixtures("classic_vario_mock", "heater_mock") @@ -144,17 +144,23 @@ async def test_set_value( [ ( "number.mock_heater_temperature_offset", - "temperature_offset", + "heater_data", + "offset", + -11, -1.1, ), ( "number.mock_heater_night_temperature_offset", - "night_temperature_offset", - 2.3, + "heater_data", + "nReduce", + -23, + -2.3, ), ( "number.mock_heater_system_led_brightness", - "sys_led", + "usrdta", + "sysLED", + 87, 87, ), ], @@ -164,23 +170,31 @@ async def test_set_value( [ ( "number.mock_classicvario_manual_speed", - "manual_speed", + "classic_vario_data", + "rel_manual_motor_speed", + 34, 34, ), ( "number.mock_classicvario_day_speed", - "day_speed", - 79, + "classic_vario_data", + "rel_motor_speed_day", + 72, + 72, ), ( "number.mock_classicvario_night_speed", - "night_speed", - 12, + "classic_vario_data", + "rel_motor_speed_night", + 20, + 20, ), ( "number.mock_classicvario_system_led_brightness", - "sys_led", - 35, + "usrdta", + "sysLED", + 20, + 20, ), ], ), @@ -191,7 +205,7 @@ async def test_state_update( eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, device_name: str, - entity_list: list[tuple[str, str, float]], + entity_list: list[tuple[str, str, str, float, float]], request: pytest.FixtureRequest, ) -> None: """Test state updates.""" @@ -205,7 +219,7 @@ async def test_state_update( await hass.async_block_till_done() for item in entity_list: - setattr(device, item[1], item[2]) + getattr(device, item[1])[item[2]] = item[3] await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() assert (state := hass.states.get(item[0])) - assert state.state == str(item[2]) + assert state.state == str(item[4]) diff --git a/tests/components/eheimdigital/test_select.py b/tests/components/eheimdigital/test_select.py index 89ec91b62a0..ab577bbe0aa 100644 --- a/tests/components/eheimdigital/test_select.py +++ b/tests/components/eheimdigital/test_select.py @@ -59,8 +59,8 @@ async def test_setup( ( "select.mock_classicvario_filter_mode", "manual", - "set_filter_mode", - (FilterMode.MANUAL,), + "pumpMode", + int(FilterMode.MANUAL), ), ], ), @@ -71,7 +71,7 @@ async def test_set_value( eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, device_name: str, - entity_list: list[tuple[str, float, str, tuple[FilterMode]]], + entity_list: list[tuple[str, str, str, int]], request: pytest.FixtureRequest, ) -> None: """Test setting a value.""" @@ -91,8 +91,8 @@ async def test_set_value( {ATTR_ENTITY_ID: item[0], ATTR_OPTION: item[1]}, blocking=True, ) - calls = [call for call in device.mock_calls if call[0] == item[2]] - assert len(calls) == 1 and calls[0][1] == item[3] + calls = [call for call in device.hub.mock_calls if call[0] == "send_packet"] + assert calls[-1][1][0][item[2]] == item[3] @pytest.mark.usefixtures("classic_vario_mock", "heater_mock") @@ -104,8 +104,10 @@ async def test_set_value( [ ( "select.mock_classicvario_filter_mode", - "filter_mode", - FilterMode.BIO, + "classic_vario_data", + "pumpMode", + int(FilterMode.BIO), + "bio", ), ], ), @@ -116,7 +118,7 @@ async def test_state_update( eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, device_name: str, - entity_list: list[tuple[str, str, FilterMode]], + entity_list: list[tuple[str, str, str, int, str]], request: pytest.FixtureRequest, ) -> None: """Test state updates.""" @@ -130,7 +132,7 @@ async def test_state_update( await hass.async_block_till_done() for item in entity_list: - setattr(device, item[1], item[2]) + getattr(device, item[1])[item[2]] = item[3] await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() assert (state := hass.states.get(item[0])) - assert state.state == item[2].name.lower() + assert state.state == item[4] diff --git a/tests/components/eheimdigital/test_sensor.py b/tests/components/eheimdigital/test_sensor.py index ece4d3eb241..42df22368a9 100644 --- a/tests/components/eheimdigital/test_sensor.py +++ b/tests/components/eheimdigital/test_sensor.py @@ -43,35 +43,58 @@ async def test_setup_classic_vario( await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) +@pytest.mark.usefixtures("classic_vario_mock") +@pytest.mark.parametrize( + ("device_name", "entity_list"), + [ + ( + "classic_vario_mock", + [ + ( + "sensor.mock_classicvario_current_speed", + "classic_vario_data", + "rel_speed", + 10, + 10, + ), + ( + "sensor.mock_classicvario_error_code", + "classic_vario_data", + "errorCode", + int(FilterErrorCode.ROTOR_STUCK), + "rotor_stuck", + ), + ( + "sensor.mock_classicvario_remaining_hours_until_service", + "classic_vario_data", + "serviceHour", + 100, + str(round(100 / 24, 1)), + ), + ], + ), + ], +) async def test_state_update( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - classic_vario_mock: MagicMock, + device_name: str, + entity_list: list[tuple[str, str, str, float, float]], + request: pytest.FixtureRequest, ) -> None: """Test the sensor state update.""" + device: MagicMock = request.getfixturevalue(device_name) await init_integration(hass, mock_config_entry) await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( - "00:00:00:00:00:03", EheimDeviceType.VERSION_EHEIM_CLASSIC_VARIO + device.mac_address, device.device_type ) + await hass.async_block_till_done() - classic_vario_mock.current_speed = 10 - classic_vario_mock.error_code = FilterErrorCode.ROTOR_STUCK - classic_vario_mock.service_hours = 100 - - await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() - - assert (state := hass.states.get("sensor.mock_classicvario_current_speed")) - assert state.state == "10" - - assert (state := hass.states.get("sensor.mock_classicvario_error_code")) - assert state.state == "rotor_stuck" - - assert ( - state := hass.states.get( - "sensor.mock_classicvario_remaining_hours_until_service" - ) - ) - assert state.state == str(round(100 / 24, 1)) + for item in entity_list: + getattr(device, item[1])[item[2]] = item[3] + await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() + assert (state := hass.states.get(item[0])) + assert state.state == str(item[4]) diff --git a/tests/components/eheimdigital/test_switch.py b/tests/components/eheimdigital/test_switch.py index 440e4776b37..4195c059504 100644 --- a/tests/components/eheimdigital/test_switch.py +++ b/tests/components/eheimdigital/test_switch.py @@ -11,8 +11,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, Platform, ) from homeassistant.core import HomeAssistant @@ -77,29 +75,58 @@ async def test_turn_on_off( blocking=True, ) - classic_vario_mock.set_active.assert_awaited_once_with(active=active) + calls = [ + call for call in classic_vario_mock.hub.mock_calls if call[0] == "send_packet" + ] + assert len(calls) == 1 + assert calls[0][1][0].get("filterActive") == int(active) +@pytest.mark.usefixtures("classic_vario_mock") +@pytest.mark.parametrize( + ("device_name", "entity_list"), + [ + ( + "classic_vario_mock", + [ + ( + "switch.mock_classicvario", + "classic_vario_data", + "filterActive", + 1, + "on", + ), + ( + "switch.mock_classicvario", + "classic_vario_data", + "filterActive", + 0, + "off", + ), + ], + ), + ], +) async def test_state_update( hass: HomeAssistant, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, - classic_vario_mock: MagicMock, + device_name: str, + entity_list: list[tuple[str, str, str, float, float]], + request: pytest.FixtureRequest, ) -> None: """Test the switch state update.""" + device: MagicMock = request.getfixturevalue(device_name) await init_integration(hass, mock_config_entry) await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( - "00:00:00:00:00:03", EheimDeviceType.VERSION_EHEIM_CLASSIC_VARIO + device.mac_address, device.device_type ) + await hass.async_block_till_done() - assert (state := hass.states.get("switch.mock_classicvario")) - assert state.state == STATE_ON - - classic_vario_mock.is_active = False - - await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() - - assert (state := hass.states.get("switch.mock_classicvario")) - assert state.state == STATE_OFF + for item in entity_list: + getattr(device, item[1])[item[2]] = item[3] + await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() + assert (state := hass.states.get(item[0])) + assert state.state == str(item[4]) diff --git a/tests/components/eheimdigital/test_time.py b/tests/components/eheimdigital/test_time.py index acb96ae4023..990a086e633 100644 --- a/tests/components/eheimdigital/test_time.py +++ b/tests/components/eheimdigital/test_time.py @@ -59,14 +59,14 @@ async def test_setup( ( "time.mock_heater_day_start_time", time(9, 0, tzinfo=timezone(timedelta(hours=1))), - "set_day_start_time", - (time(9, 0, tzinfo=timezone(timedelta(hours=1))),), + "dayStartT", + 9 * 60, ), ( "time.mock_heater_night_start_time", time(19, 0, tzinfo=timezone(timedelta(hours=1))), - "set_night_start_time", - (time(19, 0, tzinfo=timezone(timedelta(hours=1))),), + "nightStartT", + 19 * 60, ), ], ), @@ -76,14 +76,14 @@ async def test_setup( ( "time.mock_classicvario_day_start_time", time(9, 0, tzinfo=timezone(timedelta(hours=1))), - "set_day_start_time", - (time(9, 0, tzinfo=timezone(timedelta(hours=1))),), + "startTime_day", + 9 * 60, ), ( "time.mock_classicvario_night_start_time", time(19, 0, tzinfo=timezone(timedelta(hours=1))), - "set_night_start_time", - (time(19, 0, tzinfo=timezone(timedelta(hours=1))),), + "startTime_night", + 19 * 60, ), ], ), @@ -114,8 +114,8 @@ async def test_set_value( {ATTR_ENTITY_ID: item[0], ATTR_TIME: item[1]}, blocking=True, ) - calls = [call for call in device.mock_calls if call[0] == item[2]] - assert len(calls) == 1 and calls[0][1] == item[3] + calls = [call for call in device.hub.mock_calls if call[0] == "send_packet"] + assert calls[-1][1][0][item[2]] == item[3] @pytest.mark.usefixtures("classic_vario_mock", "heater_mock") @@ -127,13 +127,17 @@ async def test_set_value( [ ( "time.mock_heater_day_start_time", - "day_start_time", - time(9, 0, tzinfo=timezone(timedelta(hours=3))), + "heater_data", + "dayStartT", + 540, + time(9, 0, tzinfo=timezone(timedelta(hours=1))).isoformat(), ), ( "time.mock_heater_night_start_time", - "night_start_time", - time(19, 0, tzinfo=timezone(timedelta(hours=3))), + "heater_data", + "nightStartT", + 1140, + time(19, 0, tzinfo=timezone(timedelta(hours=1))).isoformat(), ), ], ), @@ -142,13 +146,17 @@ async def test_set_value( [ ( "time.mock_classicvario_day_start_time", - "day_start_time", - time(9, 0, tzinfo=timezone(timedelta(hours=1))), + "classic_vario_data", + "startTime_day", + 540, + time(9, 0, tzinfo=timezone(timedelta(hours=1))).isoformat(), ), ( "time.mock_classicvario_night_start_time", - "night_start_time", - time(22, 0, tzinfo=timezone(timedelta(hours=1))), + "classic_vario_data", + "startTime_night", + 1320, + time(22, 0, tzinfo=timezone(timedelta(hours=1))).isoformat(), ), ], ), @@ -159,7 +167,7 @@ async def test_state_update( eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, device_name: str, - entity_list: list[tuple[str, str, time]], + entity_list: list[tuple[str, str, str, float, str]], request: pytest.FixtureRequest, ) -> None: """Test state updates.""" @@ -173,7 +181,7 @@ async def test_state_update( await hass.async_block_till_done() for item in entity_list: - setattr(device, item[1], item[2]) + getattr(device, item[1])[item[2]] = item[3] await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() assert (state := hass.states.get(item[0])) - assert state.state == item[2].isoformat() + assert state.state == item[4] From bf92db6fd577407c3fcd78ecd30f1c5ef862f0af Mon Sep 17 00:00:00 2001 From: Sid <27780930+autinerd@users.noreply.github.com> Date: Mon, 26 May 2025 19:25:15 +0200 Subject: [PATCH 0932/1175] Add diagnostics to eheimdigital (#145382) * Add diagnotics to eheimdigital * Diagnostics are now with data in tests --- .../components/eheimdigital/diagnostics.py | 19 ++ .../eheimdigital/quality_scale.yaml | 2 +- .../snapshots/test_diagnostics.ambr | 261 ++++++++++++++++++ .../eheimdigital/test_diagnostics.py | 39 +++ 4 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/eheimdigital/diagnostics.py create mode 100644 tests/components/eheimdigital/snapshots/test_diagnostics.ambr create mode 100644 tests/components/eheimdigital/test_diagnostics.py diff --git a/homeassistant/components/eheimdigital/diagnostics.py b/homeassistant/components/eheimdigital/diagnostics.py new file mode 100644 index 00000000000..208131beabe --- /dev/null +++ b/homeassistant/components/eheimdigital/diagnostics.py @@ -0,0 +1,19 @@ +"""Diagnostics for the EHEIM Digital integration.""" + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.core import HomeAssistant + +from .coordinator import EheimDigitalConfigEntry + +TO_REDACT = {"emailAddr", "usrName"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: EheimDigitalConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return async_redact_data( + {"entry": entry.as_dict(), "data": entry.runtime_data.data}, TO_REDACT + ) diff --git a/homeassistant/components/eheimdigital/quality_scale.yaml b/homeassistant/components/eheimdigital/quality_scale.yaml index fa13c9bf4ca..c1490b352c2 100644 --- a/homeassistant/components/eheimdigital/quality_scale.yaml +++ b/homeassistant/components/eheimdigital/quality_scale.yaml @@ -43,7 +43,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: done discovery: done docs-data-update: todo diff --git a/tests/components/eheimdigital/snapshots/test_diagnostics.ambr b/tests/components/eheimdigital/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..a60952b0ef5 --- /dev/null +++ b/tests/components/eheimdigital/snapshots/test_diagnostics.ambr @@ -0,0 +1,261 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'data': dict({ + '00:00:00:00:00:01': dict({ + 'acclimate': dict({ + 'acclActive': 0, + 'currentAcclDay': 0, + 'duration': 30, + 'from': '00:00:00:00:00:01', + 'intensityReduction': 99, + 'pause': 0, + 'title': 'ACCLIMATE', + }), + 'ccv': dict({ + 'currentValues': list([ + 10, + 39, + ]), + 'from': '00:00:00:00:00:01', + 'title': 'CCV', + }), + 'clock': dict({ + 'day': 22, + 'from': '00:00:00:00:00:01', + 'hour': 5, + 'min': 53, + 'mode': 'DAYCL_MODE', + 'month': 5, + 'sec': 22, + 'title': 'CLOCK', + 'to': 'USER', + 'valid': 1, + 'year': 2025, + }), + 'cloud': dict({ + 'cloudActive': 1, + 'from': '00:00:00:00:00:01', + 'maxAmount': 90, + 'maxDuration': 1500, + 'maxIntensity': 100, + 'minDuration': 600, + 'minIntensity': 60, + 'mode': 2, + 'probability': 50, + 'title': 'CLOUD', + }), + 'moon': dict({ + 'from': '00:00:00:00:00:01', + 'maxmoonlight': 18, + 'minmoonlight': 4, + 'moonlightActive': 1, + 'moonlightCycle': 1, + 'title': 'MOON', + }), + 'usrdta': dict({ + 'aqName': 'Mock Aquarium', + 'build': list([ + '1722600896000', + '1722596503307', + ]), + 'demoUse': 0, + 'dst': 1, + 'emailAddr': '', + 'firmwareAvailable': 0, + 'firstStart': 0, + 'from': '00:00:00:00:00:01', + 'fstTime': 0, + 'groupID': 0, + 'host': 'eheimdigital', + 'language': 'EN', + 'latestAvailableRevision': list([ + -1, + -1, + -1, + -1, + ]), + 'liveTime': 832140, + 'meshing': 1, + 'mode': 'DAYCL_MODE', + 'name': 'Mock classicLEDcontrol+e', + 'netmode': 'ST', + 'power': '[[],[14]]', + 'revision': list([ + 2034, + 2034, + ]), + 'softChange': 0, + 'sstTime': 0, + 'stMail': 0, + 'stMailMode': 0, + 'sysLED': 20, + 'tID': 30, + 'tankconfig': '[[],["CLASSIC_DAYLIGHT"]]', + 'timezone': 60, + 'title': 'USRDTA', + 'to': 'USER', + 'unit': 0, + 'usrName': '', + 'version': 17, + }), + }), + '00:00:00:00:00:02': dict({ + 'heater_data': dict({ + 'active': 1, + 'alertState': 0, + 'dayStartT': 480, + 'from': '00:00:00:00:00:02', + 'hystHigh': 5, + 'hystLow': 5, + 'isHeating': 1, + 'isTemp': 242, + 'mUnit': 0, + 'mode': 0, + 'nReduce': -2, + 'nightStartT': 1200, + 'offset': 1, + 'partnerName': '', + 'sollTemp': 255, + 'sync': '', + 'title': 'HEATER_DATA', + 'to': 'USER', + }), + 'usrdta': dict({ + 'aqName': 'Mock Aquarium', + 'build': list([ + '1718889198000', + '1718868200327', + ]), + 'demoUse': 0, + 'dst': 1, + 'emailAddr': '', + 'firmwareAvailable': 0, + 'firstStart': 0, + 'from': '00:00:00:00:00:02', + 'fstTime': 0, + 'groupID': 0, + 'host': 'eheimdigital', + 'language': 'EN', + 'latestAvailableRevision': list([ + -1, + -1, + -1, + -1, + ]), + 'liveTime': 302580, + 'meshing': 1, + 'name': 'Mock Heater', + 'netmode': 'ST', + 'power': '9', + 'remote': 0, + 'revision': list([ + 1021, + 1024, + ]), + 'sstTime': 0, + 'stMail': 0, + 'stMailMode': 0, + 'sysLED': 20, + 'tID': 30, + 'tankconfig': 'HEAT400', + 'timezone': 60, + 'title': 'USRDTA', + 'to': 'USER', + 'unit': 0, + 'usrName': '', + 'version': 5, + }), + }), + '00:00:00:00:00:03': dict({ + 'classic_vario_data': dict({ + 'errorCode': 0, + 'filterActive': 1, + 'from': '00:00:00:00:00:03', + 'pulse_Time_High': 100, + 'pulse_Time_Low': 50, + 'pulse_motorSpeed_High': 100, + 'pulse_motorSpeed_Low': 20, + 'pumpMode': 16, + 'rel_manual_motor_speed': 75, + 'rel_motor_speed_day': 80, + 'rel_motor_speed_night': 20, + 'rel_speed': 75, + 'serviceHour': 360, + 'startTime_day': 480, + 'startTime_night': 1200, + 'title': 'CLASSIC_VARIO_DATA', + 'to': 'USER', + 'turnOffTime': 0, + 'turnTimeFeeding': 0, + 'version': 0, + }), + 'usrdta': dict({ + 'aqName': 'Mock Aquarium', + 'build': list([ + '1722600896000', + '1722596503307', + ]), + 'demoUse': 0, + 'dst': 1, + 'emailAddr': '', + 'firmwareAvailable': 1, + 'firstStart': 0, + 'from': '00:00:00:00:00:03', + 'fstTime': 720, + 'groupID': 0, + 'host': 'eheimdigital', + 'language': 'EN', + 'latestAvailableRevision': list([ + 1024, + 1028, + 2036, + 2036, + ]), + 'liveTime': 444600, + 'meshing': 1, + 'name': 'Mock classicVARIO', + 'netmode': 'ST', + 'power': '9', + 'revision': list([ + 2034, + 2034, + ]), + 'softChange': 0, + 'sstTime': 0, + 'stMail': 0, + 'stMailMode': 0, + 'sysLED': 100, + 'tID': 30, + 'tankconfig': 'CLASSIC-VARIO', + 'timezone': 60, + 'title': 'USRDTA', + 'to': 'USER', + 'unit': 0, + 'usrName': '', + 'version': 18, + }), + }), + }), + 'entry': dict({ + 'data': dict({ + 'host': 'eheimdigital', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'eheimdigital', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': 'Mock Title', + 'unique_id': '00:00:00:00:00:01', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/eheimdigital/test_diagnostics.py b/tests/components/eheimdigital/test_diagnostics.py new file mode 100644 index 00000000000..878bc1eb1cc --- /dev/null +++ b/tests/components/eheimdigital/test_diagnostics.py @@ -0,0 +1,39 @@ +"""Tests for the diagnostics module.""" + +from unittest.mock import MagicMock + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from .conftest import init_integration + +from tests.common import MockConfigEntry +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, + snapshot: SnapshotAssertion, + eheimdigital_hub_mock: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry diagnostics.""" + + await init_integration(hass, mock_config_entry) + + for device in eheimdigital_hub_mock.return_value.devices.values(): + await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"]( + device.mac_address, device.device_type + ) + + mock_config_entry.runtime_data.data = eheimdigital_hub_mock.return_value.devices + + result = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + assert result == snapshot(exclude=props("created_at", "modified_at", "entry_id")) From 4e1d5fbeb00ca5264fae43c508b2eaeb0a2b741f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 26 May 2025 19:28:27 +0200 Subject: [PATCH 0933/1175] Add WS command to help reset custom entity_id (#145504) * Add WS command to help reset custom entity_id * Calculate suggested object id from entity properties * Fix logic and add additional tests * Adjust test * Update folder_watcher test * Handle current entity id matches the automatic entity id * Don't store calculated_object_id * Update snapshots * Update snapshots * Update test * Tweak logic for reusing current entity_id * Improve test * Don't assign same entity_id to several entities * Prioritize custom entity name * Update snapshots * Update snapshots --- .../components/config/entity_registry.py | 57 +++ homeassistant/helpers/entity_component.py | 47 +- homeassistant/helpers/entity_platform.py | 56 ++- homeassistant/helpers/entity_registry.py | 22 +- tests/common.py | 1 + .../acaia/snapshots/test_binary_sensor.ambr | 1 + .../acaia/snapshots/test_button.ambr | 3 + .../acaia/snapshots/test_sensor.ambr | 3 + .../accuweather/snapshots/test_sensor.ambr | 131 +++++ .../accuweather/snapshots/test_weather.ambr | 1 + .../airgradient/snapshots/test_button.ambr | 3 + .../airgradient/snapshots/test_number.ambr | 2 + .../airgradient/snapshots/test_select.ambr | 11 + .../airgradient/snapshots/test_sensor.ambr | 29 ++ .../airgradient/snapshots/test_switch.ambr | 1 + .../airgradient/snapshots/test_update.ambr | 1 + .../airly/snapshots/test_sensor.ambr | 11 + .../airtouch5/snapshots/test_cover.ambr | 2 + .../airzone/snapshots/test_sensor.ambr | 24 + .../snapshots/test_binary_sensor.ambr | 2 + .../amazon_devices/snapshots/test_notify.ambr | 2 + .../amazon_devices/snapshots/test_switch.ambr | 1 + .../snapshots/test_sensor.ambr | 50 ++ .../snapshots/test_sensor.ambr | 7 + .../aosmith/snapshots/test_sensor.ambr | 2 + .../aosmith/snapshots/test_water_heater.ambr | 2 + .../apcupsd/snapshots/test_binary_sensor.ambr | 1 + .../apcupsd/snapshots/test_sensor.ambr | 41 ++ .../snapshots/test_binary_sensor.ambr | 4 + .../apsystems/snapshots/test_number.ambr | 1 + .../apsystems/snapshots/test_sensor.ambr | 9 + .../apsystems/snapshots/test_switch.ambr | 1 + .../aquacell/snapshots/test_sensor.ambr | 6 + .../arve/snapshots/test_sensor.ambr | 7 + .../autarco/snapshots/test_sensor.ambr | 16 + .../axis/snapshots/test_binary_sensor.ambr | 11 + .../axis/snapshots/test_camera.ambr | 2 + .../components/axis/snapshots/test_light.ambr | 1 + .../axis/snapshots/test_switch.ambr | 4 + .../azure_devops/snapshots/test_sensor.ambr | 10 + .../backup/snapshots/test_event.ambr | 1 + .../backup/snapshots/test_sensors.ambr | 4 + .../balboa/snapshots/test_binary_sensor.ambr | 3 + .../balboa/snapshots/test_climate.ambr | 1 + .../balboa/snapshots/test_event.ambr | 1 + .../components/balboa/snapshots/test_fan.ambr | 1 + .../balboa/snapshots/test_light.ambr | 1 + .../balboa/snapshots/test_select.ambr | 1 + .../balboa/snapshots/test_switch.ambr | 1 + .../balboa/snapshots/test_time.ambr | 4 + .../blue_current/snapshots/test_button.ambr | 3 + .../bluemaestro/snapshots/test_sensor.ambr | 5 + .../snapshots/test_binary_sensor.ambr | 29 ++ .../snapshots/test_button.ambr | 19 + .../snapshots/test_lock.ambr | 4 + .../snapshots/test_number.ambr | 2 + .../snapshots/test_select.ambr | 5 + .../snapshots/test_sensor.ambr | 62 +++ .../snapshots/test_switch.ambr | 4 + .../snapshots/test_alarm_control_panel.ambr | 3 + .../snapshots/test_binary_sensor.ambr | 63 +++ .../bosch_alarm/snapshots/test_sensor.ambr | 12 + .../bosch_alarm/snapshots/test_switch.ambr | 12 + .../bring/snapshots/test_event.ambr | 2 + .../bring/snapshots/test_sensor.ambr | 10 + .../components/bring/snapshots/test_todo.ambr | 2 + .../brother/snapshots/test_sensor.ambr | 28 ++ .../snapshots/test_climate.ambr | 1 + .../bsblan/snapshots/test_climate.ambr | 2 + .../bsblan/snapshots/test_sensor.ambr | 2 + .../bsblan/snapshots/test_water_heater.ambr | 1 + .../snapshots/test_select.ambr | 3 + .../snapshots/test_switch.ambr | 2 + .../ccm15/snapshots/test_climate.ambr | 4 + .../chacon_dio/snapshots/test_cover.ambr | 1 + .../chacon_dio/snapshots/test_switch.ambr | 1 + .../co2signal/snapshots/test_sensor.ambr | 2 + .../comelit/snapshots/test_climate.ambr | 1 + .../comelit/snapshots/test_cover.ambr | 1 + .../comelit/snapshots/test_humidifier.ambr | 2 + .../comelit/snapshots/test_light.ambr | 1 + .../comelit/snapshots/test_sensor.ambr | 1 + .../comelit/snapshots/test_switch.ambr | 1 + .../components/config/test_entity_registry.py | 169 +++++++ .../cookidoo/snapshots/test_button.ambr | 1 + .../cookidoo/snapshots/test_sensor.ambr | 2 + .../cookidoo/snapshots/test_todo.ambr | 2 + .../deako/snapshots/test_light.ambr | 3 + .../snapshots/test_alarm_control_panel.ambr | 1 + .../deconz/snapshots/test_binary_sensor.ambr | 21 + .../deconz/snapshots/test_button.ambr | 2 + .../deconz/snapshots/test_climate.ambr | 7 + .../deconz/snapshots/test_cover.ambr | 3 + .../components/deconz/snapshots/test_fan.ambr | 1 + .../deconz/snapshots/test_light.ambr | 19 + .../deconz/snapshots/test_number.ambr | 2 + .../deconz/snapshots/test_scene.ambr | 1 + .../deconz/snapshots/test_select.ambr | 10 + .../deconz/snapshots/test_sensor.ambr | 44 ++ .../snapshots/test_binary_sensor.ambr | 3 + .../snapshots/test_climate.ambr | 1 + .../snapshots/test_cover.ambr | 1 + .../snapshots/test_light.ambr | 2 + .../snapshots/test_sensor.ambr | 5 + .../snapshots/test_siren.ambr | 3 + .../snapshots/test_switch.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 1 + .../snapshots/test_button.ambr | 4 + .../snapshots/test_image.ambr | 1 + .../snapshots/test_sensor.ambr | 6 + .../snapshots/test_switch.ambr | 2 + .../snapshots/test_update.ambr | 1 + .../discovergy/snapshots/test_sensor.ambr | 5 + .../snapshots/test_binary_sensor.ambr | 10 + .../ecovacs/snapshots/test_binary_sensor.ambr | 1 + .../ecovacs/snapshots/test_button.ambr | 13 + .../ecovacs/snapshots/test_event.ambr | 1 + .../ecovacs/snapshots/test_lawn_mower.ambr | 2 + .../ecovacs/snapshots/test_number.ambr | 3 + .../ecovacs/snapshots/test_select.ambr | 1 + .../ecovacs/snapshots/test_sensor.ambr | 44 ++ .../ecovacs/snapshots/test_switch.ambr | 10 + .../eheimdigital/snapshots/test_climate.ambr | 2 + .../eheimdigital/snapshots/test_light.ambr | 5 + .../eheimdigital/snapshots/test_number.ambr | 8 + .../eheimdigital/snapshots/test_select.ambr | 1 + .../eheimdigital/snapshots/test_sensor.ambr | 3 + .../eheimdigital/snapshots/test_switch.ambr | 1 + .../eheimdigital/snapshots/test_time.ambr | 4 + .../elgato/snapshots/test_button.ambr | 2 + .../elgato/snapshots/test_light.ambr | 3 + .../elgato/snapshots/test_sensor.ambr | 5 + .../elgato/snapshots/test_switch.ambr | 2 + .../snapshots/test_alarm_control_panel.ambr | 3 + .../elmax/snapshots/test_binary_sensor.ambr | 8 + .../elmax/snapshots/test_cover.ambr | 1 + .../elmax/snapshots/test_switch.ambr | 1 + .../emoncms/snapshots/test_sensor.ambr | 1 + .../snapshots/test_switch.ambr | 4 + .../energyzero/snapshots/test_sensor.ambr | 11 + .../snapshots/test_binary_sensor.ambr | 6 + .../snapshots/test_diagnostics.ambr | 24 + .../enphase_envoy/snapshots/test_number.ambr | 8 + .../enphase_envoy/snapshots/test_select.ambr | 14 + .../enphase_envoy/snapshots/test_sensor.ambr | 470 ++++++++++++++++++ .../enphase_envoy/snapshots/test_switch.ambr | 6 + .../filesize/snapshots/test_sensor.ambr | 4 + .../snapshots/test_binary_sensor.ambr | 1 + .../flexit_bacnet/snapshots/test_climate.ambr | 1 + .../flexit_bacnet/snapshots/test_number.ambr | 11 + .../flexit_bacnet/snapshots/test_sensor.ambr | 15 + .../flexit_bacnet/snapshots/test_switch.ambr | 3 + tests/components/folder_watcher/conftest.py | 2 +- .../folder_watcher/snapshots/test_event.ambr | 1 + .../fritz/snapshots/test_button.ambr | 5 + .../fritz/snapshots/test_sensor.ambr | 16 + .../fritz/snapshots/test_switch.ambr | 12 + .../fritz/snapshots/test_update.ambr | 3 + .../snapshots/test_binary_sensor.ambr | 7 + .../fritzbox/snapshots/test_button.ambr | 1 + .../fritzbox/snapshots/test_climate.ambr | 1 + .../fritzbox/snapshots/test_cover.ambr | 1 + .../fritzbox/snapshots/test_light.ambr | 4 + .../fritzbox/snapshots/test_sensor.ambr | 16 + .../fritzbox/snapshots/test_switch.ambr | 1 + .../fronius/snapshots/test_sensor.ambr | 181 +++++++ .../snapshots/test_climate.ambr | 2 + .../fujitsu_fglair/snapshots/test_sensor.ambr | 2 + .../fyta/snapshots/test_binary_sensor.ambr | 16 + .../components/fyta/snapshots/test_image.ambr | 4 + .../fyta/snapshots/test_sensor.ambr | 30 ++ .../snapshots/test_binary_sensor.ambr | 1 + .../snapshots/test_sensor.ambr | 4 + .../snapshots/test_binary_sensor.ambr | 1 + .../geniushub/snapshots/test_climate.ambr | 7 + .../geniushub/snapshots/test_sensor.ambr | 18 + .../geniushub/snapshots/test_switch.ambr | 3 + .../gios/snapshots/test_sensor.ambr | 13 + .../glances/snapshots/test_sensor.ambr | 34 ++ .../gree/snapshots/test_climate.ambr | 1 + .../gree/snapshots/test_switch.ambr | 5 + .../snapshots/test_binary_sensor.ambr | 1 + .../habitica/snapshots/test_button.ambr | 28 ++ .../habitica/snapshots/test_calendar.ambr | 4 + .../habitica/snapshots/test_sensor.ambr | 25 + .../habitica/snapshots/test_switch.ambr | 1 + .../habitica/snapshots/test_todo.ambr | 2 + .../snapshots/test_alarm_control_panel.ambr | 1 + .../homee/snapshots/test_binary_sensor.ambr | 29 ++ .../homee/snapshots/test_button.ambr | 12 + .../homee/snapshots/test_climate.ambr | 4 + .../homee/snapshots/test_event.ambr | 1 + .../components/homee/snapshots/test_fan.ambr | 1 + .../homee/snapshots/test_light.ambr | 5 + .../components/homee/snapshots/test_lock.ambr | 1 + .../homee/snapshots/test_number.ambr | 15 + .../homee/snapshots/test_select.ambr | 1 + .../homee/snapshots/test_sensor.ambr | 34 ++ .../homee/snapshots/test_switch.ambr | 5 + .../homee/snapshots/test_valve.ambr | 1 + .../snapshots/test_init.ambr | 416 ++++++++++++++++ .../homewizard/snapshots/test_button.ambr | 1 + .../homewizard/snapshots/test_number.ambr | 2 + .../homewizard/snapshots/test_sensor.ambr | 231 +++++++++ .../homewizard/snapshots/test_switch.ambr | 11 + .../snapshots/test_binary_sensor.ambr | 4 + .../snapshots/test_button.ambr | 3 + .../snapshots/test_device_tracker.ambr | 1 + .../snapshots/test_number.ambr | 4 + .../snapshots/test_sensor.ambr | 25 + .../snapshots/test_switch.ambr | 7 + .../snapshots/test_binary_sensor.ambr | 4 + .../hydrawise/snapshots/test_sensor.ambr | 12 + .../hydrawise/snapshots/test_switch.ambr | 4 + .../hydrawise/snapshots/test_valve.ambr | 2 + .../igloohome/snapshots/test_lock.ambr | 1 + .../igloohome/snapshots/test_sensor.ambr | 1 + .../imeon_inverter/snapshots/test_sensor.ambr | 51 ++ .../imgw_pib/snapshots/test_sensor.ambr | 2 + .../immich/snapshots/test_sensor.ambr | 8 + .../snapshots/test_binary_sensor.ambr | 20 + .../incomfort/snapshots/test_climate.ambr | 4 + .../incomfort/snapshots/test_sensor.ambr | 3 + .../snapshots/test_water_heater.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 17 + .../intellifire/snapshots/test_climate.ambr | 1 + .../intellifire/snapshots/test_sensor.ambr | 10 + .../iometer/snapshots/test_binary_sensor.ambr | 2 + .../iotty/snapshots/test_switch.ambr | 1 + .../components/ipp/snapshots/test_sensor.ambr | 7 + .../iron_os/snapshots/test_binary_sensor.ambr | 1 + .../iron_os/snapshots/test_button.ambr | 2 + .../iron_os/snapshots/test_number.ambr | 20 + .../iron_os/snapshots/test_select.ambr | 10 + .../iron_os/snapshots/test_sensor.ambr | 13 + .../iron_os/snapshots/test_switch.ambr | 7 + .../iron_os/snapshots/test_update.ambr | 1 + .../israel_rail/snapshots/test_sensor.ambr | 6 + .../ista_ecotrend/snapshots/test_sensor.ambr | 16 + .../ituran/snapshots/test_device_tracker.ambr | 1 + .../ituran/snapshots/test_sensor.ambr | 6 + .../kitchen_sink/snapshots/test_switch.ambr | 2 + .../knocki/snapshots/test_event.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 4 + .../lamarzocco/snapshots/test_button.ambr | 1 + .../lamarzocco/snapshots/test_calendar.ambr | 2 + .../lamarzocco/snapshots/test_number.ambr | 5 + .../lamarzocco/snapshots/test_select.ambr | 5 + .../lamarzocco/snapshots/test_sensor.ambr | 6 + .../lamarzocco/snapshots/test_switch.ambr | 7 + .../lamarzocco/snapshots/test_update.ambr | 2 + .../lcn/snapshots/test_binary_sensor.ambr | 3 + .../lcn/snapshots/test_climate.ambr | 1 + .../components/lcn/snapshots/test_cover.ambr | 4 + .../components/lcn/snapshots/test_light.ambr | 3 + .../components/lcn/snapshots/test_scene.ambr | 2 + .../components/lcn/snapshots/test_sensor.ambr | 4 + .../components/lcn/snapshots/test_switch.ambr | 7 + .../snapshots/test_binary_sensor.ambr | 10 + .../lektrico/snapshots/test_button.ambr | 4 + .../lektrico/snapshots/test_number.ambr | 2 + .../lektrico/snapshots/test_select.ambr | 1 + .../lektrico/snapshots/test_sensor.ambr | 10 + .../lektrico/snapshots/test_switch.ambr | 2 + .../letpot/snapshots/test_binary_sensor.ambr | 7 + .../letpot/snapshots/test_sensor.ambr | 2 + .../letpot/snapshots/test_switch.ambr | 4 + .../letpot/snapshots/test_time.ambr | 2 + .../lg_thinq/snapshots/test_climate.ambr | 1 + .../lg_thinq/snapshots/test_event.ambr | 1 + .../lg_thinq/snapshots/test_number.ambr | 2 + .../lg_thinq/snapshots/test_sensor.ambr | 8 + .../snapshots/test_cover.ambr | 4 + .../snapshots/test_light.ambr | 4 + .../madvr/snapshots/test_binary_sensor.ambr | 4 + .../madvr/snapshots/test_remote.ambr | 1 + .../madvr/snapshots/test_sensor.ambr | 26 + .../mastodon/snapshots/test_sensor.ambr | 3 + .../matter/snapshots/test_binary_sensor.ambr | 20 + .../matter/snapshots/test_button.ambr | 45 ++ .../matter/snapshots/test_climate.ambr | 4 + .../matter/snapshots/test_cover.ambr | 5 + .../matter/snapshots/test_event.ambr | 6 + .../components/matter/snapshots/test_fan.ambr | 4 + .../matter/snapshots/test_light.ambr | 10 + .../matter/snapshots/test_lock.ambr | 2 + .../matter/snapshots/test_number.ambr | 33 ++ .../matter/snapshots/test_select.ambr | 44 ++ .../matter/snapshots/test_sensor.ambr | 103 ++++ .../matter/snapshots/test_switch.ambr | 19 + .../matter/snapshots/test_vacuum.ambr | 1 + .../matter/snapshots/test_valve.ambr | 1 + .../matter/snapshots/test_water_heater.ambr | 1 + .../mealie/snapshots/test_calendar.ambr | 4 + .../mealie/snapshots/test_sensor.ambr | 5 + .../mealie/snapshots/test_todo.ambr | 3 + .../meteo_france/snapshots/test_sensor.ambr | 15 + .../meteo_france/snapshots/test_weather.ambr | 1 + .../miele/snapshots/test_binary_sensor.ambr | 46 ++ .../miele/snapshots/test_button.ambr | 8 + .../miele/snapshots/test_climate.ambr | 4 + .../components/miele/snapshots/test_fan.ambr | 4 + .../miele/snapshots/test_light.ambr | 4 + .../miele/snapshots/test_sensor.ambr | 40 ++ .../miele/snapshots/test_switch.ambr | 8 + .../miele/snapshots/test_vacuum.ambr | 2 + .../snapshots/test_binary_sensor.ambr | 1 + .../snapshots/test_button.ambr | 1 + .../snapshots/test_climate.ambr | 1 + .../snapshots/test_sensor.ambr | 1 + .../monarch_money/snapshots/test_sensor.ambr | 22 + .../monzo/snapshots/test_sensor.ambr | 5 + .../snapshots/test_media_player.ambr | 3 + .../snapshots/test_binary_sensor.ambr | 7 + .../myuplink/snapshots/test_number.ambr | 8 + .../myuplink/snapshots/test_select.ambr | 2 + .../myuplink/snapshots/test_sensor.ambr | 94 ++++ .../myuplink/snapshots/test_switch.ambr | 4 + .../components/nam/snapshots/test_sensor.ambr | 33 ++ .../nanoleaf/snapshots/test_light.ambr | 1 + .../netatmo/snapshots/test_binary_sensor.ambr | 11 + .../netatmo/snapshots/test_button.ambr | 2 + .../netatmo/snapshots/test_camera.ambr | 3 + .../netatmo/snapshots/test_climate.ambr | 5 + .../netatmo/snapshots/test_cover.ambr | 2 + .../netatmo/snapshots/test_fan.ambr | 1 + .../netatmo/snapshots/test_light.ambr | 3 + .../netatmo/snapshots/test_select.ambr | 1 + .../netatmo/snapshots/test_sensor.ambr | 143 ++++++ .../netatmo/snapshots/test_switch.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 6 + .../nextcloud/snapshots/test_sensor.ambr | 80 +++ .../nextcloud/snapshots/test_update.ambr | 1 + .../nextdns/snapshots/test_binary_sensor.ambr | 2 + .../nextdns/snapshots/test_button.ambr | 1 + .../nextdns/snapshots/test_sensor.ambr | 25 + .../nextdns/snapshots/test_switch.ambr | 73 +++ .../nice_go/snapshots/test_cover.ambr | 4 + .../nice_go/snapshots/test_light.ambr | 2 + .../snapshots/test_cover.ambr | 1 + .../snapshots/test_light.ambr | 2 + .../nordpool/snapshots/test_sensor.ambr | 48 ++ .../ntfy/snapshots/test_notify.ambr | 1 + .../nuki/snapshots/test_binary_sensor.ambr | 5 + .../components/nuki/snapshots/test_lock.ambr | 2 + .../nuki/snapshots/test_sensor.ambr | 1 + .../nyt_games/snapshots/test_sensor.ambr | 12 + .../ohme/snapshots/test_button.ambr | 1 + .../ohme/snapshots/test_number.ambr | 2 + .../ohme/snapshots/test_select.ambr | 2 + .../ohme/snapshots/test_sensor.ambr | 8 + .../ohme/snapshots/test_switch.ambr | 4 + .../components/ohme/snapshots/test_time.ambr | 1 + .../omnilogic/snapshots/test_sensor.ambr | 2 + .../omnilogic/snapshots/test_switch.ambr | 2 + .../ondilo_ico/snapshots/test_sensor.ambr | 14 + .../onedrive/snapshots/test_sensor.ambr | 4 + .../onewire/snapshots/test_binary_sensor.ambr | 16 + .../onewire/snapshots/test_select.ambr | 1 + .../onewire/snapshots/test_sensor.ambr | 58 +++ .../onewire/snapshots/test_switch.ambr | 37 ++ .../openweathermap/snapshots/test_sensor.ambr | 32 ++ .../snapshots/test_weather.ambr | 3 + .../snapshots/test_water_heater.ambr | 1 + .../overseerr/snapshots/test_event.ambr | 1 + .../overseerr/snapshots/test_sensor.ambr | 7 + .../palazzetti/snapshots/test_button.ambr | 1 + .../palazzetti/snapshots/test_climate.ambr | 1 + .../palazzetti/snapshots/test_number.ambr | 3 + .../palazzetti/snapshots/test_sensor.ambr | 9 + .../paperless_ngx/snapshots/test_sensor.ambr | 14 + .../peblar/snapshots/test_binary_sensor.ambr | 2 + .../peblar/snapshots/test_button.ambr | 2 + .../peblar/snapshots/test_number.ambr | 1 + .../peblar/snapshots/test_select.ambr | 1 + .../peblar/snapshots/test_sensor.ambr | 16 + .../peblar/snapshots/test_switch.ambr | 2 + .../peblar/snapshots/test_update.ambr | 2 + .../ping/snapshots/test_binary_sensor.ambr | 1 + .../ping/snapshots/test_sensor.ambr | 3 + .../plaato/snapshots/test_binary_sensor.ambr | 2 + .../plaato/snapshots/test_sensor.ambr | 12 + .../snapshots/test_binary_sensor.ambr | 2 + .../poolsense/snapshots/test_sensor.ambr | 9 + .../powerfox/snapshots/test_sensor.ambr | 11 + .../snapshots/test_binary_sensor.ambr | 2 + .../pyload/snapshots/test_button.ambr | 4 + .../pyload/snapshots/test_sensor.ambr | 20 + .../pyload/snapshots/test_switch.ambr | 2 + .../snapshots/test_sensor.ambr | 5 + .../snapshots/test_binary_sensor.ambr | 6 + .../rainmachine/snapshots/test_button.ambr | 1 + .../rainmachine/snapshots/test_select.ambr | 1 + .../rainmachine/snapshots/test_sensor.ambr | 15 + .../rainmachine/snapshots/test_switch.ambr | 30 ++ .../rehlko/snapshots/test_binary_sensor.ambr | 3 + .../rehlko/snapshots/test_sensor.ambr | 25 + .../renault/snapshots/test_binary_sensor.ambr | 29 ++ .../renault/snapshots/test_button.ambr | 25 + .../snapshots/test_device_tracker.ambr | 6 + .../renault/snapshots/test_select.ambr | 6 + .../renault/snapshots/test_sensor.ambr | 88 ++++ .../ring/snapshots/test_binary_sensor.ambr | 5 + .../ring/snapshots/test_button.ambr | 1 + .../ring/snapshots/test_camera.ambr | 6 + .../components/ring/snapshots/test_event.ambr | 6 + .../components/ring/snapshots/test_light.ambr | 2 + .../ring/snapshots/test_number.ambr | 7 + .../ring/snapshots/test_sensor.ambr | 29 ++ .../components/ring/snapshots/test_siren.ambr | 3 + .../ring/snapshots/test_switch.ambr | 6 + .../rova/snapshots/test_sensor.ambr | 4 + .../sabnzbd/snapshots/test_binary_sensor.ambr | 1 + .../sabnzbd/snapshots/test_button.ambr | 2 + .../sabnzbd/snapshots/test_number.ambr | 1 + .../sabnzbd/snapshots/test_sensor.ambr | 11 + .../sanix/snapshots/test_sensor.ambr | 6 + .../sense/snapshots/test_binary_sensor.ambr | 2 + .../sense/snapshots/test_sensor.ambr | 51 ++ .../sensibo/snapshots/test_binary_sensor.ambr | 15 + .../sensibo/snapshots/test_button.ambr | 3 + .../sensibo/snapshots/test_climate.ambr | 3 + .../sensibo/snapshots/test_number.ambr | 6 + .../sensibo/snapshots/test_select.ambr | 2 + .../sensibo/snapshots/test_sensor.ambr | 16 + .../sensibo/snapshots/test_switch.ambr | 4 + .../sensibo/snapshots/test_update.ambr | 3 + .../snapshots/test_sensor.ambr | 24 + .../sfr_box/snapshots/test_binary_sensor.ambr | 4 + .../sfr_box/snapshots/test_button.ambr | 1 + .../sfr_box/snapshots/test_sensor.ambr | 15 + .../shelly/snapshots/test_binary_sensor.ambr | 3 + .../shelly/snapshots/test_button.ambr | 2 + .../shelly/snapshots/test_climate.ambr | 4 + .../shelly/snapshots/test_event.ambr | 1 + .../shelly/snapshots/test_number.ambr | 2 + .../shelly/snapshots/test_sensor.ambr | 5 + .../snapshots/test_binary_sensor.ambr | 8 + .../simplefin/snapshots/test_sensor.ambr | 16 + .../slide_local/snapshots/test_button.ambr | 1 + .../slide_local/snapshots/test_cover.ambr | 1 + .../slide_local/snapshots/test_switch.ambr | 1 + .../components/sma/snapshots/test_sensor.ambr | 108 ++++ .../smarla/snapshots/test_switch.ambr | 2 + .../snapshots/test_binary_sensor.ambr | 53 ++ .../smartthings/snapshots/test_button.ambr | 5 + .../smartthings/snapshots/test_climate.ambr | 17 + .../smartthings/snapshots/test_cover.ambr | 2 + .../smartthings/snapshots/test_event.ambr | 6 + .../smartthings/snapshots/test_fan.ambr | 2 + .../smartthings/snapshots/test_light.ambr | 5 + .../smartthings/snapshots/test_lock.ambr | 1 + .../snapshots/test_media_player.ambr | 6 + .../smartthings/snapshots/test_number.ambr | 10 + .../smartthings/snapshots/test_scene.ambr | 2 + .../smartthings/snapshots/test_select.ambr | 17 + .../smartthings/snapshots/test_sensor.ambr | 224 +++++++++ .../smartthings/snapshots/test_switch.ambr | 23 + .../smartthings/snapshots/test_update.ambr | 7 + .../smartthings/snapshots/test_valve.ambr | 1 + .../snapshots/test_water_heater.ambr | 3 + .../smarty/snapshots/test_binary_sensor.ambr | 3 + .../smarty/snapshots/test_button.ambr | 1 + .../components/smarty/snapshots/test_fan.ambr | 1 + .../smarty/snapshots/test_sensor.ambr | 6 + .../smarty/snapshots/test_switch.ambr | 1 + .../smlight/snapshots/test_binary_sensor.ambr | 4 + .../smlight/snapshots/test_sensor.ambr | 9 + .../smlight/snapshots/test_switch.ambr | 4 + .../smlight/snapshots/test_update.ambr | 2 + .../solarlog/snapshots/test_sensor.ambr | 27 + .../sonos/snapshots/test_media_player.ambr | 1 + .../spotify/snapshots/test_media_player.ambr | 2 + .../snapshots/test_media_player.ambr | 1 + .../stookwijzer/snapshots/test_sensor.ambr | 3 + .../snapshots/test_binary_sensor.ambr | 1 + .../snapshots/test_sensor.ambr | 3 + .../suez_water/snapshots/test_sensor.ambr | 2 + .../snapshots/test_sensor.ambr | 8 + .../snapshots/test_sensor.ambr | 6 + .../snapshots/test_binary_sensor.ambr | 2 + .../syncthru/snapshots/test_sensor.ambr | 8 + .../snapshots/test_binary_sensor.ambr | 2 + .../tailwind/snapshots/test_button.ambr | 1 + .../tailwind/snapshots/test_cover.ambr | 2 + .../tailwind/snapshots/test_number.ambr | 1 + .../tasmota/snapshots/test_sensor.ambr | 25 + .../snapshots/test_binary_sensor.ambr | 5 + .../technove/snapshots/test_number.ambr | 1 + .../technove/snapshots/test_sensor.ambr | 9 + .../technove/snapshots/test_switch.ambr | 2 + .../tedee/snapshots/test_binary_sensor.ambr | 8 + .../components/tedee/snapshots/test_lock.ambr | 3 + .../tedee/snapshots/test_sensor.ambr | 4 + .../snapshots/test_binary_sensor.ambr | 27 + .../tesla_fleet/snapshots/test_button.ambr | 6 + .../tesla_fleet/snapshots/test_climate.ambr | 6 + .../tesla_fleet/snapshots/test_cover.ambr | 15 + .../snapshots/test_device_tracker.ambr | 2 + .../tesla_fleet/snapshots/test_lock.ambr | 2 + .../snapshots/test_media_player.ambr | 2 + .../tesla_fleet/snapshots/test_number.ambr | 4 + .../tesla_fleet/snapshots/test_select.ambr | 10 + .../tesla_fleet/snapshots/test_sensor.ambr | 71 +++ .../tesla_fleet/snapshots/test_switch.ambr | 8 + .../snapshots/test_binary_sensor.ambr | 66 +++ .../teslemetry/snapshots/test_button.ambr | 6 + .../teslemetry/snapshots/test_climate.ambr | 6 + .../teslemetry/snapshots/test_cover.ambr | 14 + .../snapshots/test_device_tracker.ambr | 2 + .../teslemetry/snapshots/test_lock.ambr | 4 + .../snapshots/test_media_player.ambr | 2 + .../teslemetry/snapshots/test_number.ambr | 4 + .../teslemetry/snapshots/test_select.ambr | 8 + .../teslemetry/snapshots/test_sensor.ambr | 72 +++ .../teslemetry/snapshots/test_switch.ambr | 9 + .../teslemetry/snapshots/test_update.ambr | 2 + .../tessie/snapshots/test_binary_sensor.ambr | 30 ++ .../tessie/snapshots/test_button.ambr | 6 + .../tessie/snapshots/test_climate.ambr | 1 + .../tessie/snapshots/test_cover.ambr | 5 + .../tessie/snapshots/test_device_tracker.ambr | 2 + .../tessie/snapshots/test_lock.ambr | 2 + .../tessie/snapshots/test_media_player.ambr | 1 + .../tessie/snapshots/test_number.ambr | 5 + .../tessie/snapshots/test_select.ambr | 9 + .../tessie/snapshots/test_sensor.ambr | 44 ++ .../tessie/snapshots/test_switch.ambr | 7 + .../tessie/snapshots/test_update.ambr | 1 + .../tile/snapshots/test_binary_sensor.ambr | 1 + .../tile/snapshots/test_device_tracker.ambr | 1 + .../snapshots/test_alarm_control_panel.ambr | 2 + .../snapshots/test_binary_sensor.ambr | 25 + .../totalconnect/snapshots/test_button.ambr | 6 + .../tplink/snapshots/test_binary_sensor.ambr | 9 + .../tplink/snapshots/test_button.ambr | 14 + .../tplink/snapshots/test_camera.ambr | 1 + .../tplink/snapshots/test_climate.ambr | 1 + .../components/tplink/snapshots/test_fan.ambr | 3 + .../tplink/snapshots/test_number.ambr | 8 + .../tplink/snapshots/test_select.ambr | 3 + .../tplink/snapshots/test_sensor.ambr | 38 ++ .../tplink/snapshots/test_siren.ambr | 1 + .../tplink/snapshots/test_switch.ambr | 13 + .../tplink/snapshots/test_vacuum.ambr | 1 + .../tplink_omada/snapshots/test_sensor.ambr | 6 + .../tplink_omada/snapshots/test_switch.ambr | 2 + .../snapshots/test_binary_sensor.ambr | 2 + .../snapshots/test_device_tracker.ambr | 1 + .../tractive/snapshots/test_sensor.ambr | 10 + .../tractive/snapshots/test_switch.ambr | 3 + .../twentemilieu/snapshots/test_calendar.ambr | 1 + .../twentemilieu/snapshots/test_sensor.ambr | 5 + .../twinkly/snapshots/test_light.ambr | 1 + .../twinkly/snapshots/test_select.ambr | 1 + .../unifi/snapshots/test_button.ambr | 3 + .../unifi/snapshots/test_device_tracker.ambr | 3 + .../unifi/snapshots/test_image.ambr | 1 + .../unifi/snapshots/test_sensor.ambr | 39 ++ .../unifi/snapshots/test_switch.ambr | 11 + .../unifi/snapshots/test_update.ambr | 4 + .../uptime/snapshots/test_sensor.ambr | 1 + .../components/v2c/snapshots/test_sensor.ambr | 11 + .../velbus/snapshots/test_binary_sensor.ambr | 1 + .../velbus/snapshots/test_button.ambr | 1 + .../velbus/snapshots/test_climate.ambr | 1 + .../velbus/snapshots/test_cover.ambr | 2 + .../velbus/snapshots/test_light.ambr | 2 + .../velbus/snapshots/test_select.ambr | 1 + .../velbus/snapshots/test_sensor.ambr | 5 + .../velbus/snapshots/test_switch.ambr | 1 + .../components/vesync/snapshots/test_fan.ambr | 5 + .../vesync/snapshots/test_light.ambr | 3 + .../vesync/snapshots/test_sensor.ambr | 17 + .../vesync/snapshots/test_switch.ambr | 9 + .../vicare/snapshots/test_binary_sensor.ambr | 9 + .../vicare/snapshots/test_button.ambr | 1 + .../vicare/snapshots/test_climate.ambr | 2 + .../components/vicare/snapshots/test_fan.ambr | 3 + .../vicare/snapshots/test_number.ambr | 11 + .../vicare/snapshots/test_sensor.ambr | 56 +++ .../vicare/snapshots/test_water_heater.ambr | 2 + .../snapshots/test_button.ambr | 1 + .../snapshots/test_device_tracker.ambr | 2 + .../snapshots/test_sensor.ambr | 5 + .../watergate/snapshots/test_event.ambr | 2 + .../watergate/snapshots/test_sensor.ambr | 10 + .../snapshots/test_sensor.ambr | 15 + .../snapshots/test_weather.ambr | 1 + .../webmin/snapshots/test_sensor.ambr | 34 ++ .../weheat/snapshots/test_binary_sensor.ambr | 4 + .../weheat/snapshots/test_sensor.ambr | 19 + .../snapshots/test_binary_sensor.ambr | 2 + .../whirlpool/snapshots/test_climate.ambr | 2 + .../whirlpool/snapshots/test_sensor.ambr | 5 + .../whois/snapshots/test_sensor.ambr | 11 + .../withings/snapshots/test_sensor.ambr | 76 +++ .../wled/snapshots/test_button.ambr | 1 + .../wled/snapshots/test_number.ambr | 2 + .../wled/snapshots/test_select.ambr | 4 + .../wled/snapshots/test_switch.ambr | 4 + .../wolflink/snapshots/test_sensor.ambr | 11 + .../snapshots/test_alarm_control_panel.ambr | 1 + .../snapshots/test_binary_sensor.ambr | 10 + .../snapshots/test_button.ambr | 1 + .../yale_smart_alarm/snapshots/test_lock.ambr | 6 + .../snapshots/test_select.ambr | 6 + .../snapshots/test_switch.ambr | 6 + .../youless/snapshots/test_sensor.ambr | 22 + .../zeversolar/snapshots/test_sensor.ambr | 2 + tests/helpers/test_entity_platform.py | 1 + tests/helpers/test_entity_registry.py | 10 + 612 files changed, 7218 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index b987f249a33..d619b585230 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from typing import Any import voluptuous as vol @@ -10,18 +11,23 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin from homeassistant.core import HomeAssistant, 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.entity_component import async_get_entity_suggested_object_id from homeassistant.helpers.json import json_dumps +_LOGGER = logging.getLogger(__name__) + @callback def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" + websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids) websocket_api.async_register_command(hass, websocket_get_entities) websocket_api.async_register_command(hass, websocket_get_entity) websocket_api.async_register_command(hass, websocket_list_entities_for_display) @@ -316,3 +322,54 @@ def websocket_remove_entity( registry.async_remove(msg["entity_id"]) connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get_automatic_entity_ids", + vol.Required("entity_ids"): cv.entity_ids, + } +) +@callback +def websocket_get_automatic_entity_ids( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Return the automatic entity IDs for the given entity IDs. + + This is used to help user reset entity IDs which have been customized by the user. + """ + registry = er.async_get(hass) + + entity_ids = msg["entity_ids"] + automatic_entity_ids: dict[str, str | None] = {} + reserved_entity_ids: set[str] = set() + for entity_id in entity_ids: + if not (entry := registry.entities.get(entity_id)): + automatic_entity_ids[entity_id] = None + continue + try: + suggested = async_get_entity_suggested_object_id(hass, entity_id) + except HomeAssistantError as err: + # This is raised if the entity has no object. + _LOGGER.debug( + "Unable to get suggested object ID for %s, entity ID: %s (%s)", + entry.entity_id, + entity_id, + err, + ) + automatic_entity_ids[entity_id] = None + continue + suggested_entity_id = registry.async_generate_entity_id( + entry.domain, + suggested or f"{entry.platform}_{entry.unique_id}", + current_entity_id=entity_id, + reserved_entity_ids=reserved_entity_ids, + ) + automatic_entity_ids[entity_id] = suggested_entity_id + reserved_entity_ids.add(suggested_entity_id) + + connection.send_message( + websocket_api.result_message(msg["id"], automatic_entity_ids) + ) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 02508e9ee9e..94dd97a9af9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -29,20 +29,27 @@ from homeassistant.core import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.hass_dict import HassKey -from . import config_validation as cv, discovery, entity, service -from .entity_platform import EntityPlatform +from . import ( + config_validation as cv, + device_registry as dr, + discovery, + entity, + entity_registry as er, + service, +) +from .entity_platform import EntityPlatform, async_calculate_suggested_object_id from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) -DATA_INSTANCES = "entity_components" +DATA_INSTANCES: HassKey[dict[str, EntityComponent]] = HassKey("entity_components") @bind_hass async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" domain = entity_id.partition(".")[0] - entity_comp: EntityComponent[entity.Entity] | None entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) if entity_comp is None: @@ -60,6 +67,36 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: await entity_obj.async_update_ha_state(True) +@callback +def async_get_entity_suggested_object_id( + hass: HomeAssistant, entity_id: str +) -> str | None: + """Get the suggested object id for an entity. + + Raises HomeAssistantError if the entity is not in the registry or + is not backed by an object. + """ + entity_registry = er.async_get(hass) + if not (entity_entry := entity_registry.async_get(entity_id)): + raise HomeAssistantError(f"Entity {entity_id} is not in the registry.") + + domain = entity_id.partition(".")[0] + + if entity_entry.name: + return entity_entry.name + + if entity_entry.suggested_object_id: + return entity_entry.suggested_object_id + + entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) + if not (entity_obj := entity_comp.get_entity(entity_id) if entity_comp else None): + raise HomeAssistantError(f"Entity {entity_id} has no object.") + device: dr.DeviceEntry | None = None + if device_id := entity_entry.device_id: + device = dr.async_get(hass).async_get(device_id) + return async_calculate_suggested_object_id(entity_obj, device) + + class EntityComponent[_EntityT: entity.Entity = entity.Entity]: """The EntityComponent manages platforms that manage entities. @@ -95,7 +132,7 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]: self.async_add_entities = domain_platform.async_add_entities self.add_entities = domain_platform.add_entities self._entities: dict[str, entity.Entity] = domain_platform.domain_entities - hass.data.setdefault(DATA_INSTANCES, {})[domain] = self + hass.data.setdefault(DATA_INSTANCES, {})[domain] = self # type: ignore[assignment] @property def entities(self) -> Iterable[_EntityT]: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f543891d3f3..0423a1979bc 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -843,31 +843,23 @@ class EntityPlatform: else: device = None - if not registered_entity_id: - # Do not bother working out a suggested_object_id - # if the entity is already registered as it will - # be ignored. - # - # An entity may suggest the entity_id by setting entity_id itself - suggested_entity_id: str | None = entity.entity_id - if suggested_entity_id is not None: - suggested_object_id = split_entity_id(entity.entity_id)[1] - else: - if device and entity.has_entity_name: - device_name = device.name_by_user or device.name - if entity.use_device_name: - suggested_object_id = device_name - else: - suggested_object_id = ( - f"{device_name} {entity.suggested_object_id}" - ) - if not suggested_object_id: - suggested_object_id = entity.suggested_object_id - + calculated_object_id: str | None = None + # An entity may suggest the entity_id by setting entity_id itself + suggested_entity_id: str | None = entity.entity_id + if suggested_entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] if self.entity_namespace is not None: suggested_object_id = ( f"{self.entity_namespace} {suggested_object_id}" ) + if not registered_entity_id and suggested_entity_id is None: + # Do not bother working out a suggested_object_id + # if the entity is already registered as it will + # be ignored. + # + calculated_object_id = async_calculate_suggested_object_id( + entity, device + ) disabled_by: RegistryEntryDisabler | None = None if not entity.entity_registry_enabled_default: @@ -881,6 +873,7 @@ class EntityPlatform: self.domain, self.platform_name, entity.unique_id, + calculated_object_id=calculated_object_id, capabilities=entity.capability_attributes, config_entry=self.config_entry, config_subentry_id=config_subentry_id, @@ -1124,6 +1117,27 @@ class EntityPlatform: await asyncio.gather(*tasks) +@callback +def async_calculate_suggested_object_id( + entity: Entity, device: dev_reg.DeviceEntry | None +) -> str | None: + """Calculate the suggested object ID for an entity.""" + calculated_object_id: str | None = None + if device and entity.has_entity_name: + device_name = device.name_by_user or device.name + if entity.use_device_name: + calculated_object_id = device_name + else: + calculated_object_id = f"{device_name} {entity.suggested_object_id}" + if not calculated_object_id: + calculated_object_id = entity.suggested_object_id + + if (platform := entity.platform) and platform.entity_namespace is not None: + calculated_object_id = f"{platform.entity_namespace} {calculated_object_id}" + + return calculated_object_id + + current_platform: ContextVar[EntityPlatform | None] = ContextVar( "current_platform", default=None ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index abe0468ed17..b503ba5f787 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -79,7 +79,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 16 +STORAGE_VERSION_MINOR = 17 STORAGE_KEY = "core.entity_registry" CLEANUP_INTERVAL = 3600 * 24 @@ -198,6 +198,7 @@ class RegistryEntry: original_device_class: str | None = attr.ib() original_icon: str | None = attr.ib() original_name: str | None = attr.ib() + suggested_object_id: str | None = attr.ib() supported_features: int = attr.ib() translation_key: str | None = attr.ib() unit_of_measurement: str | None = attr.ib() @@ -359,6 +360,7 @@ class RegistryEntry: "original_icon": self.original_icon, "original_name": self.original_name, "platform": self.platform, + "suggested_object_id": self.suggested_object_id, "supported_features": self.supported_features, "translation_key": self.translation_key, "unique_id": self.unique_id, @@ -549,6 +551,11 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): for entity in data["deleted_entities"]: entity["config_subentry_id"] = None + if old_minor_version < 17: + # Version 1.17 adds suggested_object_id + for entity in data["entities"]: + entity["suggested_object_id"] = None + if old_major_version > 1: raise NotImplementedError return data @@ -807,6 +814,9 @@ class EntityRegistry(BaseRegistry): self, domain: str, suggested_object_id: str, + *, + current_entity_id: str | None = None, + reserved_entity_ids: set[str] | None = None, ) -> str: """Generate an entity ID that does not conflict. @@ -820,7 +830,10 @@ class EntityRegistry(BaseRegistry): test_string = preferred_string[:MAX_LENGTH_STATE_ENTITY_ID] tries = 1 - while not self._entity_id_available(test_string): + while ( + not self._entity_id_available(test_string) + and test_string != current_entity_id + ) or (reserved_entity_ids and test_string in reserved_entity_ids): tries += 1 len_suffix = len(str(tries)) + 1 test_string = ( @@ -837,6 +850,7 @@ class EntityRegistry(BaseRegistry): unique_id: str, *, # To influence entity ID generation + calculated_object_id: str | None = None, suggested_object_id: str | None = None, # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, @@ -909,7 +923,7 @@ class EntityRegistry(BaseRegistry): entity_id = self.async_generate_entity_id( domain, - suggested_object_id or f"{platform}_{unique_id}", + suggested_object_id or calculated_object_id or f"{platform}_{unique_id}", ) if ( @@ -943,6 +957,7 @@ class EntityRegistry(BaseRegistry): original_icon=none_if_undefined(original_icon), original_name=none_if_undefined(original_name), platform=platform, + suggested_object_id=suggested_object_id, supported_features=none_if_undefined(supported_features) or 0, translation_key=none_if_undefined(translation_key), unique_id=unique_id, @@ -1380,6 +1395,7 @@ class EntityRegistry(BaseRegistry): original_icon=entity["original_icon"], original_name=entity["original_name"], platform=entity["platform"], + suggested_object_id=entity["suggested_object_id"], supported_features=entity["supported_features"], translation_key=entity["translation_key"], unique_id=entity["unique_id"], diff --git a/tests/common.py b/tests/common.py index 9aafba74aea..8d51a1e99a1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -674,6 +674,7 @@ class RegistryEntryWithDefaults(er.RegistryEntry): original_device_class: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) original_name: str | None = attr.ib(default=None) + suggested_object_id: str | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) translation_key: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) diff --git a/tests/components/acaia/snapshots/test_binary_sensor.ambr b/tests/components/acaia/snapshots/test_binary_sensor.ambr index a9c52c052a3..3ebf6fb128f 100644 --- a/tests/components/acaia/snapshots/test_binary_sensor.ambr +++ b/tests/components/acaia/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Timer running', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'timer_running', 'unique_id': 'aa:bb:cc:dd:ee:ff_timer_running', diff --git a/tests/components/acaia/snapshots/test_button.ambr b/tests/components/acaia/snapshots/test_button.ambr index 11827c0997f..4caea489ef0 100644 --- a/tests/components/acaia/snapshots/test_button.ambr +++ b/tests/components/acaia/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reset timer', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_timer', 'unique_id': 'aa:bb:cc:dd:ee:ff_reset_timer', @@ -74,6 +75,7 @@ 'original_name': 'Start/stop timer', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_stop', 'unique_id': 'aa:bb:cc:dd:ee:ff_start_stop', @@ -121,6 +123,7 @@ 'original_name': 'Tare', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tare', 'unique_id': 'aa:bb:cc:dd:ee:ff_tare', diff --git a/tests/components/acaia/snapshots/test_sensor.ambr b/tests/components/acaia/snapshots/test_sensor.ambr index 9214db4f102..6b2585c8ba1 100644 --- a/tests/components/acaia/snapshots/test_sensor.ambr +++ b/tests/components/acaia/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_battery', @@ -84,6 +85,7 @@ 'original_name': 'Volume flow rate', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_flow_rate', @@ -136,6 +138,7 @@ 'original_name': 'Weight', 'platform': 'acaia', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_weight', diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr index cbd2e14207e..6e47f3b0c06 100644 --- a/tests/components/accuweather/snapshots/test_sensor.ambr +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Air quality day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-0', @@ -99,6 +100,7 @@ 'original_name': 'Air quality day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-1', @@ -163,6 +165,7 @@ 'original_name': 'Air quality day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-2', @@ -227,6 +230,7 @@ 'original_name': 'Air quality day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-3', @@ -291,6 +295,7 @@ 'original_name': 'Air quality day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '0123456-airquality-4', @@ -349,6 +354,7 @@ 'original_name': 'Apparent temperature', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'apparent_temperature', 'unique_id': '0123456-apparenttemperature', @@ -405,6 +411,7 @@ 'original_name': 'Cloud ceiling', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_ceiling', 'unique_id': '0123456-ceiling', @@ -458,6 +465,7 @@ 'original_name': 'Cloud cover', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover', 'unique_id': '0123456-cloudcover', @@ -508,6 +516,7 @@ 'original_name': 'Cloud cover day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-0', @@ -557,6 +566,7 @@ 'original_name': 'Cloud cover day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-1', @@ -606,6 +616,7 @@ 'original_name': 'Cloud cover day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-2', @@ -655,6 +666,7 @@ 'original_name': 'Cloud cover day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-3', @@ -704,6 +716,7 @@ 'original_name': 'Cloud cover day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_day', 'unique_id': '0123456-cloudcoverday-4', @@ -753,6 +766,7 @@ 'original_name': 'Cloud cover night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-0', @@ -802,6 +816,7 @@ 'original_name': 'Cloud cover night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-1', @@ -851,6 +866,7 @@ 'original_name': 'Cloud cover night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-2', @@ -900,6 +916,7 @@ 'original_name': 'Cloud cover night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-3', @@ -949,6 +966,7 @@ 'original_name': 'Cloud cover night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_cover_night', 'unique_id': '0123456-cloudcovernight-4', @@ -998,6 +1016,7 @@ 'original_name': 'Condition day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-0', @@ -1046,6 +1065,7 @@ 'original_name': 'Condition day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-1', @@ -1094,6 +1114,7 @@ 'original_name': 'Condition day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-2', @@ -1142,6 +1163,7 @@ 'original_name': 'Condition day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-3', @@ -1190,6 +1212,7 @@ 'original_name': 'Condition day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_day', 'unique_id': '0123456-longphraseday-4', @@ -1238,6 +1261,7 @@ 'original_name': 'Condition night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-0', @@ -1286,6 +1310,7 @@ 'original_name': 'Condition night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-1', @@ -1334,6 +1359,7 @@ 'original_name': 'Condition night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-2', @@ -1382,6 +1408,7 @@ 'original_name': 'Condition night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-3', @@ -1430,6 +1457,7 @@ 'original_name': 'Condition night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_night', 'unique_id': '0123456-longphrasenight-4', @@ -1480,6 +1508,7 @@ 'original_name': 'Dew point', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': '0123456-dewpoint', @@ -1531,6 +1560,7 @@ 'original_name': 'Grass pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-0', @@ -1581,6 +1611,7 @@ 'original_name': 'Grass pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-1', @@ -1631,6 +1662,7 @@ 'original_name': 'Grass pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-2', @@ -1681,6 +1713,7 @@ 'original_name': 'Grass pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-3', @@ -1731,6 +1764,7 @@ 'original_name': 'Grass pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grass_pollen', 'unique_id': '0123456-grass-4', @@ -1781,6 +1815,7 @@ 'original_name': 'Hours of sun day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-0', @@ -1830,6 +1865,7 @@ 'original_name': 'Hours of sun day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-1', @@ -1879,6 +1915,7 @@ 'original_name': 'Hours of sun day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-2', @@ -1928,6 +1965,7 @@ 'original_name': 'Hours of sun day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-3', @@ -1977,6 +2015,7 @@ 'original_name': 'Hours of sun day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hours_of_sun', 'unique_id': '0123456-hoursofsun-4', @@ -2028,6 +2067,7 @@ 'original_name': 'Humidity', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '0123456-relativehumidity', @@ -2079,6 +2119,7 @@ 'original_name': 'Mold pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-0', @@ -2129,6 +2170,7 @@ 'original_name': 'Mold pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-1', @@ -2179,6 +2221,7 @@ 'original_name': 'Mold pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-2', @@ -2229,6 +2272,7 @@ 'original_name': 'Mold pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-3', @@ -2279,6 +2323,7 @@ 'original_name': 'Mold pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mold_pollen', 'unique_id': '0123456-mold-4', @@ -2331,6 +2376,7 @@ 'original_name': 'Precipitation', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'precipitation', 'unique_id': '0123456-precipitation', @@ -2388,6 +2434,7 @@ 'original_name': 'Pressure', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '0123456-pressure', @@ -2445,6 +2492,7 @@ 'original_name': 'Pressure tendency', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_tendency', 'unique_id': '0123456-pressuretendency', @@ -2499,6 +2547,7 @@ 'original_name': 'Ragweed pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-0', @@ -2549,6 +2598,7 @@ 'original_name': 'Ragweed pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-1', @@ -2599,6 +2649,7 @@ 'original_name': 'Ragweed pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-2', @@ -2649,6 +2700,7 @@ 'original_name': 'Ragweed pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-3', @@ -2699,6 +2751,7 @@ 'original_name': 'Ragweed pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ragweed_pollen', 'unique_id': '0123456-ragweed-4', @@ -2751,6 +2804,7 @@ 'original_name': 'RealFeel temperature', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature', 'unique_id': '0123456-realfeeltemperature', @@ -2802,6 +2856,7 @@ 'original_name': 'RealFeel temperature max day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-0', @@ -2852,6 +2907,7 @@ 'original_name': 'RealFeel temperature max day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-1', @@ -2902,6 +2958,7 @@ 'original_name': 'RealFeel temperature max day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-2', @@ -2952,6 +3009,7 @@ 'original_name': 'RealFeel temperature max day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-3', @@ -3002,6 +3060,7 @@ 'original_name': 'RealFeel temperature max day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_max', 'unique_id': '0123456-realfeeltemperaturemax-4', @@ -3052,6 +3111,7 @@ 'original_name': 'RealFeel temperature min day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-0', @@ -3102,6 +3162,7 @@ 'original_name': 'RealFeel temperature min day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-1', @@ -3152,6 +3213,7 @@ 'original_name': 'RealFeel temperature min day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-2', @@ -3202,6 +3264,7 @@ 'original_name': 'RealFeel temperature min day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-3', @@ -3252,6 +3315,7 @@ 'original_name': 'RealFeel temperature min day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_min', 'unique_id': '0123456-realfeeltemperaturemin-4', @@ -3304,6 +3368,7 @@ 'original_name': 'RealFeel temperature shade', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade', 'unique_id': '0123456-realfeeltemperatureshade', @@ -3355,6 +3420,7 @@ 'original_name': 'RealFeel temperature shade max day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-0', @@ -3405,6 +3471,7 @@ 'original_name': 'RealFeel temperature shade max day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-1', @@ -3455,6 +3522,7 @@ 'original_name': 'RealFeel temperature shade max day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-2', @@ -3505,6 +3573,7 @@ 'original_name': 'RealFeel temperature shade max day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-3', @@ -3555,6 +3624,7 @@ 'original_name': 'RealFeel temperature shade max day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_max', 'unique_id': '0123456-realfeeltemperatureshademax-4', @@ -3605,6 +3675,7 @@ 'original_name': 'RealFeel temperature shade min day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-0', @@ -3655,6 +3726,7 @@ 'original_name': 'RealFeel temperature shade min day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-1', @@ -3705,6 +3777,7 @@ 'original_name': 'RealFeel temperature shade min day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-2', @@ -3755,6 +3828,7 @@ 'original_name': 'RealFeel temperature shade min day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-3', @@ -3805,6 +3879,7 @@ 'original_name': 'RealFeel temperature shade min day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'realfeel_temperature_shade_min', 'unique_id': '0123456-realfeeltemperatureshademin-4', @@ -3855,6 +3930,7 @@ 'original_name': 'Solar irradiance day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-0', @@ -3905,6 +3981,7 @@ 'original_name': 'Solar irradiance day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-1', @@ -3955,6 +4032,7 @@ 'original_name': 'Solar irradiance day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-2', @@ -4005,6 +4083,7 @@ 'original_name': 'Solar irradiance day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-3', @@ -4055,6 +4134,7 @@ 'original_name': 'Solar irradiance day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_day', 'unique_id': '0123456-solarirradianceday-4', @@ -4105,6 +4185,7 @@ 'original_name': 'Solar irradiance night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-0', @@ -4155,6 +4236,7 @@ 'original_name': 'Solar irradiance night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-1', @@ -4205,6 +4287,7 @@ 'original_name': 'Solar irradiance night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-2', @@ -4255,6 +4338,7 @@ 'original_name': 'Solar irradiance night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-3', @@ -4305,6 +4389,7 @@ 'original_name': 'Solar irradiance night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_irradiance_night', 'unique_id': '0123456-solarirradiancenight-4', @@ -4357,6 +4442,7 @@ 'original_name': 'Temperature', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '0123456-temperature', @@ -4408,6 +4494,7 @@ 'original_name': 'Thunderstorm probability day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-0', @@ -4457,6 +4544,7 @@ 'original_name': 'Thunderstorm probability day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-1', @@ -4506,6 +4594,7 @@ 'original_name': 'Thunderstorm probability day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-2', @@ -4555,6 +4644,7 @@ 'original_name': 'Thunderstorm probability day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-3', @@ -4604,6 +4694,7 @@ 'original_name': 'Thunderstorm probability day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_day', 'unique_id': '0123456-thunderstormprobabilityday-4', @@ -4653,6 +4744,7 @@ 'original_name': 'Thunderstorm probability night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-0', @@ -4702,6 +4794,7 @@ 'original_name': 'Thunderstorm probability night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-1', @@ -4751,6 +4844,7 @@ 'original_name': 'Thunderstorm probability night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-2', @@ -4800,6 +4894,7 @@ 'original_name': 'Thunderstorm probability night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-3', @@ -4849,6 +4944,7 @@ 'original_name': 'Thunderstorm probability night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thunderstorm_probability_night', 'unique_id': '0123456-thunderstormprobabilitynight-4', @@ -4898,6 +4994,7 @@ 'original_name': 'Tree pollen day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-0', @@ -4948,6 +5045,7 @@ 'original_name': 'Tree pollen day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-1', @@ -4998,6 +5096,7 @@ 'original_name': 'Tree pollen day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-2', @@ -5048,6 +5147,7 @@ 'original_name': 'Tree pollen day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-3', @@ -5098,6 +5198,7 @@ 'original_name': 'Tree pollen day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tree_pollen', 'unique_id': '0123456-tree-4', @@ -5150,6 +5251,7 @@ 'original_name': 'UV index', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index', 'unique_id': '0123456-uvindex', @@ -5201,6 +5303,7 @@ 'original_name': 'UV index day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-0', @@ -5251,6 +5354,7 @@ 'original_name': 'UV index day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-1', @@ -5301,6 +5405,7 @@ 'original_name': 'UV index day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-2', @@ -5351,6 +5456,7 @@ 'original_name': 'UV index day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-3', @@ -5401,6 +5507,7 @@ 'original_name': 'UV index day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index_forecast', 'unique_id': '0123456-uvindex-4', @@ -5453,6 +5560,7 @@ 'original_name': 'Wet bulb temperature', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wet_bulb_temperature', 'unique_id': '0123456-wetbulbtemperature', @@ -5506,6 +5614,7 @@ 'original_name': 'Wind chill temperature', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_chill_temperature', 'unique_id': '0123456-windchilltemperature', @@ -5559,6 +5668,7 @@ 'original_name': 'Wind gust speed', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed', 'unique_id': '0123456-windgust', @@ -5610,6 +5720,7 @@ 'original_name': 'Wind gust speed day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-0', @@ -5661,6 +5772,7 @@ 'original_name': 'Wind gust speed day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-1', @@ -5712,6 +5824,7 @@ 'original_name': 'Wind gust speed day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-2', @@ -5763,6 +5876,7 @@ 'original_name': 'Wind gust speed day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-3', @@ -5814,6 +5928,7 @@ 'original_name': 'Wind gust speed day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_day', 'unique_id': '0123456-windgustday-4', @@ -5865,6 +5980,7 @@ 'original_name': 'Wind gust speed night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-0', @@ -5916,6 +6032,7 @@ 'original_name': 'Wind gust speed night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-1', @@ -5967,6 +6084,7 @@ 'original_name': 'Wind gust speed night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-2', @@ -6018,6 +6136,7 @@ 'original_name': 'Wind gust speed night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-3', @@ -6069,6 +6188,7 @@ 'original_name': 'Wind gust speed night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust_speed_night', 'unique_id': '0123456-windgustnight-4', @@ -6122,6 +6242,7 @@ 'original_name': 'Wind speed', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed', 'unique_id': '0123456-wind', @@ -6173,6 +6294,7 @@ 'original_name': 'Wind speed day 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-0', @@ -6224,6 +6346,7 @@ 'original_name': 'Wind speed day 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-1', @@ -6275,6 +6398,7 @@ 'original_name': 'Wind speed day 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-2', @@ -6326,6 +6450,7 @@ 'original_name': 'Wind speed day 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-3', @@ -6377,6 +6502,7 @@ 'original_name': 'Wind speed day 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_day', 'unique_id': '0123456-windday-4', @@ -6428,6 +6554,7 @@ 'original_name': 'Wind speed night 0', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-0', @@ -6479,6 +6606,7 @@ 'original_name': 'Wind speed night 1', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-1', @@ -6530,6 +6658,7 @@ 'original_name': 'Wind speed night 2', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-2', @@ -6581,6 +6710,7 @@ 'original_name': 'Wind speed night 3', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-3', @@ -6632,6 +6762,7 @@ 'original_name': 'Wind speed night 4', 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed_night', 'unique_id': '0123456-windnight-4', diff --git a/tests/components/accuweather/snapshots/test_weather.ambr b/tests/components/accuweather/snapshots/test_weather.ambr index 862d79c2fde..254667d7809 100644 --- a/tests/components/accuweather/snapshots/test_weather.ambr +++ b/tests/components/accuweather/snapshots/test_weather.ambr @@ -268,6 +268,7 @@ 'original_name': None, 'platform': 'accuweather', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '0123456', diff --git a/tests/components/airgradient/snapshots/test_button.ambr b/tests/components/airgradient/snapshots/test_button.ambr index 85ad29f98f2..ca4c55230d2 100644 --- a/tests/components/airgradient/snapshots/test_button.ambr +++ b/tests/components/airgradient/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Calibrate CO2 sensor', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_calibration', 'unique_id': '84fce612f5b8-co2_calibration', @@ -74,6 +75,7 @@ 'original_name': 'Test LED bar', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_bar_test', 'unique_id': '84fce612f5b8-led_bar_test', @@ -121,6 +123,7 @@ 'original_name': 'Calibrate CO2 sensor', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_calibration', 'unique_id': '84fce612f5b8-co2_calibration', diff --git a/tests/components/airgradient/snapshots/test_number.ambr b/tests/components/airgradient/snapshots/test_number.ambr index f847a4a472d..4440f4353a1 100644 --- a/tests/components/airgradient/snapshots/test_number.ambr +++ b/tests/components/airgradient/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Display brightness', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_brightness', 'unique_id': '84fce612f5b8-display_brightness', @@ -89,6 +90,7 @@ 'original_name': 'LED bar brightness', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_bar_brightness', 'unique_id': '84fce612f5b8-led_bar_brightness', diff --git a/tests/components/airgradient/snapshots/test_select.ambr b/tests/components/airgradient/snapshots/test_select.ambr index cc080560ae5..f282d27bc61 100644 --- a/tests/components/airgradient/snapshots/test_select.ambr +++ b/tests/components/airgradient/snapshots/test_select.ambr @@ -36,6 +36,7 @@ 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_automatic_baseline_calibration', 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', @@ -96,6 +97,7 @@ 'original_name': 'Configuration source', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'configuration_control', 'unique_id': '84fce612f5b8-configuration_control', @@ -152,6 +154,7 @@ 'original_name': 'Display PM standard', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_pm_standard', 'unique_id': '84fce612f5b8-display_pm_standard', @@ -208,6 +211,7 @@ 'original_name': 'Display temperature unit', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_temperature_unit', 'unique_id': '84fce612f5b8-display_temperature_unit', @@ -265,6 +269,7 @@ 'original_name': 'LED bar mode', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_bar_mode', 'unique_id': '84fce612f5b8-led_bar_mode', @@ -325,6 +330,7 @@ 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nox_index_learning_time_offset', 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', @@ -387,6 +393,7 @@ 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voc_index_learning_time_offset', 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', @@ -450,6 +457,7 @@ 'original_name': 'CO2 automatic baseline duration', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_automatic_baseline_calibration', 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration', @@ -510,6 +518,7 @@ 'original_name': 'Configuration source', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'configuration_control', 'unique_id': '84fce612f5b8-configuration_control', @@ -569,6 +578,7 @@ 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nox_index_learning_time_offset', 'unique_id': '84fce612f5b8-nox_index_learning_time_offset', @@ -631,6 +641,7 @@ 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voc_index_learning_time_offset', 'unique_id': '84fce612f5b8-voc_index_learning_time_offset', diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 374d9a60e4e..a0daaef2bdc 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-co2', @@ -79,6 +80,7 @@ 'original_name': 'Carbon dioxide automatic baseline calibration', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_automatic_baseline_calibration_days', 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', @@ -128,6 +130,7 @@ 'original_name': 'Display brightness', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_brightness', 'unique_id': '84fce612f5b8-display_brightness', @@ -181,6 +184,7 @@ 'original_name': 'Display PM standard', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_pm_standard', 'unique_id': '84fce612f5b8-display_pm_standard', @@ -238,6 +242,7 @@ 'original_name': 'Display temperature unit', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_temperature_unit', 'unique_id': '84fce612f5b8-display_temperature_unit', @@ -292,6 +297,7 @@ 'original_name': 'Humidity', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-humidity', @@ -342,6 +348,7 @@ 'original_name': 'LED bar brightness', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_bar_brightness', 'unique_id': '84fce612f5b8-led_bar_brightness', @@ -396,6 +403,7 @@ 'original_name': 'LED bar mode', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_bar_mode', 'unique_id': '84fce612f5b8-led_bar_mode', @@ -451,6 +459,7 @@ 'original_name': 'NOx index', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nitrogen_index', 'unique_id': '84fce612f5b8-nitrogen_index', @@ -499,6 +508,7 @@ 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nox_learning_offset', 'unique_id': '84fce612f5b8-nox_learning_offset', @@ -550,6 +560,7 @@ 'original_name': 'PM0.3', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pm003_count', 'unique_id': '84fce612f5b8-pm003', @@ -601,6 +612,7 @@ 'original_name': 'PM1', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm01', @@ -653,6 +665,7 @@ 'original_name': 'PM10', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm10', @@ -705,6 +718,7 @@ 'original_name': 'PM2.5', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-pm02', @@ -757,6 +771,7 @@ 'original_name': 'Raw NOx', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raw_nitrogen', 'unique_id': '84fce612f5b8-nox_raw', @@ -808,6 +823,7 @@ 'original_name': 'Raw PM2.5', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raw_pm02', 'unique_id': '84fce612f5b8-pm02_raw', @@ -860,6 +876,7 @@ 'original_name': 'Raw VOC', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raw_total_volatile_organic_component', 'unique_id': '84fce612f5b8-tvoc_raw', @@ -911,6 +928,7 @@ 'original_name': 'Signal strength', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-signal_strength', @@ -963,6 +981,7 @@ 'original_name': 'Temperature', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-temperature', @@ -1015,6 +1034,7 @@ 'original_name': 'VOC index', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_volatile_organic_component_index', 'unique_id': '84fce612f5b8-tvoc', @@ -1063,6 +1083,7 @@ 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tvoc_learning_offset', 'unique_id': '84fce612f5b8-tvoc_learning_offset', @@ -1112,6 +1133,7 @@ 'original_name': 'Carbon dioxide automatic baseline calibration', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_automatic_baseline_calibration_days', 'unique_id': '84fce612f5b8-co2_automatic_baseline_calibration_days', @@ -1163,6 +1185,7 @@ 'original_name': 'NOx index', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nitrogen_index', 'unique_id': '84fce612f5b8-nitrogen_index', @@ -1211,6 +1234,7 @@ 'original_name': 'NOx index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nox_learning_offset', 'unique_id': '84fce612f5b8-nox_learning_offset', @@ -1262,6 +1286,7 @@ 'original_name': 'Raw NOx', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raw_nitrogen', 'unique_id': '84fce612f5b8-nox_raw', @@ -1313,6 +1338,7 @@ 'original_name': 'Raw VOC', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raw_total_volatile_organic_component', 'unique_id': '84fce612f5b8-tvoc_raw', @@ -1364,6 +1390,7 @@ 'original_name': 'Signal strength', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-signal_strength', @@ -1416,6 +1443,7 @@ 'original_name': 'VOC index', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_volatile_organic_component_index', 'unique_id': '84fce612f5b8-tvoc', @@ -1464,6 +1492,7 @@ 'original_name': 'VOC index learning offset', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tvoc_learning_offset', 'unique_id': '84fce612f5b8-tvoc_learning_offset', diff --git a/tests/components/airgradient/snapshots/test_switch.ambr b/tests/components/airgradient/snapshots/test_switch.ambr index ae2116d5b29..f39654d66a7 100644 --- a/tests/components/airgradient/snapshots/test_switch.ambr +++ b/tests/components/airgradient/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Post data to Airgradient', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'post_data_to_airgradient', 'unique_id': '84fce612f5b8-post_data_to_airgradient', diff --git a/tests/components/airgradient/snapshots/test_update.ambr b/tests/components/airgradient/snapshots/test_update.ambr index 53c815629f2..cf8ccec28dd 100644 --- a/tests/components/airgradient/snapshots/test_update.ambr +++ b/tests/components/airgradient/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Firmware', 'platform': 'airgradient', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '84fce612f5b8-update', diff --git a/tests/components/airly/snapshots/test_sensor.ambr b/tests/components/airly/snapshots/test_sensor.ambr index 134023f34e0..efd809e76ae 100644 --- a/tests/components/airly/snapshots/test_sensor.ambr +++ b/tests/components/airly/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Carbon monoxide', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co', 'unique_id': '123-456-co', @@ -87,6 +88,7 @@ 'original_name': 'Common air quality index', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'caqi', 'unique_id': '123-456-caqi', @@ -144,6 +146,7 @@ 'original_name': 'Humidity', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-humidity', @@ -200,6 +203,7 @@ 'original_name': 'Nitrogen dioxide', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-no2', @@ -258,6 +262,7 @@ 'original_name': 'Ozone', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-o3', @@ -316,6 +321,7 @@ 'original_name': 'PM1', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm1', @@ -372,6 +378,7 @@ 'original_name': 'PM10', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm10', @@ -430,6 +437,7 @@ 'original_name': 'PM2.5', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pm25', @@ -488,6 +496,7 @@ 'original_name': 'Pressure', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-pressure', @@ -544,6 +553,7 @@ 'original_name': 'Sulphur dioxide', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-so2', @@ -602,6 +612,7 @@ 'original_name': 'Temperature', 'platform': 'airly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-456-temperature', diff --git a/tests/components/airtouch5/snapshots/test_cover.ambr b/tests/components/airtouch5/snapshots/test_cover.ambr index d2ae3cddc7f..3db5075eb0f 100644 --- a/tests/components/airtouch5/snapshots/test_cover.ambr +++ b/tests/components/airtouch5/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Damper', 'platform': 'airtouch5', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'damper', 'unique_id': 'zone_1_open_percentage', @@ -77,6 +78,7 @@ 'original_name': 'Damper', 'platform': 'airtouch5', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'damper', 'unique_id': 'zone_2_open_percentage', diff --git a/tests/components/airzone/snapshots/test_sensor.ambr b/tests/components/airzone/snapshots/test_sensor.ambr index 01ebf35b282..2982f76efe7 100644 --- a/tests/components/airzone/snapshots/test_sensor.ambr +++ b/tests/components/airzone/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_2:1_humidity', @@ -81,6 +82,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_2:1_temp', @@ -133,6 +135,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_dhw_temp', @@ -185,6 +188,7 @@ 'original_name': 'RSSI', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rssi', 'unique_id': 'airzone_unique_id_ws_wifi-rssi', @@ -237,6 +241,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_4:1_temp', @@ -289,6 +294,7 @@ 'original_name': 'Battery', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:4_thermostat-battery', @@ -341,6 +347,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:4_humidity', @@ -393,6 +400,7 @@ 'original_name': 'Signal strength', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_signal', 'unique_id': 'airzone_unique_id_1:4_thermostat-signal', @@ -444,6 +452,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:4_temp', @@ -496,6 +505,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_3:1_temp', @@ -548,6 +558,7 @@ 'original_name': 'Battery', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:3_thermostat-battery', @@ -600,6 +611,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:3_humidity', @@ -652,6 +664,7 @@ 'original_name': 'Signal strength', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_signal', 'unique_id': 'airzone_unique_id_1:3_thermostat-signal', @@ -703,6 +716,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:3_temp', @@ -755,6 +769,7 @@ 'original_name': 'Battery', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:5_thermostat-battery', @@ -807,6 +822,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:5_humidity', @@ -859,6 +875,7 @@ 'original_name': 'Signal strength', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_signal', 'unique_id': 'airzone_unique_id_1:5_thermostat-signal', @@ -910,6 +927,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:5_temp', @@ -962,6 +980,7 @@ 'original_name': 'Battery', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:2_thermostat-battery', @@ -1014,6 +1033,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:2_humidity', @@ -1066,6 +1086,7 @@ 'original_name': 'Signal strength', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_signal', 'unique_id': 'airzone_unique_id_1:2_thermostat-signal', @@ -1117,6 +1138,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:2_temp', @@ -1169,6 +1191,7 @@ 'original_name': 'Humidity', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:1_humidity', @@ -1221,6 +1244,7 @@ 'original_name': 'Temperature', 'platform': 'airzone', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'airzone_unique_id_1:1_temp', diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr index 1033d63eba4..0d3a5252a73 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Bluetooth', 'platform': 'amazon_devices', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bluetooth', 'unique_id': 'echo_test_serial_number-bluetooth', @@ -74,6 +75,7 @@ 'original_name': 'Connectivity', 'platform': 'amazon_devices', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'echo_test_serial_number-online', diff --git a/tests/components/amazon_devices/snapshots/test_notify.ambr b/tests/components/amazon_devices/snapshots/test_notify.ambr index 47983abd269..a47bf7a63ae 100644 --- a/tests/components/amazon_devices/snapshots/test_notify.ambr +++ b/tests/components/amazon_devices/snapshots/test_notify.ambr @@ -27,6 +27,7 @@ 'original_name': 'Announce', 'platform': 'amazon_devices', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'announce', 'unique_id': 'echo_test_serial_number-announce', @@ -75,6 +76,7 @@ 'original_name': 'Speak', 'platform': 'amazon_devices', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'speak', 'unique_id': 'echo_test_serial_number-speak', diff --git a/tests/components/amazon_devices/snapshots/test_switch.ambr b/tests/components/amazon_devices/snapshots/test_switch.ambr index b6b1d0579d2..8a2ce8d529a 100644 --- a/tests/components/amazon_devices/snapshots/test_switch.ambr +++ b/tests/components/amazon_devices/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Do not disturb', 'platform': 'amazon_devices', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'do_not_disturb', 'unique_id': 'echo_test_serial_number-do_not_disturb', diff --git a/tests/components/ambient_network/snapshots/test_sensor.ambr b/tests/components/ambient_network/snapshots/test_sensor.ambr index ddf05c99b88..2583ac85984 100644 --- a/tests/components/ambient_network/snapshots/test_sensor.ambr +++ b/tests/components/ambient_network/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Absolute pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'absolute_pressure', 'unique_id': 'AA:AA:AA:AA:AA:AA_baromabsin', @@ -95,6 +96,7 @@ 'original_name': 'Daily rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_rain', 'unique_id': 'AA:AA:AA:AA:AA:AA_dailyrainin', @@ -152,6 +154,7 @@ 'original_name': 'Dew point', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': 'AA:AA:AA:AA:AA:AA_dewPoint', @@ -209,6 +212,7 @@ 'original_name': 'Feels like', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feels_like', 'unique_id': 'AA:AA:AA:AA:AA:AA_feelsLike', @@ -269,6 +273,7 @@ 'original_name': 'Hourly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hourly_rain', 'unique_id': 'AA:AA:AA:AA:AA:AA_hourlyrainin', @@ -326,6 +331,7 @@ 'original_name': 'Humidity', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:AA_humidity', @@ -383,6 +389,7 @@ 'original_name': 'Irradiance', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:AA_solarradiation', @@ -435,6 +442,7 @@ 'original_name': 'Last rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_rain', 'unique_id': 'AA:AA:AA:AA:AA:AA_lastRain', @@ -493,6 +501,7 @@ 'original_name': 'Max daily gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_daily_gust', 'unique_id': 'AA:AA:AA:AA:AA:AA_maxdailygust', @@ -553,6 +562,7 @@ 'original_name': 'Monthly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_rain', 'unique_id': 'AA:AA:AA:AA:AA:AA_monthlyrainin', @@ -613,6 +623,7 @@ 'original_name': 'Relative pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_pressure', 'unique_id': 'AA:AA:AA:AA:AA:AA_baromrelin', @@ -670,6 +681,7 @@ 'original_name': 'Temperature', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:AA_tempf', @@ -727,6 +739,7 @@ 'original_name': 'UV index', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index', 'unique_id': 'AA:AA:AA:AA:AA:AA_uv', @@ -786,6 +799,7 @@ 'original_name': 'Weekly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_rain', 'unique_id': 'AA:AA:AA:AA:AA:AA_weeklyrainin', @@ -843,6 +857,7 @@ 'original_name': 'Wind direction', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_direction', 'unique_id': 'AA:AA:AA:AA:AA:AA_winddir', @@ -903,6 +918,7 @@ 'original_name': 'Wind gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust', 'unique_id': 'AA:AA:AA:AA:AA:AA_windgustmph', @@ -963,6 +979,7 @@ 'original_name': 'Wind speed', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:AA_windspeedmph', @@ -1023,6 +1040,7 @@ 'original_name': 'Absolute pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'absolute_pressure', 'unique_id': 'CC:CC:CC:CC:CC:CC_baromabsin', @@ -1083,6 +1101,7 @@ 'original_name': 'Daily rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_rain', 'unique_id': 'CC:CC:CC:CC:CC:CC_dailyrainin', @@ -1140,6 +1159,7 @@ 'original_name': 'Dew point', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': 'CC:CC:CC:CC:CC:CC_dewPoint', @@ -1197,6 +1217,7 @@ 'original_name': 'Feels like', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feels_like', 'unique_id': 'CC:CC:CC:CC:CC:CC_feelsLike', @@ -1257,6 +1278,7 @@ 'original_name': 'Hourly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hourly_rain', 'unique_id': 'CC:CC:CC:CC:CC:CC_hourlyrainin', @@ -1314,6 +1336,7 @@ 'original_name': 'Humidity', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CC:CC:CC:CC:CC:CC_humidity', @@ -1371,6 +1394,7 @@ 'original_name': 'Irradiance', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CC:CC:CC:CC:CC:CC_solarradiation', @@ -1423,6 +1447,7 @@ 'original_name': 'Last rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_rain', 'unique_id': 'CC:CC:CC:CC:CC:CC_lastRain', @@ -1481,6 +1506,7 @@ 'original_name': 'Max daily gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_daily_gust', 'unique_id': 'CC:CC:CC:CC:CC:CC_maxdailygust', @@ -1541,6 +1567,7 @@ 'original_name': 'Monthly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_rain', 'unique_id': 'CC:CC:CC:CC:CC:CC_monthlyrainin', @@ -1601,6 +1628,7 @@ 'original_name': 'Relative pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_pressure', 'unique_id': 'CC:CC:CC:CC:CC:CC_baromrelin', @@ -1658,6 +1686,7 @@ 'original_name': 'Temperature', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CC:CC:CC:CC:CC:CC_tempf', @@ -1715,6 +1744,7 @@ 'original_name': 'UV index', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index', 'unique_id': 'CC:CC:CC:CC:CC:CC_uv', @@ -1774,6 +1804,7 @@ 'original_name': 'Weekly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_rain', 'unique_id': 'CC:CC:CC:CC:CC:CC_weeklyrainin', @@ -1831,6 +1862,7 @@ 'original_name': 'Wind direction', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_direction', 'unique_id': 'CC:CC:CC:CC:CC:CC_winddir', @@ -1891,6 +1923,7 @@ 'original_name': 'Wind gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust', 'unique_id': 'CC:CC:CC:CC:CC:CC_windgustmph', @@ -1951,6 +1984,7 @@ 'original_name': 'Wind speed', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CC:CC:CC:CC:CC:CC_windspeedmph', @@ -2011,6 +2045,7 @@ 'original_name': 'Absolute pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'absolute_pressure', 'unique_id': 'DD:DD:DD:DD:DD:DD_baromabsin', @@ -2070,6 +2105,7 @@ 'original_name': 'Daily rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_rain', 'unique_id': 'DD:DD:DD:DD:DD:DD_dailyrainin', @@ -2126,6 +2162,7 @@ 'original_name': 'Dew point', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': 'DD:DD:DD:DD:DD:DD_dewPoint', @@ -2182,6 +2219,7 @@ 'original_name': 'Feels like', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feels_like', 'unique_id': 'DD:DD:DD:DD:DD:DD_feelsLike', @@ -2241,6 +2279,7 @@ 'original_name': 'Hourly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hourly_rain', 'unique_id': 'DD:DD:DD:DD:DD:DD_hourlyrainin', @@ -2297,6 +2336,7 @@ 'original_name': 'Humidity', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DD:DD:DD:DD:DD:DD_humidity', @@ -2353,6 +2393,7 @@ 'original_name': 'Irradiance', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DD:DD:DD:DD:DD:DD_solarradiation', @@ -2412,6 +2453,7 @@ 'original_name': 'Max daily gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_daily_gust', 'unique_id': 'DD:DD:DD:DD:DD:DD_maxdailygust', @@ -2471,6 +2513,7 @@ 'original_name': 'Monthly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_rain', 'unique_id': 'DD:DD:DD:DD:DD:DD_monthlyrainin', @@ -2530,6 +2573,7 @@ 'original_name': 'Relative pressure', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_pressure', 'unique_id': 'DD:DD:DD:DD:DD:DD_baromrelin', @@ -2586,6 +2630,7 @@ 'original_name': 'Temperature', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DD:DD:DD:DD:DD:DD_tempf', @@ -2642,6 +2687,7 @@ 'original_name': 'UV index', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv_index', 'unique_id': 'DD:DD:DD:DD:DD:DD_uv', @@ -2700,6 +2746,7 @@ 'original_name': 'Weekly rain', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_rain', 'unique_id': 'DD:DD:DD:DD:DD:DD_weeklyrainin', @@ -2756,6 +2803,7 @@ 'original_name': 'Wind direction', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_direction', 'unique_id': 'DD:DD:DD:DD:DD:DD_winddir', @@ -2815,6 +2863,7 @@ 'original_name': 'Wind gust', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_gust', 'unique_id': 'DD:DD:DD:DD:DD:DD_windgustmph', @@ -2874,6 +2923,7 @@ 'original_name': 'Wind speed', 'platform': 'ambient_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DD:DD:DD:DD:DD:DD_windspeedmph', diff --git a/tests/components/analytics_insights/snapshots/test_sensor.ambr b/tests/components/analytics_insights/snapshots/test_sensor.ambr index 799738eb677..4b71e2fef3e 100644 --- a/tests/components/analytics_insights/snapshots/test_sensor.ambr +++ b/tests/components/analytics_insights/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'core_samba', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'addons', 'unique_id': 'addon_core_samba_active_installations', @@ -80,6 +81,7 @@ 'original_name': 'hacs (custom)', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'custom_integrations', 'unique_id': 'custom_hacs_active_installations', @@ -131,6 +133,7 @@ 'original_name': 'myq', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'core_integrations', 'unique_id': 'core_myq_active_installations', @@ -182,6 +185,7 @@ 'original_name': 'spotify', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'core_integrations', 'unique_id': 'core_spotify_active_installations', @@ -233,6 +237,7 @@ 'original_name': 'Total active installations', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_active_installations', 'unique_id': 'total_active_installations', @@ -284,6 +289,7 @@ 'original_name': 'Total reported integrations', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_reports_integrations', 'unique_id': 'total_reports_integrations', @@ -335,6 +341,7 @@ 'original_name': 'YouTube', 'platform': 'analytics_insights', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'core_integrations', 'unique_id': 'core_youtube_active_installations', diff --git a/tests/components/aosmith/snapshots/test_sensor.ambr b/tests/components/aosmith/snapshots/test_sensor.ambr index c422e8fdab5..ae0752ee1ed 100644 --- a/tests/components/aosmith/snapshots/test_sensor.ambr +++ b/tests/components/aosmith/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Energy usage', 'platform': 'aosmith', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_usage', 'unique_id': 'energy_usage_junctionId', @@ -82,6 +83,7 @@ 'original_name': 'Hot water availability', 'platform': 'aosmith', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hot_water_availability', 'unique_id': 'hot_water_availability_junctionId', diff --git a/tests/components/aosmith/snapshots/test_water_heater.ambr b/tests/components/aosmith/snapshots/test_water_heater.ambr index 43db89807b6..452b2a05e2e 100644 --- a/tests/components/aosmith/snapshots/test_water_heater.ambr +++ b/tests/components/aosmith/snapshots/test_water_heater.ambr @@ -30,6 +30,7 @@ 'original_name': None, 'platform': 'aosmith', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'junctionId', @@ -93,6 +94,7 @@ 'original_name': None, 'platform': 'aosmith', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'junctionId', diff --git a/tests/components/apcupsd/snapshots/test_binary_sensor.ambr b/tests/components/apcupsd/snapshots/test_binary_sensor.ambr index 0ab9dfb047e..898525cde9c 100644 --- a/tests/components/apcupsd/snapshots/test_binary_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Online status', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'online_status', 'unique_id': 'XXXXXXXXXXXX_statflag', diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr index 1be83198dcc..814a3c63a81 100644 --- a/tests/components/apcupsd/snapshots/test_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm delay', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_delay', 'unique_id': 'XXXXXXXXXXXX_alarmdel', @@ -77,6 +78,7 @@ 'original_name': 'Battery', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'XXXXXXXXXXXX_bcharge', @@ -127,6 +129,7 @@ 'original_name': 'Battery nominal voltage', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_nominal_voltage', 'unique_id': 'XXXXXXXXXXXX_nombattv', @@ -176,6 +179,7 @@ 'original_name': 'Battery replaced', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_replacement_date', 'unique_id': 'XXXXXXXXXXXX_battdate', @@ -223,6 +227,7 @@ 'original_name': 'Battery shutdown', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_battery_charge', 'unique_id': 'XXXXXXXXXXXX_mbattchg', @@ -271,6 +276,7 @@ 'original_name': 'Battery timeout', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_time', 'unique_id': 'XXXXXXXXXXXX_maxtime', @@ -321,6 +327,7 @@ 'original_name': 'Battery voltage', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'XXXXXXXXXXXX_battv', @@ -371,6 +378,7 @@ 'original_name': 'Cable type', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cable_type', 'unique_id': 'XXXXXXXXXXXX_cable', @@ -418,6 +426,7 @@ 'original_name': 'Daemon version', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'version', 'unique_id': 'XXXXXXXXXXXX_version', @@ -465,6 +474,7 @@ 'original_name': 'Date and time', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'date_and_time', 'unique_id': 'XXXXXXXXXXXX_end apc', @@ -512,6 +522,7 @@ 'original_name': 'Driver', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'driver', 'unique_id': 'XXXXXXXXXXXX_driver', @@ -559,6 +570,7 @@ 'original_name': 'Firmware version', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'firmware_version', 'unique_id': 'XXXXXXXXXXXX_firmware', @@ -608,6 +620,7 @@ 'original_name': 'Input voltage', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'line_voltage', 'unique_id': 'XXXXXXXXXXXX_linev', @@ -660,6 +673,7 @@ 'original_name': 'Internal temperature', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'internal_temperature', 'unique_id': 'XXXXXXXXXXXX_itemp', @@ -710,6 +724,7 @@ 'original_name': 'Last self-test', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_self_test', 'unique_id': 'XXXXXXXXXXXX_laststest', @@ -757,6 +772,7 @@ 'original_name': 'Last transfer', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_transfer', 'unique_id': 'XXXXXXXXXXXX_lastxfer', @@ -806,6 +822,7 @@ 'original_name': 'Load', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_capacity', 'unique_id': 'XXXXXXXXXXXX_loadpct', @@ -855,6 +872,7 @@ 'original_name': 'Mode', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ups_mode', 'unique_id': 'XXXXXXXXXXXX_upsmode', @@ -902,6 +920,7 @@ 'original_name': 'Model', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'model', 'unique_id': 'XXXXXXXXXXXX_model', @@ -949,6 +968,7 @@ 'original_name': 'Name', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ups_name', 'unique_id': 'XXXXXXXXXXXX_upsname', @@ -996,6 +1016,7 @@ 'original_name': 'Nominal apparent power', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nominal_apparent_power', 'unique_id': 'XXXXXXXXXXXX_nomapnt', @@ -1045,6 +1066,7 @@ 'original_name': 'Nominal input voltage', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nominal_input_voltage', 'unique_id': 'XXXXXXXXXXXX_nominv', @@ -1094,6 +1116,7 @@ 'original_name': 'Nominal output power', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nominal_output_power', 'unique_id': 'XXXXXXXXXXXX_nompower', @@ -1145,6 +1168,7 @@ 'original_name': 'Output current', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_current', 'unique_id': 'XXXXXXXXXXXX_outcurnt', @@ -1195,6 +1219,7 @@ 'original_name': 'Self-test interval', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'self_test_interval', 'unique_id': 'XXXXXXXXXXXX_stesti', @@ -1243,6 +1268,7 @@ 'original_name': 'Self-test result', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'self_test_result', 'unique_id': 'XXXXXXXXXXXX_selftest', @@ -1290,6 +1316,7 @@ 'original_name': 'Sensitivity', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensitivity', 'unique_id': 'XXXXXXXXXXXX_sense', @@ -1337,6 +1364,7 @@ 'original_name': 'Serial number', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'serial_number', 'unique_id': 'XXXXXXXXXXXX_serialno', @@ -1384,6 +1412,7 @@ 'original_name': 'Shutdown time', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'min_time', 'unique_id': 'XXXXXXXXXXXX_mintimel', @@ -1432,6 +1461,7 @@ 'original_name': 'Status', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'XXXXXXXXXXXX_status', @@ -1479,6 +1509,7 @@ 'original_name': 'Status data', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'apc_status', 'unique_id': 'XXXXXXXXXXXX_apc', @@ -1526,6 +1557,7 @@ 'original_name': 'Status date', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'date', 'unique_id': 'XXXXXXXXXXXX_date', @@ -1573,6 +1605,7 @@ 'original_name': 'Status flag', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'online_status', 'unique_id': 'XXXXXXXXXXXX_statflag', @@ -1622,6 +1655,7 @@ 'original_name': 'Time left', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_left', 'unique_id': 'XXXXXXXXXXXX_timeleft', @@ -1674,6 +1708,7 @@ 'original_name': 'Time on battery', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_on_battery', 'unique_id': 'XXXXXXXXXXXX_tonbatt', @@ -1726,6 +1761,7 @@ 'original_name': 'Total time on battery', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_time_on_battery', 'unique_id': 'XXXXXXXXXXXX_cumonbatt', @@ -1778,6 +1814,7 @@ 'original_name': 'Transfer count', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfer_count', 'unique_id': 'XXXXXXXXXXXX_numxfers', @@ -1826,6 +1863,7 @@ 'original_name': 'Transfer from battery', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfer_from_battery', 'unique_id': 'XXXXXXXXXXXX_xoffbatt', @@ -1873,6 +1911,7 @@ 'original_name': 'Transfer high', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfer_high', 'unique_id': 'XXXXXXXXXXXX_hitrans', @@ -1922,6 +1961,7 @@ 'original_name': 'Transfer low', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfer_low', 'unique_id': 'XXXXXXXXXXXX_lotrans', @@ -1971,6 +2011,7 @@ 'original_name': 'Transfer to battery', 'platform': 'apcupsd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfer_to_battery', 'unique_id': 'XXXXXXXXXXXX_xonbatt', diff --git a/tests/components/apsystems/snapshots/test_binary_sensor.ambr b/tests/components/apsystems/snapshots/test_binary_sensor.ambr index d2e73347c83..d8088288461 100644 --- a/tests/components/apsystems/snapshots/test_binary_sensor.ambr +++ b/tests/components/apsystems/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'DC 1 short circuit error status', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dc_1_short_circuit_error_status', 'unique_id': 'MY_SERIAL_NUMBER_dc_1_short_circuit_error_status', @@ -75,6 +76,7 @@ 'original_name': 'DC 2 short circuit error status', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dc_2_short_circuit_error_status', 'unique_id': 'MY_SERIAL_NUMBER_dc_2_short_circuit_error_status', @@ -123,6 +125,7 @@ 'original_name': 'Off-grid status', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_grid_status', 'unique_id': 'MY_SERIAL_NUMBER_off_grid_status', @@ -171,6 +174,7 @@ 'original_name': 'Output fault status', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_fault_status', 'unique_id': 'MY_SERIAL_NUMBER_output_fault_status', diff --git a/tests/components/apsystems/snapshots/test_number.ambr b/tests/components/apsystems/snapshots/test_number.ambr index 21141de7d64..7d02e6e16c4 100644 --- a/tests/components/apsystems/snapshots/test_number.ambr +++ b/tests/components/apsystems/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Max output', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_output', 'unique_id': 'MY_SERIAL_NUMBER_output_limit', diff --git a/tests/components/apsystems/snapshots/test_sensor.ambr b/tests/components/apsystems/snapshots/test_sensor.ambr index 251a8d8428c..42021d88001 100644 --- a/tests/components/apsystems/snapshots/test_sensor.ambr +++ b/tests/components/apsystems/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Lifetime production of P1', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_p1', 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p1', @@ -81,6 +82,7 @@ 'original_name': 'Lifetime production of P2', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_p2', 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production_p2', @@ -133,6 +135,7 @@ 'original_name': 'Power of P1', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power_p1', 'unique_id': 'MY_SERIAL_NUMBER_total_power_p1', @@ -185,6 +188,7 @@ 'original_name': 'Power of P2', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power_p2', 'unique_id': 'MY_SERIAL_NUMBER_total_power_p2', @@ -237,6 +241,7 @@ 'original_name': 'Production of today', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'today_production', 'unique_id': 'MY_SERIAL_NUMBER_today_production', @@ -289,6 +294,7 @@ 'original_name': 'Production of today from P1', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'today_production_p1', 'unique_id': 'MY_SERIAL_NUMBER_today_production_p1', @@ -341,6 +347,7 @@ 'original_name': 'Production of today from P2', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'today_production_p2', 'unique_id': 'MY_SERIAL_NUMBER_today_production_p2', @@ -393,6 +400,7 @@ 'original_name': 'Total lifetime production', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': 'MY_SERIAL_NUMBER_lifetime_production', @@ -445,6 +453,7 @@ 'original_name': 'Total power', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power', 'unique_id': 'MY_SERIAL_NUMBER_total_power', diff --git a/tests/components/apsystems/snapshots/test_switch.ambr b/tests/components/apsystems/snapshots/test_switch.ambr index a9f74ee5517..2b3ccbab6c4 100644 --- a/tests/components/apsystems/snapshots/test_switch.ambr +++ b/tests/components/apsystems/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Inverter status', 'platform': 'apsystems', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'inverter_status', 'unique_id': 'MY_SERIAL_NUMBER_inverter_status', diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr index eeac14c000d..f032f8937de 100644 --- a/tests/components/aquacell/snapshots/test_sensor.ambr +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DSN-battery', @@ -78,6 +79,7 @@ 'original_name': 'Salt left side percentage', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt_left_side_percentage', 'unique_id': 'DSN-salt_left_side_percentage', @@ -127,6 +129,7 @@ 'original_name': 'Salt left side time remaining', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt_left_side_time_remaining', 'unique_id': 'DSN-salt_left_side_time_remaining', @@ -178,6 +181,7 @@ 'original_name': 'Salt right side percentage', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt_right_side_percentage', 'unique_id': 'DSN-salt_right_side_percentage', @@ -227,6 +231,7 @@ 'original_name': 'Salt right side time remaining', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt_right_side_time_remaining', 'unique_id': 'DSN-salt_right_side_time_remaining', @@ -282,6 +287,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wi_fi_strength', 'unique_id': 'DSN-wi_fi_strength', diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr index ed2494c3197..a0f23adf339 100644 --- a/tests/components/arve/snapshots/test_sensor.ambr +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Air quality index', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_AQI', @@ -65,6 +66,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_CO2', @@ -101,6 +103,7 @@ 'original_name': 'Humidity', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_Humidity', @@ -137,6 +140,7 @@ 'original_name': 'PM10', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_PM10', @@ -173,6 +177,7 @@ 'original_name': 'PM2.5', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_PM25', @@ -209,6 +214,7 @@ 'original_name': 'Temperature', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-serial-number_Temperature', @@ -245,6 +251,7 @@ 'original_name': 'Total volatile organic compounds', 'platform': 'arve', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tvoc', 'unique_id': 'test-serial-number_TVOC', diff --git a/tests/components/autarco/snapshots/test_sensor.ambr b/tests/components/autarco/snapshots/test_sensor.ambr index d57f4be5da0..23af1b9c990 100644 --- a/tests/components/autarco/snapshots/test_sensor.ambr +++ b/tests/components/autarco/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Charged month', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charged_month', 'unique_id': '1_battery_charged_month', @@ -81,6 +82,7 @@ 'original_name': 'Charged today', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charged_today', 'unique_id': '1_battery_charged_today', @@ -133,6 +135,7 @@ 'original_name': 'Charged total', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charged_total', 'unique_id': '1_battery_charged_total', @@ -185,6 +188,7 @@ 'original_name': 'Discharged month', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'discharged_month', 'unique_id': '1_battery_discharged_month', @@ -237,6 +241,7 @@ 'original_name': 'Discharged today', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'discharged_today', 'unique_id': '1_battery_discharged_today', @@ -289,6 +294,7 @@ 'original_name': 'Discharged total', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'discharged_total', 'unique_id': '1_battery_discharged_total', @@ -341,6 +347,7 @@ 'original_name': 'Flow now', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flow_now', 'unique_id': '1_battery_flow_now', @@ -393,6 +400,7 @@ 'original_name': 'State of charge', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_of_charge', 'unique_id': '1_battery_state_of_charge', @@ -445,6 +453,7 @@ 'original_name': 'Energy AC output total', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'out_ac_energy_total', 'unique_id': 'test-serial-1_out_ac_energy_total', @@ -497,6 +506,7 @@ 'original_name': 'Power AC output', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'out_ac_power', 'unique_id': 'test-serial-1_out_ac_power', @@ -549,6 +559,7 @@ 'original_name': 'Energy AC output total', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'out_ac_energy_total', 'unique_id': 'test-serial-2_out_ac_energy_total', @@ -601,6 +612,7 @@ 'original_name': 'Power AC output', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'out_ac_power', 'unique_id': 'test-serial-2_out_ac_power', @@ -653,6 +665,7 @@ 'original_name': 'Energy production month', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_production_month', 'unique_id': '1_solar_energy_production_month', @@ -705,6 +718,7 @@ 'original_name': 'Energy production today', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_production_today', 'unique_id': '1_solar_energy_production_today', @@ -757,6 +771,7 @@ 'original_name': 'Energy production total', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_production_total', 'unique_id': '1_solar_energy_production_total', @@ -809,6 +824,7 @@ 'original_name': 'Power production', 'platform': 'autarco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_production', 'unique_id': '1_solar_power_production', diff --git a/tests/components/axis/snapshots/test_binary_sensor.ambr b/tests/components/axis/snapshots/test_binary_sensor.ambr index 6c0f3ead473..fb762800c12 100644 --- a/tests/components/axis/snapshots/test_binary_sensor.ambr +++ b/tests/components/axis/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'DayNight 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:VideoSource/tnsaxis:DayNightVision-1', @@ -75,6 +76,7 @@ 'original_name': 'Object Analytics Device1Scenario8', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8-Device1Scenario8', @@ -123,6 +125,7 @@ 'original_name': 'Sound 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:AudioSource/tnsaxis:TriggerLevel-1', @@ -171,6 +174,7 @@ 'original_name': 'PIR sensor', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:IO/Port-0', @@ -219,6 +223,7 @@ 'original_name': 'PIR 0', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Sensor/PIR-0', @@ -267,6 +272,7 @@ 'original_name': 'Fence Guard Profile 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/FenceGuard/Camera1Profile1-Camera1Profile1', @@ -315,6 +321,7 @@ 'original_name': 'Motion Guard Profile 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/MotionGuard/Camera1Profile1-Camera1Profile1', @@ -363,6 +370,7 @@ 'original_name': 'Loitering Guard Profile 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/LoiteringGuard/Camera1Profile1-Camera1Profile1', @@ -411,6 +419,7 @@ 'original_name': 'VMD4 Profile 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1-Camera1Profile1', @@ -459,6 +468,7 @@ 'original_name': 'Object Analytics Scenario 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario1-Device1Scenario1', @@ -507,6 +517,7 @@ 'original_name': 'VMD4 Camera1Profile9', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9-Camera1Profile9', diff --git a/tests/components/axis/snapshots/test_camera.ambr b/tests/components/axis/snapshots/test_camera.ambr index d323a209dc8..68b9cd07e53 100644 --- a/tests/components/axis/snapshots/test_camera.ambr +++ b/tests/components/axis/snapshots/test_camera.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-camera', @@ -77,6 +78,7 @@ 'original_name': None, 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-camera', diff --git a/tests/components/axis/snapshots/test_light.ambr b/tests/components/axis/snapshots/test_light.ambr index d8d01543ee5..aec750ecda3 100644 --- a/tests/components/axis/snapshots/test_light.ambr +++ b/tests/components/axis/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'IR Light 0', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/tnsaxis:Light/Status-0', diff --git a/tests/components/axis/snapshots/test_switch.ambr b/tests/components/axis/snapshots/test_switch.ambr index fa6091550e5..1e9a2d0b068 100644 --- a/tests/components/axis/snapshots/test_switch.ambr +++ b/tests/components/axis/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Doorbell', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-0', @@ -75,6 +76,7 @@ 'original_name': 'Relay 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-1', @@ -123,6 +125,7 @@ 'original_name': 'Doorbell', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-0', @@ -171,6 +174,7 @@ 'original_name': 'Relay 1', 'platform': 'axis', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:40:8c:12:34:56-tns1:Device/Trigger/Relay-1', diff --git a/tests/components/azure_devops/snapshots/test_sensor.ambr b/tests/components/azure_devops/snapshots/test_sensor.ambr index 3fe4d470a63..865cd79ee1f 100644 --- a/tests/components/azure_devops/snapshots/test_sensor.ambr +++ b/tests/components/azure_devops/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'CI latest build', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latest_build', 'unique_id': 'testorg_1234_9876_latest_build', @@ -86,6 +87,7 @@ 'original_name': 'CI latest build finish time', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'finish_time', 'unique_id': 'testorg_1234_9876_finish_time', @@ -134,6 +136,7 @@ 'original_name': 'CI latest build ID', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'build_id', 'unique_id': 'testorg_1234_9876_build_id', @@ -181,6 +184,7 @@ 'original_name': 'CI latest build queue time', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'queue_time', 'unique_id': 'testorg_1234_9876_queue_time', @@ -229,6 +233,7 @@ 'original_name': 'CI latest build reason', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reason', 'unique_id': 'testorg_1234_9876_reason', @@ -276,6 +281,7 @@ 'original_name': 'CI latest build result', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'result', 'unique_id': 'testorg_1234_9876_result', @@ -323,6 +329,7 @@ 'original_name': 'CI latest build source branch', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'source_branch', 'unique_id': 'testorg_1234_9876_source_branch', @@ -370,6 +377,7 @@ 'original_name': 'CI latest build source version', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'source_version', 'unique_id': 'testorg_1234_9876_source_version', @@ -417,6 +425,7 @@ 'original_name': 'CI latest build start time', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_time', 'unique_id': 'testorg_1234_9876_start_time', @@ -465,6 +474,7 @@ 'original_name': 'CI latest build URL', 'platform': 'azure_devops', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'url', 'unique_id': 'testorg_1234_9876_url', diff --git a/tests/components/backup/snapshots/test_event.ambr b/tests/components/backup/snapshots/test_event.ambr index 6ee11c808ad..78f60bf8d20 100644 --- a/tests/components/backup/snapshots/test_event.ambr +++ b/tests/components/backup/snapshots/test_event.ambr @@ -33,6 +33,7 @@ 'original_name': 'Automatic backup', 'platform': 'backup', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'automatic_backup_event', 'unique_id': 'automatic_backup_event', diff --git a/tests/components/backup/snapshots/test_sensors.ambr b/tests/components/backup/snapshots/test_sensors.ambr index b68d706dfb3..034ca91239b 100644 --- a/tests/components/backup/snapshots/test_sensors.ambr +++ b/tests/components/backup/snapshots/test_sensors.ambr @@ -35,6 +35,7 @@ 'original_name': 'Backup Manager state', 'platform': 'backup', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_manager_state', 'unique_id': 'backup_manager_state', @@ -90,6 +91,7 @@ 'original_name': 'Last attempted automatic backup', 'platform': 'backup', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_attempted_automatic_backup', 'unique_id': 'last_attempted_automatic_backup', @@ -138,6 +140,7 @@ 'original_name': 'Last successful automatic backup', 'platform': 'backup', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_successful_automatic_backup', 'unique_id': 'last_successful_automatic_backup', @@ -186,6 +189,7 @@ 'original_name': 'Next scheduled automatic backup', 'platform': 'backup', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_scheduled_automatic_backup', 'unique_id': 'next_scheduled_automatic_backup', diff --git a/tests/components/balboa/snapshots/test_binary_sensor.ambr b/tests/components/balboa/snapshots/test_binary_sensor.ambr index 4aa0f1d71fe..51f1dfa8e3f 100644 --- a/tests/components/balboa/snapshots/test_binary_sensor.ambr +++ b/tests/components/balboa/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Circulation pump', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'circ_pump', 'unique_id': 'FakeSpa-Circ Pump-c0ffee', @@ -75,6 +76,7 @@ 'original_name': 'Filter cycle 1', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_1', 'unique_id': 'FakeSpa-Filter1-c0ffee', @@ -123,6 +125,7 @@ 'original_name': 'Filter cycle 2', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_2', 'unique_id': 'FakeSpa-Filter2-c0ffee', diff --git a/tests/components/balboa/snapshots/test_climate.ambr b/tests/components/balboa/snapshots/test_climate.ambr index 70e33c4065f..b616c77de7d 100644 --- a/tests/components/balboa/snapshots/test_climate.ambr +++ b/tests/components/balboa/snapshots/test_climate.ambr @@ -38,6 +38,7 @@ 'original_name': None, 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'balboa', 'unique_id': 'FakeSpa-Climate-c0ffee', diff --git a/tests/components/balboa/snapshots/test_event.ambr b/tests/components/balboa/snapshots/test_event.ambr index fc8f591a9fc..2a9b5540101 100644 --- a/tests/components/balboa/snapshots/test_event.ambr +++ b/tests/components/balboa/snapshots/test_event.ambr @@ -48,6 +48,7 @@ 'original_name': 'Fault', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'FakeSpa-fault-c0ffee', diff --git a/tests/components/balboa/snapshots/test_fan.ambr b/tests/components/balboa/snapshots/test_fan.ambr index 4df73c3178c..e4d619dc536 100644 --- a/tests/components/balboa/snapshots/test_fan.ambr +++ b/tests/components/balboa/snapshots/test_fan.ambr @@ -29,6 +29,7 @@ 'original_name': 'Pump 1', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'pump', 'unique_id': 'FakeSpa-Pump 1-c0ffee', diff --git a/tests/components/balboa/snapshots/test_light.ambr b/tests/components/balboa/snapshots/test_light.ambr index fdfd7af1d0c..af4b4f973e7 100644 --- a/tests/components/balboa/snapshots/test_light.ambr +++ b/tests/components/balboa/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Light', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'only_light', 'unique_id': 'FakeSpa-Light-c0ffee', diff --git a/tests/components/balboa/snapshots/test_select.ambr b/tests/components/balboa/snapshots/test_select.ambr index 68368bf3602..ae0aafa449e 100644 --- a/tests/components/balboa/snapshots/test_select.ambr +++ b/tests/components/balboa/snapshots/test_select.ambr @@ -32,6 +32,7 @@ 'original_name': 'Temperature range', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_range', 'unique_id': 'FakeSpa-TempHiLow-c0ffee', diff --git a/tests/components/balboa/snapshots/test_switch.ambr b/tests/components/balboa/snapshots/test_switch.ambr index ad63fcdf387..886e07f64bf 100644 --- a/tests/components/balboa/snapshots/test_switch.ambr +++ b/tests/components/balboa/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter cycle 2 enabled', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_cycle_2_enabled', 'unique_id': 'FakeSpa-filter_cycle_2_enabled-c0ffee', diff --git a/tests/components/balboa/snapshots/test_time.ambr b/tests/components/balboa/snapshots/test_time.ambr index 6b27717e2d3..2d1f9c42e95 100644 --- a/tests/components/balboa/snapshots/test_time.ambr +++ b/tests/components/balboa/snapshots/test_time.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter cycle 1 end', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_cycle_end', 'unique_id': 'FakeSpa-filter_cycle_1_end-c0ffee', @@ -74,6 +75,7 @@ 'original_name': 'Filter cycle 1 start', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_cycle_start', 'unique_id': 'FakeSpa-filter_cycle_1_start-c0ffee', @@ -121,6 +123,7 @@ 'original_name': 'Filter cycle 2 end', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_cycle_end', 'unique_id': 'FakeSpa-filter_cycle_2_end-c0ffee', @@ -168,6 +171,7 @@ 'original_name': 'Filter cycle 2 start', 'platform': 'balboa', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_cycle_start', 'unique_id': 'FakeSpa-filter_cycle_2_start-c0ffee', diff --git a/tests/components/blue_current/snapshots/test_button.ambr b/tests/components/blue_current/snapshots/test_button.ambr index 0dc27892ceb..36a043630ea 100644 --- a/tests/components/blue_current/snapshots/test_button.ambr +++ b/tests/components/blue_current/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reboot', 'platform': 'blue_current', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reboot', 'unique_id': 'reboot_101', @@ -75,6 +76,7 @@ 'original_name': 'Reset', 'platform': 'blue_current', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset', 'unique_id': 'reset_101', @@ -123,6 +125,7 @@ 'original_name': 'Stop charge session', 'platform': 'blue_current', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge_session', 'unique_id': 'stop_charge_session_101', diff --git a/tests/components/bluemaestro/snapshots/test_sensor.ambr b/tests/components/bluemaestro/snapshots/test_sensor.ambr index 48f20aa97b5..0848baf1571 100644 --- a/tests/components/bluemaestro/snapshots/test_sensor.ambr +++ b/tests/components/bluemaestro/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'bluemaestro', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff-battery', @@ -81,6 +82,7 @@ 'original_name': 'Dew point', 'platform': 'bluemaestro', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': 'aa:bb:cc:dd:ee:ff-dew_point', @@ -133,6 +135,7 @@ 'original_name': 'Humidity', 'platform': 'bluemaestro', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff-humidity', @@ -185,6 +188,7 @@ 'original_name': 'Signal strength', 'platform': 'bluemaestro', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff-signal_strength', @@ -237,6 +241,7 @@ 'original_name': 'Temperature', 'platform': 'bluemaestro', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff-temperature', diff --git a/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr index 0e5a1a7622a..3a7cdd86be1 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBY00000000REXI01-charging_status', @@ -75,6 +76,7 @@ 'original_name': 'Check control messages', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'check_control_messages', 'unique_id': 'WBY00000000REXI01-check_control_messages', @@ -123,6 +125,7 @@ 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_based_services', 'unique_id': 'WBY00000000REXI01-condition_based_services', @@ -177,6 +180,7 @@ 'original_name': 'Connection status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_status', 'unique_id': 'WBY00000000REXI01-connection_status', @@ -225,6 +229,7 @@ 'original_name': 'Door lock state', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_state', 'unique_id': 'WBY00000000REXI01-door_lock_state', @@ -274,6 +279,7 @@ 'original_name': 'Lids', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lids', 'unique_id': 'WBY00000000REXI01-lids', @@ -329,6 +335,7 @@ 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pre_entry_climatization_enabled', 'unique_id': 'WBY00000000REXI01-is_pre_entry_climatization_enabled', @@ -376,6 +383,7 @@ 'original_name': 'Windows', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'WBY00000000REXI01-windows', @@ -426,6 +434,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBA00000000DEMO02-charging_status', @@ -474,6 +483,7 @@ 'original_name': 'Check control messages', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'check_control_messages', 'unique_id': 'WBA00000000DEMO02-check_control_messages', @@ -523,6 +533,7 @@ 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_based_services', 'unique_id': 'WBA00000000DEMO02-condition_based_services', @@ -582,6 +593,7 @@ 'original_name': 'Connection status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_status', 'unique_id': 'WBA00000000DEMO02-connection_status', @@ -630,6 +642,7 @@ 'original_name': 'Door lock state', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_state', 'unique_id': 'WBA00000000DEMO02-door_lock_state', @@ -679,6 +692,7 @@ 'original_name': 'Lids', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lids', 'unique_id': 'WBA00000000DEMO02-lids', @@ -733,6 +747,7 @@ 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pre_entry_climatization_enabled', 'unique_id': 'WBA00000000DEMO02-is_pre_entry_climatization_enabled', @@ -780,6 +795,7 @@ 'original_name': 'Windows', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'WBA00000000DEMO02-windows', @@ -833,6 +849,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBA00000000DEMO01-charging_status', @@ -881,6 +898,7 @@ 'original_name': 'Check control messages', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'check_control_messages', 'unique_id': 'WBA00000000DEMO01-check_control_messages', @@ -930,6 +948,7 @@ 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_based_services', 'unique_id': 'WBA00000000DEMO01-condition_based_services', @@ -989,6 +1008,7 @@ 'original_name': 'Connection status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_status', 'unique_id': 'WBA00000000DEMO01-connection_status', @@ -1037,6 +1057,7 @@ 'original_name': 'Door lock state', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_state', 'unique_id': 'WBA00000000DEMO01-door_lock_state', @@ -1086,6 +1107,7 @@ 'original_name': 'Lids', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lids', 'unique_id': 'WBA00000000DEMO01-lids', @@ -1141,6 +1163,7 @@ 'original_name': 'Pre-entry climatization', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pre_entry_climatization_enabled', 'unique_id': 'WBA00000000DEMO01-is_pre_entry_climatization_enabled', @@ -1188,6 +1211,7 @@ 'original_name': 'Windows', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'WBA00000000DEMO01-windows', @@ -1241,6 +1265,7 @@ 'original_name': 'Check control messages', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'check_control_messages', 'unique_id': 'WBA00000000DEMO03-check_control_messages', @@ -1291,6 +1316,7 @@ 'original_name': 'Condition-based services', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'condition_based_services', 'unique_id': 'WBA00000000DEMO03-condition_based_services', @@ -1353,6 +1379,7 @@ 'original_name': 'Door lock state', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_state', 'unique_id': 'WBA00000000DEMO03-door_lock_state', @@ -1402,6 +1429,7 @@ 'original_name': 'Lids', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lids', 'unique_id': 'WBA00000000DEMO03-lids', @@ -1456,6 +1484,7 @@ 'original_name': 'Windows', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'WBA00000000DEMO03-windows', diff --git a/tests/components/bmw_connected_drive/snapshots/test_button.ambr b/tests/components/bmw_connected_drive/snapshots/test_button.ambr index 5072b918d2e..f8946f8c668 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_button.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Activate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activate_air_conditioning', 'unique_id': 'WBY00000000REXI01-activate_air_conditioning', @@ -74,6 +75,7 @@ 'original_name': 'Find vehicle', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'find_vehicle', 'unique_id': 'WBY00000000REXI01-find_vehicle', @@ -121,6 +123,7 @@ 'original_name': 'Flash lights', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_flash', 'unique_id': 'WBY00000000REXI01-light_flash', @@ -168,6 +171,7 @@ 'original_name': 'Sound horn', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sound_horn', 'unique_id': 'WBY00000000REXI01-sound_horn', @@ -215,6 +219,7 @@ 'original_name': 'Activate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activate_air_conditioning', 'unique_id': 'WBA00000000DEMO02-activate_air_conditioning', @@ -262,6 +267,7 @@ 'original_name': 'Deactivate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'deactivate_air_conditioning', 'unique_id': 'WBA00000000DEMO02-deactivate_air_conditioning', @@ -309,6 +315,7 @@ 'original_name': 'Find vehicle', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'find_vehicle', 'unique_id': 'WBA00000000DEMO02-find_vehicle', @@ -356,6 +363,7 @@ 'original_name': 'Flash lights', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_flash', 'unique_id': 'WBA00000000DEMO02-light_flash', @@ -403,6 +411,7 @@ 'original_name': 'Sound horn', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sound_horn', 'unique_id': 'WBA00000000DEMO02-sound_horn', @@ -450,6 +459,7 @@ 'original_name': 'Activate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activate_air_conditioning', 'unique_id': 'WBA00000000DEMO01-activate_air_conditioning', @@ -497,6 +507,7 @@ 'original_name': 'Deactivate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'deactivate_air_conditioning', 'unique_id': 'WBA00000000DEMO01-deactivate_air_conditioning', @@ -544,6 +555,7 @@ 'original_name': 'Find vehicle', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'find_vehicle', 'unique_id': 'WBA00000000DEMO01-find_vehicle', @@ -591,6 +603,7 @@ 'original_name': 'Flash lights', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_flash', 'unique_id': 'WBA00000000DEMO01-light_flash', @@ -638,6 +651,7 @@ 'original_name': 'Sound horn', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sound_horn', 'unique_id': 'WBA00000000DEMO01-sound_horn', @@ -685,6 +699,7 @@ 'original_name': 'Activate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activate_air_conditioning', 'unique_id': 'WBA00000000DEMO03-activate_air_conditioning', @@ -732,6 +747,7 @@ 'original_name': 'Deactivate air conditioning', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'deactivate_air_conditioning', 'unique_id': 'WBA00000000DEMO03-deactivate_air_conditioning', @@ -779,6 +795,7 @@ 'original_name': 'Find vehicle', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'find_vehicle', 'unique_id': 'WBA00000000DEMO03-find_vehicle', @@ -826,6 +843,7 @@ 'original_name': 'Flash lights', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_flash', 'unique_id': 'WBA00000000DEMO03-light_flash', @@ -873,6 +891,7 @@ 'original_name': 'Sound horn', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sound_horn', 'unique_id': 'WBA00000000DEMO03-sound_horn', diff --git a/tests/components/bmw_connected_drive/snapshots/test_lock.ambr b/tests/components/bmw_connected_drive/snapshots/test_lock.ambr index 3dc4e59b7b1..47eee9fdb15 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_lock.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': 'Lock', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': 'WBY00000000REXI01-lock', @@ -76,6 +77,7 @@ 'original_name': 'Lock', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': 'WBA00000000DEMO02-lock', @@ -125,6 +127,7 @@ 'original_name': 'Lock', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': 'WBA00000000DEMO01-lock', @@ -174,6 +177,7 @@ 'original_name': 'Lock', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': 'WBA00000000DEMO03-lock', diff --git a/tests/components/bmw_connected_drive/snapshots/test_number.ambr b/tests/components/bmw_connected_drive/snapshots/test_number.ambr index 866e52e7982..c86ed54197c 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_number.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Target SoC', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'target_soc', 'unique_id': 'WBA00000000DEMO02-target_soc', @@ -89,6 +90,7 @@ 'original_name': 'Target SoC', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'target_soc', 'unique_id': 'WBA00000000DEMO01-target_soc', diff --git a/tests/components/bmw_connected_drive/snapshots/test_select.ambr b/tests/components/bmw_connected_drive/snapshots/test_select.ambr index 0edead03f26..15334fc72b8 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_select.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Charging mode', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_mode', 'unique_id': 'WBY00000000REXI01-charging_mode', @@ -101,6 +102,7 @@ 'original_name': 'AC charging limit', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ac_limit', 'unique_id': 'WBA00000000DEMO02-ac_limit', @@ -170,6 +172,7 @@ 'original_name': 'Charging mode', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_mode', 'unique_id': 'WBA00000000DEMO02-charging_mode', @@ -238,6 +241,7 @@ 'original_name': 'AC charging limit', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ac_limit', 'unique_id': 'WBA00000000DEMO01-ac_limit', @@ -307,6 +311,7 @@ 'original_name': 'Charging mode', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_mode', 'unique_id': 'WBA00000000DEMO01-charging_mode', diff --git a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr index 230025fc865..2f7d2847ad6 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_sensor.ambr @@ -30,6 +30,7 @@ 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ac_current_limit', 'unique_id': 'WBY00000000REXI01-charging_profile.ac_current_limit', @@ -79,6 +80,7 @@ 'original_name': 'Charging end time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_end_time', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.charging_end_time', @@ -127,6 +129,7 @@ 'original_name': 'Charging start time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_start_time', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.charging_start_time', @@ -190,6 +193,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.charging_status', @@ -255,6 +259,7 @@ 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_target', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.charging_target', @@ -308,6 +313,7 @@ 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'WBY00000000REXI01-mileage', @@ -363,6 +369,7 @@ 'original_name': 'Remaining battery percent', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_battery_percent', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_battery_percent', @@ -418,6 +425,7 @@ 'original_name': 'Remaining fuel', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_fuel', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_fuel', @@ -473,6 +481,7 @@ 'original_name': 'Remaining fuel percent', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_fuel_percent', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_fuel_percent', @@ -527,6 +536,7 @@ 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_electric', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_range_electric', @@ -582,6 +592,7 @@ 'original_name': 'Remaining range fuel', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_fuel', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_range_fuel', @@ -637,6 +648,7 @@ 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_total', 'unique_id': 'WBY00000000REXI01-fuel_and_battery.remaining_range_total', @@ -690,6 +702,7 @@ 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ac_current_limit', 'unique_id': 'WBA00000000DEMO02-charging_profile.ac_current_limit', @@ -739,6 +752,7 @@ 'original_name': 'Charging end time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_end_time', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.charging_end_time', @@ -787,6 +801,7 @@ 'original_name': 'Charging start time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_start_time', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.charging_start_time', @@ -850,6 +865,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.charging_status', @@ -915,6 +931,7 @@ 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_target', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.charging_target', @@ -971,6 +988,7 @@ 'original_name': 'Climate status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_status', 'unique_id': 'WBA00000000DEMO02-climate.activity', @@ -1034,6 +1052,7 @@ 'original_name': 'Front left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_target_pressure', 'unique_id': 'WBA00000000DEMO02-tires.front_left.target_pressure', @@ -1092,6 +1111,7 @@ 'original_name': 'Front left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_current_pressure', 'unique_id': 'WBA00000000DEMO02-tires.front_left.current_pressure', @@ -1150,6 +1170,7 @@ 'original_name': 'Front right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_target_pressure', 'unique_id': 'WBA00000000DEMO02-tires.front_right.target_pressure', @@ -1208,6 +1229,7 @@ 'original_name': 'Front right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_current_pressure', 'unique_id': 'WBA00000000DEMO02-tires.front_right.current_pressure', @@ -1263,6 +1285,7 @@ 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'WBA00000000DEMO02-mileage', @@ -1321,6 +1344,7 @@ 'original_name': 'Rear left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_target_pressure', 'unique_id': 'WBA00000000DEMO02-tires.rear_left.target_pressure', @@ -1379,6 +1403,7 @@ 'original_name': 'Rear left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_current_pressure', 'unique_id': 'WBA00000000DEMO02-tires.rear_left.current_pressure', @@ -1437,6 +1462,7 @@ 'original_name': 'Rear right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_target_pressure', 'unique_id': 'WBA00000000DEMO02-tires.rear_right.target_pressure', @@ -1495,6 +1521,7 @@ 'original_name': 'Rear right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_current_pressure', 'unique_id': 'WBA00000000DEMO02-tires.rear_right.current_pressure', @@ -1550,6 +1577,7 @@ 'original_name': 'Remaining battery percent', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_battery_percent', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.remaining_battery_percent', @@ -1605,6 +1633,7 @@ 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_electric', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.remaining_range_electric', @@ -1660,6 +1689,7 @@ 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_total', 'unique_id': 'WBA00000000DEMO02-fuel_and_battery.remaining_range_total', @@ -1713,6 +1743,7 @@ 'original_name': 'AC current limit', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ac_current_limit', 'unique_id': 'WBA00000000DEMO01-charging_profile.ac_current_limit', @@ -1762,6 +1793,7 @@ 'original_name': 'Charging end time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_end_time', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.charging_end_time', @@ -1810,6 +1842,7 @@ 'original_name': 'Charging start time', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_start_time', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.charging_start_time', @@ -1873,6 +1906,7 @@ 'original_name': 'Charging status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_status', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.charging_status', @@ -1938,6 +1972,7 @@ 'original_name': 'Charging target', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_target', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.charging_target', @@ -1994,6 +2029,7 @@ 'original_name': 'Climate status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_status', 'unique_id': 'WBA00000000DEMO01-climate.activity', @@ -2057,6 +2093,7 @@ 'original_name': 'Front left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_target_pressure', 'unique_id': 'WBA00000000DEMO01-tires.front_left.target_pressure', @@ -2115,6 +2152,7 @@ 'original_name': 'Front left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_current_pressure', 'unique_id': 'WBA00000000DEMO01-tires.front_left.current_pressure', @@ -2173,6 +2211,7 @@ 'original_name': 'Front right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_target_pressure', 'unique_id': 'WBA00000000DEMO01-tires.front_right.target_pressure', @@ -2231,6 +2270,7 @@ 'original_name': 'Front right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_current_pressure', 'unique_id': 'WBA00000000DEMO01-tires.front_right.current_pressure', @@ -2286,6 +2326,7 @@ 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'WBA00000000DEMO01-mileage', @@ -2344,6 +2385,7 @@ 'original_name': 'Rear left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_target_pressure', 'unique_id': 'WBA00000000DEMO01-tires.rear_left.target_pressure', @@ -2402,6 +2444,7 @@ 'original_name': 'Rear left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_current_pressure', 'unique_id': 'WBA00000000DEMO01-tires.rear_left.current_pressure', @@ -2460,6 +2503,7 @@ 'original_name': 'Rear right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_target_pressure', 'unique_id': 'WBA00000000DEMO01-tires.rear_right.target_pressure', @@ -2518,6 +2562,7 @@ 'original_name': 'Rear right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_current_pressure', 'unique_id': 'WBA00000000DEMO01-tires.rear_right.current_pressure', @@ -2573,6 +2618,7 @@ 'original_name': 'Remaining battery percent', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_battery_percent', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.remaining_battery_percent', @@ -2628,6 +2674,7 @@ 'original_name': 'Remaining range electric', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_electric', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.remaining_range_electric', @@ -2683,6 +2730,7 @@ 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_total', 'unique_id': 'WBA00000000DEMO01-fuel_and_battery.remaining_range_total', @@ -2741,6 +2789,7 @@ 'original_name': 'Climate status', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_status', 'unique_id': 'WBA00000000DEMO03-climate.activity', @@ -2804,6 +2853,7 @@ 'original_name': 'Front left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_target_pressure', 'unique_id': 'WBA00000000DEMO03-tires.front_left.target_pressure', @@ -2862,6 +2912,7 @@ 'original_name': 'Front left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_left_current_pressure', 'unique_id': 'WBA00000000DEMO03-tires.front_left.current_pressure', @@ -2920,6 +2971,7 @@ 'original_name': 'Front right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_target_pressure', 'unique_id': 'WBA00000000DEMO03-tires.front_right.target_pressure', @@ -2978,6 +3030,7 @@ 'original_name': 'Front right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'front_right_current_pressure', 'unique_id': 'WBA00000000DEMO03-tires.front_right.current_pressure', @@ -3033,6 +3086,7 @@ 'original_name': 'Mileage', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'WBA00000000DEMO03-mileage', @@ -3091,6 +3145,7 @@ 'original_name': 'Rear left target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_target_pressure', 'unique_id': 'WBA00000000DEMO03-tires.rear_left.target_pressure', @@ -3149,6 +3204,7 @@ 'original_name': 'Rear left tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_current_pressure', 'unique_id': 'WBA00000000DEMO03-tires.rear_left.current_pressure', @@ -3207,6 +3263,7 @@ 'original_name': 'Rear right target pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_target_pressure', 'unique_id': 'WBA00000000DEMO03-tires.rear_right.target_pressure', @@ -3265,6 +3322,7 @@ 'original_name': 'Rear right tire pressure', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_current_pressure', 'unique_id': 'WBA00000000DEMO03-tires.rear_right.current_pressure', @@ -3320,6 +3378,7 @@ 'original_name': 'Remaining fuel', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_fuel', 'unique_id': 'WBA00000000DEMO03-fuel_and_battery.remaining_fuel', @@ -3375,6 +3434,7 @@ 'original_name': 'Remaining fuel percent', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_fuel_percent', 'unique_id': 'WBA00000000DEMO03-fuel_and_battery.remaining_fuel_percent', @@ -3429,6 +3489,7 @@ 'original_name': 'Remaining range fuel', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_fuel', 'unique_id': 'WBA00000000DEMO03-fuel_and_battery.remaining_range_fuel', @@ -3484,6 +3545,7 @@ 'original_name': 'Remaining range total', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_range_total', 'unique_id': 'WBA00000000DEMO03-fuel_and_battery.remaining_range_total', diff --git a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr index ce6ebc21f51..afd52e82d90 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_switch.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Climate', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate', 'unique_id': 'WBA00000000DEMO02-climate', @@ -74,6 +75,7 @@ 'original_name': 'Charging', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging', 'unique_id': 'WBA00000000DEMO01-charging', @@ -121,6 +123,7 @@ 'original_name': 'Climate', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate', 'unique_id': 'WBA00000000DEMO01-climate', @@ -168,6 +171,7 @@ 'original_name': 'Climate', 'platform': 'bmw_connected_drive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate', 'unique_id': 'WBA00000000DEMO03-climate', diff --git a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr index 26c67879f7c..ea50a006de0 100644 --- a/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/bosch_alarm/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1', @@ -78,6 +79,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1234567890_area_1', @@ -129,6 +131,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1', diff --git a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr index da11b9d4692..e3444777ff0 100644 --- a/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Area ready to arm away', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_away', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', @@ -74,6 +75,7 @@ 'original_name': 'Area ready to arm home', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_home', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', @@ -121,6 +123,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', @@ -168,6 +171,7 @@ 'original_name': 'AC Failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_ac_fail', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', @@ -216,6 +220,7 @@ 'original_name': 'Battery', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', @@ -264,6 +269,7 @@ 'original_name': 'Battery missing', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_battery_mising', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', @@ -312,6 +318,7 @@ 'original_name': 'CRC failure in panel configuration', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', @@ -360,6 +367,7 @@ 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', @@ -407,6 +415,7 @@ 'original_name': 'Log overflow', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_overflow', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', @@ -455,6 +464,7 @@ 'original_name': 'Log threshold reached', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_threshold', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', @@ -503,6 +513,7 @@ 'original_name': 'Phone line failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_phone_line_failure', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', @@ -551,6 +562,7 @@ 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', @@ -599,6 +611,7 @@ 'original_name': 'Problem', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', @@ -647,6 +660,7 @@ 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', @@ -695,6 +709,7 @@ 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', @@ -743,6 +758,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', @@ -790,6 +806,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', @@ -837,6 +854,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', @@ -884,6 +902,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', @@ -931,6 +950,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', @@ -978,6 +998,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', @@ -1025,6 +1046,7 @@ 'original_name': 'Area ready to arm away', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_away', 'unique_id': '1234567890_area_1_ready_to_arm_away', @@ -1072,6 +1094,7 @@ 'original_name': 'Area ready to arm home', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_home', 'unique_id': '1234567890_area_1_ready_to_arm_home', @@ -1119,6 +1142,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_6', @@ -1166,6 +1190,7 @@ 'original_name': 'AC Failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_ac_fail', 'unique_id': '1234567890_fault_panel_fault_ac_fail', @@ -1214,6 +1239,7 @@ 'original_name': 'Battery', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_fault_panel_fault_battery_low', @@ -1262,6 +1288,7 @@ 'original_name': 'Battery missing', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_battery_mising', 'unique_id': '1234567890_fault_panel_fault_battery_mising', @@ -1310,6 +1337,7 @@ 'original_name': 'CRC failure in panel configuration', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', 'unique_id': '1234567890_fault_panel_fault_parameter_crc_fail_in_pif', @@ -1358,6 +1386,7 @@ 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', 'unique_id': '1234567890_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', @@ -1405,6 +1434,7 @@ 'original_name': 'Log overflow', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_overflow', 'unique_id': '1234567890_fault_panel_fault_log_overflow', @@ -1453,6 +1483,7 @@ 'original_name': 'Log threshold reached', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_threshold', 'unique_id': '1234567890_fault_panel_fault_log_threshold', @@ -1501,6 +1532,7 @@ 'original_name': 'Phone line failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_phone_line_failure', 'unique_id': '1234567890_fault_panel_fault_phone_line_failure', @@ -1549,6 +1581,7 @@ 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', 'unique_id': '1234567890_fault_panel_fault_point_bus_fail_since_rps_hang_up', @@ -1597,6 +1630,7 @@ 'original_name': 'Problem', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', 'unique_id': '1234567890_fault_panel_fault_communication_fail_since_rps_hang_up', @@ -1645,6 +1679,7 @@ 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', 'unique_id': '1234567890_fault_panel_fault_sdi_fail_since_rps_hang_up', @@ -1693,6 +1728,7 @@ 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', 'unique_id': '1234567890_fault_panel_fault_user_code_tamper_since_rps_hang_up', @@ -1741,6 +1777,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_3', @@ -1788,6 +1825,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_1', @@ -1835,6 +1873,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_5', @@ -1882,6 +1921,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_2', @@ -1929,6 +1969,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_4', @@ -1976,6 +2017,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_point_0', @@ -2023,6 +2065,7 @@ 'original_name': 'Area ready to arm away', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_away', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_away', @@ -2070,6 +2113,7 @@ 'original_name': 'Area ready to arm home', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'area_ready_to_arm_home', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_ready_to_arm_home', @@ -2117,6 +2161,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_6', @@ -2164,6 +2209,7 @@ 'original_name': 'AC Failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_ac_fail', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_ac_fail', @@ -2212,6 +2258,7 @@ 'original_name': 'Battery', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_low', @@ -2260,6 +2307,7 @@ 'original_name': 'Battery missing', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_battery_mising', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_battery_mising', @@ -2308,6 +2356,7 @@ 'original_name': 'CRC failure in panel configuration', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_parameter_crc_fail_in_pif', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_parameter_crc_fail_in_pif', @@ -2356,6 +2405,7 @@ 'original_name': 'Failure to call RPS since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_fail_to_call_rps_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_fail_to_call_rps_since_rps_hang_up', @@ -2403,6 +2453,7 @@ 'original_name': 'Log overflow', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_overflow', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_overflow', @@ -2451,6 +2502,7 @@ 'original_name': 'Log threshold reached', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_log_threshold', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_log_threshold', @@ -2499,6 +2551,7 @@ 'original_name': 'Phone line failure', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_phone_line_failure', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_phone_line_failure', @@ -2547,6 +2600,7 @@ 'original_name': 'Point bus failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_point_bus_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_point_bus_fail_since_rps_hang_up', @@ -2595,6 +2649,7 @@ 'original_name': 'Problem', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_communication_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_communication_fail_since_rps_hang_up', @@ -2643,6 +2698,7 @@ 'original_name': 'SDI failure since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_sdi_fail_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_sdi_fail_since_rps_hang_up', @@ -2691,6 +2747,7 @@ 'original_name': 'User code tamper since last RPS connection', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panel_fault_user_code_tamper_since_rps_hang_up', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_fault_panel_fault_user_code_tamper_since_rps_hang_up', @@ -2739,6 +2796,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_3', @@ -2786,6 +2844,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_1', @@ -2833,6 +2892,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_5', @@ -2880,6 +2940,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_2', @@ -2927,6 +2988,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_4', @@ -2974,6 +3036,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_point_0', diff --git a/tests/components/bosch_alarm/snapshots/test_sensor.ambr b/tests/components/bosch_alarm/snapshots/test_sensor.ambr index 4f4c55dd845..dc229c15918 100644 --- a/tests/components/bosch_alarm/snapshots/test_sensor.ambr +++ b/tests/components/bosch_alarm/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Burglary alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_burglary', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', @@ -74,6 +75,7 @@ 'original_name': 'Faulting points', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'faulting_points', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_faulting_points', @@ -122,6 +124,7 @@ 'original_name': 'Fire alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_fire', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', @@ -169,6 +172,7 @@ 'original_name': 'Gas alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_gas', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', @@ -216,6 +220,7 @@ 'original_name': 'Burglary alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_burglary', 'unique_id': '1234567890_area_1_alarms_burglary', @@ -263,6 +268,7 @@ 'original_name': 'Faulting points', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'faulting_points', 'unique_id': '1234567890_area_1_faulting_points', @@ -311,6 +317,7 @@ 'original_name': 'Fire alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_fire', 'unique_id': '1234567890_area_1_alarms_fire', @@ -358,6 +365,7 @@ 'original_name': 'Gas alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_gas', 'unique_id': '1234567890_area_1_alarms_gas', @@ -405,6 +413,7 @@ 'original_name': 'Burglary alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_burglary', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_burglary', @@ -452,6 +461,7 @@ 'original_name': 'Faulting points', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'faulting_points', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_faulting_points', @@ -500,6 +510,7 @@ 'original_name': 'Fire alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_fire', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_fire', @@ -547,6 +558,7 @@ 'original_name': 'Gas alarm issues', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_gas', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_area_1_alarms_gas', diff --git a/tests/components/bosch_alarm/snapshots/test_switch.ambr b/tests/components/bosch_alarm/snapshots/test_switch.ambr index 0604787924f..f9e4d063e50 100644 --- a/tests/components/bosch_alarm/snapshots/test_switch.ambr +++ b/tests/components/bosch_alarm/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Locked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'locked', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', @@ -74,6 +75,7 @@ 'original_name': 'Momentarily unlocked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cycling', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', @@ -121,6 +123,7 @@ 'original_name': 'Secured', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'secured', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', @@ -168,6 +171,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', @@ -215,6 +219,7 @@ 'original_name': 'Locked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'locked', 'unique_id': '1234567890_door_1_locked', @@ -262,6 +267,7 @@ 'original_name': 'Momentarily unlocked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cycling', 'unique_id': '1234567890_door_1_cycling', @@ -309,6 +315,7 @@ 'original_name': 'Secured', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'secured', 'unique_id': '1234567890_door_1_secured', @@ -356,6 +363,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890_output_1', @@ -403,6 +411,7 @@ 'original_name': 'Locked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'locked', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_locked', @@ -450,6 +459,7 @@ 'original_name': 'Momentarily unlocked', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cycling', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_cycling', @@ -497,6 +507,7 @@ 'original_name': 'Secured', 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'secured', 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_door_1_secured', @@ -544,6 +555,7 @@ 'original_name': None, 'platform': 'bosch_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JQ917ACKQ33HHM7YCFXYZX51_output_1', diff --git a/tests/components/bring/snapshots/test_event.ambr b/tests/components/bring/snapshots/test_event.ambr index 0bcdcb5b565..ceaef2bef87 100644 --- a/tests/components/bring/snapshots/test_event.ambr +++ b/tests/components/bring/snapshots/test_event.ambr @@ -33,6 +33,7 @@ 'original_name': 'Activities', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activities', 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_activities', @@ -117,6 +118,7 @@ 'original_name': 'Activities', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activities', 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_activities', diff --git a/tests/components/bring/snapshots/test_sensor.ambr b/tests/components/bring/snapshots/test_sensor.ambr index eb307d31396..f3b37fd8b21 100644 --- a/tests/components/bring/snapshots/test_sensor.ambr +++ b/tests/components/bring/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Discount only', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_discounted', @@ -81,6 +82,7 @@ 'original_name': 'List access', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_list_access', @@ -134,6 +136,7 @@ 'original_name': 'On occasion', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_convenient', @@ -205,6 +208,7 @@ 'original_name': 'Region & language', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_list_language', @@ -275,6 +279,7 @@ 'original_name': 'Urgent', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd_urgent', @@ -323,6 +328,7 @@ 'original_name': 'Discount only', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_discounted', @@ -377,6 +383,7 @@ 'original_name': 'List access', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_list_access', @@ -430,6 +437,7 @@ 'original_name': 'On occasion', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_convenient', @@ -501,6 +509,7 @@ 'original_name': 'Region & language', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_list_language', @@ -571,6 +580,7 @@ 'original_name': 'Urgent', 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5_urgent', diff --git a/tests/components/bring/snapshots/test_todo.ambr b/tests/components/bring/snapshots/test_todo.ambr index 46146415bf6..bc65c6b020b 100644 --- a/tests/components/bring/snapshots/test_todo.ambr +++ b/tests/components/bring/snapshots/test_todo.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'shopping_list', 'unique_id': '00000000-00000000-00000000-00000000_b4776778-7f6c-496e-951b-92a35d3db0dd', @@ -75,6 +76,7 @@ 'original_name': None, 'platform': 'bring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'shopping_list', 'unique_id': '00000000-00000000-00000000-00000000_e542eef6-dba7-4c31-a52c-29e6ab9d83a5', diff --git a/tests/components/brother/snapshots/test_sensor.ambr b/tests/components/brother/snapshots/test_sensor.ambr index 847ea0a2c6b..b25d6a20a65 100644 --- a/tests/components/brother/snapshots/test_sensor.ambr +++ b/tests/components/brother/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'B/W pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bw_pages', 'unique_id': '0123456789_bw_counter', @@ -80,6 +81,7 @@ 'original_name': 'Belt unit remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'belt_unit_remaining_life', 'unique_id': '0123456789_belt_unit_remaining_life', @@ -131,6 +133,7 @@ 'original_name': 'Black drum page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'black_drum_page_counter', 'unique_id': '0123456789_black_drum_counter', @@ -182,6 +185,7 @@ 'original_name': 'Black drum remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'black_drum_remaining_life', 'unique_id': '0123456789_black_drum_remaining_life', @@ -233,6 +237,7 @@ 'original_name': 'Black drum remaining pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'black_drum_remaining_pages', 'unique_id': '0123456789_black_drum_remaining_pages', @@ -284,6 +289,7 @@ 'original_name': 'Black toner remaining', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'black_toner_remaining', 'unique_id': '0123456789_black_toner_remaining', @@ -335,6 +341,7 @@ 'original_name': 'Color pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'color_pages', 'unique_id': '0123456789_color_counter', @@ -386,6 +393,7 @@ 'original_name': 'Cyan drum page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cyan_drum_page_counter', 'unique_id': '0123456789_cyan_drum_counter', @@ -437,6 +445,7 @@ 'original_name': 'Cyan drum remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cyan_drum_remaining_life', 'unique_id': '0123456789_cyan_drum_remaining_life', @@ -488,6 +497,7 @@ 'original_name': 'Cyan drum remaining pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cyan_drum_remaining_pages', 'unique_id': '0123456789_cyan_drum_remaining_pages', @@ -539,6 +549,7 @@ 'original_name': 'Cyan toner remaining', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cyan_toner_remaining', 'unique_id': '0123456789_cyan_toner_remaining', @@ -590,6 +601,7 @@ 'original_name': 'Drum page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drum_page_counter', 'unique_id': '0123456789_drum_counter', @@ -641,6 +653,7 @@ 'original_name': 'Drum remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drum_remaining_life', 'unique_id': '0123456789_drum_remaining_life', @@ -692,6 +705,7 @@ 'original_name': 'Drum remaining pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drum_remaining_pages', 'unique_id': '0123456789_drum_remaining_pages', @@ -743,6 +757,7 @@ 'original_name': 'Duplex unit page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'duplex_unit_page_counter', 'unique_id': '0123456789_duplex_unit_pages_counter', @@ -794,6 +809,7 @@ 'original_name': 'Fuser remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fuser_remaining_life', 'unique_id': '0123456789_fuser_remaining_life', @@ -843,6 +859,7 @@ 'original_name': 'Last restart', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_restart', 'unique_id': '0123456789_uptime', @@ -893,6 +910,7 @@ 'original_name': 'Magenta drum page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'magenta_drum_page_counter', 'unique_id': '0123456789_magenta_drum_counter', @@ -944,6 +962,7 @@ 'original_name': 'Magenta drum remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'magenta_drum_remaining_life', 'unique_id': '0123456789_magenta_drum_remaining_life', @@ -995,6 +1014,7 @@ 'original_name': 'Magenta drum remaining pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'magenta_drum_remaining_pages', 'unique_id': '0123456789_magenta_drum_remaining_pages', @@ -1046,6 +1066,7 @@ 'original_name': 'Magenta toner remaining', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'magenta_toner_remaining', 'unique_id': '0123456789_magenta_toner_remaining', @@ -1097,6 +1118,7 @@ 'original_name': 'Page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'page_counter', 'unique_id': '0123456789_page_counter', @@ -1148,6 +1170,7 @@ 'original_name': 'PF Kit 1 remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pf_kit_1_remaining_life', 'unique_id': '0123456789_pf_kit_1_remaining_life', @@ -1197,6 +1220,7 @@ 'original_name': 'Status', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': '0123456789_status', @@ -1246,6 +1270,7 @@ 'original_name': 'Yellow drum page counter', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yellow_drum_page_counter', 'unique_id': '0123456789_yellow_drum_counter', @@ -1297,6 +1322,7 @@ 'original_name': 'Yellow drum remaining lifetime', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yellow_drum_remaining_life', 'unique_id': '0123456789_yellow_drum_remaining_life', @@ -1348,6 +1374,7 @@ 'original_name': 'Yellow drum remaining pages', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yellow_drum_remaining_pages', 'unique_id': '0123456789_yellow_drum_remaining_pages', @@ -1399,6 +1426,7 @@ 'original_name': 'Yellow toner remaining', 'platform': 'brother', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yellow_toner_remaining', 'unique_id': '0123456789_yellow_toner_remaining', diff --git a/tests/components/bryant_evolution/snapshots/test_climate.ambr b/tests/components/bryant_evolution/snapshots/test_climate.ambr index 3aeaf66329f..4b38e532139 100644 --- a/tests/components/bryant_evolution/snapshots/test_climate.ambr +++ b/tests/components/bryant_evolution/snapshots/test_climate.ambr @@ -42,6 +42,7 @@ 'original_name': None, 'platform': 'bryant_evolution', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J3XJZSTEF6G5V0QJX6HBC94T-S1-Z1', diff --git a/tests/components/bsblan/snapshots/test_climate.ambr b/tests/components/bsblan/snapshots/test_climate.ambr index 70d13f1cb95..9efd1b79e29 100644 --- a/tests/components/bsblan/snapshots/test_climate.ambr +++ b/tests/components/bsblan/snapshots/test_climate.ambr @@ -39,6 +39,7 @@ 'original_name': None, 'platform': 'bsblan', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:80:41:19:69:90-climate', @@ -113,6 +114,7 @@ 'original_name': None, 'platform': 'bsblan', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:80:41:19:69:90-climate', diff --git a/tests/components/bsblan/snapshots/test_sensor.ambr b/tests/components/bsblan/snapshots/test_sensor.ambr index df7ceecc957..f87c9a8e9be 100644 --- a/tests/components/bsblan/snapshots/test_sensor.ambr +++ b/tests/components/bsblan/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Current Temperature', 'platform': 'bsblan', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_temperature', 'unique_id': '00:80:41:19:69:90-current_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Outside Temperature', 'platform': 'bsblan', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': '00:80:41:19:69:90-outside_temperature', diff --git a/tests/components/bsblan/snapshots/test_water_heater.ambr b/tests/components/bsblan/snapshots/test_water_heater.ambr index 37fdb14aca9..4ff20fd06d4 100644 --- a/tests/components/bsblan/snapshots/test_water_heater.ambr +++ b/tests/components/bsblan/snapshots/test_water_heater.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'bsblan', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:80:41:19:69:90', diff --git a/tests/components/cambridge_audio/snapshots/test_select.ambr b/tests/components/cambridge_audio/snapshots/test_select.ambr index c83e101f363..8e95966bc6a 100644 --- a/tests/components/cambridge_audio/snapshots/test_select.ambr +++ b/tests/components/cambridge_audio/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Audio output', 'platform': 'cambridge_audio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'audio_output', 'unique_id': '0020c2d8-audio_output', @@ -91,6 +92,7 @@ 'original_name': 'Control Bus mode', 'platform': 'cambridge_audio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'control_bus_mode', 'unique_id': '0020c2d8-control_bus_mode', @@ -149,6 +151,7 @@ 'original_name': 'Display brightness', 'platform': 'cambridge_audio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display_brightness', 'unique_id': '0020c2d8-display_brightness', diff --git a/tests/components/cambridge_audio/snapshots/test_switch.ambr b/tests/components/cambridge_audio/snapshots/test_switch.ambr index cd4326fdcc3..63ac2b8a00c 100644 --- a/tests/components/cambridge_audio/snapshots/test_switch.ambr +++ b/tests/components/cambridge_audio/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Early update', 'platform': 'cambridge_audio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'early_update', 'unique_id': '0020c2d8-early_update', @@ -74,6 +75,7 @@ 'original_name': 'Pre-Amp', 'platform': 'cambridge_audio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pre_amp', 'unique_id': '0020c2d8-pre_amp', diff --git a/tests/components/ccm15/snapshots/test_climate.ambr b/tests/components/ccm15/snapshots/test_climate.ambr index a3cda75463f..d71672ce40c 100644 --- a/tests/components/ccm15/snapshots/test_climate.ambr +++ b/tests/components/ccm15/snapshots/test_climate.ambr @@ -49,6 +49,7 @@ 'original_name': None, 'platform': 'ccm15', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1.1.1.1.0', @@ -105,6 +106,7 @@ 'original_name': None, 'platform': 'ccm15', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1.1.1.1.1', @@ -241,6 +243,7 @@ 'original_name': None, 'platform': 'ccm15', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1.1.1.1.0', @@ -297,6 +300,7 @@ 'original_name': None, 'platform': 'ccm15', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1.1.1.1.1', diff --git a/tests/components/chacon_dio/snapshots/test_cover.ambr b/tests/components/chacon_dio/snapshots/test_cover.ambr index afac3359410..79d09957600 100644 --- a/tests/components/chacon_dio/snapshots/test_cover.ambr +++ b/tests/components/chacon_dio/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'chacon_dio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'L4HActuator_idmock1', diff --git a/tests/components/chacon_dio/snapshots/test_switch.ambr b/tests/components/chacon_dio/snapshots/test_switch.ambr index a2620005531..ab8ef0fef36 100644 --- a/tests/components/chacon_dio/snapshots/test_switch.ambr +++ b/tests/components/chacon_dio/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'chacon_dio', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'L4HActuator_idmock1', diff --git a/tests/components/co2signal/snapshots/test_sensor.ambr b/tests/components/co2signal/snapshots/test_sensor.ambr index 1e241735102..03f6123ec7c 100644 --- a/tests/components/co2signal/snapshots/test_sensor.ambr +++ b/tests/components/co2signal/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'CO2 intensity', 'platform': 'co2signal', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'carbon_intensity', 'unique_id': '904a74160aa6f335526706bee85dfb83_co2intensity', @@ -82,6 +83,7 @@ 'original_name': 'Grid fossil fuel percentage', 'platform': 'co2signal', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fossil_fuel_percentage', 'unique_id': '904a74160aa6f335526706bee85dfb83_fossilFuelPercentage', diff --git a/tests/components/comelit/snapshots/test_climate.ambr b/tests/components/comelit/snapshots/test_climate.ambr index 1f8ce4a3caf..c55836793f7 100644 --- a/tests/components/comelit/snapshots/test_climate.ambr +++ b/tests/components/comelit/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': 'serial_bridge_config_entry_id-0', diff --git a/tests/components/comelit/snapshots/test_cover.ambr b/tests/components/comelit/snapshots/test_cover.ambr index 17189344cd1..a0575a19d2b 100644 --- a/tests/components/comelit/snapshots/test_cover.ambr +++ b/tests/components/comelit/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'serial_bridge_config_entry_id-0', diff --git a/tests/components/comelit/snapshots/test_humidifier.ambr b/tests/components/comelit/snapshots/test_humidifier.ambr index ffe53d09c5d..587bc8513f2 100644 --- a/tests/components/comelit/snapshots/test_humidifier.ambr +++ b/tests/components/comelit/snapshots/test_humidifier.ambr @@ -34,6 +34,7 @@ 'original_name': 'Dehumidifier', 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'dehumidifier', 'unique_id': 'serial_bridge_config_entry_id-0-dehumidifier', @@ -100,6 +101,7 @@ 'original_name': 'Humidifier', 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'humidifier', 'unique_id': 'serial_bridge_config_entry_id-0-humidifier', diff --git a/tests/components/comelit/snapshots/test_light.ambr b/tests/components/comelit/snapshots/test_light.ambr index c60c962e23d..734ce177673 100644 --- a/tests/components/comelit/snapshots/test_light.ambr +++ b/tests/components/comelit/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'serial_bridge_config_entry_id-0', diff --git a/tests/components/comelit/snapshots/test_sensor.ambr b/tests/components/comelit/snapshots/test_sensor.ambr index dabae2a1bf0..602b9a9cad3 100644 --- a/tests/components/comelit/snapshots/test_sensor.ambr +++ b/tests/components/comelit/snapshots/test_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'zone_status', 'unique_id': 'vedo_config_entry_id-0', diff --git a/tests/components/comelit/snapshots/test_switch.ambr b/tests/components/comelit/snapshots/test_switch.ambr index eddecfabb7a..d41394ed245 100644 --- a/tests/components/comelit/snapshots/test_switch.ambr +++ b/tests/components/comelit/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'comelit', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'serial_bridge_config_entry_id-other-0', diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index ea7a65f25d3..15a7ac70ac7 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,6 +1,7 @@ """Test entity_registry API.""" from datetime import datetime +import logging from freezegun.api import FrozenDateTimeFactory import pytest @@ -11,6 +12,7 @@ from homeassistant.const import ATTR_ICON, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryDisabler +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_registry import ( RegistryEntryDisabler, RegistryEntryHider, @@ -1288,3 +1290,170 @@ async def test_remove_non_existing_entity( msg = await client.receive_json() assert not msg["success"] + + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "test_domain" + + +async def test_get_automatic_entity_ids( + hass: HomeAssistant, client: MockHAClientWebSocket +) -> None: + """Test get_automatic_entity_ids.""" + mock_registry( + hass, + { + "test_domain.test_1": RegistryEntryWithDefaults( + entity_id="test_domain.test_1", + unique_id="uniq1", + platform="test_domain", + ), + "test_domain.test_2": RegistryEntryWithDefaults( + entity_id="test_domain.test_2", + unique_id="uniq2", + platform="test_domain", + suggested_object_id="collision", + ), + "test_domain.test_3": RegistryEntryWithDefaults( + entity_id="test_domain.test_3", + name="Name by User 3", + unique_id="uniq3", + platform="test_domain", + suggested_object_id="suggested_3", + ), + "test_domain.test_4": RegistryEntryWithDefaults( + entity_id="test_domain.test_4", + name="Name by User 4", + unique_id="uniq4", + platform="test_domain", + ), + "test_domain.test_5": RegistryEntryWithDefaults( + entity_id="test_domain.test_5", + unique_id="uniq5", + platform="test_domain", + ), + "test_domain.test_6": RegistryEntryWithDefaults( + entity_id="test_domain.test_6", + name="Test 6", + unique_id="uniq6", + platform="test_domain", + ), + "test_domain.test_7": RegistryEntryWithDefaults( + entity_id="test_domain.test_7", + unique_id="uniq7", + platform="test_domain", + suggested_object_id="test_7", + ), + "test_domain.not_unique": RegistryEntryWithDefaults( + entity_id="test_domain.not_unique", + unique_id="not_unique_1", + platform="test_domain", + suggested_object_id="not_unique", + ), + "test_domain.not_unique_2": RegistryEntryWithDefaults( + entity_id="test_domain.not_unique_2", + name="Not Unique", + unique_id="not_unique_2", + platform="test_domain", + ), + "test_domain.not_unique_3": RegistryEntryWithDefaults( + entity_id="test_domain.not_unique_3", + unique_id="not_unique_3", + platform="test_domain", + suggested_object_id="not_unique", + ), + "test_domain.also_not_unique_changed_1": RegistryEntryWithDefaults( + entity_id="test_domain.also_not_unique_changed_1", + unique_id="also_not_unique_1", + platform="test_domain", + ), + "test_domain.also_not_unique_changed_2": RegistryEntryWithDefaults( + entity_id="test_domain.also_not_unique_changed_2", + unique_id="also_not_unique_2", + platform="test_domain", + ), + "test_domain.collision": RegistryEntryWithDefaults( + entity_id="test_domain.collision", + unique_id="uniq_collision", + platform="test_platform", + ), + }, + ) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + await component.async_setup({}) + entity2 = MockEntity(unique_id="uniq2", name="Entity Name 2") + entity3 = MockEntity(unique_id="uniq3", name="Entity Name 3") + entity4 = MockEntity(unique_id="uniq4", name="Entity Name 4") + entity5 = MockEntity(unique_id="uniq5", name="Entity Name 5") + entity6 = MockEntity(unique_id="uniq6", name="Entity Name 6") + entity7 = MockEntity(unique_id="uniq7", name="Entity Name 7") + entity8 = MockEntity(unique_id="not_unique_1", name="Entity Name 8") + entity9 = MockEntity(unique_id="not_unique_2", name="Entity Name 9") + entity10 = MockEntity(unique_id="not_unique_3", name="Not unique") + entity11 = MockEntity(unique_id="also_not_unique_1", name="Also not unique") + entity12 = MockEntity(unique_id="also_not_unique_2", name="Also not unique") + await component.async_add_entities( + [ + entity2, + entity3, + entity4, + entity5, + entity6, + entity7, + entity8, + entity9, + entity10, + entity11, + entity12, + ] + ) + + await client.send_json_auto_id( + { + "type": "config/entity_registry/get_automatic_entity_ids", + "entity_ids": [ + "test_domain.test_1", + "test_domain.test_2", + "test_domain.test_3", + "test_domain.test_4", + "test_domain.test_5", + "test_domain.test_6", + "test_domain.test_7", + "test_domain.not_unique", + "test_domain.not_unique_2", + "test_domain.not_unique_3", + "test_domain.also_not_unique_changed_1", + "test_domain.also_not_unique_changed_2", + "test_domain.unknown", + ], + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + # No entity object for test_domain.test_1 + "test_domain.test_1": None, + # The suggested_object_id is taken, fall back to suggested_object_id + _2 + "test_domain.test_2": "test_domain.collision_2", + # name set by user has higher priority than suggested_object_id or entity + "test_domain.test_3": "test_domain.name_by_user_3", + # name set by user has higher priority than entity properties + "test_domain.test_4": "test_domain.name_by_user_4", + # No suggested_object_id or name, fall back to entity properties + "test_domain.test_5": "test_domain.entity_name_5", + # automatic entity id matches current entity id + "test_domain.test_6": "test_domain.test_6", + "test_domain.test_7": "test_domain.test_7", + # colliding entity ids keep current entity id + "test_domain.not_unique": "test_domain.not_unique", + "test_domain.not_unique_2": "test_domain.not_unique_2", + "test_domain.not_unique_3": "test_domain.not_unique_3", + # Don't reuse entity id + "test_domain.also_not_unique_changed_1": "test_domain.also_not_unique", + "test_domain.also_not_unique_changed_2": "test_domain.also_not_unique_2", + # no test_domain.unknown in registry + "test_domain.unknown": None, + } diff --git a/tests/components/cookidoo/snapshots/test_button.ambr b/tests/components/cookidoo/snapshots/test_button.ambr index f316b0cfc82..43244132ae2 100644 --- a/tests/components/cookidoo/snapshots/test_button.ambr +++ b/tests/components/cookidoo/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Clear shopping list and additional purchases', 'platform': 'cookidoo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'todo_clear', 'unique_id': 'sub_uuid_todo_clear', diff --git a/tests/components/cookidoo/snapshots/test_sensor.ambr b/tests/components/cookidoo/snapshots/test_sensor.ambr index ca861241971..6b311cfea86 100644 --- a/tests/components/cookidoo/snapshots/test_sensor.ambr +++ b/tests/components/cookidoo/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Subscription', 'platform': 'cookidoo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'sub_uuid_subscription', @@ -86,6 +87,7 @@ 'original_name': 'Subscription expiration date', 'platform': 'cookidoo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'sub_uuid_expires', diff --git a/tests/components/cookidoo/snapshots/test_todo.ambr b/tests/components/cookidoo/snapshots/test_todo.ambr index 5b2c7552548..620d3c55db7 100644 --- a/tests/components/cookidoo/snapshots/test_todo.ambr +++ b/tests/components/cookidoo/snapshots/test_todo.ambr @@ -27,6 +27,7 @@ 'original_name': 'Additional purchases', 'platform': 'cookidoo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'additional_item_list', 'unique_id': 'sub_uuid_additional_items', @@ -75,6 +76,7 @@ 'original_name': 'Shopping list', 'platform': 'cookidoo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ingredient_list', 'unique_id': 'sub_uuid_ingredients', diff --git a/tests/components/deako/snapshots/test_light.ambr b/tests/components/deako/snapshots/test_light.ambr index f5ef5fd19e8..bed3bc366e8 100644 --- a/tests/components/deako/snapshots/test_light.ambr +++ b/tests/components/deako/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': None, 'platform': 'deako', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'uuid', @@ -88,6 +89,7 @@ 'original_name': None, 'platform': 'deako', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'uuid', @@ -144,6 +146,7 @@ 'original_name': None, 'platform': 'deako', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'some_device', diff --git a/tests/components/deconz/snapshots/test_alarm_control_panel.ambr b/tests/components/deconz/snapshots/test_alarm_control_panel.ambr index e1a6126498c..95c5cada755 100644 --- a/tests/components/deconz/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/deconz/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': 'Keypad', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', diff --git a/tests/components/deconz/snapshots/test_binary_sensor.ambr b/tests/components/deconz/snapshots/test_binary_sensor.ambr index 6b348d3ed0a..6fb1140ec6f 100644 --- a/tests/components/deconz/snapshots/test_binary_sensor.ambr +++ b/tests/components/deconz/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm 10', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-alarm', @@ -77,6 +78,7 @@ 'original_name': 'Cave CO', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-carbon_monoxide', @@ -126,6 +128,7 @@ 'original_name': 'Cave CO Low Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-low_battery', @@ -174,6 +177,7 @@ 'original_name': 'Cave CO Tampered', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-tampered', @@ -222,6 +226,7 @@ 'original_name': 'Presence sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-presence', @@ -273,6 +278,7 @@ 'original_name': 'Presence sensor Low Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-low_battery', @@ -321,6 +327,7 @@ 'original_name': 'Presence sensor Tampered', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-tampered', @@ -369,6 +376,7 @@ 'original_name': 'sensor_kitchen_smoke', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-fire', @@ -418,6 +426,7 @@ 'original_name': 'sensor_kitchen_smoke Test Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode', @@ -466,6 +475,7 @@ 'original_name': 'sensor_kitchen_smoke', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-fire', @@ -515,6 +525,7 @@ 'original_name': 'sensor_kitchen_smoke Test Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode', @@ -563,6 +574,7 @@ 'original_name': 'Kitchen Switch', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'kitchen-switch-flag', @@ -611,6 +623,7 @@ 'original_name': 'Back Door', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:2b:96:b4-01-0006-open', @@ -661,6 +674,7 @@ 'original_name': 'Motion sensor 4', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:17:88:01:03:28:8c:9b-02-0406-presence', @@ -711,6 +725,7 @@ 'original_name': 'water2', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-water', @@ -761,6 +776,7 @@ 'original_name': 'water2 Low Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-low_battery', @@ -809,6 +825,7 @@ 'original_name': 'water2 Tampered', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:2f:07:db-01-0500-tampered', @@ -857,6 +874,7 @@ 'original_name': 'Vibration 1', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:a5:21:24-01-0101-vibration', @@ -914,6 +932,7 @@ 'original_name': 'Presence sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-presence', @@ -965,6 +984,7 @@ 'original_name': 'Presence sensor Low Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-low_battery', @@ -1013,6 +1033,7 @@ 'original_name': 'Presence sensor Tampered', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-tampered', diff --git a/tests/components/deconz/snapshots/test_button.ambr b/tests/components/deconz/snapshots/test_button.ambr index b7ad00cdacd..237b0e1e50f 100644 --- a/tests/components/deconz/snapshots/test_button.ambr +++ b/tests/components/deconz/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Scene Store Current Scene', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01234E56789A/groups/1/scenes/1-store', @@ -75,6 +76,7 @@ 'original_name': 'Aqara FP1 Reset Presence', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-reset_presence', diff --git a/tests/components/deconz/snapshots/test_climate.ambr b/tests/components/deconz/snapshots/test_climate.ambr index f8d572ab2ca..cdae69abbcb 100644 --- a/tests/components/deconz/snapshots/test_climate.ambr +++ b/tests/components/deconz/snapshots/test_climate.ambr @@ -45,6 +45,7 @@ 'original_name': 'Zen-01', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', @@ -133,6 +134,7 @@ 'original_name': 'Zen-01', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', @@ -230,6 +232,7 @@ 'original_name': 'Zen-01', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:24:46:00:00:11:6f:56-01-0201', @@ -318,6 +321,7 @@ 'original_name': 'Thermostat', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', @@ -385,6 +389,7 @@ 'original_name': 'CLIP thermostat', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:02-00', @@ -451,6 +456,7 @@ 'original_name': 'Thermostat', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', @@ -518,6 +524,7 @@ 'original_name': 'thermostat', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '14:b4:57:ff:fe:d5:4e:77-01-0201', diff --git a/tests/components/deconz/snapshots/test_cover.ambr b/tests/components/deconz/snapshots/test_cover.ambr index 41ff4e950a8..15e51b8443f 100644 --- a/tests/components/deconz/snapshots/test_cover.ambr +++ b/tests/components/deconz/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Window covering device', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-00', @@ -77,6 +78,7 @@ 'original_name': 'Vent', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:22:a3:00:00:00:00:00-01', @@ -128,6 +130,7 @@ 'original_name': 'Covering device', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:24:46:00:00:12:34:56-01', diff --git a/tests/components/deconz/snapshots/test_fan.ambr b/tests/components/deconz/snapshots/test_fan.ambr index 6a260c39673..d8d6f7703f2 100644 --- a/tests/components/deconz/snapshots/test_fan.ambr +++ b/tests/components/deconz/snapshots/test_fan.ambr @@ -29,6 +29,7 @@ 'original_name': 'Ceiling fan', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:22:a3:00:00:27:8b:81-01', diff --git a/tests/components/deconz/snapshots/test_light.ambr b/tests/components/deconz/snapshots/test_light.ambr index 212ccd84d0c..39ce5e46236 100644 --- a/tests/components/deconz/snapshots/test_light.ambr +++ b/tests/components/deconz/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Dimmable light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:02-00', @@ -97,6 +98,7 @@ 'original_name': None, 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01234E56789A-/groups/0', @@ -183,6 +185,7 @@ 'original_name': 'RGB light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', @@ -262,6 +265,7 @@ 'original_name': 'Tunable white light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-00', @@ -339,6 +343,7 @@ 'original_name': 'Dimmable light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:02-00', @@ -405,6 +410,7 @@ 'original_name': None, 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01234E56789A-/groups/0', @@ -491,6 +497,7 @@ 'original_name': 'RGB light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', @@ -570,6 +577,7 @@ 'original_name': 'Tunable white light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-00', @@ -647,6 +655,7 @@ 'original_name': 'Dimmable light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:02-00', @@ -713,6 +722,7 @@ 'original_name': None, 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01234E56789A-/groups/0', @@ -799,6 +809,7 @@ 'original_name': 'RGB light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00', @@ -878,6 +889,7 @@ 'original_name': 'Tunable white light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-00', @@ -964,6 +976,7 @@ 'original_name': 'Hue Go', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:17:88:01:01:23:45:67-00', @@ -1056,6 +1069,7 @@ 'original_name': 'Hue Ensis', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:17:88:01:01:23:45:67-01', @@ -1157,6 +1171,7 @@ 'original_name': 'LIDL xmas light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '58:8e:81:ff:fe:db:7b:be-01', @@ -1251,6 +1266,7 @@ 'original_name': 'Hue White Ambiance', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:17:88:01:01:23:45:67-02', @@ -1328,6 +1344,7 @@ 'original_name': 'Hue Filament', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:17:88:01:01:23:45:67-03', @@ -1386,6 +1403,7 @@ 'original_name': 'Simple Light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:01:23:45:67-01', @@ -1457,6 +1475,7 @@ 'original_name': 'Gradient light', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:17:88:01:0b:0c:0d:0e-0f', diff --git a/tests/components/deconz/snapshots/test_number.ambr b/tests/components/deconz/snapshots/test_number.ambr index 173d5e87043..d264740e4c2 100644 --- a/tests/components/deconz/snapshots/test_number.ambr +++ b/tests/components/deconz/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Presence sensor Delay', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-delay', @@ -88,6 +89,7 @@ 'original_name': 'Presence sensor Duration', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-duration', diff --git a/tests/components/deconz/snapshots/test_scene.ambr b/tests/components/deconz/snapshots/test_scene.ambr index 21456afaea1..4c04c6661d5 100644 --- a/tests/components/deconz/snapshots/test_scene.ambr +++ b/tests/components/deconz/snapshots/test_scene.ambr @@ -27,6 +27,7 @@ 'original_name': 'Scene', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01234E56789A/groups/1/scenes/1', diff --git a/tests/components/deconz/snapshots/test_select.ambr b/tests/components/deconz/snapshots/test_select.ambr index 7fa2aaf11cb..5b8dc9509a7 100644 --- a/tests/components/deconz/snapshots/test_select.ambr +++ b/tests/components/deconz/snapshots/test_select.ambr @@ -32,6 +32,7 @@ 'original_name': 'Aqara FP1 Device Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', @@ -89,6 +90,7 @@ 'original_name': 'Aqara FP1 Sensitivity', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', @@ -147,6 +149,7 @@ 'original_name': 'Aqara FP1 Trigger Distance', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', @@ -204,6 +207,7 @@ 'original_name': 'Aqara FP1 Device Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', @@ -261,6 +265,7 @@ 'original_name': 'Aqara FP1 Sensitivity', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', @@ -319,6 +324,7 @@ 'original_name': 'Aqara FP1 Trigger Distance', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', @@ -376,6 +382,7 @@ 'original_name': 'Aqara FP1 Device Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-device_mode', @@ -433,6 +440,7 @@ 'original_name': 'Aqara FP1 Sensitivity', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-sensitivity', @@ -491,6 +499,7 @@ 'original_name': 'Aqara FP1 Trigger Distance', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-0406-trigger_distance', @@ -553,6 +562,7 @@ 'original_name': 'IKEA Starkvind Fan Mode', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '0c:43:14:ff:fe:6c:20:12-01-fc7d-fan_mode', diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr index be397f0e22a..6e683241b6b 100644 --- a/tests/components/deconz/snapshots/test_sensor.ambr +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'CLIP Flur', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/sensors/3-status', @@ -77,6 +78,7 @@ 'original_name': 'CLIP light level sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-00-light_level', @@ -129,6 +131,7 @@ 'original_name': 'Light level sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-light_level', @@ -184,6 +187,7 @@ 'original_name': 'Light level sensor Temperature', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:00-00-internal_temperature', @@ -234,6 +238,7 @@ 'original_name': 'BOSCH Air quality sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality', @@ -283,6 +288,7 @@ 'original_name': 'BOSCH Air quality sensor PPB', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb', @@ -332,6 +338,7 @@ 'original_name': 'BOSCH Air quality sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality', @@ -381,6 +388,7 @@ 'original_name': 'BOSCH Air quality sensor PPB', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:12:4b:00:14:4d:00:07-02-fdef-air_quality_ppb', @@ -430,6 +438,7 @@ 'original_name': 'FSM_STATE Motion stair', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'fsm-state-1520195376277-status', @@ -483,6 +492,7 @@ 'original_name': 'Mi temperature 1', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0405-humidity', @@ -536,6 +546,7 @@ 'original_name': 'Mi temperature 1 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0405-battery', @@ -592,6 +603,7 @@ 'original_name': 'Soil Sensor', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-moisture', @@ -644,6 +656,7 @@ 'original_name': 'Soil Sensor Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a4:c1:38:fe:86:8f:07:a3-01-0408-battery', @@ -697,6 +710,7 @@ 'original_name': 'Motion sensor 4', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:17:88:01:03:28:8c:9b-02-0400-light_level', @@ -752,6 +766,7 @@ 'original_name': 'Motion sensor 4 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:17:88:01:03:28:8c:9b-02-0400-battery', @@ -807,6 +822,7 @@ 'original_name': 'STARKVIND AirPurifier PM25', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042a-particulate_matter_pm2_5', @@ -859,6 +875,7 @@ 'original_name': 'Power 16', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:0d:6f:00:0b:7a:64:29-01-0b04-power', @@ -914,6 +931,7 @@ 'original_name': 'Mi temperature 1', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0403-pressure', @@ -967,6 +985,7 @@ 'original_name': 'Mi temperature 1 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0403-battery', @@ -1023,6 +1042,7 @@ 'original_name': 'Mi temperature 1', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0402-temperature', @@ -1076,6 +1096,7 @@ 'original_name': 'Mi temperature 1 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:45:dc:53-01-0402-battery', @@ -1127,6 +1148,7 @@ 'original_name': 'eTRV Séjour', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'cc:cc:cc:ff:fe:38:4d:b3-01-000a-last_set', @@ -1177,6 +1199,7 @@ 'original_name': 'eTRV Séjour Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'cc:cc:cc:ff:fe:38:4d:b3-01-000a-battery', @@ -1230,6 +1253,7 @@ 'original_name': 'Alarm 10 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-battery', @@ -1284,6 +1308,7 @@ 'original_name': 'Alarm 10 Temperature', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:15:8d:00:02:b5:d1:80-01-0500-internal_temperature', @@ -1336,6 +1361,7 @@ 'original_name': 'AirQuality 1 CH2O', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', @@ -1388,6 +1414,7 @@ 'original_name': 'AirQuality 1 CO2', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', @@ -1440,6 +1467,7 @@ 'original_name': 'AirQuality 1 PM25', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', @@ -1492,6 +1520,7 @@ 'original_name': 'AirQuality 1 PPB', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', @@ -1543,6 +1572,7 @@ 'original_name': 'Dimmer switch 3 Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:17:88:01:02:0e:32:a3-02-fc00-battery', @@ -1601,6 +1631,7 @@ 'original_name': 'IKEA Starkvind Filter time', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '0c:43:14:ff:fe:6c:20:12-01-fc7d-air_purifier_filter_run_time', @@ -1652,6 +1683,7 @@ 'original_name': 'AirQuality 1 CH2O', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', @@ -1704,6 +1736,7 @@ 'original_name': 'AirQuality 1 CO2', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', @@ -1756,6 +1789,7 @@ 'original_name': 'AirQuality 1 PM25', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', @@ -1808,6 +1842,7 @@ 'original_name': 'AirQuality 1 PPB', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', @@ -1859,6 +1894,7 @@ 'original_name': 'AirQuality 1 CH2O', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_formaldehyde', @@ -1911,6 +1947,7 @@ 'original_name': 'AirQuality 1 CO2', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_co2', @@ -1963,6 +2000,7 @@ 'original_name': 'AirQuality 1 PM25', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_pm2_5', @@ -2015,6 +2053,7 @@ 'original_name': 'AirQuality 1 PPB', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00:00:01-02-0113-air_quality_ppb', @@ -2066,6 +2105,7 @@ 'original_name': 'FYRTUR block-out roller blind Battery', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:0d:6f:ff:fe:01:23:45-01-0001-battery', @@ -2119,6 +2159,7 @@ 'original_name': 'CarbonDioxide 35', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-040d-carbon_dioxide', @@ -2171,6 +2212,7 @@ 'original_name': 'Consumption 15', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:0d:6f:00:0b:7a:64:29-01-0702-consumption', @@ -2223,6 +2265,7 @@ 'original_name': 'Daylight', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01:23:4E:FF:FF:56:78:9A-01-daylight_status', @@ -2275,6 +2318,7 @@ 'original_name': 'Formaldehyde 34', 'platform': 'deconz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xx:xx:xx:xx:xx:xx:xx:xx-01-042b-formaldehyde', diff --git a/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr b/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr index 659420c1590..cb0c03e4b4e 100644 --- a/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr +++ b/tests/components/devolo_home_control/snapshots/test_binary_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': 'Door', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Test', @@ -89,6 +90,7 @@ 'original_name': 'Overload', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overload', 'unique_id': 'Overload', @@ -136,6 +138,7 @@ 'original_name': 'Button 1', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': 'Test_1', diff --git a/tests/components/devolo_home_control/snapshots/test_climate.ambr b/tests/components/devolo_home_control/snapshots/test_climate.ambr index 96ffe45c4a4..a42eece1bf8 100644 --- a/tests/components/devolo_home_control/snapshots/test_climate.ambr +++ b/tests/components/devolo_home_control/snapshots/test_climate.ambr @@ -56,6 +56,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'Test', diff --git a/tests/components/devolo_home_control/snapshots/test_cover.ambr b/tests/components/devolo_home_control/snapshots/test_cover.ambr index 44bff626923..53a2582bd3d 100644 --- a/tests/components/devolo_home_control/snapshots/test_cover.ambr +++ b/tests/components/devolo_home_control/snapshots/test_cover.ambr @@ -43,6 +43,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'devolo.Blinds', diff --git a/tests/components/devolo_home_control/snapshots/test_light.ambr b/tests/components/devolo_home_control/snapshots/test_light.ambr index 11dc768a519..f66fd4add1f 100644 --- a/tests/components/devolo_home_control/snapshots/test_light.ambr +++ b/tests/components/devolo_home_control/snapshots/test_light.ambr @@ -50,6 +50,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.Dimmer:Test', @@ -107,6 +108,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.Dimmer:Test', diff --git a/tests/components/devolo_home_control/snapshots/test_sensor.ambr b/tests/components/devolo_home_control/snapshots/test_sensor.ambr index 7cca8b23e77..a93ce7d6ceb 100644 --- a/tests/components/devolo_home_control/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_control/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'original_name': 'Battery', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.BatterySensor:Test', @@ -96,6 +97,7 @@ 'original_name': 'Brightness', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness', 'unique_id': 'devolo.MultiLevelSensor:Test', @@ -148,6 +150,7 @@ 'original_name': 'Power', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.Meter:Test_current', @@ -200,6 +203,7 @@ 'original_name': 'Energy', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.Meter:Test_total', @@ -252,6 +256,7 @@ 'original_name': 'Temperature', 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.MultiLevelSensor:Test', diff --git a/tests/components/devolo_home_control/snapshots/test_siren.ambr b/tests/components/devolo_home_control/snapshots/test_siren.ambr index 41b68574065..463af865ad8 100644 --- a/tests/components/devolo_home_control/snapshots/test_siren.ambr +++ b/tests/components/devolo_home_control/snapshots/test_siren.ambr @@ -48,6 +48,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'devolo.SirenMultiLevelSwitch:Test', @@ -103,6 +104,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'devolo.SirenMultiLevelSwitch:Test', @@ -158,6 +160,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'devolo.SirenMultiLevelSwitch:Test', diff --git a/tests/components/devolo_home_control/snapshots/test_switch.ambr b/tests/components/devolo_home_control/snapshots/test_switch.ambr index d3097716092..1047f0580c5 100644 --- a/tests/components/devolo_home_control/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_control/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'original_name': None, 'platform': 'devolo_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'devolo.BinarySwitch:Test', diff --git a/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr index a33fdf084dd..5099c9881e7 100644 --- a/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_binary_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': 'Connected to router', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connected_to_router', 'unique_id': '1234567890_connected_to_router', diff --git a/tests/components/devolo_home_network/snapshots/test_button.ambr b/tests/components/devolo_home_network/snapshots/test_button.ambr index 31d8ebf31a0..d7c1ae06a6b 100644 --- a/tests/components/devolo_home_network/snapshots/test_button.ambr +++ b/tests/components/devolo_home_network/snapshots/test_button.ambr @@ -41,6 +41,7 @@ 'original_name': 'Identify device with a blinking LED', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'identify', 'unique_id': '1234567890_identify', @@ -89,6 +90,7 @@ 'original_name': 'Restart device', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restart', 'unique_id': '1234567890_restart', @@ -136,6 +138,7 @@ 'original_name': 'Start PLC pairing', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pairing', 'unique_id': '1234567890_pairing', @@ -183,6 +186,7 @@ 'original_name': 'Start WPS', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_wps', 'unique_id': '1234567890_start_wps', diff --git a/tests/components/devolo_home_network/snapshots/test_image.ambr b/tests/components/devolo_home_network/snapshots/test_image.ambr index 3772672d8cb..5817b502eff 100644 --- a/tests/components/devolo_home_network/snapshots/test_image.ambr +++ b/tests/components/devolo_home_network/snapshots/test_image.ambr @@ -27,6 +27,7 @@ 'original_name': 'Guest Wi-Fi credentials as QR code', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'image_guest_wifi', 'unique_id': '1234567890_image_guest_wifi', diff --git a/tests/components/devolo_home_network/snapshots/test_sensor.ambr b/tests/components/devolo_home_network/snapshots/test_sensor.ambr index 9e2d8879ac9..d22916552a5 100644 --- a/tests/components/devolo_home_network/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_network/snapshots/test_sensor.ambr @@ -40,6 +40,7 @@ 'original_name': 'Connected PLC devices', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connected_plc_devices', 'unique_id': '1234567890_connected_plc_devices', @@ -90,6 +91,7 @@ 'original_name': 'Connected Wi-Fi clients', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connected_wifi_clients', 'unique_id': '1234567890_connected_wifi_clients', @@ -138,6 +140,7 @@ 'original_name': 'Last restart of the device', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_restart', 'unique_id': '1234567890_last_restart', @@ -185,6 +188,7 @@ 'original_name': 'Neighboring Wi-Fi networks', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'neighboring_wifi_networks', 'unique_id': '1234567890_neighboring_wifi_networks', @@ -237,6 +241,7 @@ 'original_name': 'PLC downlink PHY rate (test2)', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plc_rx_rate', 'unique_id': '1234567890_plc_rx_rate_11:22:33:44:55:66', @@ -289,6 +294,7 @@ 'original_name': 'PLC downlink PHY rate (test2)', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plc_rx_rate', 'unique_id': '1234567890_plc_rx_rate_11:22:33:44:55:66', diff --git a/tests/components/devolo_home_network/snapshots/test_switch.ambr b/tests/components/devolo_home_network/snapshots/test_switch.ambr index 6499bb9a17b..85b36b425b4 100644 --- a/tests/components/devolo_home_network/snapshots/test_switch.ambr +++ b/tests/components/devolo_home_network/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'original_name': 'Enable guest Wi-Fi', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'switch_guest_wifi', 'unique_id': '1234567890_switch_guest_wifi', @@ -87,6 +88,7 @@ 'original_name': 'Enable LEDs', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'switch_leds', 'unique_id': '1234567890_switch_leds', diff --git a/tests/components/devolo_home_network/snapshots/test_update.ambr b/tests/components/devolo_home_network/snapshots/test_update.ambr index f4d1c0480cf..92301447ac9 100644 --- a/tests/components/devolo_home_network/snapshots/test_update.ambr +++ b/tests/components/devolo_home_network/snapshots/test_update.ambr @@ -53,6 +53,7 @@ 'original_name': 'Firmware', 'platform': 'devolo_home_network', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'regular_firmware', 'unique_id': '1234567890_regular_firmware', diff --git a/tests/components/discovergy/snapshots/test_sensor.ambr b/tests/components/discovergy/snapshots/test_sensor.ambr index 866a57c8dda..84da04a7114 100644 --- a/tests/components/discovergy/snapshots/test_sensor.ambr +++ b/tests/components/discovergy/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Last transmitted', 'platform': 'discovergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_transmitted', 'unique_id': 'abc123-last_transmitted', @@ -69,6 +70,7 @@ 'original_name': 'Total consumption', 'platform': 'discovergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_consumption', 'unique_id': 'abc123-energy', @@ -124,6 +126,7 @@ 'original_name': 'Total power', 'platform': 'discovergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power', 'unique_id': 'abc123-power', @@ -174,6 +177,7 @@ 'original_name': 'Last transmitted', 'platform': 'discovergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_transmitted', 'unique_id': 'def456-last_transmitted', @@ -216,6 +220,7 @@ 'original_name': 'Total gas consumption', 'platform': 'discovergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_gas_consumption', 'unique_id': 'def456-volume', diff --git a/tests/components/drop_connect/snapshots/test_binary_sensor.ambr b/tests/components/drop_connect/snapshots/test_binary_sensor.ambr index 8d83482e208..0db2fe508e9 100644 --- a/tests/components/drop_connect/snapshots/test_binary_sensor.ambr +++ b/tests/components/drop_connect/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Power', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DROP-1_C0FFEE_81_power', @@ -75,6 +76,7 @@ 'original_name': 'Sensor', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alert_sensor', 'unique_id': 'DROP-1_C0FFEE_81_alert_sensor', @@ -123,6 +125,7 @@ 'original_name': 'Leak detected', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak', 'unique_id': 'DROP-1_C0FFEE_255_leak', @@ -171,6 +174,7 @@ 'original_name': 'Notification unread', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pending_notification', 'unique_id': 'DROP-1_C0FFEE_255_pending_notification', @@ -218,6 +222,7 @@ 'original_name': 'Leak detected', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak', 'unique_id': 'DROP-1_C0FFEE_20_leak', @@ -266,6 +271,7 @@ 'original_name': 'Leak detected', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak', 'unique_id': 'DROP-1_C0FFEE_78_leak', @@ -314,6 +320,7 @@ 'original_name': 'Leak detected', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak', 'unique_id': 'DROP-1_C0FFEE_83_leak', @@ -362,6 +369,7 @@ 'original_name': 'Pump status', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump', 'unique_id': 'DROP-1_C0FFEE_83_pump', @@ -409,6 +417,7 @@ 'original_name': 'Leak detected', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak', 'unique_id': 'DROP-1_C0FFEE_255_leak', @@ -457,6 +466,7 @@ 'original_name': 'Reserve capacity in use', 'platform': 'drop_connect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_in_use', 'unique_id': 'DROP-1_C0FFEE_0_reserve_in_use', diff --git a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr index 59e2f5a24b7..205ce783b8c 100644 --- a/tests/components/ecovacs/snapshots/test_binary_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Mop attached', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_mop_attached', 'unique_id': 'E1234567890000000001_water_mop_attached', diff --git a/tests/components/ecovacs/snapshots/test_button.ambr b/tests/components/ecovacs/snapshots/test_button.ambr index 2c657080c12..21b7d6105f1 100644 --- a/tests/components/ecovacs/snapshots/test_button.ambr +++ b/tests/components/ecovacs/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reset blade lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_blade', 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_blade', @@ -74,6 +75,7 @@ 'original_name': 'Reset lens brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_lens_brush', 'unique_id': '8516fbb1-17f1-4194-0000000_reset_lifespan_lens_brush', @@ -121,6 +123,7 @@ 'original_name': 'Empty dustbin', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'station_action_empty_dustbin', 'unique_id': '8516fbb1-17f1-4194-0000001_station_action_empty_dustbin', @@ -168,6 +171,7 @@ 'original_name': 'Relocate', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relocate', 'unique_id': '8516fbb1-17f1-4194-0000001_relocate', @@ -215,6 +219,7 @@ 'original_name': 'Reset filter lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_filter', 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_filter', @@ -262,6 +267,7 @@ 'original_name': 'Reset main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_brush', 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_brush', @@ -309,6 +315,7 @@ 'original_name': 'Reset round mop lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_round_mop', 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_round_mop', @@ -356,6 +363,7 @@ 'original_name': 'Reset side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_side_brush', 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_side_brush', @@ -403,6 +411,7 @@ 'original_name': 'Reset unit care lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_unit_care', 'unique_id': '8516fbb1-17f1-4194-0000001_reset_lifespan_unit_care', @@ -450,6 +459,7 @@ 'original_name': 'Relocate', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relocate', 'unique_id': 'E1234567890000000001_relocate', @@ -497,6 +507,7 @@ 'original_name': 'Reset filter lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_filter', 'unique_id': 'E1234567890000000001_reset_lifespan_filter', @@ -544,6 +555,7 @@ 'original_name': 'Reset main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_brush', 'unique_id': 'E1234567890000000001_reset_lifespan_brush', @@ -591,6 +603,7 @@ 'original_name': 'Reset side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_lifespan_side_brush', 'unique_id': 'E1234567890000000001_reset_lifespan_side_brush', diff --git a/tests/components/ecovacs/snapshots/test_event.ambr b/tests/components/ecovacs/snapshots/test_event.ambr index d29bf8dd57a..3f72a803c6d 100644 --- a/tests/components/ecovacs/snapshots/test_event.ambr +++ b/tests/components/ecovacs/snapshots/test_event.ambr @@ -33,6 +33,7 @@ 'original_name': 'Last job', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_job', 'unique_id': 'E1234567890000000001_stats_report', diff --git a/tests/components/ecovacs/snapshots/test_lawn_mower.ambr b/tests/components/ecovacs/snapshots/test_lawn_mower.ambr index 6367872c7f7..99f4ba25bd4 100644 --- a/tests/components/ecovacs/snapshots/test_lawn_mower.ambr +++ b/tests/components/ecovacs/snapshots/test_lawn_mower.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000000_mower', @@ -61,6 +62,7 @@ 'original_name': None, 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000000_mower', diff --git a/tests/components/ecovacs/snapshots/test_number.ambr b/tests/components/ecovacs/snapshots/test_number.ambr index 952fa4556b0..b89a490c772 100644 --- a/tests/components/ecovacs/snapshots/test_number.ambr +++ b/tests/components/ecovacs/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Cut direction', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cut_direction', 'unique_id': '8516fbb1-17f1-4194-0000000_cut_direction', @@ -89,6 +90,7 @@ 'original_name': 'Volume', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '8516fbb1-17f1-4194-0000000_volume', @@ -145,6 +147,7 @@ 'original_name': 'Volume', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': 'E1234567890000000001_volume', diff --git a/tests/components/ecovacs/snapshots/test_select.ambr b/tests/components/ecovacs/snapshots/test_select.ambr index 354afca1178..420a4a2d48e 100644 --- a/tests/components/ecovacs/snapshots/test_select.ambr +++ b/tests/components/ecovacs/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Water flow level', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_amount', 'unique_id': 'E1234567890000000001_water_amount', diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 468ff0a29f8..4c242103d14 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_filter', 'unique_id': 'E1234567890000000003_lifespan_filter', @@ -75,6 +76,7 @@ 'original_name': 'Main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_main_brush', 'unique_id': 'E1234567890000000003_lifespan_main_brush', @@ -123,6 +125,7 @@ 'original_name': 'Side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_side_brush', 'unique_id': 'E1234567890000000003_lifespan_side_brush', @@ -181,6 +184,7 @@ 'original_name': 'Area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_stats_area', @@ -230,6 +234,7 @@ 'original_name': 'Battery', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000000_battery_level', @@ -279,6 +284,7 @@ 'original_name': 'Blade lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_blade', 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_blade', @@ -330,6 +336,7 @@ 'original_name': 'Cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_time', 'unique_id': '8516fbb1-17f1-4194-0000000_stats_time', @@ -379,6 +386,7 @@ 'original_name': 'Error', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error', 'unique_id': '8516fbb1-17f1-4194-0000000_error', @@ -427,6 +435,7 @@ 'original_name': 'IP address', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ip', 'unique_id': '8516fbb1-17f1-4194-0000000_network_ip', @@ -474,6 +483,7 @@ 'original_name': 'Lens brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_lens_brush', 'unique_id': '8516fbb1-17f1-4194-0000000_lifespan_lens_brush', @@ -524,6 +534,7 @@ 'original_name': 'Total area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area', @@ -579,6 +590,7 @@ 'original_name': 'Total cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_time', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_time', @@ -631,6 +643,7 @@ 'original_name': 'Total cleanings', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_cleanings', 'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_cleanings', @@ -679,6 +692,7 @@ 'original_name': 'Wi-Fi RSSI', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rssi', 'unique_id': '8516fbb1-17f1-4194-0000000_network_rssi', @@ -726,6 +740,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ssid', 'unique_id': '8516fbb1-17f1-4194-0000000_network_ssid', @@ -776,6 +791,7 @@ 'original_name': 'Area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': '8516fbb1-17f1-4194-0000001_stats_area', @@ -825,6 +841,7 @@ 'original_name': 'Battery', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '8516fbb1-17f1-4194-0000001_battery_level', @@ -877,6 +894,7 @@ 'original_name': 'Cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_time', 'unique_id': '8516fbb1-17f1-4194-0000001_stats_time', @@ -926,6 +944,7 @@ 'original_name': 'Error', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error', 'unique_id': '8516fbb1-17f1-4194-0000001_error', @@ -974,6 +993,7 @@ 'original_name': 'Filter lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_filter', 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_filter', @@ -1022,6 +1042,7 @@ 'original_name': 'IP address', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ip', 'unique_id': '8516fbb1-17f1-4194-0000001_network_ip', @@ -1069,6 +1090,7 @@ 'original_name': 'Main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_brush', 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_brush', @@ -1117,6 +1139,7 @@ 'original_name': 'Round mop lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_round_mop', 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_round_mop', @@ -1165,6 +1188,7 @@ 'original_name': 'Side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_side_brush', 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_side_brush', @@ -1218,6 +1242,7 @@ 'original_name': 'Station state', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'station_state', 'unique_id': '8516fbb1-17f1-4194-0000001_station_state', @@ -1272,6 +1297,7 @@ 'original_name': 'Total area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_area', @@ -1327,6 +1353,7 @@ 'original_name': 'Total cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_time', 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_time', @@ -1379,6 +1406,7 @@ 'original_name': 'Total cleanings', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_cleanings', 'unique_id': '8516fbb1-17f1-4194-0000001_total_stats_cleanings', @@ -1427,6 +1455,7 @@ 'original_name': 'Unit care lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_unit_care', 'unique_id': '8516fbb1-17f1-4194-0000001_lifespan_unit_care', @@ -1475,6 +1504,7 @@ 'original_name': 'Wi-Fi RSSI', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rssi', 'unique_id': '8516fbb1-17f1-4194-0000001_network_rssi', @@ -1522,6 +1552,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ssid', 'unique_id': '8516fbb1-17f1-4194-0000001_network_ssid', @@ -1572,6 +1603,7 @@ 'original_name': 'Area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_area', 'unique_id': 'E1234567890000000001_stats_area', @@ -1621,6 +1653,7 @@ 'original_name': 'Battery', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'E1234567890000000001_battery_level', @@ -1673,6 +1706,7 @@ 'original_name': 'Cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stats_time', 'unique_id': 'E1234567890000000001_stats_time', @@ -1722,6 +1756,7 @@ 'original_name': 'Error', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error', 'unique_id': 'E1234567890000000001_error', @@ -1770,6 +1805,7 @@ 'original_name': 'Filter lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_filter', 'unique_id': 'E1234567890000000001_lifespan_filter', @@ -1818,6 +1854,7 @@ 'original_name': 'IP address', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ip', 'unique_id': 'E1234567890000000001_network_ip', @@ -1865,6 +1902,7 @@ 'original_name': 'Main brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_brush', 'unique_id': 'E1234567890000000001_lifespan_brush', @@ -1913,6 +1951,7 @@ 'original_name': 'Side brush lifespan', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifespan_side_brush', 'unique_id': 'E1234567890000000001_lifespan_side_brush', @@ -1963,6 +2002,7 @@ 'original_name': 'Total area cleaned', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_area', 'unique_id': 'E1234567890000000001_total_stats_area', @@ -2018,6 +2058,7 @@ 'original_name': 'Total cleaning duration', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_time', 'unique_id': 'E1234567890000000001_total_stats_time', @@ -2070,6 +2111,7 @@ 'original_name': 'Total cleanings', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_stats_cleanings', 'unique_id': 'E1234567890000000001_total_stats_cleanings', @@ -2118,6 +2160,7 @@ 'original_name': 'Wi-Fi RSSI', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rssi', 'unique_id': 'E1234567890000000001_network_rssi', @@ -2165,6 +2208,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_ssid', 'unique_id': 'E1234567890000000001_network_ssid', diff --git a/tests/components/ecovacs/snapshots/test_switch.ambr b/tests/components/ecovacs/snapshots/test_switch.ambr index 48aa9d8fc17..e56142c2d82 100644 --- a/tests/components/ecovacs/snapshots/test_switch.ambr +++ b/tests/components/ecovacs/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Advanced mode', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'advanced_mode', 'unique_id': '8516fbb1-17f1-4194-0000000_advanced_mode', @@ -74,6 +75,7 @@ 'original_name': 'Border switch', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'border_switch', 'unique_id': '8516fbb1-17f1-4194-0000000_border_switch', @@ -121,6 +123,7 @@ 'original_name': 'Child lock', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '8516fbb1-17f1-4194-0000000_child_lock', @@ -168,6 +171,7 @@ 'original_name': 'Cross map border warning', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cross_map_border_warning', 'unique_id': '8516fbb1-17f1-4194-0000000_cross_map_border_warning', @@ -215,6 +219,7 @@ 'original_name': 'Move up warning', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'move_up_warning', 'unique_id': '8516fbb1-17f1-4194-0000000_move_up_warning', @@ -262,6 +267,7 @@ 'original_name': 'Safe protect', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'safe_protect', 'unique_id': '8516fbb1-17f1-4194-0000000_safe_protect', @@ -309,6 +315,7 @@ 'original_name': 'True detect', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'true_detect', 'unique_id': '8516fbb1-17f1-4194-0000000_true_detect', @@ -356,6 +363,7 @@ 'original_name': 'Advanced mode', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'advanced_mode', 'unique_id': 'E1234567890000000001_advanced_mode', @@ -403,6 +411,7 @@ 'original_name': 'Carpet auto-boost suction', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'carpet_auto_fan_boost', 'unique_id': 'E1234567890000000001_carpet_auto_fan_boost', @@ -450,6 +459,7 @@ 'original_name': 'Continuous cleaning', 'platform': 'ecovacs', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'continuous_cleaning', 'unique_id': 'E1234567890000000001_continuous_cleaning', diff --git a/tests/components/eheimdigital/snapshots/test_climate.ambr b/tests/components/eheimdigital/snapshots/test_climate.ambr index 73c7cf638e8..24b503f2ed7 100644 --- a/tests/components/eheimdigital/snapshots/test_climate.ambr +++ b/tests/components/eheimdigital/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': None, 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'heater', 'unique_id': '00:00:00:00:00:02', @@ -117,6 +118,7 @@ 'original_name': None, 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'heater', 'unique_id': '00:00:00:00:00:02', diff --git a/tests/components/eheimdigital/snapshots/test_light.ambr b/tests/components/eheimdigital/snapshots/test_light.ambr index b2398a6a419..f9dedeb5cfc 100644 --- a/tests/components/eheimdigital/snapshots/test_light.ambr +++ b/tests/components/eheimdigital/snapshots/test_light.ambr @@ -34,6 +34,7 @@ 'original_name': 'Channel 1', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'channel', 'unique_id': '00:00:00:00:00:01_1', @@ -98,6 +99,7 @@ 'original_name': 'Channel 0', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'channel', 'unique_id': '00:00:00:00:00:01_0', @@ -162,6 +164,7 @@ 'original_name': 'Channel 1', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'channel', 'unique_id': '00:00:00:00:00:01_1', @@ -226,6 +229,7 @@ 'original_name': 'Channel 0', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'channel', 'unique_id': '00:00:00:00:00:01_0', @@ -290,6 +294,7 @@ 'original_name': 'Channel 1', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'channel', 'unique_id': '00:00:00:00:00:01_1', diff --git a/tests/components/eheimdigital/snapshots/test_number.ambr b/tests/components/eheimdigital/snapshots/test_number.ambr index 554e7c9c3a3..4f3b0e46287 100644 --- a/tests/components/eheimdigital/snapshots/test_number.ambr +++ b/tests/components/eheimdigital/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'System LED brightness', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'system_led', 'unique_id': '00:00:00:00:00:01_system_led', @@ -89,6 +90,7 @@ 'original_name': 'Day speed', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'day_speed', 'unique_id': '00:00:00:00:00:03_day_speed', @@ -146,6 +148,7 @@ 'original_name': 'Manual speed', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_speed', 'unique_id': '00:00:00:00:00:03_manual_speed', @@ -203,6 +206,7 @@ 'original_name': 'Night speed', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'night_speed', 'unique_id': '00:00:00:00:00:03_night_speed', @@ -260,6 +264,7 @@ 'original_name': 'System LED brightness', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'system_led', 'unique_id': '00:00:00:00:00:03_system_led', @@ -317,6 +322,7 @@ 'original_name': 'Night temperature offset', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'night_temperature_offset', 'unique_id': '00:00:00:00:00:02_night_temperature_offset', @@ -374,6 +380,7 @@ 'original_name': 'System LED brightness', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'system_led', 'unique_id': '00:00:00:00:00:02_system_led', @@ -431,6 +438,7 @@ 'original_name': 'Temperature offset', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_offset', 'unique_id': '00:00:00:00:00:02_temperature_offset', diff --git a/tests/components/eheimdigital/snapshots/test_select.ambr b/tests/components/eheimdigital/snapshots/test_select.ambr index 5416f5a2d78..e7e0fee16c5 100644 --- a/tests/components/eheimdigital/snapshots/test_select.ambr +++ b/tests/components/eheimdigital/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Filter mode', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_mode', 'unique_id': '00:00:00:00:00:03_filter_mode', diff --git a/tests/components/eheimdigital/snapshots/test_sensor.ambr b/tests/components/eheimdigital/snapshots/test_sensor.ambr index c5a3d700331..7d86d92eaf8 100644 --- a/tests/components/eheimdigital/snapshots/test_sensor.ambr +++ b/tests/components/eheimdigital/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Current speed', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_speed', 'unique_id': '00:00:00:00:00:03_current_speed', @@ -81,6 +82,7 @@ 'original_name': 'Error code', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_code', 'unique_id': '00:00:00:00:00:03_error_code', @@ -137,6 +139,7 @@ 'original_name': 'Remaining hours until service', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'service_hours', 'unique_id': '00:00:00:00:00:03_service_hours', diff --git a/tests/components/eheimdigital/snapshots/test_switch.ambr b/tests/components/eheimdigital/snapshots/test_switch.ambr index 73d229cb4ba..5c5456d8840 100644 --- a/tests/components/eheimdigital/snapshots/test_switch.ambr +++ b/tests/components/eheimdigital/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_active', 'unique_id': '00:00:00:00:00:03', diff --git a/tests/components/eheimdigital/snapshots/test_time.ambr b/tests/components/eheimdigital/snapshots/test_time.ambr index bdd4bdaddb7..754846b4d2b 100644 --- a/tests/components/eheimdigital/snapshots/test_time.ambr +++ b/tests/components/eheimdigital/snapshots/test_time.ambr @@ -27,6 +27,7 @@ 'original_name': 'Day start time', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'day_start_time', 'unique_id': '00:00:00:00:00:03_day_start_time', @@ -74,6 +75,7 @@ 'original_name': 'Night start time', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'night_start_time', 'unique_id': '00:00:00:00:00:03_night_start_time', @@ -121,6 +123,7 @@ 'original_name': 'Day start time', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'day_start_time', 'unique_id': '00:00:00:00:00:02_day_start_time', @@ -168,6 +171,7 @@ 'original_name': 'Night start time', 'platform': 'eheimdigital', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'night_start_time', 'unique_id': '00:00:00:00:00:02_night_start_time', diff --git a/tests/components/elgato/snapshots/test_button.ambr b/tests/components/elgato/snapshots/test_button.ambr index 81a817f2738..2f1c2107b52 100644 --- a/tests/components/elgato/snapshots/test_button.ambr +++ b/tests/components/elgato/snapshots/test_button.ambr @@ -41,6 +41,7 @@ 'original_name': 'Identify', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'GW24L1A02987_identify', @@ -126,6 +127,7 @@ 'original_name': 'Restart', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'GW24L1A02987_restart', diff --git a/tests/components/elgato/snapshots/test_light.ambr b/tests/components/elgato/snapshots/test_light.ambr index 84f7ca45843..16f20224079 100644 --- a/tests/components/elgato/snapshots/test_light.ambr +++ b/tests/components/elgato/snapshots/test_light.ambr @@ -73,6 +73,7 @@ 'original_name': None, 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CN11A1A00001', @@ -192,6 +193,7 @@ 'original_name': None, 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CN11A1A00001', @@ -311,6 +313,7 @@ 'original_name': None, 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'CN11A1A00001', diff --git a/tests/components/elgato/snapshots/test_sensor.ambr b/tests/components/elgato/snapshots/test_sensor.ambr index f64893798e9..3592e88f975 100644 --- a/tests/components/elgato/snapshots/test_sensor.ambr +++ b/tests/components/elgato/snapshots/test_sensor.ambr @@ -48,6 +48,7 @@ 'original_name': 'Battery', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'GW24L1A02987_battery', @@ -143,6 +144,7 @@ 'original_name': 'Battery voltage', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': 'GW24L1A02987_voltage', @@ -238,6 +240,7 @@ 'original_name': 'Charging current', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_charge_current', 'unique_id': 'GW24L1A02987_input_charge_current', @@ -330,6 +333,7 @@ 'original_name': 'Charging power', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_power', 'unique_id': 'GW24L1A02987_charge_power', @@ -425,6 +429,7 @@ 'original_name': 'Charging voltage', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_charge_voltage', 'unique_id': 'GW24L1A02987_input_charge_voltage', diff --git a/tests/components/elgato/snapshots/test_switch.ambr b/tests/components/elgato/snapshots/test_switch.ambr index 254e4deb7d9..f29c16d0cae 100644 --- a/tests/components/elgato/snapshots/test_switch.ambr +++ b/tests/components/elgato/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'original_name': 'Energy saving', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saving', 'unique_id': 'GW24L1A02987_energy_saving', @@ -124,6 +125,7 @@ 'original_name': 'Studio mode', 'platform': 'elgato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', 'unique_id': 'GW24L1A02987_bypass', diff --git a/tests/components/elmax/snapshots/test_alarm_control_panel.ambr b/tests/components/elmax/snapshots/test_alarm_control_panel.ambr index 2bf3aa48430..77d41d50710 100644 --- a/tests/components/elmax/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/elmax/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': 'AREA 1', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '13762559c53cd093171-area-0', @@ -78,6 +79,7 @@ 'original_name': 'AREA 2', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '13762559c53cd093171-area-1', @@ -129,6 +131,7 @@ 'original_name': 'AREA 3', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '13762559c53cd093171-area-2', diff --git a/tests/components/elmax/snapshots/test_binary_sensor.ambr b/tests/components/elmax/snapshots/test_binary_sensor.ambr index 7515547406e..5fb9b9fd06e 100644 --- a/tests/components/elmax/snapshots/test_binary_sensor.ambr +++ b/tests/components/elmax/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'ZONA 01', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-0', @@ -75,6 +76,7 @@ 'original_name': 'ZONA 02e', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-1', @@ -123,6 +125,7 @@ 'original_name': 'ZONA 03a', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-2', @@ -171,6 +174,7 @@ 'original_name': 'ZONA 04', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-3', @@ -219,6 +223,7 @@ 'original_name': 'ZONA 05', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-4', @@ -267,6 +272,7 @@ 'original_name': 'ZONA 06', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-5', @@ -315,6 +321,7 @@ 'original_name': 'ZONA 07', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-6', @@ -363,6 +370,7 @@ 'original_name': 'ZONA 08', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-zona-7', diff --git a/tests/components/elmax/snapshots/test_cover.ambr b/tests/components/elmax/snapshots/test_cover.ambr index 8cb230e1523..5d30dc6a570 100644 --- a/tests/components/elmax/snapshots/test_cover.ambr +++ b/tests/components/elmax/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'ESPAN.DOM.01', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '13762559c53cd093171-tapparella-0', diff --git a/tests/components/elmax/snapshots/test_switch.ambr b/tests/components/elmax/snapshots/test_switch.ambr index f5845223717..d278c3e9854 100644 --- a/tests/components/elmax/snapshots/test_switch.ambr +++ b/tests/components/elmax/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'USCITA 02', 'platform': 'elmax', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '13762559c53cd093171-uscita-1', diff --git a/tests/components/emoncms/snapshots/test_sensor.ambr b/tests/components/emoncms/snapshots/test_sensor.ambr index 6dc19155863..7dc6f0674e4 100644 --- a/tests/components/emoncms/snapshots/test_sensor.ambr +++ b/tests/components/emoncms/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Temperature tag parameter 1', 'platform': 'emoncms', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '123-53535292-1', diff --git a/tests/components/energenie_power_sockets/snapshots/test_switch.ambr b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr index 99595168157..56e6bc52361 100644 --- a/tests/components/energenie_power_sockets/snapshots/test_switch.ambr +++ b/tests/components/energenie_power_sockets/snapshots/test_switch.ambr @@ -41,6 +41,7 @@ 'original_name': 'Socket 0', 'platform': 'energenie_power_sockets', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'socket', 'unique_id': 'DYPS:00:11:22_0', @@ -89,6 +90,7 @@ 'original_name': 'Socket 1', 'platform': 'energenie_power_sockets', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'socket', 'unique_id': 'DYPS:00:11:22_1', @@ -137,6 +139,7 @@ 'original_name': 'Socket 2', 'platform': 'energenie_power_sockets', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'socket', 'unique_id': 'DYPS:00:11:22_2', @@ -185,6 +188,7 @@ 'original_name': 'Socket 3', 'platform': 'energenie_power_sockets', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'socket', 'unique_id': 'DYPS:00:11:22_3', diff --git a/tests/components/energyzero/snapshots/test_sensor.ambr b/tests/components/energyzero/snapshots/test_sensor.ambr index 5407ac8f0e9..c0041bc0e50 100644 --- a/tests/components/energyzero/snapshots/test_sensor.ambr +++ b/tests/components/energyzero/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Average - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_average_price', 'supported_features': 0, 'translation_key': 'average_price', 'unique_id': '12345_today_energy_average_price', @@ -78,6 +79,7 @@ 'original_name': 'Current hour', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_current_hour_price', 'supported_features': 0, 'translation_key': 'current_hour_price', 'unique_id': '12345_today_energy_current_hour_price', @@ -128,6 +130,7 @@ 'original_name': 'Time of highest price - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_highest_price_time', 'supported_features': 0, 'translation_key': 'highest_price_time', 'unique_id': '12345_today_energy_highest_price_time', @@ -177,6 +180,7 @@ 'original_name': 'Hours priced equal or lower than current - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_hours_priced_equal_or_lower', 'supported_features': 0, 'translation_key': 'hours_priced_equal_or_lower', 'unique_id': '12345_today_energy_hours_priced_equal_or_lower', @@ -226,6 +230,7 @@ 'original_name': 'Time of lowest price - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_lowest_price_time', 'supported_features': 0, 'translation_key': 'lowest_price_time', 'unique_id': '12345_today_energy_lowest_price_time', @@ -275,6 +280,7 @@ 'original_name': 'Highest price - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_max_price', 'supported_features': 0, 'translation_key': 'max_price', 'unique_id': '12345_today_energy_max_price', @@ -324,6 +330,7 @@ 'original_name': 'Lowest price - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_min_price', 'supported_features': 0, 'translation_key': 'min_price', 'unique_id': '12345_today_energy_min_price', @@ -373,6 +380,7 @@ 'original_name': 'Next hour', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_next_hour_price', 'supported_features': 0, 'translation_key': 'next_hour_price', 'unique_id': '12345_today_energy_next_hour_price', @@ -422,6 +430,7 @@ 'original_name': 'Current percentage of highest price - today', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_energy_percentage_of_max', 'supported_features': 0, 'translation_key': 'percentage_of_max', 'unique_id': '12345_today_energy_percentage_of_max', @@ -473,6 +482,7 @@ 'original_name': 'Current hour', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_gas_current_hour_price', 'supported_features': 0, 'translation_key': 'current_hour_price', 'unique_id': '12345_today_gas_current_hour_price', @@ -523,6 +533,7 @@ 'original_name': 'Next hour', 'platform': 'energyzero', 'previous_unique_id': None, + 'suggested_object_id': 'energyzero_today_gas_next_hour_price', 'supported_features': 0, 'translation_key': 'next_hour_price', 'unique_id': '12345_today_gas_next_hour_price', diff --git a/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr index e4810c21226..bbf35621c6c 100644 --- a/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Communicating', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'communicating', 'unique_id': '123456_communicating', @@ -75,6 +76,7 @@ 'original_name': 'DC switch', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dc_switch', 'unique_id': '123456_dc_switch', @@ -122,6 +124,7 @@ 'original_name': 'Communicating', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'communicating', 'unique_id': '123456_communicating', @@ -170,6 +173,7 @@ 'original_name': 'DC switch', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dc_switch', 'unique_id': '123456_dc_switch', @@ -217,6 +221,7 @@ 'original_name': 'Communicating', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'communicating', 'unique_id': '654321_communicating', @@ -265,6 +270,7 @@ 'original_name': 'Grid status', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_status', 'unique_id': '654321_mains_oper_state', diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index 650fb0bb810..f02f594a2ec 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -100,6 +100,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '<>_production', @@ -152,6 +153,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '<>_daily_production', @@ -202,6 +204,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '<>_seven_days_production', @@ -253,6 +256,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '<>_lifetime_production', @@ -338,6 +342,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -382,6 +387,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -549,6 +555,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '<>_production', @@ -601,6 +608,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '<>_daily_production', @@ -651,6 +659,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '<>_seven_days_production', @@ -702,6 +711,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '<>_lifetime_production', @@ -787,6 +797,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -831,6 +842,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -1040,6 +1052,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '<>_production', @@ -1092,6 +1105,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '<>_daily_production', @@ -1142,6 +1156,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '<>_seven_days_production', @@ -1193,6 +1208,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '<>_lifetime_production', @@ -1278,6 +1294,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -1322,6 +1339,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -1545,6 +1563,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -1589,6 +1608,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -1675,6 +1695,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '<>_production', @@ -1727,6 +1748,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '<>_daily_production', @@ -1777,6 +1799,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '<>_seven_days_production', @@ -1828,6 +1851,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '<>_lifetime_production', diff --git a/tests/components/enphase_envoy/snapshots/test_number.ambr b/tests/components/enphase_envoy/snapshots/test_number.ambr index eb8f5266f32..461d4028fbe 100644 --- a/tests/components/enphase_envoy/snapshots/test_number.ambr +++ b/tests/components/enphase_envoy/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Reserve battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_soc', 'unique_id': '1234_reserve_soc', @@ -90,6 +91,7 @@ 'original_name': 'Reserve battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_soc', 'unique_id': '654321_reserve_soc', @@ -148,6 +150,7 @@ 'original_name': 'Cutoff battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cutoff_battery_level', 'unique_id': '654321_relay_NC1_soc_low', @@ -205,6 +208,7 @@ 'original_name': 'Restore battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restore_battery_level', 'unique_id': '654321_relay_NC1_soc_high', @@ -262,6 +266,7 @@ 'original_name': 'Cutoff battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cutoff_battery_level', 'unique_id': '654321_relay_NC2_soc_low', @@ -319,6 +324,7 @@ 'original_name': 'Restore battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restore_battery_level', 'unique_id': '654321_relay_NC2_soc_high', @@ -376,6 +382,7 @@ 'original_name': 'Cutoff battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cutoff_battery_level', 'unique_id': '654321_relay_NC3_soc_low', @@ -433,6 +440,7 @@ 'original_name': 'Restore battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restore_battery_level', 'unique_id': '654321_relay_NC3_soc_high', diff --git a/tests/components/enphase_envoy/snapshots/test_select.ambr b/tests/components/enphase_envoy/snapshots/test_select.ambr index d8238926dfd..006b2c1a3fe 100644 --- a/tests/components/enphase_envoy/snapshots/test_select.ambr +++ b/tests/components/enphase_envoy/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Storage mode', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_mode', 'unique_id': '1234_storage_mode', @@ -91,6 +92,7 @@ 'original_name': 'Storage mode', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_mode', 'unique_id': '654321_storage_mode', @@ -150,6 +152,7 @@ 'original_name': 'Generator action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_generator_action', 'unique_id': '654321_relay_NC1_generator_action', @@ -210,6 +213,7 @@ 'original_name': 'Grid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_grid_action', 'unique_id': '654321_relay_NC1_grid_action', @@ -270,6 +274,7 @@ 'original_name': 'Microgrid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_microgrid_action', 'unique_id': '654321_relay_NC1_microgrid_action', @@ -328,6 +333,7 @@ 'original_name': 'Mode', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_mode', 'unique_id': '654321_relay_NC1_mode', @@ -386,6 +392,7 @@ 'original_name': 'Generator action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_generator_action', 'unique_id': '654321_relay_NC2_generator_action', @@ -446,6 +453,7 @@ 'original_name': 'Grid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_grid_action', 'unique_id': '654321_relay_NC2_grid_action', @@ -506,6 +514,7 @@ 'original_name': 'Microgrid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_microgrid_action', 'unique_id': '654321_relay_NC2_microgrid_action', @@ -564,6 +573,7 @@ 'original_name': 'Mode', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_mode', 'unique_id': '654321_relay_NC2_mode', @@ -622,6 +632,7 @@ 'original_name': 'Generator action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_generator_action', 'unique_id': '654321_relay_NC3_generator_action', @@ -682,6 +693,7 @@ 'original_name': 'Grid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_grid_action', 'unique_id': '654321_relay_NC3_grid_action', @@ -742,6 +754,7 @@ 'original_name': 'Microgrid action', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_microgrid_action', 'unique_id': '654321_relay_NC3_microgrid_action', @@ -800,6 +813,7 @@ 'original_name': 'Mode', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_mode', 'unique_id': '654321_relay_NC3_mode', diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index 101caaf1aea..82f5aad2e25 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -91,6 +92,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -148,6 +150,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -206,6 +209,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -258,6 +262,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -308,6 +313,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -364,6 +370,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -422,6 +429,7 @@ 'original_name': 'Current net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption', 'unique_id': '1234_net_consumption', @@ -480,6 +488,7 @@ 'original_name': 'Current power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption', 'unique_id': '1234_consumption', @@ -538,6 +547,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -594,6 +604,7 @@ 'original_name': 'Energy consumption last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption', 'unique_id': '1234_seven_days_consumption', @@ -651,6 +662,7 @@ 'original_name': 'Energy consumption today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption', 'unique_id': '1234_daily_consumption', @@ -707,6 +719,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -764,6 +777,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -819,6 +833,7 @@ 'original_name': 'Frequency net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency', 'unique_id': '1234_frequency', @@ -874,6 +889,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -932,6 +948,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -990,6 +1007,7 @@ 'original_name': 'Lifetime energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption', 'unique_id': '1234_lifetime_consumption', @@ -1048,6 +1066,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -1106,6 +1125,7 @@ 'original_name': 'Lifetime net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption', 'unique_id': '1234_lifetime_net_consumption', @@ -1164,6 +1184,7 @@ 'original_name': 'Lifetime net energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production', 'unique_id': '1234_lifetime_net_production', @@ -1214,6 +1235,7 @@ 'original_name': 'Meter status flags active net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags', 'unique_id': '1234_net_consumption_ct_status_flags', @@ -1261,6 +1283,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -1314,6 +1337,7 @@ 'original_name': 'Metering status net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status', 'unique_id': '1234_net_consumption_ct_metering_status', @@ -1373,6 +1397,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -1434,6 +1459,7 @@ 'original_name': 'Net consumption CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current', 'unique_id': '1234_net_ct_current', @@ -1489,6 +1515,7 @@ 'original_name': 'Power factor net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor', 'unique_id': '1234_net_ct_powerfactor', @@ -1543,6 +1570,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -1600,6 +1628,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -1658,6 +1687,7 @@ 'original_name': 'Voltage net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage', 'unique_id': '1234_voltage', @@ -1716,6 +1746,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -1768,6 +1799,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -1818,6 +1850,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -1866,6 +1899,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_acb_soc', @@ -1922,6 +1956,7 @@ 'original_name': 'Battery state', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'acb_battery_state', 'unique_id': '1234_acb_battery_state', @@ -1976,6 +2011,7 @@ 'original_name': 'Power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_acb_power', @@ -2025,6 +2061,7 @@ 'original_name': 'Apparent power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_apparent_power_mva', @@ -2074,6 +2111,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_soc', @@ -2123,6 +2161,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '123456_last_reported', @@ -2171,6 +2210,7 @@ 'original_name': 'Power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_real_power_mw', @@ -2220,6 +2260,7 @@ 'original_name': 'Temperature', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_temperature', @@ -2269,6 +2310,7 @@ 'original_name': 'Aggregated available battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aggregated_available_energy', 'unique_id': '1234_aggregated_available_energy', @@ -2318,6 +2360,7 @@ 'original_name': 'Aggregated Battery capacity', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aggregated_max_capacity', 'unique_id': '1234_aggregated_max_battery_capacity', @@ -2367,6 +2410,7 @@ 'original_name': 'Aggregated battery soc', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aggregated_soc', 'unique_id': '1234_aggregated_soc', @@ -2416,6 +2460,7 @@ 'original_name': 'Available ACB battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'acb_available_energy', 'unique_id': '1234_acb_available_energy', @@ -2465,6 +2510,7 @@ 'original_name': 'Available battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'available_energy', 'unique_id': '1234_available_energy', @@ -2522,6 +2568,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -2572,6 +2619,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_battery_level', @@ -2621,6 +2669,7 @@ 'original_name': 'Battery capacity', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_capacity', 'unique_id': '1234_max_capacity', @@ -2678,6 +2727,7 @@ 'original_name': 'Current net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption', 'unique_id': '1234_net_consumption', @@ -2736,6 +2786,7 @@ 'original_name': 'Current net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l1', @@ -2794,6 +2845,7 @@ 'original_name': 'Current net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l2', @@ -2852,6 +2904,7 @@ 'original_name': 'Current net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l3', @@ -2910,6 +2963,7 @@ 'original_name': 'Current power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption', 'unique_id': '1234_consumption', @@ -2968,6 +3022,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -3024,6 +3079,7 @@ 'original_name': 'Energy consumption last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption', 'unique_id': '1234_seven_days_consumption', @@ -3081,6 +3137,7 @@ 'original_name': 'Energy consumption today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption', 'unique_id': '1234_daily_consumption', @@ -3137,6 +3194,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -3194,6 +3252,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -3249,6 +3308,7 @@ 'original_name': 'Frequency net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency', 'unique_id': '1234_frequency', @@ -3304,6 +3364,7 @@ 'original_name': 'Frequency net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l1', @@ -3359,6 +3420,7 @@ 'original_name': 'Frequency net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l2', @@ -3414,6 +3476,7 @@ 'original_name': 'Frequency net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l3', @@ -3469,6 +3532,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -3524,6 +3588,7 @@ 'original_name': 'Frequency production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l1', @@ -3579,6 +3644,7 @@ 'original_name': 'Frequency production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l2', @@ -3634,6 +3700,7 @@ 'original_name': 'Frequency production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l3', @@ -3692,6 +3759,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -3750,6 +3818,7 @@ 'original_name': 'Lifetime energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption', 'unique_id': '1234_lifetime_consumption', @@ -3808,6 +3877,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -3866,6 +3936,7 @@ 'original_name': 'Lifetime net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption', 'unique_id': '1234_lifetime_net_consumption', @@ -3924,6 +3995,7 @@ 'original_name': 'Lifetime net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l1', @@ -3982,6 +4054,7 @@ 'original_name': 'Lifetime net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l2', @@ -4040,6 +4113,7 @@ 'original_name': 'Lifetime net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l3', @@ -4098,6 +4172,7 @@ 'original_name': 'Lifetime net energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production', 'unique_id': '1234_lifetime_net_production', @@ -4156,6 +4231,7 @@ 'original_name': 'Lifetime net energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l1', @@ -4214,6 +4290,7 @@ 'original_name': 'Lifetime net energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l2', @@ -4272,6 +4349,7 @@ 'original_name': 'Lifetime net energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l3', @@ -4322,6 +4400,7 @@ 'original_name': 'Meter status flags active net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags', 'unique_id': '1234_net_consumption_ct_status_flags', @@ -4369,6 +4448,7 @@ 'original_name': 'Meter status flags active net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l1', @@ -4416,6 +4496,7 @@ 'original_name': 'Meter status flags active net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l2', @@ -4463,6 +4544,7 @@ 'original_name': 'Meter status flags active net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l3', @@ -4510,6 +4592,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -4557,6 +4640,7 @@ 'original_name': 'Meter status flags active production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l1', @@ -4604,6 +4688,7 @@ 'original_name': 'Meter status flags active production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l2', @@ -4651,6 +4736,7 @@ 'original_name': 'Meter status flags active production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l3', @@ -4704,6 +4790,7 @@ 'original_name': 'Metering status net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status', 'unique_id': '1234_net_consumption_ct_metering_status', @@ -4763,6 +4850,7 @@ 'original_name': 'Metering status net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l1', @@ -4822,6 +4910,7 @@ 'original_name': 'Metering status net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l2', @@ -4881,6 +4970,7 @@ 'original_name': 'Metering status net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l3', @@ -4940,6 +5030,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -4999,6 +5090,7 @@ 'original_name': 'Metering status production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l1', @@ -5058,6 +5150,7 @@ 'original_name': 'Metering status production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l2', @@ -5117,6 +5210,7 @@ 'original_name': 'Metering status production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l3', @@ -5178,6 +5272,7 @@ 'original_name': 'Net consumption CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current', 'unique_id': '1234_net_ct_current', @@ -5236,6 +5331,7 @@ 'original_name': 'Net consumption CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l1', @@ -5294,6 +5390,7 @@ 'original_name': 'Net consumption CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l2', @@ -5352,6 +5449,7 @@ 'original_name': 'Net consumption CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l3', @@ -5407,6 +5505,7 @@ 'original_name': 'Power factor net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor', 'unique_id': '1234_net_ct_powerfactor', @@ -5461,6 +5560,7 @@ 'original_name': 'Power factor net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l1', @@ -5515,6 +5615,7 @@ 'original_name': 'Power factor net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l2', @@ -5569,6 +5670,7 @@ 'original_name': 'Power factor net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l3', @@ -5623,6 +5725,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -5677,6 +5780,7 @@ 'original_name': 'Power factor production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l1', @@ -5731,6 +5835,7 @@ 'original_name': 'Power factor production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l2', @@ -5785,6 +5890,7 @@ 'original_name': 'Power factor production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l3', @@ -5842,6 +5948,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -5900,6 +6007,7 @@ 'original_name': 'Production CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l1', @@ -5958,6 +6066,7 @@ 'original_name': 'Production CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l2', @@ -6016,6 +6125,7 @@ 'original_name': 'Production CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l3', @@ -6066,6 +6176,7 @@ 'original_name': 'Reserve battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_energy', 'unique_id': '1234_reserve_energy', @@ -6115,6 +6226,7 @@ 'original_name': 'Reserve battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_soc', 'unique_id': '1234_reserve_soc', @@ -6172,6 +6284,7 @@ 'original_name': 'Voltage net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage', 'unique_id': '1234_voltage', @@ -6230,6 +6343,7 @@ 'original_name': 'Voltage net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l1', @@ -6288,6 +6402,7 @@ 'original_name': 'Voltage net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l2', @@ -6346,6 +6461,7 @@ 'original_name': 'Voltage net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l3', @@ -6404,6 +6520,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -6462,6 +6579,7 @@ 'original_name': 'Voltage production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l1', @@ -6520,6 +6638,7 @@ 'original_name': 'Voltage production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l2', @@ -6578,6 +6697,7 @@ 'original_name': 'Voltage production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l3', @@ -6630,6 +6750,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -6680,6 +6801,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -6728,6 +6850,7 @@ 'original_name': 'Apparent power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_apparent_power_mva', @@ -6777,6 +6900,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_soc', @@ -6826,6 +6950,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '123456_last_reported', @@ -6874,6 +6999,7 @@ 'original_name': 'Power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_real_power_mw', @@ -6923,6 +7049,7 @@ 'original_name': 'Temperature', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_temperature', @@ -6972,6 +7099,7 @@ 'original_name': 'Available battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'available_energy', 'unique_id': '1234_available_energy', @@ -7029,6 +7157,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -7079,6 +7208,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_battery_level', @@ -7128,6 +7258,7 @@ 'original_name': 'Battery capacity', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_capacity', 'unique_id': '1234_max_capacity', @@ -7185,6 +7316,7 @@ 'original_name': 'Current net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption', 'unique_id': '1234_net_consumption', @@ -7243,6 +7375,7 @@ 'original_name': 'Current net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l1', @@ -7301,6 +7434,7 @@ 'original_name': 'Current net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l2', @@ -7359,6 +7493,7 @@ 'original_name': 'Current net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l3', @@ -7417,6 +7552,7 @@ 'original_name': 'Current power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption', 'unique_id': '1234_consumption', @@ -7475,6 +7611,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -7531,6 +7668,7 @@ 'original_name': 'Energy consumption last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption', 'unique_id': '1234_seven_days_consumption', @@ -7588,6 +7726,7 @@ 'original_name': 'Energy consumption today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption', 'unique_id': '1234_daily_consumption', @@ -7644,6 +7783,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -7701,6 +7841,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -7756,6 +7897,7 @@ 'original_name': 'Frequency net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency', 'unique_id': '1234_frequency', @@ -7811,6 +7953,7 @@ 'original_name': 'Frequency net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l1', @@ -7866,6 +8009,7 @@ 'original_name': 'Frequency net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l2', @@ -7921,6 +8065,7 @@ 'original_name': 'Frequency net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l3', @@ -7976,6 +8121,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -8031,6 +8177,7 @@ 'original_name': 'Frequency production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l1', @@ -8086,6 +8233,7 @@ 'original_name': 'Frequency production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l2', @@ -8141,6 +8289,7 @@ 'original_name': 'Frequency production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l3', @@ -8199,6 +8348,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -8257,6 +8407,7 @@ 'original_name': 'Lifetime energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption', 'unique_id': '1234_lifetime_consumption', @@ -8315,6 +8466,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -8373,6 +8525,7 @@ 'original_name': 'Lifetime net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption', 'unique_id': '1234_lifetime_net_consumption', @@ -8431,6 +8584,7 @@ 'original_name': 'Lifetime net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l1', @@ -8489,6 +8643,7 @@ 'original_name': 'Lifetime net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l2', @@ -8547,6 +8702,7 @@ 'original_name': 'Lifetime net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l3', @@ -8605,6 +8761,7 @@ 'original_name': 'Lifetime net energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production', 'unique_id': '1234_lifetime_net_production', @@ -8663,6 +8820,7 @@ 'original_name': 'Lifetime net energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l1', @@ -8721,6 +8879,7 @@ 'original_name': 'Lifetime net energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l2', @@ -8779,6 +8938,7 @@ 'original_name': 'Lifetime net energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l3', @@ -8829,6 +8989,7 @@ 'original_name': 'Meter status flags active net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags', 'unique_id': '1234_net_consumption_ct_status_flags', @@ -8876,6 +9037,7 @@ 'original_name': 'Meter status flags active net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l1', @@ -8923,6 +9085,7 @@ 'original_name': 'Meter status flags active net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l2', @@ -8970,6 +9133,7 @@ 'original_name': 'Meter status flags active net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l3', @@ -9017,6 +9181,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -9064,6 +9229,7 @@ 'original_name': 'Meter status flags active production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l1', @@ -9111,6 +9277,7 @@ 'original_name': 'Meter status flags active production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l2', @@ -9158,6 +9325,7 @@ 'original_name': 'Meter status flags active production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l3', @@ -9211,6 +9379,7 @@ 'original_name': 'Metering status net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status', 'unique_id': '1234_net_consumption_ct_metering_status', @@ -9270,6 +9439,7 @@ 'original_name': 'Metering status net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l1', @@ -9329,6 +9499,7 @@ 'original_name': 'Metering status net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l2', @@ -9388,6 +9559,7 @@ 'original_name': 'Metering status net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l3', @@ -9447,6 +9619,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -9506,6 +9679,7 @@ 'original_name': 'Metering status production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l1', @@ -9565,6 +9739,7 @@ 'original_name': 'Metering status production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l2', @@ -9624,6 +9799,7 @@ 'original_name': 'Metering status production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l3', @@ -9685,6 +9861,7 @@ 'original_name': 'Net consumption CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current', 'unique_id': '1234_net_ct_current', @@ -9743,6 +9920,7 @@ 'original_name': 'Net consumption CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l1', @@ -9801,6 +9979,7 @@ 'original_name': 'Net consumption CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l2', @@ -9859,6 +10038,7 @@ 'original_name': 'Net consumption CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l3', @@ -9914,6 +10094,7 @@ 'original_name': 'Power factor net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor', 'unique_id': '1234_net_ct_powerfactor', @@ -9968,6 +10149,7 @@ 'original_name': 'Power factor net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l1', @@ -10022,6 +10204,7 @@ 'original_name': 'Power factor net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l2', @@ -10076,6 +10259,7 @@ 'original_name': 'Power factor net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l3', @@ -10130,6 +10314,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -10184,6 +10369,7 @@ 'original_name': 'Power factor production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l1', @@ -10238,6 +10424,7 @@ 'original_name': 'Power factor production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l2', @@ -10292,6 +10479,7 @@ 'original_name': 'Power factor production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l3', @@ -10349,6 +10537,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -10407,6 +10596,7 @@ 'original_name': 'Production CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l1', @@ -10465,6 +10655,7 @@ 'original_name': 'Production CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l2', @@ -10523,6 +10714,7 @@ 'original_name': 'Production CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l3', @@ -10573,6 +10765,7 @@ 'original_name': 'Reserve battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_energy', 'unique_id': '1234_reserve_energy', @@ -10622,6 +10815,7 @@ 'original_name': 'Reserve battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_soc', 'unique_id': '1234_reserve_soc', @@ -10679,6 +10873,7 @@ 'original_name': 'Voltage net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage', 'unique_id': '1234_voltage', @@ -10737,6 +10932,7 @@ 'original_name': 'Voltage net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l1', @@ -10795,6 +10991,7 @@ 'original_name': 'Voltage net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l2', @@ -10853,6 +11050,7 @@ 'original_name': 'Voltage net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l3', @@ -10911,6 +11109,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -10969,6 +11168,7 @@ 'original_name': 'Voltage production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l1', @@ -11027,6 +11227,7 @@ 'original_name': 'Voltage production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l2', @@ -11085,6 +11286,7 @@ 'original_name': 'Voltage production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l3', @@ -11137,6 +11339,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -11187,6 +11390,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -11235,6 +11439,7 @@ 'original_name': 'Apparent power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_apparent_power_mva', @@ -11284,6 +11489,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_soc', @@ -11333,6 +11539,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '123456_last_reported', @@ -11381,6 +11588,7 @@ 'original_name': 'Power', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_real_power_mw', @@ -11430,6 +11638,7 @@ 'original_name': 'Temperature', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_temperature', @@ -11479,6 +11688,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '654321_last_reported', @@ -11527,6 +11737,7 @@ 'original_name': 'Temperature', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '654321_temperature', @@ -11576,6 +11787,7 @@ 'original_name': 'Available battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'available_energy', 'unique_id': '1234_available_energy', @@ -11633,6 +11845,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -11691,6 +11904,7 @@ 'original_name': 'Balanced net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l1', @@ -11749,6 +11963,7 @@ 'original_name': 'Balanced net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l2', @@ -11807,6 +12022,7 @@ 'original_name': 'Balanced net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l3', @@ -11857,6 +12073,7 @@ 'original_name': 'Battery', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_battery_level', @@ -11906,6 +12123,7 @@ 'original_name': 'Battery capacity', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_capacity', 'unique_id': '1234_max_capacity', @@ -11963,6 +12181,7 @@ 'original_name': 'Current battery discharge', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_discharge', 'unique_id': '1234_battery_discharge', @@ -12021,6 +12240,7 @@ 'original_name': 'Current battery discharge l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_discharge_phase', 'unique_id': '1234_battery_discharge_l1', @@ -12079,6 +12299,7 @@ 'original_name': 'Current battery discharge l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_discharge_phase', 'unique_id': '1234_battery_discharge_l2', @@ -12137,6 +12358,7 @@ 'original_name': 'Current battery discharge l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_discharge_phase', 'unique_id': '1234_battery_discharge_l3', @@ -12195,6 +12417,7 @@ 'original_name': 'Current net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption', 'unique_id': '1234_net_consumption', @@ -12253,6 +12476,7 @@ 'original_name': 'Current net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l1', @@ -12311,6 +12535,7 @@ 'original_name': 'Current net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l2', @@ -12369,6 +12594,7 @@ 'original_name': 'Current net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l3', @@ -12427,6 +12653,7 @@ 'original_name': 'Current power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption', 'unique_id': '1234_consumption', @@ -12485,6 +12712,7 @@ 'original_name': 'Current power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l1', @@ -12543,6 +12771,7 @@ 'original_name': 'Current power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l2', @@ -12601,6 +12830,7 @@ 'original_name': 'Current power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l3', @@ -12659,6 +12889,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -12717,6 +12948,7 @@ 'original_name': 'Current power production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l1', @@ -12775,6 +13007,7 @@ 'original_name': 'Current power production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l2', @@ -12833,6 +13066,7 @@ 'original_name': 'Current power production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l3', @@ -12889,6 +13123,7 @@ 'original_name': 'Energy consumption last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption', 'unique_id': '1234_seven_days_consumption', @@ -12944,6 +13179,7 @@ 'original_name': 'Energy consumption last seven days l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l1', @@ -12999,6 +13235,7 @@ 'original_name': 'Energy consumption last seven days l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l2', @@ -13054,6 +13291,7 @@ 'original_name': 'Energy consumption last seven days l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l3', @@ -13111,6 +13349,7 @@ 'original_name': 'Energy consumption today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption', 'unique_id': '1234_daily_consumption', @@ -13169,6 +13408,7 @@ 'original_name': 'Energy consumption today l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l1', @@ -13227,6 +13467,7 @@ 'original_name': 'Energy consumption today l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l2', @@ -13285,6 +13526,7 @@ 'original_name': 'Energy consumption today l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l3', @@ -13341,6 +13583,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -13396,6 +13639,7 @@ 'original_name': 'Energy production last seven days l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l1', @@ -13451,6 +13695,7 @@ 'original_name': 'Energy production last seven days l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l2', @@ -13506,6 +13751,7 @@ 'original_name': 'Energy production last seven days l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l3', @@ -13563,6 +13809,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -13621,6 +13868,7 @@ 'original_name': 'Energy production today l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l1', @@ -13679,6 +13927,7 @@ 'original_name': 'Energy production today l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l2', @@ -13737,6 +13986,7 @@ 'original_name': 'Energy production today l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l3', @@ -13792,6 +14042,7 @@ 'original_name': 'Frequency net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency', 'unique_id': '1234_frequency', @@ -13847,6 +14098,7 @@ 'original_name': 'Frequency net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l1', @@ -13902,6 +14154,7 @@ 'original_name': 'Frequency net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l2', @@ -13957,6 +14210,7 @@ 'original_name': 'Frequency net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l3', @@ -14012,6 +14266,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -14067,6 +14322,7 @@ 'original_name': 'Frequency production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l1', @@ -14122,6 +14378,7 @@ 'original_name': 'Frequency production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l2', @@ -14177,6 +14434,7 @@ 'original_name': 'Frequency production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l3', @@ -14232,6 +14490,7 @@ 'original_name': 'Frequency storage CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_frequency', 'unique_id': '1234_storage_ct_frequency', @@ -14287,6 +14546,7 @@ 'original_name': 'Frequency storage CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_frequency_phase', 'unique_id': '1234_storage_ct_frequency_l1', @@ -14342,6 +14602,7 @@ 'original_name': 'Frequency storage CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_frequency_phase', 'unique_id': '1234_storage_ct_frequency_l2', @@ -14397,6 +14658,7 @@ 'original_name': 'Frequency storage CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_frequency_phase', 'unique_id': '1234_storage_ct_frequency_l3', @@ -14455,6 +14717,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -14513,6 +14776,7 @@ 'original_name': 'Lifetime balanced net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l1', @@ -14571,6 +14835,7 @@ 'original_name': 'Lifetime balanced net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l2', @@ -14629,6 +14894,7 @@ 'original_name': 'Lifetime balanced net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l3', @@ -14687,6 +14953,7 @@ 'original_name': 'Lifetime battery energy charged', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_charged', 'unique_id': '1234_lifetime_battery_charged', @@ -14745,6 +15012,7 @@ 'original_name': 'Lifetime battery energy charged l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_charged_phase', 'unique_id': '1234_lifetime_battery_charged_l1', @@ -14803,6 +15071,7 @@ 'original_name': 'Lifetime battery energy charged l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_charged_phase', 'unique_id': '1234_lifetime_battery_charged_l2', @@ -14861,6 +15130,7 @@ 'original_name': 'Lifetime battery energy charged l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_charged_phase', 'unique_id': '1234_lifetime_battery_charged_l3', @@ -14919,6 +15189,7 @@ 'original_name': 'Lifetime battery energy discharged', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_discharged', 'unique_id': '1234_lifetime_battery_discharged', @@ -14977,6 +15248,7 @@ 'original_name': 'Lifetime battery energy discharged l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_discharged_phase', 'unique_id': '1234_lifetime_battery_discharged_l1', @@ -15035,6 +15307,7 @@ 'original_name': 'Lifetime battery energy discharged l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_discharged_phase', 'unique_id': '1234_lifetime_battery_discharged_l2', @@ -15093,6 +15366,7 @@ 'original_name': 'Lifetime battery energy discharged l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_battery_discharged_phase', 'unique_id': '1234_lifetime_battery_discharged_l3', @@ -15151,6 +15425,7 @@ 'original_name': 'Lifetime energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption', 'unique_id': '1234_lifetime_consumption', @@ -15209,6 +15484,7 @@ 'original_name': 'Lifetime energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l1', @@ -15267,6 +15543,7 @@ 'original_name': 'Lifetime energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l2', @@ -15325,6 +15602,7 @@ 'original_name': 'Lifetime energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l3', @@ -15383,6 +15661,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -15441,6 +15720,7 @@ 'original_name': 'Lifetime energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l1', @@ -15499,6 +15779,7 @@ 'original_name': 'Lifetime energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l2', @@ -15557,6 +15838,7 @@ 'original_name': 'Lifetime energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l3', @@ -15615,6 +15897,7 @@ 'original_name': 'Lifetime net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption', 'unique_id': '1234_lifetime_net_consumption', @@ -15673,6 +15956,7 @@ 'original_name': 'Lifetime net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l1', @@ -15731,6 +16015,7 @@ 'original_name': 'Lifetime net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l2', @@ -15789,6 +16074,7 @@ 'original_name': 'Lifetime net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l3', @@ -15847,6 +16133,7 @@ 'original_name': 'Lifetime net energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production', 'unique_id': '1234_lifetime_net_production', @@ -15905,6 +16192,7 @@ 'original_name': 'Lifetime net energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l1', @@ -15963,6 +16251,7 @@ 'original_name': 'Lifetime net energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l2', @@ -16021,6 +16310,7 @@ 'original_name': 'Lifetime net energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l3', @@ -16071,6 +16361,7 @@ 'original_name': 'Meter status flags active net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags', 'unique_id': '1234_net_consumption_ct_status_flags', @@ -16118,6 +16409,7 @@ 'original_name': 'Meter status flags active net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l1', @@ -16165,6 +16457,7 @@ 'original_name': 'Meter status flags active net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l2', @@ -16212,6 +16505,7 @@ 'original_name': 'Meter status flags active net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l3', @@ -16259,6 +16553,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -16306,6 +16601,7 @@ 'original_name': 'Meter status flags active production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l1', @@ -16353,6 +16649,7 @@ 'original_name': 'Meter status flags active production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l2', @@ -16400,6 +16697,7 @@ 'original_name': 'Meter status flags active production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l3', @@ -16447,6 +16745,7 @@ 'original_name': 'Meter status flags active storage CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_status_flags', 'unique_id': '1234_storage_ct_status_flags', @@ -16494,6 +16793,7 @@ 'original_name': 'Meter status flags active storage CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_status_flags_phase', 'unique_id': '1234_storage_ct_status_flags_l1', @@ -16541,6 +16841,7 @@ 'original_name': 'Meter status flags active storage CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_status_flags_phase', 'unique_id': '1234_storage_ct_status_flags_l2', @@ -16588,6 +16889,7 @@ 'original_name': 'Meter status flags active storage CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_status_flags_phase', 'unique_id': '1234_storage_ct_status_flags_l3', @@ -16641,6 +16943,7 @@ 'original_name': 'Metering status net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status', 'unique_id': '1234_net_consumption_ct_metering_status', @@ -16700,6 +17003,7 @@ 'original_name': 'Metering status net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l1', @@ -16759,6 +17063,7 @@ 'original_name': 'Metering status net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l2', @@ -16818,6 +17123,7 @@ 'original_name': 'Metering status net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l3', @@ -16877,6 +17183,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -16936,6 +17243,7 @@ 'original_name': 'Metering status production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l1', @@ -16995,6 +17303,7 @@ 'original_name': 'Metering status production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l2', @@ -17054,6 +17363,7 @@ 'original_name': 'Metering status production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l3', @@ -17113,6 +17423,7 @@ 'original_name': 'Metering status storage CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_metering_status', 'unique_id': '1234_storage_ct_metering_status', @@ -17172,6 +17483,7 @@ 'original_name': 'Metering status storage CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_metering_status_phase', 'unique_id': '1234_storage_ct_metering_status_l1', @@ -17231,6 +17543,7 @@ 'original_name': 'Metering status storage CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_metering_status_phase', 'unique_id': '1234_storage_ct_metering_status_l2', @@ -17290,6 +17603,7 @@ 'original_name': 'Metering status storage CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_metering_status_phase', 'unique_id': '1234_storage_ct_metering_status_l3', @@ -17351,6 +17665,7 @@ 'original_name': 'Net consumption CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current', 'unique_id': '1234_net_ct_current', @@ -17409,6 +17724,7 @@ 'original_name': 'Net consumption CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l1', @@ -17467,6 +17783,7 @@ 'original_name': 'Net consumption CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l2', @@ -17525,6 +17842,7 @@ 'original_name': 'Net consumption CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l3', @@ -17580,6 +17898,7 @@ 'original_name': 'Power factor net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor', 'unique_id': '1234_net_ct_powerfactor', @@ -17634,6 +17953,7 @@ 'original_name': 'Power factor net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l1', @@ -17688,6 +18008,7 @@ 'original_name': 'Power factor net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l2', @@ -17742,6 +18063,7 @@ 'original_name': 'Power factor net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l3', @@ -17796,6 +18118,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -17850,6 +18173,7 @@ 'original_name': 'Power factor production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l1', @@ -17904,6 +18228,7 @@ 'original_name': 'Power factor production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l2', @@ -17958,6 +18283,7 @@ 'original_name': 'Power factor production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l3', @@ -18012,6 +18338,7 @@ 'original_name': 'Power factor storage CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_powerfactor', 'unique_id': '1234_storage_ct_powerfactor', @@ -18066,6 +18393,7 @@ 'original_name': 'Power factor storage CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_powerfactor_phase', 'unique_id': '1234_storage_ct_powerfactor_l1', @@ -18120,6 +18448,7 @@ 'original_name': 'Power factor storage CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_powerfactor_phase', 'unique_id': '1234_storage_ct_powerfactor_l2', @@ -18174,6 +18503,7 @@ 'original_name': 'Power factor storage CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_powerfactor_phase', 'unique_id': '1234_storage_ct_powerfactor_l3', @@ -18231,6 +18561,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -18289,6 +18620,7 @@ 'original_name': 'Production CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l1', @@ -18347,6 +18679,7 @@ 'original_name': 'Production CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l2', @@ -18405,6 +18738,7 @@ 'original_name': 'Production CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l3', @@ -18455,6 +18789,7 @@ 'original_name': 'Reserve battery energy', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_energy', 'unique_id': '1234_reserve_energy', @@ -18504,6 +18839,7 @@ 'original_name': 'Reserve battery level', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reserve_soc', 'unique_id': '1234_reserve_soc', @@ -18561,6 +18897,7 @@ 'original_name': 'Storage CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_current', 'unique_id': '1234_storage_ct_current', @@ -18619,6 +18956,7 @@ 'original_name': 'Storage CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_current_phase', 'unique_id': '1234_storage_ct_current_l1', @@ -18677,6 +19015,7 @@ 'original_name': 'Storage CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_current_phase', 'unique_id': '1234_storage_ct_current_l2', @@ -18735,6 +19074,7 @@ 'original_name': 'Storage CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_current_phase', 'unique_id': '1234_storage_ct_current_l3', @@ -18793,6 +19133,7 @@ 'original_name': 'Voltage net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage', 'unique_id': '1234_voltage', @@ -18851,6 +19192,7 @@ 'original_name': 'Voltage net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l1', @@ -18909,6 +19251,7 @@ 'original_name': 'Voltage net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l2', @@ -18967,6 +19310,7 @@ 'original_name': 'Voltage net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l3', @@ -19025,6 +19369,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -19083,6 +19428,7 @@ 'original_name': 'Voltage production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l1', @@ -19141,6 +19487,7 @@ 'original_name': 'Voltage production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l2', @@ -19199,6 +19546,7 @@ 'original_name': 'Voltage production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l3', @@ -19257,6 +19605,7 @@ 'original_name': 'Voltage storage CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_voltage', 'unique_id': '1234_storage_voltage', @@ -19315,6 +19664,7 @@ 'original_name': 'Voltage storage CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_voltage_phase', 'unique_id': '1234_storage_voltage_l1', @@ -19373,6 +19723,7 @@ 'original_name': 'Voltage storage CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_voltage_phase', 'unique_id': '1234_storage_voltage_l2', @@ -19431,6 +19782,7 @@ 'original_name': 'Voltage storage CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_ct_voltage_phase', 'unique_id': '1234_storage_voltage_l3', @@ -19483,6 +19835,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -19533,6 +19886,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -19589,6 +19943,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -19647,6 +20002,7 @@ 'original_name': 'Balanced net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l1', @@ -19705,6 +20061,7 @@ 'original_name': 'Balanced net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l2', @@ -19763,6 +20120,7 @@ 'original_name': 'Balanced net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption_phase', 'unique_id': '1234_balanced_net_consumption_l3', @@ -19821,6 +20179,7 @@ 'original_name': 'Current net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption', 'unique_id': '1234_net_consumption', @@ -19879,6 +20238,7 @@ 'original_name': 'Current net power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l1', @@ -19937,6 +20297,7 @@ 'original_name': 'Current net power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l2', @@ -19995,6 +20356,7 @@ 'original_name': 'Current net power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_consumption_phase', 'unique_id': '1234_net_consumption_l3', @@ -20053,6 +20415,7 @@ 'original_name': 'Current power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption', 'unique_id': '1234_consumption', @@ -20111,6 +20474,7 @@ 'original_name': 'Current power consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l1', @@ -20169,6 +20533,7 @@ 'original_name': 'Current power consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l2', @@ -20227,6 +20592,7 @@ 'original_name': 'Current power consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_consumption_phase', 'unique_id': '1234_consumption_l3', @@ -20285,6 +20651,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -20343,6 +20710,7 @@ 'original_name': 'Current power production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l1', @@ -20401,6 +20769,7 @@ 'original_name': 'Current power production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l2', @@ -20459,6 +20828,7 @@ 'original_name': 'Current power production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production_phase', 'unique_id': '1234_production_l3', @@ -20515,6 +20885,7 @@ 'original_name': 'Energy consumption last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption', 'unique_id': '1234_seven_days_consumption', @@ -20570,6 +20941,7 @@ 'original_name': 'Energy consumption last seven days l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l1', @@ -20625,6 +20997,7 @@ 'original_name': 'Energy consumption last seven days l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l2', @@ -20680,6 +21053,7 @@ 'original_name': 'Energy consumption last seven days l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_consumption_phase', 'unique_id': '1234_seven_days_consumption_l3', @@ -20737,6 +21111,7 @@ 'original_name': 'Energy consumption today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption', 'unique_id': '1234_daily_consumption', @@ -20795,6 +21170,7 @@ 'original_name': 'Energy consumption today l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l1', @@ -20853,6 +21229,7 @@ 'original_name': 'Energy consumption today l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l2', @@ -20911,6 +21288,7 @@ 'original_name': 'Energy consumption today l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_consumption_phase', 'unique_id': '1234_daily_consumption_l3', @@ -20967,6 +21345,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -21022,6 +21401,7 @@ 'original_name': 'Energy production last seven days l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l1', @@ -21077,6 +21457,7 @@ 'original_name': 'Energy production last seven days l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l2', @@ -21132,6 +21513,7 @@ 'original_name': 'Energy production last seven days l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production_phase', 'unique_id': '1234_seven_days_production_l3', @@ -21189,6 +21571,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -21247,6 +21630,7 @@ 'original_name': 'Energy production today l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l1', @@ -21305,6 +21689,7 @@ 'original_name': 'Energy production today l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l2', @@ -21363,6 +21748,7 @@ 'original_name': 'Energy production today l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production_phase', 'unique_id': '1234_daily_production_l3', @@ -21418,6 +21804,7 @@ 'original_name': 'Frequency net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency', 'unique_id': '1234_frequency', @@ -21473,6 +21860,7 @@ 'original_name': 'Frequency net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l1', @@ -21528,6 +21916,7 @@ 'original_name': 'Frequency net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l2', @@ -21583,6 +21972,7 @@ 'original_name': 'Frequency net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_frequency_phase', 'unique_id': '1234_frequency_l3', @@ -21638,6 +22028,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -21693,6 +22084,7 @@ 'original_name': 'Frequency production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l1', @@ -21748,6 +22140,7 @@ 'original_name': 'Frequency production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l2', @@ -21803,6 +22196,7 @@ 'original_name': 'Frequency production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency_phase', 'unique_id': '1234_production_ct_frequency_l3', @@ -21861,6 +22255,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -21919,6 +22314,7 @@ 'original_name': 'Lifetime balanced net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l1', @@ -21977,6 +22373,7 @@ 'original_name': 'Lifetime balanced net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l2', @@ -22035,6 +22432,7 @@ 'original_name': 'Lifetime balanced net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption_phase', 'unique_id': '1234_lifetime_balanced_net_consumption_l3', @@ -22093,6 +22491,7 @@ 'original_name': 'Lifetime energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption', 'unique_id': '1234_lifetime_consumption', @@ -22151,6 +22550,7 @@ 'original_name': 'Lifetime energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l1', @@ -22209,6 +22609,7 @@ 'original_name': 'Lifetime energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l2', @@ -22267,6 +22668,7 @@ 'original_name': 'Lifetime energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_consumption_phase', 'unique_id': '1234_lifetime_consumption_l3', @@ -22325,6 +22727,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -22383,6 +22786,7 @@ 'original_name': 'Lifetime energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l1', @@ -22441,6 +22845,7 @@ 'original_name': 'Lifetime energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l2', @@ -22499,6 +22904,7 @@ 'original_name': 'Lifetime energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production_phase', 'unique_id': '1234_lifetime_production_l3', @@ -22557,6 +22963,7 @@ 'original_name': 'Lifetime net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption', 'unique_id': '1234_lifetime_net_consumption', @@ -22615,6 +23022,7 @@ 'original_name': 'Lifetime net energy consumption l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l1', @@ -22673,6 +23081,7 @@ 'original_name': 'Lifetime net energy consumption l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l2', @@ -22731,6 +23140,7 @@ 'original_name': 'Lifetime net energy consumption l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_consumption_phase', 'unique_id': '1234_lifetime_net_consumption_l3', @@ -22789,6 +23199,7 @@ 'original_name': 'Lifetime net energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production', 'unique_id': '1234_lifetime_net_production', @@ -22847,6 +23258,7 @@ 'original_name': 'Lifetime net energy production l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l1', @@ -22905,6 +23317,7 @@ 'original_name': 'Lifetime net energy production l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l2', @@ -22963,6 +23376,7 @@ 'original_name': 'Lifetime net energy production l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_net_production_phase', 'unique_id': '1234_lifetime_net_production_l3', @@ -23013,6 +23427,7 @@ 'original_name': 'Meter status flags active net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags', 'unique_id': '1234_net_consumption_ct_status_flags', @@ -23060,6 +23475,7 @@ 'original_name': 'Meter status flags active net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l1', @@ -23107,6 +23523,7 @@ 'original_name': 'Meter status flags active net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l2', @@ -23154,6 +23571,7 @@ 'original_name': 'Meter status flags active net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_status_flags_phase', 'unique_id': '1234_net_consumption_ct_status_flags_l3', @@ -23201,6 +23619,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -23248,6 +23667,7 @@ 'original_name': 'Meter status flags active production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l1', @@ -23295,6 +23715,7 @@ 'original_name': 'Meter status flags active production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l2', @@ -23342,6 +23763,7 @@ 'original_name': 'Meter status flags active production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags_phase', 'unique_id': '1234_production_ct_status_flags_l3', @@ -23395,6 +23817,7 @@ 'original_name': 'Metering status net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status', 'unique_id': '1234_net_consumption_ct_metering_status', @@ -23454,6 +23877,7 @@ 'original_name': 'Metering status net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l1', @@ -23513,6 +23937,7 @@ 'original_name': 'Metering status net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l2', @@ -23572,6 +23997,7 @@ 'original_name': 'Metering status net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_metering_status_phase', 'unique_id': '1234_net_consumption_ct_metering_status_l3', @@ -23631,6 +24057,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -23690,6 +24117,7 @@ 'original_name': 'Metering status production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l1', @@ -23749,6 +24177,7 @@ 'original_name': 'Metering status production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l2', @@ -23808,6 +24237,7 @@ 'original_name': 'Metering status production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status_phase', 'unique_id': '1234_production_ct_metering_status_l3', @@ -23869,6 +24299,7 @@ 'original_name': 'Net consumption CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current', 'unique_id': '1234_net_ct_current', @@ -23927,6 +24358,7 @@ 'original_name': 'Net consumption CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l1', @@ -23985,6 +24417,7 @@ 'original_name': 'Net consumption CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l2', @@ -24043,6 +24476,7 @@ 'original_name': 'Net consumption CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_current_phase', 'unique_id': '1234_net_ct_current_l3', @@ -24098,6 +24532,7 @@ 'original_name': 'Power factor net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor', 'unique_id': '1234_net_ct_powerfactor', @@ -24152,6 +24587,7 @@ 'original_name': 'Power factor net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l1', @@ -24206,6 +24642,7 @@ 'original_name': 'Power factor net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l2', @@ -24260,6 +24697,7 @@ 'original_name': 'Power factor net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_powerfactor_phase', 'unique_id': '1234_net_ct_powerfactor_l3', @@ -24314,6 +24752,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -24368,6 +24807,7 @@ 'original_name': 'Power factor production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l1', @@ -24422,6 +24862,7 @@ 'original_name': 'Power factor production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l2', @@ -24476,6 +24917,7 @@ 'original_name': 'Power factor production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor_phase', 'unique_id': '1234_production_ct_powerfactor_l3', @@ -24533,6 +24975,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -24591,6 +25034,7 @@ 'original_name': 'Production CT current l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l1', @@ -24649,6 +25093,7 @@ 'original_name': 'Production CT current l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l2', @@ -24707,6 +25152,7 @@ 'original_name': 'Production CT current l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current_phase', 'unique_id': '1234_production_ct_current_l3', @@ -24765,6 +25211,7 @@ 'original_name': 'Voltage net consumption CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage', 'unique_id': '1234_voltage', @@ -24823,6 +25270,7 @@ 'original_name': 'Voltage net consumption CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l1', @@ -24881,6 +25329,7 @@ 'original_name': 'Voltage net consumption CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l2', @@ -24939,6 +25388,7 @@ 'original_name': 'Voltage net consumption CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_ct_voltage_phase', 'unique_id': '1234_voltage_l3', @@ -24997,6 +25447,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -25055,6 +25506,7 @@ 'original_name': 'Voltage production CT l1', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l1', @@ -25113,6 +25565,7 @@ 'original_name': 'Voltage production CT l2', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l2', @@ -25171,6 +25624,7 @@ 'original_name': 'Voltage production CT l3', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage_phase', 'unique_id': '1234_production_ct_voltage_l3', @@ -25223,6 +25677,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -25273,6 +25728,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', @@ -25329,6 +25785,7 @@ 'original_name': 'Balanced net power consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balanced_net_consumption', 'unique_id': '1234_balanced_net_consumption', @@ -25387,6 +25844,7 @@ 'original_name': 'Current power production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power_production', 'unique_id': '1234_production', @@ -25443,6 +25901,7 @@ 'original_name': 'Energy production last seven days', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seven_days_production', 'unique_id': '1234_seven_days_production', @@ -25500,6 +25959,7 @@ 'original_name': 'Energy production today', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_production', 'unique_id': '1234_daily_production', @@ -25555,6 +26015,7 @@ 'original_name': 'Frequency production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_frequency', 'unique_id': '1234_production_ct_frequency', @@ -25613,6 +26074,7 @@ 'original_name': 'Lifetime balanced net energy consumption', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_balanced_net_consumption', 'unique_id': '1234_lifetime_balanced_net_consumption', @@ -25671,6 +26133,7 @@ 'original_name': 'Lifetime energy production', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_production', 'unique_id': '1234_lifetime_production', @@ -25721,6 +26184,7 @@ 'original_name': 'Meter status flags active production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_status_flags', 'unique_id': '1234_production_ct_status_flags', @@ -25774,6 +26238,7 @@ 'original_name': 'Metering status production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_metering_status', 'unique_id': '1234_production_ct_metering_status', @@ -25832,6 +26297,7 @@ 'original_name': 'Power factor production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_powerfactor', 'unique_id': '1234_production_ct_powerfactor', @@ -25889,6 +26355,7 @@ 'original_name': 'Production CT current', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_current', 'unique_id': '1234_production_ct_current', @@ -25947,6 +26414,7 @@ 'original_name': 'Voltage production CT', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'production_ct_voltage', 'unique_id': '1234_production_ct_voltage', @@ -25999,6 +26467,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1', @@ -26049,6 +26518,7 @@ 'original_name': 'Last reported', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_reported', 'unique_id': '1_last_reported', diff --git a/tests/components/enphase_envoy/snapshots/test_switch.ambr b/tests/components/enphase_envoy/snapshots/test_switch.ambr index 77b682cb948..2a00e46b6af 100644 --- a/tests/components/enphase_envoy/snapshots/test_switch.ambr +++ b/tests/components/enphase_envoy/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge from grid', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_from_grid', 'unique_id': '1234_charge_from_grid', @@ -74,6 +75,7 @@ 'original_name': 'Charge from grid', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_from_grid', 'unique_id': '654321_charge_from_grid', @@ -121,6 +123,7 @@ 'original_name': 'Grid enabled', 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_enabled', 'unique_id': '654321_mains_admin_state', @@ -168,6 +171,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_status', 'unique_id': '654321_relay_NC1_relay_status', @@ -215,6 +219,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_status', 'unique_id': '654321_relay_NC2_relay_status', @@ -262,6 +267,7 @@ 'original_name': None, 'platform': 'enphase_envoy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relay_status', 'unique_id': '654321_relay_NC3_relay_status', diff --git a/tests/components/filesize/snapshots/test_sensor.ambr b/tests/components/filesize/snapshots/test_sensor.ambr index e7f6f9d042b..d78be02f5a7 100644 --- a/tests/components/filesize/snapshots/test_sensor.ambr +++ b/tests/components/filesize/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Created', 'platform': 'filesize', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'created', 'unique_id': '01JD5CTQMH9FKEFQKZJ8MMBQ3X-created', @@ -75,6 +76,7 @@ 'original_name': 'Last updated', 'platform': 'filesize', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_updated', 'unique_id': '01JD5CTQMH9FKEFQKZJ8MMBQ3X-last_updated', @@ -125,6 +127,7 @@ 'original_name': 'Size', 'platform': 'filesize', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'size', 'unique_id': '01JD5CTQMH9FKEFQKZJ8MMBQ3X', @@ -177,6 +180,7 @@ 'original_name': 'Size in bytes', 'platform': 'filesize', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'size_bytes', 'unique_id': '01JD5CTQMH9FKEFQKZJ8MMBQ3X-bytes', diff --git a/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr index 0b45e1f19be..d8408a63aa6 100644 --- a/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Air filter polluted', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_filter_polluted', 'unique_id': '0000-0001-air_filter_polluted', diff --git a/tests/components/flexit_bacnet/snapshots/test_climate.ambr b/tests/components/flexit_bacnet/snapshots/test_climate.ambr index d15fc291a16..a58927be917 100644 --- a/tests/components/flexit_bacnet/snapshots/test_climate.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': None, 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '0000-0001', diff --git a/tests/components/flexit_bacnet/snapshots/test_number.ambr b/tests/components/flexit_bacnet/snapshots/test_number.ambr index 622ec81e45d..6a307a9b463 100644 --- a/tests/components/flexit_bacnet/snapshots/test_number.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Away extract fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'away_extract_fan_setpoint', 'unique_id': '0000-0001-away_extract_fan_setpoint', @@ -90,6 +91,7 @@ 'original_name': 'Away supply fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'away_supply_fan_setpoint', 'unique_id': '0000-0001-away_supply_fan_setpoint', @@ -148,6 +150,7 @@ 'original_name': 'Cooker hood extract fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooker_hood_extract_fan_setpoint', 'unique_id': '0000-0001-cooker_hood_extract_fan_setpoint', @@ -206,6 +209,7 @@ 'original_name': 'Cooker hood supply fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooker_hood_supply_fan_setpoint', 'unique_id': '0000-0001-cooker_hood_supply_fan_setpoint', @@ -264,6 +268,7 @@ 'original_name': 'Fireplace extract fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fireplace_extract_fan_setpoint', 'unique_id': '0000-0001-fireplace_extract_fan_setpoint', @@ -322,6 +327,7 @@ 'original_name': 'Fireplace mode runtime', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fireplace_mode_runtime', 'unique_id': '0000-0001-fireplace_mode_runtime', @@ -380,6 +386,7 @@ 'original_name': 'Fireplace supply fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fireplace_supply_fan_setpoint', 'unique_id': '0000-0001-fireplace_supply_fan_setpoint', @@ -438,6 +445,7 @@ 'original_name': 'High extract fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'high_extract_fan_setpoint', 'unique_id': '0000-0001-high_extract_fan_setpoint', @@ -496,6 +504,7 @@ 'original_name': 'High supply fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'high_supply_fan_setpoint', 'unique_id': '0000-0001-high_supply_fan_setpoint', @@ -554,6 +563,7 @@ 'original_name': 'Home extract fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'home_extract_fan_setpoint', 'unique_id': '0000-0001-home_extract_fan_setpoint', @@ -612,6 +622,7 @@ 'original_name': 'Home supply fan setpoint', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'home_supply_fan_setpoint', 'unique_id': '0000-0001-home_supply_fan_setpoint', diff --git a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr index b265a4402dc..3567a976a6c 100644 --- a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Air filter operating time', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_filter_operating_time', 'unique_id': '0000-0001-air_filter_operating_time', @@ -84,6 +85,7 @@ 'original_name': 'Electric heater power', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electric_heater_power', 'unique_id': '0000-0001-electric_heater_power', @@ -135,6 +137,7 @@ 'original_name': 'Exhaust air fan', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exhaust_air_fan_rpm', 'unique_id': '0000-0001-exhaust_air_fan_rpm', @@ -186,6 +189,7 @@ 'original_name': 'Exhaust air fan control signal', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exhaust_air_fan_control_signal', 'unique_id': '0000-0001-exhaust_air_fan_control_signal', @@ -235,6 +239,7 @@ 'original_name': 'Exhaust air temperature', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exhaust_air_temperature', 'unique_id': '0000-0001-exhaust_air_temperature', @@ -284,6 +289,7 @@ 'original_name': 'Extract air temperature', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'extract_air_temperature', 'unique_id': '0000-0001-extract_air_temperature', @@ -338,6 +344,7 @@ 'original_name': 'Fireplace ventilation remaining duration', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fireplace_ventilation_remaining_duration', 'unique_id': '0000-0001-fireplace_ventilation_remaining_duration', @@ -390,6 +397,7 @@ 'original_name': 'Heat exchanger efficiency', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_exchanger_efficiency', 'unique_id': '0000-0001-heat_exchanger_efficiency', @@ -441,6 +449,7 @@ 'original_name': 'Heat exchanger speed', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_exchanger_speed', 'unique_id': '0000-0001-heat_exchanger_speed', @@ -490,6 +499,7 @@ 'original_name': 'Outside air temperature', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_air_temperature', 'unique_id': '0000-0001-outside_air_temperature', @@ -544,6 +554,7 @@ 'original_name': 'Rapid ventilation remaining duration', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rapid_ventilation_remaining_duration', 'unique_id': '0000-0001-rapid_ventilation_remaining_duration', @@ -594,6 +605,7 @@ 'original_name': 'Room temperature', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'room_temperature', 'unique_id': '0000-0001-room_temperature', @@ -645,6 +657,7 @@ 'original_name': 'Supply air fan', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_air_fan_rpm', 'unique_id': '0000-0001-supply_air_fan_rpm', @@ -696,6 +709,7 @@ 'original_name': 'Supply air fan control signal', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_air_fan_control_signal', 'unique_id': '0000-0001-supply_air_fan_control_signal', @@ -745,6 +759,7 @@ 'original_name': 'Supply air temperature', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_air_temperature', 'unique_id': '0000-0001-supply_air_temperature', diff --git a/tests/components/flexit_bacnet/snapshots/test_switch.ambr b/tests/components/flexit_bacnet/snapshots/test_switch.ambr index 0e27c2e938a..6ac6f904758 100644 --- a/tests/components/flexit_bacnet/snapshots/test_switch.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Cooker hood mode', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooker_hood_mode', 'unique_id': '0000-0001-cooker_hood_mode', @@ -75,6 +76,7 @@ 'original_name': 'Electric heater', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electric_heater', 'unique_id': '0000-0001-electric_heater', @@ -123,6 +125,7 @@ 'original_name': 'Fireplace mode', 'platform': 'flexit_bacnet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fireplace_mode', 'unique_id': '0000-0001-fireplace_mode', diff --git a/tests/components/folder_watcher/conftest.py b/tests/components/folder_watcher/conftest.py index ed0adea7a7d..1c7744fa8f5 100644 --- a/tests/components/folder_watcher/conftest.py +++ b/tests/components/folder_watcher/conftest.py @@ -36,7 +36,7 @@ async def load_int( config_entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_USER, - title=f"Folder Watcher {path!s}", + title=f"Folder Watcher {tmp_path.parts[-1]!s}", data={}, options={"folder": str(path), "patterns": ["*"]}, entry_id="1", diff --git a/tests/components/folder_watcher/snapshots/test_event.ambr b/tests/components/folder_watcher/snapshots/test_event.ambr index 1101380703a..1514a9121c6 100644 --- a/tests/components/folder_watcher/snapshots/test_event.ambr +++ b/tests/components/folder_watcher/snapshots/test_event.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'folder_watcher', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'folder_watcher', 'unique_id': '1', diff --git a/tests/components/fritz/snapshots/test_button.ambr b/tests/components/fritz/snapshots/test_button.ambr index 748d8c1ba29..ac222fa72d3 100644 --- a/tests/components/fritz/snapshots/test_button.ambr +++ b/tests/components/fritz/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Cleanup', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cleanup', 'unique_id': '1C:ED:6F:12:34:11-cleanup', @@ -74,6 +75,7 @@ 'original_name': 'Firmware update', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'firmware_update', 'unique_id': '1C:ED:6F:12:34:11-firmware_update', @@ -122,6 +124,7 @@ 'original_name': 'Reconnect', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reconnect', 'unique_id': '1C:ED:6F:12:34:11-reconnect', @@ -170,6 +173,7 @@ 'original_name': 'Restart', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-reboot', @@ -218,6 +222,7 @@ 'original_name': 'printer Wake on LAN', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:BB:CC:00:11:22_wake_on_lan', diff --git a/tests/components/fritz/snapshots/test_sensor.ambr b/tests/components/fritz/snapshots/test_sensor.ambr index ffdd3d23f50..d2bf4884db3 100644 --- a/tests/components/fritz/snapshots/test_sensor.ambr +++ b/tests/components/fritz/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Connection uptime', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_uptime', 'unique_id': '1C:ED:6F:12:34:11-connection_uptime', @@ -77,6 +78,7 @@ 'original_name': 'Download throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'kb_s_received', 'unique_id': '1C:ED:6F:12:34:11-kb_s_received', @@ -127,6 +129,7 @@ 'original_name': 'External IP', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'external_ip', 'unique_id': '1C:ED:6F:12:34:11-external_ip', @@ -174,6 +177,7 @@ 'original_name': 'External IPv6', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'external_ipv6', 'unique_id': '1C:ED:6F:12:34:11-external_ipv6', @@ -223,6 +227,7 @@ 'original_name': 'GB received', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gb_received', 'unique_id': '1C:ED:6F:12:34:11-gb_received', @@ -275,6 +280,7 @@ 'original_name': 'GB sent', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gb_sent', 'unique_id': '1C:ED:6F:12:34:11-gb_sent', @@ -325,6 +331,7 @@ 'original_name': 'Last restart', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_uptime', 'unique_id': '1C:ED:6F:12:34:11-device_uptime', @@ -373,6 +380,7 @@ 'original_name': 'Link download noise margin', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_noise_margin_received', 'unique_id': '1C:ED:6F:12:34:11-link_noise_margin_received', @@ -421,6 +429,7 @@ 'original_name': 'Link download power attenuation', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_attenuation_received', 'unique_id': '1C:ED:6F:12:34:11-link_attenuation_received', @@ -469,6 +478,7 @@ 'original_name': 'Link download throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_kb_s_received', 'unique_id': '1C:ED:6F:12:34:11-link_kb_s_received', @@ -518,6 +528,7 @@ 'original_name': 'Link upload noise margin', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_noise_margin_sent', 'unique_id': '1C:ED:6F:12:34:11-link_noise_margin_sent', @@ -566,6 +577,7 @@ 'original_name': 'Link upload power attenuation', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_attenuation_sent', 'unique_id': '1C:ED:6F:12:34:11-link_attenuation_sent', @@ -614,6 +626,7 @@ 'original_name': 'Link upload throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_kb_s_sent', 'unique_id': '1C:ED:6F:12:34:11-link_kb_s_sent', @@ -663,6 +676,7 @@ 'original_name': 'Max connection download throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_kb_s_received', 'unique_id': '1C:ED:6F:12:34:11-max_kb_s_received', @@ -712,6 +726,7 @@ 'original_name': 'Max connection upload throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_kb_s_sent', 'unique_id': '1C:ED:6F:12:34:11-max_kb_s_sent', @@ -763,6 +778,7 @@ 'original_name': 'Upload throughput', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'kb_s_sent', 'unique_id': '1C:ED:6F:12:34:11-kb_s_sent', diff --git a/tests/components/fritz/snapshots/test_switch.ambr b/tests/components/fritz/snapshots/test_switch.ambr index a1097d3333b..08046c988d6 100644 --- a/tests/components/fritz/snapshots/test_switch.ambr +++ b/tests/components/fritz/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_2_4ghz', @@ -75,6 +76,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi (5Ghz)', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_5ghz', @@ -123,6 +125,7 @@ 'original_name': 'printer Internet Access', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:BB:CC:00:11:22_internet_access', @@ -171,6 +174,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi', @@ -219,6 +223,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi2', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi2', @@ -267,6 +272,7 @@ 'original_name': 'printer Internet Access', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:BB:CC:00:11:22_internet_access', @@ -315,6 +321,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi (2.4Ghz)', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_2_4ghz', @@ -363,6 +370,7 @@ 'original_name': 'Mock Title Wi-Fi WiFi+ (5Ghz)', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_wifi_5ghz', @@ -411,6 +419,7 @@ 'original_name': 'printer Internet Access', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:BB:CC:00:11:22_internet_access', @@ -459,6 +468,7 @@ 'original_name': 'Call deflection 0', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-call_deflection_0', @@ -513,6 +523,7 @@ 'original_name': 'Mock Title Wi-Fi MyWifi', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-wi_fi_mywifi', @@ -561,6 +572,7 @@ 'original_name': 'printer Internet Access', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:BB:CC:00:11:22_internet_access', diff --git a/tests/components/fritz/snapshots/test_update.ambr b/tests/components/fritz/snapshots/test_update.ambr index 746823e9dc9..ee683cc492f 100644 --- a/tests/components/fritz/snapshots/test_update.ambr +++ b/tests/components/fritz/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'FRITZ!OS', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-update', @@ -86,6 +87,7 @@ 'original_name': 'FRITZ!OS', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-update', @@ -145,6 +147,7 @@ 'original_name': 'FRITZ!OS', 'platform': 'fritz', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1C:ED:6F:12:34:11-update', diff --git a/tests/components/fritzbox/snapshots/test_binary_sensor.ambr b/tests/components/fritzbox/snapshots/test_binary_sensor.ambr index 1d645947ceb..01d483fca2d 100644 --- a/tests/components/fritzbox/snapshots/test_binary_sensor.ambr +++ b/tests/components/fritzbox/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm', 'unique_id': '12345 1234567_alarm', @@ -75,6 +76,7 @@ 'original_name': 'Battery', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_battery_low', @@ -123,6 +125,7 @@ 'original_name': 'Button lock on device', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': '12345 1234567_lock', @@ -171,6 +174,7 @@ 'original_name': 'Button lock via UI', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_lock', 'unique_id': '12345 1234567_device_lock', @@ -219,6 +223,7 @@ 'original_name': 'Holiday mode', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'holiday_active', 'unique_id': '12345 1234567_holiday_active', @@ -266,6 +271,7 @@ 'original_name': 'Open window detected', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'window_open', 'unique_id': '12345 1234567_window_open', @@ -313,6 +319,7 @@ 'original_name': 'Summer mode', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'summer_active', 'unique_id': '12345 1234567_summer_active', diff --git a/tests/components/fritzbox/snapshots/test_button.ambr b/tests/components/fritzbox/snapshots/test_button.ambr index 95e757da3cc..fc5285cddc6 100644 --- a/tests/components/fritzbox/snapshots/test_button.ambr +++ b/tests/components/fritzbox/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', diff --git a/tests/components/fritzbox/snapshots/test_climate.ambr b/tests/components/fritzbox/snapshots/test_climate.ambr index 26e06105152..423472c078e 100644 --- a/tests/components/fritzbox/snapshots/test_climate.ambr +++ b/tests/components/fritzbox/snapshots/test_climate.ambr @@ -39,6 +39,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '12345 1234567', diff --git a/tests/components/fritzbox/snapshots/test_cover.ambr b/tests/components/fritzbox/snapshots/test_cover.ambr index ce6b305e154..6138086e140 100644 --- a/tests/components/fritzbox/snapshots/test_cover.ambr +++ b/tests/components/fritzbox/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12345 1234567', diff --git a/tests/components/fritzbox/snapshots/test_light.ambr b/tests/components/fritzbox/snapshots/test_light.ambr index f6f4516bdec..bb92b3133c6 100644 --- a/tests/components/fritzbox/snapshots/test_light.ambr +++ b/tests/components/fritzbox/snapshots/test_light.ambr @@ -36,6 +36,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', @@ -118,6 +119,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', @@ -195,6 +197,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', @@ -252,6 +255,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', diff --git a/tests/components/fritzbox/snapshots/test_sensor.ambr b/tests/components/fritzbox/snapshots/test_sensor.ambr index 68f8e161d07..a3522202661 100644 --- a/tests/components/fritzbox/snapshots/test_sensor.ambr +++ b/tests/components/fritzbox/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_battery', @@ -81,6 +82,7 @@ 'original_name': 'Battery', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_battery', @@ -131,6 +133,7 @@ 'original_name': 'Comfort temperature', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'comfort_temperature', 'unique_id': '12345 1234567_comfort_temperature', @@ -180,6 +183,7 @@ 'original_name': 'Current scheduled preset', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'scheduled_preset', 'unique_id': '12345 1234567_scheduled_preset', @@ -227,6 +231,7 @@ 'original_name': 'Eco temperature', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'eco_temperature', 'unique_id': '12345 1234567_eco_temperature', @@ -276,6 +281,7 @@ 'original_name': 'Next scheduled change time', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextchange_time', 'unique_id': '12345 1234567_nextchange_time', @@ -324,6 +330,7 @@ 'original_name': 'Next scheduled preset', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextchange_preset', 'unique_id': '12345 1234567_nextchange_preset', @@ -371,6 +378,7 @@ 'original_name': 'Next scheduled temperature', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextchange_temperature', 'unique_id': '12345 1234567_nextchange_temperature', @@ -422,6 +430,7 @@ 'original_name': 'Battery', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_battery', @@ -474,6 +483,7 @@ 'original_name': 'Humidity', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_humidity', @@ -526,6 +536,7 @@ 'original_name': 'Temperature', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_temperature', @@ -578,6 +589,7 @@ 'original_name': 'Current', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_electric_current', @@ -630,6 +642,7 @@ 'original_name': 'Energy', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_total_energy', @@ -682,6 +695,7 @@ 'original_name': 'Power', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_power_consumption', @@ -734,6 +748,7 @@ 'original_name': 'Temperature', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_temperature', @@ -786,6 +801,7 @@ 'original_name': 'Voltage', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567_voltage', diff --git a/tests/components/fritzbox/snapshots/test_switch.ambr b/tests/components/fritzbox/snapshots/test_switch.ambr index 23deb8183fc..b58c37a7619 100644 --- a/tests/components/fritzbox/snapshots/test_switch.ambr +++ b/tests/components/fritzbox/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'fake_name', 'platform': 'fritzbox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345 1234567', diff --git a/tests/components/fronius/snapshots/test_sensor.ambr b/tests/components/fronius/snapshots/test_sensor.ambr index 1c718910428..d26ee76d909 100644 --- a/tests/components/fronius/snapshots/test_sensor.ambr +++ b/tests/components/fronius/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'AC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac', 'unique_id': '12345678-current_ac', @@ -81,6 +82,7 @@ 'original_name': 'AC power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_ac', 'unique_id': '12345678-power_ac', @@ -133,6 +135,7 @@ 'original_name': 'AC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', 'unique_id': '12345678-voltage_ac', @@ -185,6 +188,7 @@ 'original_name': 'DC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc', 'unique_id': '12345678-current_dc', @@ -237,6 +241,7 @@ 'original_name': 'DC current 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc_mppt_no', 'unique_id': '12345678-current_dc_2', @@ -289,6 +294,7 @@ 'original_name': 'DC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': '12345678-voltage_dc', @@ -341,6 +347,7 @@ 'original_name': 'DC voltage 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc_mppt_no', 'unique_id': '12345678-voltage_dc_2', @@ -391,6 +398,7 @@ 'original_name': 'Error code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_code', 'unique_id': '12345678-error_code', @@ -537,6 +545,7 @@ 'original_name': 'Error message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_message', 'unique_id': '12345678-error_message', @@ -685,6 +694,7 @@ 'original_name': 'Frequency', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_ac', 'unique_id': '12345678-frequency_ac', @@ -735,6 +745,7 @@ 'original_name': 'Inverter state', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'inverter_state', 'unique_id': '12345678-inverter_state', @@ -782,6 +793,7 @@ 'original_name': 'Status code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_code', 'unique_id': '12345678-status_code', @@ -840,6 +852,7 @@ 'original_name': 'Status message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_message', 'unique_id': '12345678-status_message', @@ -900,6 +913,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': '12345678-energy_total', @@ -952,6 +966,7 @@ 'original_name': 'Apparent power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent', 'unique_id': '1234567890-power_apparent', @@ -1004,6 +1019,7 @@ 'original_name': 'Apparent power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_1', 'unique_id': '1234567890-power_apparent_phase_1', @@ -1056,6 +1072,7 @@ 'original_name': 'Apparent power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_2', 'unique_id': '1234567890-power_apparent_phase_2', @@ -1108,6 +1125,7 @@ 'original_name': 'Apparent power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_3', 'unique_id': '1234567890-power_apparent_phase_3', @@ -1160,6 +1178,7 @@ 'original_name': 'Current phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_1', 'unique_id': '1234567890-current_ac_phase_1', @@ -1212,6 +1231,7 @@ 'original_name': 'Current phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_2', 'unique_id': '1234567890-current_ac_phase_2', @@ -1264,6 +1284,7 @@ 'original_name': 'Current phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_3', 'unique_id': '1234567890-current_ac_phase_3', @@ -1316,6 +1337,7 @@ 'original_name': 'Frequency phase average', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_phase_average', 'unique_id': '1234567890-frequency_phase_average', @@ -1366,6 +1388,7 @@ 'original_name': 'Meter location', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location', 'unique_id': '1234567890-meter_location', @@ -1421,6 +1444,7 @@ 'original_name': 'Meter location description', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location_description', 'unique_id': '1234567890-meter_location_description', @@ -1478,6 +1502,7 @@ 'original_name': 'Power factor', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor', 'unique_id': '1234567890-power_factor', @@ -1529,6 +1554,7 @@ 'original_name': 'Power factor phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_1', 'unique_id': '1234567890-power_factor_phase_1', @@ -1580,6 +1606,7 @@ 'original_name': 'Power factor phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_2', 'unique_id': '1234567890-power_factor_phase_2', @@ -1631,6 +1658,7 @@ 'original_name': 'Power factor phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_3', 'unique_id': '1234567890-power_factor_phase_3', @@ -1682,6 +1710,7 @@ 'original_name': 'Reactive energy consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_reactive_ac_consumed', 'unique_id': '1234567890-energy_reactive_ac_consumed', @@ -1733,6 +1762,7 @@ 'original_name': 'Reactive energy produced', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_reactive_ac_produced', 'unique_id': '1234567890-energy_reactive_ac_produced', @@ -1784,6 +1814,7 @@ 'original_name': 'Reactive power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive', 'unique_id': '1234567890-power_reactive', @@ -1836,6 +1867,7 @@ 'original_name': 'Reactive power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_1', 'unique_id': '1234567890-power_reactive_phase_1', @@ -1888,6 +1920,7 @@ 'original_name': 'Reactive power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_2', 'unique_id': '1234567890-power_reactive_phase_2', @@ -1940,6 +1973,7 @@ 'original_name': 'Reactive power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_3', 'unique_id': '1234567890-power_reactive_phase_3', @@ -1992,6 +2026,7 @@ 'original_name': 'Real energy consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_consumed', 'unique_id': '1234567890-energy_real_consumed', @@ -2044,6 +2079,7 @@ 'original_name': 'Real energy minus', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_ac_minus', 'unique_id': '1234567890-energy_real_ac_minus', @@ -2096,6 +2132,7 @@ 'original_name': 'Real energy plus', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_ac_plus', 'unique_id': '1234567890-energy_real_ac_plus', @@ -2148,6 +2185,7 @@ 'original_name': 'Real energy produced', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_produced', 'unique_id': '1234567890-energy_real_produced', @@ -2200,6 +2238,7 @@ 'original_name': 'Real power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real', 'unique_id': '1234567890-power_real', @@ -2252,6 +2291,7 @@ 'original_name': 'Real power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_1', 'unique_id': '1234567890-power_real_phase_1', @@ -2304,6 +2344,7 @@ 'original_name': 'Real power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_2', 'unique_id': '1234567890-power_real_phase_2', @@ -2356,6 +2397,7 @@ 'original_name': 'Real power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_3', 'unique_id': '1234567890-power_real_phase_3', @@ -2408,6 +2450,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_1', 'unique_id': '1234567890-voltage_ac_phase_1', @@ -2460,6 +2503,7 @@ 'original_name': 'Voltage phase 1-2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_12', 'unique_id': '1234567890-voltage_ac_phase_to_phase_12', @@ -2512,6 +2556,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_2', 'unique_id': '1234567890-voltage_ac_phase_2', @@ -2564,6 +2609,7 @@ 'original_name': 'Voltage phase 2-3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_23', 'unique_id': '1234567890-voltage_ac_phase_to_phase_23', @@ -2616,6 +2662,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_3', 'unique_id': '1234567890-voltage_ac_phase_3', @@ -2668,6 +2715,7 @@ 'original_name': 'Voltage phase 3-1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_31', 'unique_id': '1234567890-voltage_ac_phase_to_phase_31', @@ -2718,6 +2766,7 @@ 'original_name': 'Meter mode', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_mode', 'unique_id': 'solar_net_123.4567890-power_flow-meter_mode', @@ -2767,6 +2816,7 @@ 'original_name': 'Power grid', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid', @@ -2819,6 +2869,7 @@ 'original_name': 'Power grid export', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_export', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid_export', @@ -2871,6 +2922,7 @@ 'original_name': 'Power grid import', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_import', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid_import', @@ -2923,6 +2975,7 @@ 'original_name': 'Power load', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load', 'unique_id': 'solar_net_123.4567890-power_flow-power_load', @@ -2975,6 +3028,7 @@ 'original_name': 'Power load consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_consumed', 'unique_id': 'solar_net_123.4567890-power_flow-power_load_consumed', @@ -3027,6 +3081,7 @@ 'original_name': 'Power load generated', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_generated', 'unique_id': 'solar_net_123.4567890-power_flow-power_load_generated', @@ -3079,6 +3134,7 @@ 'original_name': 'Power photovoltaics', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_photovoltaics', 'unique_id': 'solar_net_123.4567890-power_flow-power_photovoltaics', @@ -3131,6 +3187,7 @@ 'original_name': 'Relative autonomy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_autonomy', 'unique_id': 'solar_net_123.4567890-power_flow-relative_autonomy', @@ -3182,6 +3239,7 @@ 'original_name': 'Relative self-consumption', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_self_consumption', 'unique_id': 'solar_net_123.4567890-power_flow-relative_self_consumption', @@ -3233,6 +3291,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': 'solar_net_123.4567890-power_flow-energy_total', @@ -3285,6 +3344,7 @@ 'original_name': 'DC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc', 'unique_id': 'P030T020Z2001234567 -current_dc', @@ -3337,6 +3397,7 @@ 'original_name': 'DC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': 'P030T020Z2001234567 -voltage_dc', @@ -3387,6 +3448,7 @@ 'original_name': 'Designed capacity', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'capacity_designed', 'unique_id': 'P030T020Z2001234567 -capacity_designed', @@ -3435,6 +3497,7 @@ 'original_name': 'Maximum capacity', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'capacity_maximum', 'unique_id': 'P030T020Z2001234567 -capacity_maximum', @@ -3485,6 +3548,7 @@ 'original_name': 'State of charge', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_of_charge', 'unique_id': 'P030T020Z2001234567 -state_of_charge', @@ -3537,6 +3601,7 @@ 'original_name': 'Temperature', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_cell', 'unique_id': 'P030T020Z2001234567 -temperature_cell', @@ -3589,6 +3654,7 @@ 'original_name': 'AC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac', 'unique_id': '12345678-current_ac', @@ -3641,6 +3707,7 @@ 'original_name': 'AC power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_ac', 'unique_id': '12345678-power_ac', @@ -3693,6 +3760,7 @@ 'original_name': 'AC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', 'unique_id': '12345678-voltage_ac', @@ -3745,6 +3813,7 @@ 'original_name': 'DC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc', 'unique_id': '12345678-current_dc', @@ -3797,6 +3866,7 @@ 'original_name': 'DC current 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc_mppt_no', 'unique_id': '12345678-current_dc_2', @@ -3849,6 +3919,7 @@ 'original_name': 'DC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': '12345678-voltage_dc', @@ -3901,6 +3972,7 @@ 'original_name': 'DC voltage 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc_mppt_no', 'unique_id': '12345678-voltage_dc_2', @@ -3951,6 +4023,7 @@ 'original_name': 'Error code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_code', 'unique_id': '12345678-error_code', @@ -4097,6 +4170,7 @@ 'original_name': 'Error message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_message', 'unique_id': '12345678-error_message', @@ -4245,6 +4319,7 @@ 'original_name': 'Frequency', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_ac', 'unique_id': '12345678-frequency_ac', @@ -4295,6 +4370,7 @@ 'original_name': 'Inverter state', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'inverter_state', 'unique_id': '12345678-inverter_state', @@ -4342,6 +4418,7 @@ 'original_name': 'Status code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_code', 'unique_id': '12345678-status_code', @@ -4400,6 +4477,7 @@ 'original_name': 'Status message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_message', 'unique_id': '12345678-status_message', @@ -4460,6 +4538,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': '12345678-energy_total', @@ -4512,6 +4591,7 @@ 'original_name': 'Energy consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_ac_consumed', 'unique_id': '23456789-energy_real_ac_consumed', @@ -4564,6 +4644,7 @@ 'original_name': 'Power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_ac', 'unique_id': '23456789-power_real_ac', @@ -4614,6 +4695,7 @@ 'original_name': 'State code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_code', 'unique_id': '23456789-state_code', @@ -4670,6 +4752,7 @@ 'original_name': 'State message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_message', 'unique_id': '23456789-state_message', @@ -4728,6 +4811,7 @@ 'original_name': 'Temperature', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_channel_1', 'unique_id': '23456789-temperature_channel_1', @@ -4780,6 +4864,7 @@ 'original_name': 'Apparent power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent', 'unique_id': '1234567890-power_apparent', @@ -4832,6 +4917,7 @@ 'original_name': 'Apparent power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_1', 'unique_id': '1234567890-power_apparent_phase_1', @@ -4884,6 +4970,7 @@ 'original_name': 'Apparent power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_2', 'unique_id': '1234567890-power_apparent_phase_2', @@ -4936,6 +5023,7 @@ 'original_name': 'Apparent power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_apparent_phase_3', 'unique_id': '1234567890-power_apparent_phase_3', @@ -4988,6 +5076,7 @@ 'original_name': 'Current phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_1', 'unique_id': '1234567890-current_ac_phase_1', @@ -5040,6 +5129,7 @@ 'original_name': 'Current phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_2', 'unique_id': '1234567890-current_ac_phase_2', @@ -5092,6 +5182,7 @@ 'original_name': 'Current phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac_phase_3', 'unique_id': '1234567890-current_ac_phase_3', @@ -5144,6 +5235,7 @@ 'original_name': 'Frequency phase average', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_phase_average', 'unique_id': '1234567890-frequency_phase_average', @@ -5194,6 +5286,7 @@ 'original_name': 'Meter location', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location', 'unique_id': '1234567890-meter_location', @@ -5249,6 +5342,7 @@ 'original_name': 'Meter location description', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location_description', 'unique_id': '1234567890-meter_location_description', @@ -5306,6 +5400,7 @@ 'original_name': 'Power factor', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor', 'unique_id': '1234567890-power_factor', @@ -5357,6 +5452,7 @@ 'original_name': 'Power factor phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_1', 'unique_id': '1234567890-power_factor_phase_1', @@ -5408,6 +5504,7 @@ 'original_name': 'Power factor phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_2', 'unique_id': '1234567890-power_factor_phase_2', @@ -5459,6 +5556,7 @@ 'original_name': 'Power factor phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_factor_phase_3', 'unique_id': '1234567890-power_factor_phase_3', @@ -5510,6 +5608,7 @@ 'original_name': 'Reactive energy consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_reactive_ac_consumed', 'unique_id': '1234567890-energy_reactive_ac_consumed', @@ -5561,6 +5660,7 @@ 'original_name': 'Reactive energy produced', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_reactive_ac_produced', 'unique_id': '1234567890-energy_reactive_ac_produced', @@ -5612,6 +5712,7 @@ 'original_name': 'Reactive power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive', 'unique_id': '1234567890-power_reactive', @@ -5664,6 +5765,7 @@ 'original_name': 'Reactive power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_1', 'unique_id': '1234567890-power_reactive_phase_1', @@ -5716,6 +5818,7 @@ 'original_name': 'Reactive power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_2', 'unique_id': '1234567890-power_reactive_phase_2', @@ -5768,6 +5871,7 @@ 'original_name': 'Reactive power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_reactive_phase_3', 'unique_id': '1234567890-power_reactive_phase_3', @@ -5820,6 +5924,7 @@ 'original_name': 'Real energy consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_consumed', 'unique_id': '1234567890-energy_real_consumed', @@ -5872,6 +5977,7 @@ 'original_name': 'Real energy minus', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_ac_minus', 'unique_id': '1234567890-energy_real_ac_minus', @@ -5924,6 +6030,7 @@ 'original_name': 'Real energy plus', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_ac_plus', 'unique_id': '1234567890-energy_real_ac_plus', @@ -5976,6 +6083,7 @@ 'original_name': 'Real energy produced', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_real_produced', 'unique_id': '1234567890-energy_real_produced', @@ -6028,6 +6136,7 @@ 'original_name': 'Real power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real', 'unique_id': '1234567890-power_real', @@ -6080,6 +6189,7 @@ 'original_name': 'Real power phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_1', 'unique_id': '1234567890-power_real_phase_1', @@ -6132,6 +6242,7 @@ 'original_name': 'Real power phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_2', 'unique_id': '1234567890-power_real_phase_2', @@ -6184,6 +6295,7 @@ 'original_name': 'Real power phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real_phase_3', 'unique_id': '1234567890-power_real_phase_3', @@ -6236,6 +6348,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_1', 'unique_id': '1234567890-voltage_ac_phase_1', @@ -6288,6 +6401,7 @@ 'original_name': 'Voltage phase 1-2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_12', 'unique_id': '1234567890-voltage_ac_phase_to_phase_12', @@ -6340,6 +6454,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_2', 'unique_id': '1234567890-voltage_ac_phase_2', @@ -6392,6 +6507,7 @@ 'original_name': 'Voltage phase 2-3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_23', 'unique_id': '1234567890-voltage_ac_phase_to_phase_23', @@ -6444,6 +6560,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_3', 'unique_id': '1234567890-voltage_ac_phase_3', @@ -6496,6 +6613,7 @@ 'original_name': 'Voltage phase 3-1', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac_phase_to_phase_31', 'unique_id': '1234567890-voltage_ac_phase_to_phase_31', @@ -6546,6 +6664,7 @@ 'original_name': 'Meter mode', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_mode', 'unique_id': 'solar_net_12345678-power_flow-meter_mode', @@ -6595,6 +6714,7 @@ 'original_name': 'Power battery', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_battery', 'unique_id': 'solar_net_12345678-power_flow-power_battery', @@ -6647,6 +6767,7 @@ 'original_name': 'Power battery charge', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_battery_charge', 'unique_id': 'solar_net_12345678-power_flow-power_battery_charge', @@ -6699,6 +6820,7 @@ 'original_name': 'Power battery discharge', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_battery_discharge', 'unique_id': 'solar_net_12345678-power_flow-power_battery_discharge', @@ -6751,6 +6873,7 @@ 'original_name': 'Power grid', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid', 'unique_id': 'solar_net_12345678-power_flow-power_grid', @@ -6803,6 +6926,7 @@ 'original_name': 'Power grid export', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_export', 'unique_id': 'solar_net_12345678-power_flow-power_grid_export', @@ -6855,6 +6979,7 @@ 'original_name': 'Power grid import', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_import', 'unique_id': 'solar_net_12345678-power_flow-power_grid_import', @@ -6907,6 +7032,7 @@ 'original_name': 'Power load', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load', 'unique_id': 'solar_net_12345678-power_flow-power_load', @@ -6959,6 +7085,7 @@ 'original_name': 'Power load consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_consumed', 'unique_id': 'solar_net_12345678-power_flow-power_load_consumed', @@ -7011,6 +7138,7 @@ 'original_name': 'Power load generated', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_generated', 'unique_id': 'solar_net_12345678-power_flow-power_load_generated', @@ -7063,6 +7191,7 @@ 'original_name': 'Power photovoltaics', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_photovoltaics', 'unique_id': 'solar_net_12345678-power_flow-power_photovoltaics', @@ -7115,6 +7244,7 @@ 'original_name': 'Relative autonomy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_autonomy', 'unique_id': 'solar_net_12345678-power_flow-relative_autonomy', @@ -7166,6 +7296,7 @@ 'original_name': 'Relative self-consumption', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_self_consumption', 'unique_id': 'solar_net_12345678-power_flow-relative_self_consumption', @@ -7217,6 +7348,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': 'solar_net_12345678-power_flow-energy_total', @@ -7269,6 +7401,7 @@ 'original_name': 'AC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac', 'unique_id': '234567-current_ac', @@ -7321,6 +7454,7 @@ 'original_name': 'AC power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_ac', 'unique_id': '234567-power_ac', @@ -7373,6 +7507,7 @@ 'original_name': 'AC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', 'unique_id': '234567-voltage_ac', @@ -7425,6 +7560,7 @@ 'original_name': 'DC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc', 'unique_id': '234567-current_dc', @@ -7477,6 +7613,7 @@ 'original_name': 'DC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': '234567-voltage_dc', @@ -7529,6 +7666,7 @@ 'original_name': 'Energy day', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_day', 'unique_id': '234567-energy_day', @@ -7581,6 +7719,7 @@ 'original_name': 'Energy year', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_year', 'unique_id': '234567-energy_year', @@ -7631,6 +7770,7 @@ 'original_name': 'Error code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_code', 'unique_id': '234567-error_code', @@ -7777,6 +7917,7 @@ 'original_name': 'Error message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_message', 'unique_id': '234567-error_message', @@ -7925,6 +8066,7 @@ 'original_name': 'Frequency', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_ac', 'unique_id': '234567-frequency_ac', @@ -7975,6 +8117,7 @@ 'original_name': 'LED color', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_color', 'unique_id': '234567-led_color', @@ -8022,6 +8165,7 @@ 'original_name': 'LED state', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_state', 'unique_id': '234567-led_state', @@ -8069,6 +8213,7 @@ 'original_name': 'Status code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_code', 'unique_id': '234567-status_code', @@ -8127,6 +8272,7 @@ 'original_name': 'Status message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_message', 'unique_id': '234567-status_message', @@ -8187,6 +8333,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': '234567-energy_total', @@ -8239,6 +8386,7 @@ 'original_name': 'AC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_ac', 'unique_id': '123456-current_ac', @@ -8291,6 +8439,7 @@ 'original_name': 'AC power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_ac', 'unique_id': '123456-power_ac', @@ -8343,6 +8492,7 @@ 'original_name': 'AC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', 'unique_id': '123456-voltage_ac', @@ -8395,6 +8545,7 @@ 'original_name': 'DC current', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_dc', 'unique_id': '123456-current_dc', @@ -8447,6 +8598,7 @@ 'original_name': 'DC voltage', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': '123456-voltage_dc', @@ -8499,6 +8651,7 @@ 'original_name': 'Energy day', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_day', 'unique_id': '123456-energy_day', @@ -8551,6 +8704,7 @@ 'original_name': 'Energy year', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_year', 'unique_id': '123456-energy_year', @@ -8601,6 +8755,7 @@ 'original_name': 'Error code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_code', 'unique_id': '123456-error_code', @@ -8747,6 +8902,7 @@ 'original_name': 'Error message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error_message', 'unique_id': '123456-error_message', @@ -8895,6 +9051,7 @@ 'original_name': 'Frequency', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frequency_ac', 'unique_id': '123456-frequency_ac', @@ -8945,6 +9102,7 @@ 'original_name': 'LED color', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_color', 'unique_id': '123456-led_color', @@ -8992,6 +9150,7 @@ 'original_name': 'LED state', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_state', 'unique_id': '123456-led_state', @@ -9039,6 +9198,7 @@ 'original_name': 'Status code', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_code', 'unique_id': '123456-status_code', @@ -9097,6 +9257,7 @@ 'original_name': 'Status message', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_message', 'unique_id': '123456-status_message', @@ -9157,6 +9318,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': '123456-energy_total', @@ -9207,6 +9369,7 @@ 'original_name': 'Meter location', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location', 'unique_id': 'solar_net_123.4567890:S0 Meter at inverter 1-meter_location', @@ -9262,6 +9425,7 @@ 'original_name': 'Meter location description', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_location_description', 'unique_id': 'solar_net_123.4567890:S0 Meter at inverter 1-meter_location_description', @@ -9319,6 +9483,7 @@ 'original_name': 'Real power', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_real', 'unique_id': 'solar_net_123.4567890:S0 Meter at inverter 1-power_real', @@ -9371,6 +9536,7 @@ 'original_name': 'CO₂ factor', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2_factor', 'unique_id': '123.4567890-co2_factor', @@ -9422,6 +9588,7 @@ 'original_name': 'Energy day', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_day', 'unique_id': 'solar_net_123.4567890-power_flow-energy_day', @@ -9474,6 +9641,7 @@ 'original_name': 'Energy year', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_year', 'unique_id': 'solar_net_123.4567890-power_flow-energy_year', @@ -9526,6 +9694,7 @@ 'original_name': 'Grid export tariff', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cash_factor', 'unique_id': '123.4567890-cash_factor', @@ -9577,6 +9746,7 @@ 'original_name': 'Grid import tariff', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'delivery_factor', 'unique_id': '123.4567890-delivery_factor', @@ -9626,6 +9796,7 @@ 'original_name': 'Meter mode', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_mode', 'unique_id': 'solar_net_123.4567890-power_flow-meter_mode', @@ -9675,6 +9846,7 @@ 'original_name': 'Power grid', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid', @@ -9727,6 +9899,7 @@ 'original_name': 'Power grid export', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_export', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid_export', @@ -9779,6 +9952,7 @@ 'original_name': 'Power grid import', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_grid_import', 'unique_id': 'solar_net_123.4567890-power_flow-power_grid_import', @@ -9831,6 +10005,7 @@ 'original_name': 'Power load', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load', 'unique_id': 'solar_net_123.4567890-power_flow-power_load', @@ -9883,6 +10058,7 @@ 'original_name': 'Power load consumed', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_consumed', 'unique_id': 'solar_net_123.4567890-power_flow-power_load_consumed', @@ -9935,6 +10111,7 @@ 'original_name': 'Power load generated', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_load_generated', 'unique_id': 'solar_net_123.4567890-power_flow-power_load_generated', @@ -9987,6 +10164,7 @@ 'original_name': 'Power photovoltaics', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_photovoltaics', 'unique_id': 'solar_net_123.4567890-power_flow-power_photovoltaics', @@ -10039,6 +10217,7 @@ 'original_name': 'Relative autonomy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_autonomy', 'unique_id': 'solar_net_123.4567890-power_flow-relative_autonomy', @@ -10090,6 +10269,7 @@ 'original_name': 'Relative self-consumption', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relative_self_consumption', 'unique_id': 'solar_net_123.4567890-power_flow-relative_self_consumption', @@ -10141,6 +10321,7 @@ 'original_name': 'Total energy', 'platform': 'fronius', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': 'solar_net_123.4567890-power_flow-energy_total', diff --git a/tests/components/fujitsu_fglair/snapshots/test_climate.ambr b/tests/components/fujitsu_fglair/snapshots/test_climate.ambr index 21c5b3429f4..e432d6a258a 100644 --- a/tests/components/fujitsu_fglair/snapshots/test_climate.ambr +++ b/tests/components/fujitsu_fglair/snapshots/test_climate.ambr @@ -49,6 +49,7 @@ 'original_name': None, 'platform': 'fujitsu_fglair', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'testserial123', @@ -144,6 +145,7 @@ 'original_name': None, 'platform': 'fujitsu_fglair', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'testserial345', diff --git a/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr b/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr index 751ad3cd2d9..cf22c24c427 100644 --- a/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr +++ b/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Outside temperature', 'platform': 'fujitsu_fglair', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fglair_outside_temp', 'unique_id': 'testserial123_outside_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Outside temperature', 'platform': 'fujitsu_fglair', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fglair_outside_temp', 'unique_id': 'testserial345_outside_temperature', diff --git a/tests/components/fyta/snapshots/test_binary_sensor.ambr b/tests/components/fyta/snapshots/test_binary_sensor.ambr index 1218a3da71c..4483c9cdb86 100644 --- a/tests/components/fyta/snapshots/test_binary_sensor.ambr +++ b/tests/components/fyta/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-low_battery', @@ -75,6 +76,7 @@ 'original_name': 'Light notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_light', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-notification_light', @@ -122,6 +124,7 @@ 'original_name': 'Nutrition notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_nutrition', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-notification_nutrition', @@ -169,6 +172,7 @@ 'original_name': 'Productive plant', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'productive_plant', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-productive_plant', @@ -216,6 +220,7 @@ 'original_name': 'Repotted', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'repotted', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-repotted', @@ -263,6 +268,7 @@ 'original_name': 'Temperature notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_temperature', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-notification_temperature', @@ -310,6 +316,7 @@ 'original_name': 'Update', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-sensor_update_available', @@ -358,6 +365,7 @@ 'original_name': 'Water notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_water', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-notification_water', @@ -405,6 +413,7 @@ 'original_name': 'Battery', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-low_battery', @@ -453,6 +462,7 @@ 'original_name': 'Light notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_light', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-notification_light', @@ -500,6 +510,7 @@ 'original_name': 'Nutrition notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_nutrition', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-notification_nutrition', @@ -547,6 +558,7 @@ 'original_name': 'Productive plant', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'productive_plant', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-productive_plant', @@ -594,6 +606,7 @@ 'original_name': 'Repotted', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'repotted', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-repotted', @@ -641,6 +654,7 @@ 'original_name': 'Temperature notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_temperature', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-notification_temperature', @@ -688,6 +702,7 @@ 'original_name': 'Update', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-sensor_update_available', @@ -736,6 +751,7 @@ 'original_name': 'Water notification', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_water', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-notification_water', diff --git a/tests/components/fyta/snapshots/test_image.ambr b/tests/components/fyta/snapshots/test_image.ambr index d36472f91b9..fd39c372b28 100644 --- a/tests/components/fyta/snapshots/test_image.ambr +++ b/tests/components/fyta/snapshots/test_image.ambr @@ -27,6 +27,7 @@ 'original_name': 'Plant image', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_image', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-plant_image', @@ -76,6 +77,7 @@ 'original_name': 'User image', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_image_user', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-plant_image_user', @@ -125,6 +127,7 @@ 'original_name': 'Plant image', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_image', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image', @@ -174,6 +177,7 @@ 'original_name': 'User image', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_image_user', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-plant_image_user', diff --git a/tests/components/fyta/snapshots/test_sensor.ambr b/tests/components/fyta/snapshots/test_sensor.ambr index c43a7446f11..6a835b9697e 100644 --- a/tests/components/fyta/snapshots/test_sensor.ambr +++ b/tests/components/fyta/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-battery_level', @@ -79,6 +80,7 @@ 'original_name': 'Last fertilized', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_fertilised', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-fertilise_last', @@ -129,6 +131,7 @@ 'original_name': 'Light', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-light', @@ -187,6 +190,7 @@ 'original_name': 'Light state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-light_status', @@ -245,6 +249,7 @@ 'original_name': 'Moisture', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-moisture', @@ -304,6 +309,7 @@ 'original_name': 'Moisture state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-moisture_status', @@ -360,6 +366,7 @@ 'original_name': 'Next fertilization', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_fertilisation', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-fertilise_next', @@ -417,6 +424,7 @@ 'original_name': 'Nutrients state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nutrients_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-nutrients_status', @@ -475,6 +483,7 @@ 'original_name': 'pH', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-ph', @@ -531,6 +540,7 @@ 'original_name': 'Plant state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-status', @@ -587,6 +597,7 @@ 'original_name': 'Salinity', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salinity', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-salinity', @@ -646,6 +657,7 @@ 'original_name': 'Salinity state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salinity_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-salinity_status', @@ -702,6 +714,7 @@ 'original_name': 'Scientific name', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'scientific_name', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-scientific_name', @@ -751,6 +764,7 @@ 'original_name': 'Temperature', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-temperature', @@ -810,6 +824,7 @@ 'original_name': 'Temperature state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-0-temperature_status', @@ -868,6 +883,7 @@ 'original_name': 'Battery', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-battery_level', @@ -918,6 +934,7 @@ 'original_name': 'Last fertilized', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_fertilised', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-fertilise_last', @@ -968,6 +985,7 @@ 'original_name': 'Light', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-light', @@ -1026,6 +1044,7 @@ 'original_name': 'Light state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-light_status', @@ -1084,6 +1103,7 @@ 'original_name': 'Moisture', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-moisture', @@ -1143,6 +1163,7 @@ 'original_name': 'Moisture state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-moisture_status', @@ -1199,6 +1220,7 @@ 'original_name': 'Next fertilization', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_fertilisation', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-fertilise_next', @@ -1256,6 +1278,7 @@ 'original_name': 'Nutrients state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nutrients_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-nutrients_status', @@ -1314,6 +1337,7 @@ 'original_name': 'pH', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-ph', @@ -1370,6 +1394,7 @@ 'original_name': 'Plant state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plant_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-status', @@ -1426,6 +1451,7 @@ 'original_name': 'Salinity', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salinity', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-salinity', @@ -1485,6 +1511,7 @@ 'original_name': 'Salinity state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salinity_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-salinity_status', @@ -1541,6 +1568,7 @@ 'original_name': 'Scientific name', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'scientific_name', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-scientific_name', @@ -1590,6 +1618,7 @@ 'original_name': 'Temperature', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-temperature', @@ -1649,6 +1678,7 @@ 'original_name': 'Temperature state', 'platform': 'fyta', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_status', 'unique_id': 'ce5f5431554d101905d31797e1232da8-1-temperature_status', diff --git a/tests/components/garages_amsterdam/snapshots/test_binary_sensor.ambr b/tests/components/garages_amsterdam/snapshots/test_binary_sensor.ambr index b93a8656ecc..d70ebc38b2c 100644 --- a/tests/components/garages_amsterdam/snapshots/test_binary_sensor.ambr +++ b/tests/components/garages_amsterdam/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'State', 'platform': 'garages_amsterdam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': 'IJDok-state', diff --git a/tests/components/garages_amsterdam/snapshots/test_sensor.ambr b/tests/components/garages_amsterdam/snapshots/test_sensor.ambr index 3453817da10..f47d8b9788a 100644 --- a/tests/components/garages_amsterdam/snapshots/test_sensor.ambr +++ b/tests/components/garages_amsterdam/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Long parking capacity', 'platform': 'garages_amsterdam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'long_capacity', 'unique_id': 'IJDok-long_capacity', @@ -78,6 +79,7 @@ 'original_name': 'Long parking free space', 'platform': 'garages_amsterdam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'free_space_long', 'unique_id': 'IJDok-free_space_long', @@ -128,6 +130,7 @@ 'original_name': 'Short parking capacity', 'platform': 'garages_amsterdam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'short_capacity', 'unique_id': 'IJDok-short_capacity', @@ -179,6 +182,7 @@ 'original_name': 'Short parking free space', 'platform': 'garages_amsterdam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'free_space_short', 'unique_id': 'IJDok-free_space_short', diff --git a/tests/components/geniushub/snapshots/test_binary_sensor.ambr b/tests/components/geniushub/snapshots/test_binary_sensor.ambr index c295ab8d10a..07f8ecb297d 100644 --- a/tests/components/geniushub/snapshots/test_binary_sensor.ambr +++ b/tests/components/geniushub/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Single Channel Receiver 22', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_22', diff --git a/tests/components/geniushub/snapshots/test_climate.ambr b/tests/components/geniushub/snapshots/test_climate.ambr index 8f897c84559..c80e54420e7 100644 --- a/tests/components/geniushub/snapshots/test_climate.ambr +++ b/tests/components/geniushub/snapshots/test_climate.ambr @@ -37,6 +37,7 @@ 'original_name': 'Bedroom', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_29', @@ -118,6 +119,7 @@ 'original_name': 'Ensuite', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_5', @@ -201,6 +203,7 @@ 'original_name': 'Guest room', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_7', @@ -284,6 +287,7 @@ 'original_name': 'Hall', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_2', @@ -367,6 +371,7 @@ 'original_name': 'Kitchen', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_3', @@ -449,6 +454,7 @@ 'original_name': 'Lounge', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_1', @@ -530,6 +536,7 @@ 'original_name': 'Study', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_30', diff --git a/tests/components/geniushub/snapshots/test_sensor.ambr b/tests/components/geniushub/snapshots/test_sensor.ambr index aaf3030d4a4..53594845b99 100644 --- a/tests/components/geniushub/snapshots/test_sensor.ambr +++ b/tests/components/geniushub/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'GeniusHub Errors', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_Errors', @@ -76,6 +77,7 @@ 'original_name': 'GeniusHub Information', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_Information', @@ -125,6 +127,7 @@ 'original_name': 'GeniusHub Warnings', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_Warnings', @@ -174,6 +177,7 @@ 'original_name': 'Radiator Valve 11', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_11', @@ -228,6 +232,7 @@ 'original_name': 'Radiator Valve 56', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_56', @@ -282,6 +287,7 @@ 'original_name': 'Radiator Valve 68', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_68', @@ -336,6 +342,7 @@ 'original_name': 'Radiator Valve 78', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_78', @@ -390,6 +397,7 @@ 'original_name': 'Radiator Valve 85', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_85', @@ -444,6 +452,7 @@ 'original_name': 'Radiator Valve 88', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_88', @@ -498,6 +507,7 @@ 'original_name': 'Radiator Valve 89', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_89', @@ -552,6 +562,7 @@ 'original_name': 'Radiator Valve 90', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_90', @@ -606,6 +617,7 @@ 'original_name': 'Room Sensor 16', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_16', @@ -662,6 +674,7 @@ 'original_name': 'Room Sensor 17', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_17', @@ -718,6 +731,7 @@ 'original_name': 'Room Sensor 18', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_18', @@ -774,6 +788,7 @@ 'original_name': 'Room Sensor 20', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_20', @@ -830,6 +845,7 @@ 'original_name': 'Room Sensor 21', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_21', @@ -886,6 +902,7 @@ 'original_name': 'Room Sensor 50', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_50', @@ -942,6 +959,7 @@ 'original_name': 'Room Sensor 53', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_device_53', diff --git a/tests/components/geniushub/snapshots/test_switch.ambr b/tests/components/geniushub/snapshots/test_switch.ambr index cc0451b4e94..f20717182c0 100644 --- a/tests/components/geniushub/snapshots/test_switch.ambr +++ b/tests/components/geniushub/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Bedroom Socket', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_27', @@ -83,6 +84,7 @@ 'original_name': 'Kitchen Socket', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_28', @@ -139,6 +141,7 @@ 'original_name': 'Study Socket', 'platform': 'geniushub', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01J71MQF0EC62D620DGYNG2R8H_zone_32', diff --git a/tests/components/gios/snapshots/test_sensor.ambr b/tests/components/gios/snapshots/test_sensor.ambr index ab8a2359d0c..fd74cc222c8 100644 --- a/tests/components/gios/snapshots/test_sensor.ambr +++ b/tests/components/gios/snapshots/test_sensor.ambr @@ -36,6 +36,7 @@ 'original_name': 'Air quality index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aqi', 'unique_id': '123-aqi', @@ -98,6 +99,7 @@ 'original_name': 'Benzene', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'c6h6', 'unique_id': '123-c6h6', @@ -153,6 +155,7 @@ 'original_name': 'Carbon monoxide', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co', 'unique_id': '123-co', @@ -208,6 +211,7 @@ 'original_name': 'Nitrogen dioxide', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-no2', @@ -268,6 +272,7 @@ 'original_name': 'Nitrogen dioxide index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'no2_index', 'unique_id': '123-no2-index', @@ -330,6 +335,7 @@ 'original_name': 'Ozone', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-o3', @@ -390,6 +396,7 @@ 'original_name': 'Ozone index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'o3_index', 'unique_id': '123-o3-index', @@ -452,6 +459,7 @@ 'original_name': 'PM10', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-pm10', @@ -512,6 +520,7 @@ 'original_name': 'PM10 index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pm10_index', 'unique_id': '123-pm10-index', @@ -574,6 +583,7 @@ 'original_name': 'PM2.5', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-pm25', @@ -634,6 +644,7 @@ 'original_name': 'PM2.5 index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pm25_index', 'unique_id': '123-pm25-index', @@ -696,6 +707,7 @@ 'original_name': 'Sulphur dioxide', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123-so2', @@ -756,6 +768,7 @@ 'original_name': 'Sulphur dioxide index', 'platform': 'gios', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'so2_index', 'unique_id': '123-so2-index', diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index baac4c5b056..536e48bef55 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Containers active', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'container_active', 'unique_id': 'test--docker_active', @@ -79,6 +80,7 @@ 'original_name': 'Containers CPU usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'container_cpu_usage', 'unique_id': 'test--docker_cpu_use', @@ -130,6 +132,7 @@ 'original_name': 'Containers memory used', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'container_memory_used', 'unique_id': 'test--docker_memory_use', @@ -182,6 +185,7 @@ 'original_name': 'cpu_thermal 1 temperature', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': 'test-cpu_thermal 1-temperature_core', @@ -237,6 +241,7 @@ 'original_name': 'dummy0 RX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rx', 'unique_id': 'test-dummy0-rx', @@ -292,6 +297,7 @@ 'original_name': 'dummy0 TX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_tx', 'unique_id': 'test-dummy0-tx', @@ -344,6 +350,7 @@ 'original_name': 'err_temp temperature', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': 'test-err_temp-temperature_hdd', @@ -399,6 +406,7 @@ 'original_name': 'eth0 RX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rx', 'unique_id': 'test-eth0-rx', @@ -454,6 +462,7 @@ 'original_name': 'eth0 TX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_tx', 'unique_id': 'test-eth0-tx', @@ -509,6 +518,7 @@ 'original_name': 'lo RX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_rx', 'unique_id': 'test-lo-rx', @@ -564,6 +574,7 @@ 'original_name': 'lo TX', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'network_tx', 'unique_id': 'test-lo-tx', @@ -616,6 +627,7 @@ 'original_name': 'md1 available', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raid_available', 'unique_id': 'test-md1-available', @@ -666,6 +678,7 @@ 'original_name': 'md1 used', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raid_used', 'unique_id': 'test-md1-used', @@ -716,6 +729,7 @@ 'original_name': 'md3 available', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raid_available', 'unique_id': 'test-md3-available', @@ -766,6 +780,7 @@ 'original_name': 'md3 used', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raid_used', 'unique_id': 'test-md3-used', @@ -816,6 +831,7 @@ 'original_name': '/media disk free', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_free', 'unique_id': 'test-/media-disk_free', @@ -868,6 +884,7 @@ 'original_name': '/media disk usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_usage', 'unique_id': 'test-/media-disk_use_percent', @@ -919,6 +936,7 @@ 'original_name': '/media disk used', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_used', 'unique_id': 'test-/media-disk_use', @@ -971,6 +989,7 @@ 'original_name': 'Memory free', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'memory_free', 'unique_id': 'test--memory_free', @@ -1023,6 +1042,7 @@ 'original_name': 'Memory usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'memory_usage', 'unique_id': 'test--memory_use_percent', @@ -1074,6 +1094,7 @@ 'original_name': 'Memory use', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'memory_use', 'unique_id': 'test--memory_use', @@ -1126,6 +1147,7 @@ 'original_name': 'na_temp temperature', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': 'test-na_temp-temperature_hdd', @@ -1178,6 +1200,7 @@ 'original_name': 'NVIDIA GeForce RTX 3080 (GPU 0) fan speed', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_speed', 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-fan_speed', @@ -1229,6 +1252,7 @@ 'original_name': 'NVIDIA GeForce RTX 3080 (GPU 0) memory usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gpu_memory_usage', 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-mem', @@ -1283,6 +1307,7 @@ 'original_name': 'NVIDIA GeForce RTX 3080 (GPU 0) processor usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gpu_processor_usage', 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-proc', @@ -1334,6 +1359,7 @@ 'original_name': 'NVIDIA GeForce RTX 3080 (GPU 0) temperature', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': 'test-NVIDIA GeForce RTX 3080 (GPU 0)-temperature', @@ -1389,6 +1415,7 @@ 'original_name': 'nvme0n1 disk read', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diskio_read', 'unique_id': 'test-nvme0n1-read', @@ -1444,6 +1471,7 @@ 'original_name': 'nvme0n1 disk write', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diskio_write', 'unique_id': 'test-nvme0n1-write', @@ -1499,6 +1527,7 @@ 'original_name': 'sda disk read', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diskio_read', 'unique_id': 'test-sda-read', @@ -1554,6 +1583,7 @@ 'original_name': 'sda disk write', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diskio_write', 'unique_id': 'test-sda-write', @@ -1606,6 +1636,7 @@ 'original_name': '/ssl disk free', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_free', 'unique_id': 'test-/ssl-disk_free', @@ -1658,6 +1689,7 @@ 'original_name': '/ssl disk usage', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_usage', 'unique_id': 'test-/ssl-disk_use_percent', @@ -1709,6 +1741,7 @@ 'original_name': '/ssl disk used', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_used', 'unique_id': 'test-/ssl-disk_use', @@ -1759,6 +1792,7 @@ 'original_name': 'Uptime', 'platform': 'glances', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': 'test--uptime', diff --git a/tests/components/gree/snapshots/test_climate.ambr b/tests/components/gree/snapshots/test_climate.ambr index 9111b909f04..5a6ce0ce5a7 100644 --- a/tests/components/gree/snapshots/test_climate.ambr +++ b/tests/components/gree/snapshots/test_climate.ambr @@ -114,6 +114,7 @@ 'original_name': None, 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'aabbcc112233', diff --git a/tests/components/gree/snapshots/test_switch.ambr b/tests/components/gree/snapshots/test_switch.ambr index c3fa3ae24c7..982afef30e8 100644 --- a/tests/components/gree/snapshots/test_switch.ambr +++ b/tests/components/gree/snapshots/test_switch.ambr @@ -92,6 +92,7 @@ 'original_name': 'Panel light', 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'aabbcc112233_Panel Light', @@ -124,6 +125,7 @@ 'original_name': 'Quiet mode', 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'quiet', 'unique_id': 'aabbcc112233_Quiet', @@ -156,6 +158,7 @@ 'original_name': 'Fresh air', 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fresh_air', 'unique_id': 'aabbcc112233_Fresh Air', @@ -188,6 +191,7 @@ 'original_name': 'Xtra fan', 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'xfan', 'unique_id': 'aabbcc112233_XFan', @@ -220,6 +224,7 @@ 'original_name': 'Health mode', 'platform': 'gree', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_mode', 'unique_id': 'aabbcc112233_Health mode', diff --git a/tests/components/habitica/snapshots/test_binary_sensor.ambr b/tests/components/habitica/snapshots/test_binary_sensor.ambr index ffe4ce83d0e..247063f2ae8 100644 --- a/tests/components/habitica/snapshots/test_binary_sensor.ambr +++ b/tests/components/habitica/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Pending quest invitation', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_quest', diff --git a/tests/components/habitica/snapshots/test_button.ambr b/tests/components/habitica/snapshots/test_button.ambr index 5c6ad640039..9d7e2411590 100644 --- a/tests/components/habitica/snapshots/test_button.ambr +++ b/tests/components/habitica/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Allocate all stat points', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points', @@ -74,6 +75,7 @@ 'original_name': 'Blessing', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_heal_all', @@ -122,6 +124,7 @@ 'original_name': 'Buy a health potion', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion', @@ -170,6 +173,7 @@ 'original_name': 'Healing light', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_heal', @@ -218,6 +222,7 @@ 'original_name': 'Protective aura', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_protect_aura', @@ -266,6 +271,7 @@ 'original_name': 'Revive from death', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive', @@ -313,6 +319,7 @@ 'original_name': 'Searing brightness', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_brightness', @@ -361,6 +368,7 @@ 'original_name': 'Start my day', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron', @@ -408,6 +416,7 @@ 'original_name': 'Allocate all stat points', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points', @@ -455,6 +464,7 @@ 'original_name': 'Buy a health potion', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion', @@ -503,6 +513,7 @@ 'original_name': 'Revive from death', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive', @@ -550,6 +561,7 @@ 'original_name': 'Start my day', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron', @@ -597,6 +609,7 @@ 'original_name': 'Stealth', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_stealth', @@ -645,6 +658,7 @@ 'original_name': 'Tools of the trade', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_tools_of_trade', @@ -693,6 +707,7 @@ 'original_name': 'Allocate all stat points', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points', @@ -740,6 +755,7 @@ 'original_name': 'Buy a health potion', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion', @@ -788,6 +804,7 @@ 'original_name': 'Defensive stance', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_defensive_stance', @@ -836,6 +853,7 @@ 'original_name': 'Intimidating gaze', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_intimidate', @@ -884,6 +902,7 @@ 'original_name': 'Revive from death', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive', @@ -931,6 +950,7 @@ 'original_name': 'Start my day', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron', @@ -978,6 +998,7 @@ 'original_name': 'Valorous presence', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_valorous_presence', @@ -1026,6 +1047,7 @@ 'original_name': 'Allocate all stat points', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points', @@ -1073,6 +1095,7 @@ 'original_name': 'Buy a health potion', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion', @@ -1121,6 +1144,7 @@ 'original_name': 'Chilling frost', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_frost', @@ -1169,6 +1193,7 @@ 'original_name': 'Earthquake', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_earth', @@ -1217,6 +1242,7 @@ 'original_name': 'Ethereal surge', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mpheal', @@ -1265,6 +1291,7 @@ 'original_name': 'Revive from death', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive', @@ -1312,6 +1339,7 @@ 'original_name': 'Start my day', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron', diff --git a/tests/components/habitica/snapshots/test_calendar.ambr b/tests/components/habitica/snapshots/test_calendar.ambr index c7f12684efe..a59b984c63e 100644 --- a/tests/components/habitica/snapshots/test_calendar.ambr +++ b/tests/components/habitica/snapshots/test_calendar.ambr @@ -955,6 +955,7 @@ 'original_name': 'Dailies', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_dailys', @@ -1009,6 +1010,7 @@ 'original_name': 'Daily reminders', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_daily_reminders', @@ -1062,6 +1064,7 @@ 'original_name': 'To-do reminders', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todo_reminders', @@ -1115,6 +1118,7 @@ 'original_name': "To-Do's", 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todos', diff --git a/tests/components/habitica/snapshots/test_sensor.ambr b/tests/components/habitica/snapshots/test_sensor.ambr index b5b1009a73f..06f9ff9a6cd 100644 --- a/tests/components/habitica/snapshots/test_sensor.ambr +++ b/tests/components/habitica/snapshots/test_sensor.ambr @@ -34,6 +34,7 @@ 'original_name': 'Class', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_class', @@ -92,6 +93,7 @@ 'original_name': 'Constitution', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_constitution', @@ -145,6 +147,7 @@ 'original_name': 'Display name', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_display_name', @@ -197,6 +200,7 @@ 'original_name': 'Eggs', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_eggs_total', @@ -249,6 +253,7 @@ 'original_name': 'Experience', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_experience', @@ -301,6 +306,7 @@ 'original_name': 'Gems', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_gems', @@ -353,6 +359,7 @@ 'original_name': 'Gold', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_gold', @@ -402,6 +409,7 @@ 'original_name': 'Habits', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': 'test_user_habits', 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_habits', @@ -609,6 +617,7 @@ 'original_name': 'Hatching potions', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_hatching_potions_total', @@ -664,6 +673,7 @@ 'original_name': 'Health', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_health', @@ -716,6 +726,7 @@ 'original_name': 'Intelligence', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_intelligence', @@ -769,6 +780,7 @@ 'original_name': 'Level', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_level', @@ -819,6 +831,7 @@ 'original_name': 'Mana', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mana', @@ -868,6 +881,7 @@ 'original_name': 'Max. health', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': 'test_user_max_health', 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_health_max', @@ -916,6 +930,7 @@ 'original_name': 'Max. mana', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mana_max', @@ -968,6 +983,7 @@ 'original_name': 'Mystic hourglasses', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_trinkets', @@ -1017,6 +1033,7 @@ 'original_name': 'Next level', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_experience_max', @@ -1069,6 +1086,7 @@ 'original_name': 'Pending damage', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_damage', @@ -1118,6 +1136,7 @@ 'original_name': 'Pending quest items', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_quest_items', @@ -1169,6 +1188,7 @@ 'original_name': 'Perception', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_perception', @@ -1222,6 +1242,7 @@ 'original_name': 'Pet food', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_food_total', @@ -1274,6 +1295,7 @@ 'original_name': 'Quest scrolls', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_quest_scrolls', @@ -1327,6 +1349,7 @@ 'original_name': 'Rewards', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': 'test_user_rewards', 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_rewards', @@ -1418,6 +1441,7 @@ 'original_name': 'Saddles', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_saddle', @@ -1470,6 +1494,7 @@ 'original_name': 'Strength', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_strength', diff --git a/tests/components/habitica/snapshots/test_switch.ambr b/tests/components/habitica/snapshots/test_switch.ambr index e8122f77c6e..7794f8f5e8d 100644 --- a/tests/components/habitica/snapshots/test_switch.ambr +++ b/tests/components/habitica/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Rest in the inn', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_sleep', diff --git a/tests/components/habitica/snapshots/test_todo.ambr b/tests/components/habitica/snapshots/test_todo.ambr index fef9404a0f0..52f901322a3 100644 --- a/tests/components/habitica/snapshots/test_todo.ambr +++ b/tests/components/habitica/snapshots/test_todo.ambr @@ -141,6 +141,7 @@ 'original_name': 'Dailies', 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_dailys', @@ -189,6 +190,7 @@ 'original_name': "To-Do's", 'platform': 'habitica', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todos', diff --git a/tests/components/homee/snapshots/test_alarm_control_panel.ambr b/tests/components/homee/snapshots/test_alarm_control_panel.ambr index 59a22f74080..8095831965a 100644 --- a/tests/components/homee/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/homee/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': 'Status', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee_mode', 'unique_id': '00055511EECC--1-1', diff --git a/tests/components/homee/snapshots/test_binary_sensor.ambr b/tests/components/homee/snapshots/test_binary_sensor.ambr index 4926c048f5b..0e9f02edf6c 100644 --- a/tests/components/homee/snapshots/test_binary_sensor.ambr +++ b/tests/components/homee/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '00055511EECC-1-1', @@ -75,6 +76,7 @@ 'original_name': 'Blackout', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'blackout_alarm', 'unique_id': '00055511EECC-1-2', @@ -123,6 +125,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'carbon_dioxide', 'unique_id': '00055511EECC-1-4', @@ -171,6 +174,7 @@ 'original_name': 'Carbon monoxide', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'carbon_monoxide', 'unique_id': '00055511EECC-1-3', @@ -219,6 +223,7 @@ 'original_name': 'Flood', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flood', 'unique_id': '00055511EECC-1-5', @@ -267,6 +272,7 @@ 'original_name': 'High temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'high_temperature', 'unique_id': '00055511EECC-1-6', @@ -315,6 +321,7 @@ 'original_name': 'Leak', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leak_alarm', 'unique_id': '00055511EECC-1-7', @@ -363,6 +370,7 @@ 'original_name': 'Load', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_alarm', 'unique_id': '00055511EECC-1-8', @@ -410,6 +418,7 @@ 'original_name': 'Lock', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': '00055511EECC-1-9', @@ -458,6 +467,7 @@ 'original_name': 'Low temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'low_temperature', 'unique_id': '00055511EECC-1-10', @@ -506,6 +516,7 @@ 'original_name': 'Malfunction', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'malfunction', 'unique_id': '00055511EECC-1-11', @@ -554,6 +565,7 @@ 'original_name': 'Maximum level', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'maximum', 'unique_id': '00055511EECC-1-12', @@ -602,6 +614,7 @@ 'original_name': 'Minimum level', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'minimum', 'unique_id': '00055511EECC-1-13', @@ -650,6 +663,7 @@ 'original_name': 'Motion', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion', 'unique_id': '00055511EECC-1-14', @@ -698,6 +712,7 @@ 'original_name': 'Motor blocked', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motor_blocked', 'unique_id': '00055511EECC-1-15', @@ -746,6 +761,7 @@ 'original_name': 'Opening', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'opening', 'unique_id': '00055511EECC-1-17', @@ -794,6 +810,7 @@ 'original_name': 'Overcurrent', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overcurrent', 'unique_id': '00055511EECC-1-18', @@ -842,6 +859,7 @@ 'original_name': 'Overload', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overload', 'unique_id': '00055511EECC-1-19', @@ -890,6 +908,7 @@ 'original_name': 'Plug', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug', 'unique_id': '00055511EECC-1-16', @@ -938,6 +957,7 @@ 'original_name': 'Power', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00055511EECC-1-21', @@ -986,6 +1006,7 @@ 'original_name': 'Presence', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'presence', 'unique_id': '00055511EECC-1-20', @@ -1034,6 +1055,7 @@ 'original_name': 'Rain', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rain', 'unique_id': '00055511EECC-1-22', @@ -1082,6 +1104,7 @@ 'original_name': 'Replace filter', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'replace_filter', 'unique_id': '00055511EECC-1-23', @@ -1130,6 +1153,7 @@ 'original_name': 'Smoke', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smoke', 'unique_id': '00055511EECC-1-24', @@ -1178,6 +1202,7 @@ 'original_name': 'Storage', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage', 'unique_id': '00055511EECC-1-25', @@ -1226,6 +1251,7 @@ 'original_name': 'Surge', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'surge', 'unique_id': '00055511EECC-1-26', @@ -1274,6 +1300,7 @@ 'original_name': 'Tamper', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tamper', 'unique_id': '00055511EECC-1-27', @@ -1322,6 +1349,7 @@ 'original_name': 'Voltage drop', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_drop', 'unique_id': '00055511EECC-1-28', @@ -1370,6 +1398,7 @@ 'original_name': 'Water', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water', 'unique_id': '00055511EECC-1-29', diff --git a/tests/components/homee/snapshots/test_button.ambr b/tests/components/homee/snapshots/test_button.ambr index be2bbae539b..eea7e8ffd06 100644 --- a/tests/components/homee/snapshots/test_button.ambr +++ b/tests/components/homee/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00055511EECC-1-4', @@ -74,6 +75,7 @@ 'original_name': 'Automatic mode', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'automatic_mode', 'unique_id': '00055511EECC-1-1', @@ -121,6 +123,7 @@ 'original_name': 'Briefly open', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'briefly_open', 'unique_id': '00055511EECC-1-2', @@ -168,6 +171,7 @@ 'original_name': 'Identification mode', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'identification_mode', 'unique_id': '00055511EECC-1-3', @@ -216,6 +220,7 @@ 'original_name': 'Impulse 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'impulse_instance', 'unique_id': '00055511EECC-1-5', @@ -263,6 +268,7 @@ 'original_name': 'Impulse 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'impulse_instance', 'unique_id': '00055511EECC-1-6', @@ -310,6 +316,7 @@ 'original_name': 'Light', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': '00055511EECC-1-7', @@ -357,6 +364,7 @@ 'original_name': 'Open partially', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'open_partial', 'unique_id': '00055511EECC-1-8', @@ -404,6 +412,7 @@ 'original_name': 'Open permanently', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'permanently_open', 'unique_id': '00055511EECC-1-9', @@ -451,6 +460,7 @@ 'original_name': 'Reset meter 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_meter_instance', 'unique_id': '00055511EECC-1-10', @@ -498,6 +508,7 @@ 'original_name': 'Reset meter 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_meter_instance', 'unique_id': '00055511EECC-1-11', @@ -545,6 +556,7 @@ 'original_name': 'Ventilate', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ventilate', 'unique_id': '00055511EECC-1-12', diff --git a/tests/components/homee/snapshots/test_climate.ambr b/tests/components/homee/snapshots/test_climate.ambr index b79538ddcf0..2c94c5ef8e0 100644 --- a/tests/components/homee/snapshots/test_climate.ambr +++ b/tests/components/homee/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee', 'unique_id': '00055511EECC-1-1', @@ -98,6 +99,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee', 'unique_id': '00055511EECC-2-1', @@ -163,6 +165,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee', 'unique_id': '00055511EECC-3-1', @@ -235,6 +238,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee', 'unique_id': '00055511EECC-4-1', diff --git a/tests/components/homee/snapshots/test_event.ambr b/tests/components/homee/snapshots/test_event.ambr index 45194526ef0..b3f544bcc4e 100644 --- a/tests/components/homee/snapshots/test_event.ambr +++ b/tests/components/homee/snapshots/test_event.ambr @@ -40,6 +40,7 @@ 'original_name': 'Up/down remote', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'up_down_remote', 'unique_id': '00055511EECC-1-1', diff --git a/tests/components/homee/snapshots/test_fan.ambr b/tests/components/homee/snapshots/test_fan.ambr index f680ec63e0f..b6d77582aaf 100644 --- a/tests/components/homee/snapshots/test_fan.ambr +++ b/tests/components/homee/snapshots/test_fan.ambr @@ -33,6 +33,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'homee', 'unique_id': '00055511EECC-77', diff --git a/tests/components/homee/snapshots/test_light.ambr b/tests/components/homee/snapshots/test_light.ambr index 3c766552467..2f22d95ae8d 100644 --- a/tests/components/homee/snapshots/test_light.ambr +++ b/tests/components/homee/snapshots/test_light.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00055511EECC-2-12', @@ -116,6 +117,7 @@ 'original_name': 'Light 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_instance', 'unique_id': '00055511EECC-1-1', @@ -198,6 +200,7 @@ 'original_name': 'Light 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_instance', 'unique_id': '00055511EECC-1-5', @@ -265,6 +268,7 @@ 'original_name': 'Light 3', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_instance', 'unique_id': '00055511EECC-1-9', @@ -322,6 +326,7 @@ 'original_name': 'Light 4', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_instance', 'unique_id': '00055511EECC-1-11', diff --git a/tests/components/homee/snapshots/test_lock.ambr b/tests/components/homee/snapshots/test_lock.ambr index d055039cca4..41563d6be41 100644 --- a/tests/components/homee/snapshots/test_lock.ambr +++ b/tests/components/homee/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00055511EECC-1-1', diff --git a/tests/components/homee/snapshots/test_number.ambr b/tests/components/homee/snapshots/test_number.ambr index 1fa2e0ef697..53569fe8734 100644 --- a/tests/components/homee/snapshots/test_number.ambr +++ b/tests/components/homee/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Down-movement duration', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'down_time', 'unique_id': '00055511EECC-1-3', @@ -90,6 +91,7 @@ 'original_name': 'Down position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'down_position', 'unique_id': '00055511EECC-1-1', @@ -147,6 +149,7 @@ 'original_name': 'Down slat position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'down_slat_position', 'unique_id': '00055511EECC-1-2', @@ -204,6 +207,7 @@ 'original_name': 'End position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'endposition_configuration', 'unique_id': '00055511EECC-1-4', @@ -260,6 +264,7 @@ 'original_name': 'Maximum slat angle', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'slat_max_angle', 'unique_id': '00055511EECC-1-9', @@ -317,6 +322,7 @@ 'original_name': 'Minimum slat angle', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'slat_min_angle', 'unique_id': '00055511EECC-1-10', @@ -374,6 +380,7 @@ 'original_name': 'Motion alarm delay', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_alarm_cancelation_delay', 'unique_id': '00055511EECC-1-5', @@ -432,6 +439,7 @@ 'original_name': 'Polling interval', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'polling_interval', 'unique_id': '00055511EECC-1-7', @@ -490,6 +498,7 @@ 'original_name': 'Slat steps', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'slat_steps', 'unique_id': '00055511EECC-1-11', @@ -546,6 +555,7 @@ 'original_name': 'Slat turn duration', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'shutter_slat_time', 'unique_id': '00055511EECC-1-8', @@ -604,6 +614,7 @@ 'original_name': 'Temperature offset', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_offset', 'unique_id': '00055511EECC-1-12', @@ -661,6 +672,7 @@ 'original_name': 'Threshold for wind trigger', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_monitoring_state', 'unique_id': '00055511EECC-1-16', @@ -719,6 +731,7 @@ 'original_name': 'Up-movement duration', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'up_time', 'unique_id': '00055511EECC-1-13', @@ -777,6 +790,7 @@ 'original_name': 'Wake-up interval', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wake_up_interval', 'unique_id': '00055511EECC-1-14', @@ -835,6 +849,7 @@ 'original_name': 'Window open sensibility', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'open_window_detection_sensibility', 'unique_id': '00055511EECC-1-6', diff --git a/tests/components/homee/snapshots/test_select.ambr b/tests/components/homee/snapshots/test_select.ambr index 9fa831230c2..9f52f75e691 100644 --- a/tests/components/homee/snapshots/test_select.ambr +++ b/tests/components/homee/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Repeater mode', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'repeater_mode', 'unique_id': '00055511EECC-1-1', diff --git a/tests/components/homee/snapshots/test_sensor.ambr b/tests/components/homee/snapshots/test_sensor.ambr index ff04f245504..52bbe4aae3e 100644 --- a/tests/components/homee/snapshots/test_sensor.ambr +++ b/tests/components/homee/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '00055511EECC-1-3', @@ -81,6 +82,7 @@ 'original_name': 'Battery', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_instance', 'unique_id': '00055511EECC-1-34', @@ -133,6 +135,7 @@ 'original_name': 'Current 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_instance', 'unique_id': '00055511EECC-1-7', @@ -185,6 +188,7 @@ 'original_name': 'Current 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_instance', 'unique_id': '00055511EECC-1-8', @@ -237,6 +241,7 @@ 'original_name': 'Dawn', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dawn', 'unique_id': '00055511EECC-1-10', @@ -289,6 +294,7 @@ 'original_name': 'Device temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_temperature', 'unique_id': '00055511EECC-1-11', @@ -341,6 +347,7 @@ 'original_name': 'Energy 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_instance', 'unique_id': '00055511EECC-1-1', @@ -393,6 +400,7 @@ 'original_name': 'Energy 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_instance', 'unique_id': '00055511EECC-1-2', @@ -445,6 +453,7 @@ 'original_name': 'Exhaust motor speed', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exhaust_motor_revs', 'unique_id': '00055511EECC-1-12', @@ -496,6 +505,7 @@ 'original_name': 'Humidity', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '00055511EECC-1-22', @@ -548,6 +558,7 @@ 'original_name': 'Illuminance', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness', 'unique_id': '00055511EECC-1-4', @@ -599,6 +610,7 @@ 'original_name': 'Illuminance 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness_instance', 'unique_id': '00055511EECC-1-5', @@ -651,6 +663,7 @@ 'original_name': 'Illuminance 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness_instance', 'unique_id': '00055511EECC-1-6', @@ -703,6 +716,7 @@ 'original_name': 'Indoor humidity', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_humidity', 'unique_id': '00055511EECC-1-13', @@ -755,6 +769,7 @@ 'original_name': 'Indoor temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_temperature', 'unique_id': '00055511EECC-1-14', @@ -807,6 +822,7 @@ 'original_name': 'Intake motor speed', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'intake_motor_revs', 'unique_id': '00055511EECC-1-15', @@ -858,6 +874,7 @@ 'original_name': 'Level', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'level', 'unique_id': '00055511EECC-1-16', @@ -910,6 +927,7 @@ 'original_name': 'Link quality', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_quality', 'unique_id': '00055511EECC-1-17', @@ -975,6 +993,7 @@ 'original_name': 'Node state', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'node_state', 'unique_id': '00055511EECC-1-state', @@ -1041,6 +1060,7 @@ 'original_name': 'Operating hours', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_hours', 'unique_id': '00055511EECC-1-18', @@ -1093,6 +1113,7 @@ 'original_name': 'Outdoor humidity', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outdoor_humidity', 'unique_id': '00055511EECC-1-19', @@ -1145,6 +1166,7 @@ 'original_name': 'Outdoor temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outdoor_temperature', 'unique_id': '00055511EECC-1-20', @@ -1197,6 +1219,7 @@ 'original_name': 'Position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'position', 'unique_id': '00055511EECC-1-21', @@ -1254,6 +1277,7 @@ 'original_name': 'State', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'up_down', 'unique_id': '00055511EECC-1-28', @@ -1311,6 +1335,7 @@ 'original_name': 'Temperature', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '00055511EECC-1-23', @@ -1363,6 +1388,7 @@ 'original_name': 'Total current', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_current', 'unique_id': '00055511EECC-1-25', @@ -1415,6 +1441,7 @@ 'original_name': 'Total energy', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy', 'unique_id': '00055511EECC-1-24', @@ -1467,6 +1494,7 @@ 'original_name': 'Total power', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power', 'unique_id': '00055511EECC-1-26', @@ -1519,6 +1547,7 @@ 'original_name': 'Total voltage', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_voltage', 'unique_id': '00055511EECC-1-27', @@ -1571,6 +1600,7 @@ 'original_name': 'Ultraviolet', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uv', 'unique_id': '00055511EECC-1-29', @@ -1621,6 +1651,7 @@ 'original_name': 'Voltage 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_instance', 'unique_id': '00055511EECC-1-30', @@ -1673,6 +1704,7 @@ 'original_name': 'Voltage 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_instance', 'unique_id': '00055511EECC-1-31', @@ -1728,6 +1760,7 @@ 'original_name': 'Wind speed', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_speed', 'unique_id': '00055511EECC-1-32', @@ -1784,6 +1817,7 @@ 'original_name': 'Window position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'window_position', 'unique_id': '00055511EECC-1-33', diff --git a/tests/components/homee/snapshots/test_switch.ambr b/tests/components/homee/snapshots/test_switch.ambr index 43c1773cede..c8d68301884 100644 --- a/tests/components/homee/snapshots/test_switch.ambr +++ b/tests/components/homee/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Child lock', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'external_binary_input', 'unique_id': '00055511EECC-1-1', @@ -75,6 +76,7 @@ 'original_name': 'Manual operation', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_operation', 'unique_id': '00055511EECC-1-2', @@ -123,6 +125,7 @@ 'original_name': 'Switch 1', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_instance', 'unique_id': '00055511EECC-1-3', @@ -171,6 +174,7 @@ 'original_name': 'Switch 2', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_instance', 'unique_id': '00055511EECC-1-4', @@ -219,6 +223,7 @@ 'original_name': 'Watchdog', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'watchdog', 'unique_id': '00055511EECC-1-5', diff --git a/tests/components/homee/snapshots/test_valve.ambr b/tests/components/homee/snapshots/test_valve.ambr index c76ecc6e780..bdf6d9f381c 100644 --- a/tests/components/homee/snapshots/test_valve.ambr +++ b/tests/components/homee/snapshots/test_valve.ambr @@ -27,6 +27,7 @@ 'original_name': 'Valve position', 'platform': 'homee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'valve_position', 'unique_id': '00055511EECC-1-1', diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 324040f850f..3d7b276c472 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -66,6 +66,7 @@ 'original_name': 'Airversa AP2 1808 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -112,6 +113,7 @@ 'original_name': 'Airversa AP2 1808 AirPurifier', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_32832', @@ -165,6 +167,7 @@ 'original_name': 'Airversa AP2 1808 Air Purifier Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_purifier_state_target', 'unique_id': '00:00:00:00:00:00_1_32832_32837', @@ -216,6 +219,7 @@ 'original_name': 'Airversa AP2 1808 Air Purifier Status', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_purifier_state_current', 'unique_id': '00:00:00:00:00:00_1_32832_32836', @@ -265,6 +269,7 @@ 'original_name': 'Airversa AP2 1808 Air Quality', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_2576_2579', @@ -310,6 +315,7 @@ 'original_name': 'Airversa AP2 1808 Filter lifetime', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_32896_32900', @@ -355,6 +361,7 @@ 'original_name': 'Airversa AP2 1808 PM2.5 Density', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_2576_2580', @@ -408,6 +415,7 @@ 'original_name': 'Airversa AP2 1808 Thread Capabilities', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thread_node_capabilities', 'unique_id': '00:00:00:00:00:00_1_112_115', @@ -468,6 +476,7 @@ 'original_name': 'Airversa AP2 1808 Thread Status', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thread_status', 'unique_id': '00:00:00:00:00:00_1_112_117', @@ -519,6 +528,7 @@ 'original_name': 'Airversa AP2 1808 Lock Physical Controls', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock_physical_controls', 'unique_id': '00:00:00:00:00:00_1_32832_32839', @@ -560,6 +570,7 @@ 'original_name': 'Airversa AP2 1808 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_32832_32843', @@ -601,6 +612,7 @@ 'original_name': 'Airversa AP2 1808 Sleep Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sleep_mode', 'unique_id': '00:00:00:00:00:00_1_32832_32842', @@ -685,6 +697,7 @@ 'original_name': 'eufy HomeBase2-0AAA Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -766,6 +779,7 @@ 'original_name': 'eufyCam2-0000 Motion Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_160', @@ -808,6 +822,7 @@ 'original_name': 'eufyCam2-0000 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_1_2', @@ -850,6 +865,7 @@ 'original_name': 'eufyCam2-0000', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4', @@ -894,6 +910,7 @@ 'original_name': 'eufyCam2-0000 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_101', @@ -939,6 +956,7 @@ 'original_name': 'eufyCam2-0000 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_4_80_83', @@ -1019,6 +1037,7 @@ 'original_name': 'eufyCam2-000A Motion Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_160', @@ -1061,6 +1080,7 @@ 'original_name': 'eufyCam2-000A Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_2', @@ -1103,6 +1123,7 @@ 'original_name': 'eufyCam2-000A', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2', @@ -1147,6 +1168,7 @@ 'original_name': 'eufyCam2-000A Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_101', @@ -1192,6 +1214,7 @@ 'original_name': 'eufyCam2-000A Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_2_80_83', @@ -1272,6 +1295,7 @@ 'original_name': 'eufyCam2-000A Motion Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_160', @@ -1314,6 +1338,7 @@ 'original_name': 'eufyCam2-000A Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_2', @@ -1356,6 +1381,7 @@ 'original_name': 'eufyCam2-000A', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3', @@ -1400,6 +1426,7 @@ 'original_name': 'eufyCam2-000A Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_101', @@ -1445,6 +1472,7 @@ 'original_name': 'eufyCam2-000A Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_3_80_83', @@ -1529,6 +1557,7 @@ 'original_name': 'Aqara-Hub-E1-00A0 Security System', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -1574,6 +1603,7 @@ 'original_name': 'Aqara-Hub-E1-00A0 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_65537', @@ -1621,6 +1651,7 @@ 'original_name': 'Aqara-Hub-E1-00A0 Volume', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '00:00:00:00:00:00_1_17_1114116', @@ -1666,6 +1697,7 @@ 'original_name': 'Aqara-Hub-E1-00A0 Pairing Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pairing_mode', 'unique_id': '00:00:00:00:00:00_1_17_1114117', @@ -1746,6 +1778,7 @@ 'original_name': 'Contact Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_33_4', @@ -1788,6 +1821,7 @@ 'original_name': 'Contact Sensor Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_33_1_65537', @@ -1832,6 +1866,7 @@ 'original_name': 'Contact Sensor Battery Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_33_5', @@ -1920,6 +1955,7 @@ 'original_name': 'Aqara Hub-1563 Security System', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_66304', @@ -1965,6 +2001,7 @@ 'original_name': 'Aqara Hub-1563 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -2016,6 +2053,7 @@ 'original_name': 'Aqara Hub-1563 Lightbulb-1563', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_65792', @@ -2078,6 +2116,7 @@ 'original_name': 'Aqara Hub-1563 Volume', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '00:00:00:00:00:00_1_65536_65541', @@ -2123,6 +2162,7 @@ 'original_name': 'Aqara Hub-1563 Pairing Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pairing_mode', 'unique_id': '00:00:00:00:00:00_1_65536_65538', @@ -2207,6 +2247,7 @@ 'original_name': 'Programmable Switch Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_65537', @@ -2251,6 +2292,7 @@ 'original_name': 'Programmable Switch Battery Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_5', @@ -2339,6 +2381,7 @@ 'original_name': 'ArloBabyA0 Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_500', @@ -2381,6 +2424,7 @@ 'original_name': 'ArloBabyA0 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -2423,6 +2467,7 @@ 'original_name': 'ArloBabyA0', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1', @@ -2474,6 +2519,7 @@ 'original_name': 'ArloBabyA0 Nightlight', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1100', @@ -2533,6 +2579,7 @@ 'original_name': 'ArloBabyA0 Air Quality', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_800_802', @@ -2578,6 +2625,7 @@ 'original_name': 'ArloBabyA0 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_700', @@ -2625,6 +2673,7 @@ 'original_name': 'ArloBabyA0 Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_900', @@ -2671,6 +2720,7 @@ 'original_name': 'ArloBabyA0 Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1000', @@ -2715,6 +2765,7 @@ 'original_name': 'ArloBabyA0 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_300_302', @@ -2756,6 +2807,7 @@ 'original_name': 'ArloBabyA0 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_400_402', @@ -2840,6 +2892,7 @@ 'original_name': 'InWall Outlet-0394DE Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -2884,6 +2937,7 @@ 'original_name': 'InWall Outlet-0394DE Current', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_13_18', @@ -2930,6 +2984,7 @@ 'original_name': 'InWall Outlet-0394DE Current', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_25_30', @@ -2976,6 +3031,7 @@ 'original_name': 'InWall Outlet-0394DE Energy kWh', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_13_20', @@ -3022,6 +3078,7 @@ 'original_name': 'InWall Outlet-0394DE Energy kWh', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_25_32', @@ -3068,6 +3125,7 @@ 'original_name': 'InWall Outlet-0394DE Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_13_19', @@ -3114,6 +3172,7 @@ 'original_name': 'InWall Outlet-0394DE Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_25_31', @@ -3158,6 +3217,7 @@ 'original_name': 'InWall Outlet-0394DE Outlet A', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_13', @@ -3200,6 +3260,7 @@ 'original_name': 'InWall Outlet-0394DE Outlet B', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_25', @@ -3285,6 +3346,7 @@ 'original_name': 'Basement', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_56', @@ -3327,6 +3389,7 @@ 'original_name': 'Basement Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_1_4101', @@ -3371,6 +3434,7 @@ 'original_name': 'Basement Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_55', @@ -3454,6 +3518,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_56', @@ -3496,6 +3561,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_57', @@ -3538,6 +3604,7 @@ 'original_name': 'HomeW Clear Hold', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_48', @@ -3579,6 +3646,7 @@ 'original_name': 'HomeW Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -3632,6 +3700,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -3697,6 +3766,7 @@ 'original_name': 'HomeW Current Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecobee_mode', 'unique_id': '00:00:00:00:00:00_1_16_33', @@ -3748,6 +3818,7 @@ 'original_name': 'HomeW Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_16_21', @@ -3795,6 +3866,7 @@ 'original_name': 'HomeW Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_24', @@ -3841,6 +3913,7 @@ 'original_name': 'HomeW Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_19', @@ -3924,6 +3997,7 @@ 'original_name': 'Kitchen', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_56', @@ -3966,6 +4040,7 @@ 'original_name': 'Kitchen Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_2053', @@ -4010,6 +4085,7 @@ 'original_name': 'Kitchen Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_55', @@ -4093,6 +4169,7 @@ 'original_name': 'Porch', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_56', @@ -4135,6 +4212,7 @@ 'original_name': 'Porch Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_3077', @@ -4179,6 +4257,7 @@ 'original_name': 'Porch Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_55', @@ -4266,6 +4345,7 @@ 'original_name': 'Basement Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608960_56', @@ -4308,6 +4388,7 @@ 'original_name': 'Basement Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608960_57', @@ -4350,6 +4431,7 @@ 'original_name': 'Basement Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608960_1_6', @@ -4394,6 +4476,7 @@ 'original_name': 'Basement Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608960_192', @@ -4441,6 +4524,7 @@ 'original_name': 'Basement Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608960_208', @@ -4524,6 +4608,7 @@ 'original_name': 'Basement Window 1 Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360914_224', @@ -4566,6 +4651,7 @@ 'original_name': 'Basement Window 1 Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360914_56', @@ -4608,6 +4694,7 @@ 'original_name': 'Basement Window 1 Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360914_57', @@ -4650,6 +4737,7 @@ 'original_name': 'Basement Window 1 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360914_1_6', @@ -4694,6 +4782,7 @@ 'original_name': 'Basement Window 1 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360914_192', @@ -4778,6 +4867,7 @@ 'original_name': 'Deck Door Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360921_224', @@ -4820,6 +4910,7 @@ 'original_name': 'Deck Door Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360921_56', @@ -4862,6 +4953,7 @@ 'original_name': 'Deck Door Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360921_57', @@ -4904,6 +4996,7 @@ 'original_name': 'Deck Door Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360921_1_6', @@ -4948,6 +5041,7 @@ 'original_name': 'Deck Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360921_192', @@ -5032,6 +5126,7 @@ 'original_name': 'Front Door Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527970_224', @@ -5074,6 +5169,7 @@ 'original_name': 'Front Door Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527970_56', @@ -5116,6 +5212,7 @@ 'original_name': 'Front Door Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527970_57', @@ -5158,6 +5255,7 @@ 'original_name': 'Front Door Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527970_1_6', @@ -5202,6 +5300,7 @@ 'original_name': 'Front Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527970_192', @@ -5286,6 +5385,7 @@ 'original_name': 'Garage Door Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527962_224', @@ -5328,6 +5428,7 @@ 'original_name': 'Garage Door Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527962_56', @@ -5370,6 +5471,7 @@ 'original_name': 'Garage Door Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527962_57', @@ -5412,6 +5514,7 @@ 'original_name': 'Garage Door Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527962_1_6', @@ -5456,6 +5559,7 @@ 'original_name': 'Garage Door Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298527962_192', @@ -5540,6 +5644,7 @@ 'original_name': 'Living Room Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016858_56', @@ -5582,6 +5687,7 @@ 'original_name': 'Living Room Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016858_57', @@ -5624,6 +5730,7 @@ 'original_name': 'Living Room Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016858_1_6', @@ -5668,6 +5775,7 @@ 'original_name': 'Living Room Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016858_192', @@ -5715,6 +5823,7 @@ 'original_name': 'Living Room Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016858_208', @@ -5798,6 +5907,7 @@ 'original_name': 'Living Room Window 1 Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360712_224', @@ -5840,6 +5950,7 @@ 'original_name': 'Living Room Window 1 Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360712_56', @@ -5882,6 +5993,7 @@ 'original_name': 'Living Room Window 1 Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360712_57', @@ -5924,6 +6036,7 @@ 'original_name': 'Living Room Window 1 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360712_1_6', @@ -5968,6 +6081,7 @@ 'original_name': 'Living Room Window 1 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298360712_192', @@ -6052,6 +6166,7 @@ 'original_name': 'Loft window Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298649931_224', @@ -6094,6 +6209,7 @@ 'original_name': 'Loft window Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298649931_56', @@ -6136,6 +6252,7 @@ 'original_name': 'Loft window Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298649931_57', @@ -6178,6 +6295,7 @@ 'original_name': 'Loft window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298649931_1_6', @@ -6222,6 +6340,7 @@ 'original_name': 'Loft window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298649931_192', @@ -6306,6 +6425,7 @@ 'original_name': 'Master BR Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608971_56', @@ -6348,6 +6468,7 @@ 'original_name': 'Master BR Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608971_57', @@ -6390,6 +6511,7 @@ 'original_name': 'Master BR Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608971_1_6', @@ -6434,6 +6556,7 @@ 'original_name': 'Master BR Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608971_192', @@ -6481,6 +6604,7 @@ 'original_name': 'Master BR Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295608971_208', @@ -6564,6 +6688,7 @@ 'original_name': 'Master BR Window Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298584118_224', @@ -6606,6 +6731,7 @@ 'original_name': 'Master BR Window Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298584118_56', @@ -6648,6 +6774,7 @@ 'original_name': 'Master BR Window Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298584118_57', @@ -6690,6 +6817,7 @@ 'original_name': 'Master BR Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298584118_1_6', @@ -6734,6 +6862,7 @@ 'original_name': 'Master BR Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298584118_192', @@ -6818,6 +6947,7 @@ 'original_name': 'Thermostat Clear Hold', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_48', @@ -6859,6 +6989,7 @@ 'original_name': 'Thermostat Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -6914,6 +7045,7 @@ 'original_name': 'Thermostat', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -6981,6 +7113,7 @@ 'original_name': 'Thermostat Current Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecobee_mode', 'unique_id': '00:00:00:00:00:00_1_16_33', @@ -7032,6 +7165,7 @@ 'original_name': 'Thermostat Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_16_21', @@ -7079,6 +7213,7 @@ 'original_name': 'Thermostat Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_24', @@ -7125,6 +7260,7 @@ 'original_name': 'Thermostat Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_19', @@ -7208,6 +7344,7 @@ 'original_name': 'Upstairs BR Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016969_56', @@ -7250,6 +7387,7 @@ 'original_name': 'Upstairs BR Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016969_57', @@ -7292,6 +7430,7 @@ 'original_name': 'Upstairs BR Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016969_1_6', @@ -7336,6 +7475,7 @@ 'original_name': 'Upstairs BR Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016969_192', @@ -7383,6 +7523,7 @@ 'original_name': 'Upstairs BR Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4295016969_208', @@ -7466,6 +7607,7 @@ 'original_name': 'Upstairs BR Window Contact', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298568508_224', @@ -7508,6 +7650,7 @@ 'original_name': 'Upstairs BR Window Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298568508_56', @@ -7550,6 +7693,7 @@ 'original_name': 'Upstairs BR Window Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298568508_57', @@ -7592,6 +7736,7 @@ 'original_name': 'Upstairs BR Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298568508_1_6', @@ -7636,6 +7781,7 @@ 'original_name': 'Upstairs BR Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4298568508_192', @@ -7724,6 +7870,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_56', @@ -7766,6 +7913,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_57', @@ -7808,6 +7956,7 @@ 'original_name': 'HomeW Clear Hold', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_48', @@ -7849,6 +7998,7 @@ 'original_name': 'HomeW Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -7902,6 +8052,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -7967,6 +8118,7 @@ 'original_name': 'HomeW Current Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecobee_mode', 'unique_id': '00:00:00:00:00:00_1_16_33', @@ -8018,6 +8170,7 @@ 'original_name': 'HomeW Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_16_21', @@ -8065,6 +8218,7 @@ 'original_name': 'HomeW Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_24', @@ -8111,6 +8265,7 @@ 'original_name': 'HomeW Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_19', @@ -8198,6 +8353,7 @@ 'original_name': 'Basement', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_56', @@ -8240,6 +8396,7 @@ 'original_name': 'Basement Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_1_4101', @@ -8321,6 +8478,7 @@ 'original_name': 'HomeW Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -8374,6 +8532,7 @@ 'original_name': 'HomeW', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -8438,6 +8597,7 @@ 'original_name': 'HomeW Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_16_21', @@ -8485,6 +8645,7 @@ 'original_name': 'HomeW Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_24', @@ -8531,6 +8692,7 @@ 'original_name': 'HomeW Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_19', @@ -8614,6 +8776,7 @@ 'original_name': 'Kitchen', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_56', @@ -8656,6 +8819,7 @@ 'original_name': 'Kitchen Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_2053', @@ -8700,6 +8864,7 @@ 'original_name': 'Kitchen Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_55', @@ -8783,6 +8948,7 @@ 'original_name': 'Porch', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_56', @@ -8825,6 +8991,7 @@ 'original_name': 'Porch Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_3077', @@ -8869,6 +9036,7 @@ 'original_name': 'Porch Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_55', @@ -8956,6 +9124,7 @@ 'original_name': 'My ecobee Motion', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_56', @@ -8998,6 +9167,7 @@ 'original_name': 'My ecobee Occupancy', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_57', @@ -9040,6 +9210,7 @@ 'original_name': 'My ecobee Clear Hold', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_48', @@ -9081,6 +9252,7 @@ 'original_name': 'My ecobee Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -9138,6 +9310,7 @@ 'original_name': 'My ecobee', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -9208,6 +9381,7 @@ 'original_name': 'My ecobee Current Mode', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecobee_mode', 'unique_id': '00:00:00:00:00:00_1_16_33', @@ -9259,6 +9433,7 @@ 'original_name': 'My ecobee Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_16_21', @@ -9306,6 +9481,7 @@ 'original_name': 'My ecobee Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_24', @@ -9352,6 +9528,7 @@ 'original_name': 'My ecobee Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16_19', @@ -9439,6 +9616,7 @@ 'original_name': 'Master Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_56', @@ -9481,6 +9659,7 @@ 'original_name': 'Master Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_57', @@ -9523,6 +9702,7 @@ 'original_name': 'Master Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -9567,6 +9747,7 @@ 'original_name': 'Master Fan Light Level', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_27', @@ -9613,6 +9794,7 @@ 'original_name': 'Master Fan Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_55', @@ -9657,6 +9839,7 @@ 'original_name': 'Master Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_16', @@ -9741,6 +9924,7 @@ 'original_name': 'Eve Degree AA11 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_3', @@ -9788,6 +9972,7 @@ 'original_name': 'Eve Degree AA11 Elevation', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elevation', 'unique_id': '00:00:00:00:00:00_1_30_33', @@ -9838,6 +10023,7 @@ 'original_name': 'Eve Degree AA11 Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_22_25', @@ -9885,6 +10071,7 @@ 'original_name': 'Eve Degree AA11 Air Pressure', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_30_32', @@ -9931,6 +10118,7 @@ 'original_name': 'Eve Degree AA11 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_17', @@ -9978,6 +10166,7 @@ 'original_name': 'Eve Degree AA11 Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_27', @@ -10024,6 +10213,7 @@ 'original_name': 'Eve Degree AA11 Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_22', @@ -10111,6 +10301,7 @@ 'original_name': 'Eve Energy 50FF Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_3', @@ -10155,6 +10346,7 @@ 'original_name': 'Eve Energy 50FF Amps', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_28_33', @@ -10201,6 +10393,7 @@ 'original_name': 'Eve Energy 50FF Energy kWh', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_28_35', @@ -10247,6 +10440,7 @@ 'original_name': 'Eve Energy 50FF Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_28_34', @@ -10293,6 +10487,7 @@ 'original_name': 'Eve Energy 50FF Volts', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_28_32', @@ -10337,6 +10532,7 @@ 'original_name': 'Eve Energy 50FF', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_28', @@ -10379,6 +10575,7 @@ 'original_name': 'Eve Energy 50FF Lock Physical Controls', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock_physical_controls', 'unique_id': '00:00:00:00:00:00_1_28_36', @@ -10463,6 +10660,7 @@ 'original_name': 'HAA-C718B3 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -10505,6 +10703,7 @@ 'original_name': 'HAA-C718B3 Setup', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'setup', 'unique_id': '00:00:00:00:00:00_1_1010_1012', @@ -10546,6 +10745,7 @@ 'original_name': 'HAA-C718B3 Update', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1010_1011', @@ -10591,6 +10791,7 @@ 'original_name': 'HAA-C718B3', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -10681,6 +10882,7 @@ 'original_name': 'HAA-C718B3 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -10723,6 +10925,7 @@ 'original_name': 'HAA-C718B3', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -10807,6 +11010,7 @@ 'original_name': 'Family Room North Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_1_155', @@ -10849,6 +11053,7 @@ 'original_name': 'Family Room North', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_166', @@ -10894,6 +11099,7 @@ 'original_name': 'Family Room North Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_162', @@ -10978,6 +11184,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -11059,6 +11266,7 @@ 'original_name': 'Kitchen Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_1_2', @@ -11101,6 +11309,7 @@ 'original_name': 'Kitchen Window', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_13', @@ -11146,6 +11355,7 @@ 'original_name': 'Kitchen Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_9', @@ -11234,6 +11444,7 @@ 'original_name': 'Ceiling Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_1_2', @@ -11279,6 +11490,7 @@ 'original_name': 'Ceiling Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_8', @@ -11365,6 +11577,7 @@ 'original_name': 'Home Assistant Bridge Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -11446,6 +11659,7 @@ 'original_name': 'Living Room Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_1_2', @@ -11491,6 +11705,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', @@ -11582,6 +11797,7 @@ 'original_name': '89 Living Room Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_1_163', @@ -11634,6 +11850,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169', @@ -11691,6 +11908,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_175', @@ -11745,6 +11963,7 @@ 'original_name': '89 Living Room Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1233851541_169_174', @@ -11792,6 +12011,7 @@ 'original_name': '89 Living Room Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169_180', @@ -11838,6 +12058,7 @@ 'original_name': '89 Living Room Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169_172', @@ -11921,6 +12142,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -12006,6 +12228,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -12087,6 +12310,7 @@ 'original_name': 'Laundry Smoke ED78 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_1_597', @@ -12133,6 +12357,7 @@ 'original_name': 'Laundry Smoke ED78', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_608', @@ -12182,6 +12407,7 @@ 'original_name': 'Laundry Smoke ED78 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_604', @@ -12270,6 +12496,7 @@ 'original_name': 'Family Room North Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_1_155', @@ -12312,6 +12539,7 @@ 'original_name': 'Family Room North', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_166', @@ -12357,6 +12585,7 @@ 'original_name': 'Family Room North Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_123016423_162', @@ -12441,6 +12670,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -12522,6 +12752,7 @@ 'original_name': 'Kitchen Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_1_2', @@ -12564,6 +12795,7 @@ 'original_name': 'Kitchen Window', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_13', @@ -12609,6 +12841,7 @@ 'original_name': 'Kitchen Window Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_878448248_9', @@ -12697,6 +12930,7 @@ 'original_name': 'Ceiling Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_1_2', @@ -12742,6 +12976,7 @@ 'original_name': 'Ceiling Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_766313939_8', @@ -12828,6 +13063,7 @@ 'original_name': 'Home Assistant Bridge Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -12909,6 +13145,7 @@ 'original_name': 'Living Room Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_1_2', @@ -12954,6 +13191,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', @@ -13046,6 +13284,7 @@ 'original_name': 'Home Assistant Bridge Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -13127,6 +13366,7 @@ 'original_name': 'Living Room Fan Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_1_2', @@ -13172,6 +13412,7 @@ 'original_name': 'Living Room Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1256851357_8', @@ -13264,6 +13505,7 @@ 'original_name': '89 Living Room Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_1_163', @@ -13320,6 +13562,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169', @@ -13382,6 +13625,7 @@ 'original_name': '89 Living Room', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_175', @@ -13436,6 +13680,7 @@ 'original_name': '89 Living Room Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1233851541_169_174', @@ -13483,6 +13728,7 @@ 'original_name': '89 Living Room Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169_180', @@ -13529,6 +13775,7 @@ 'original_name': '89 Living Room Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1233851541_169_172', @@ -13612,6 +13859,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -13697,6 +13945,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -13778,6 +14027,7 @@ 'original_name': 'Humidifier 182A Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_1_2', @@ -13827,6 +14077,7 @@ 'original_name': 'Humidifier 182A', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_8', @@ -13881,6 +14132,7 @@ 'original_name': 'Humidifier 182A Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_8_9', @@ -13968,6 +14220,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -14049,6 +14302,7 @@ 'original_name': 'Humidifier 182A Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_1_2', @@ -14098,6 +14352,7 @@ 'original_name': 'Humidifier 182A', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_8', @@ -14152,6 +14407,7 @@ 'original_name': 'Humidifier 182A Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_293334836_8_9', @@ -14239,6 +14495,7 @@ 'original_name': 'HASS Bridge S6 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -14320,6 +14577,7 @@ 'original_name': 'Laundry Smoke ED78 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_1_597', @@ -14371,6 +14629,7 @@ 'original_name': 'Laundry Smoke ED78', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_608', @@ -14430,6 +14689,7 @@ 'original_name': 'Laundry Smoke ED78 Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3982136094_604', @@ -14518,6 +14778,7 @@ 'original_name': 'Air Conditioner Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -14576,6 +14837,7 @@ 'original_name': 'Air Conditioner SlaveID 1', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_9', @@ -14639,6 +14901,7 @@ 'original_name': 'Air Conditioner Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_9_11', @@ -14726,6 +14989,7 @@ 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462395276914_1_6', @@ -14776,6 +15040,7 @@ 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462395276914_2816', @@ -14871,6 +15136,7 @@ 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462395276939_1_6', @@ -14921,6 +15187,7 @@ 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462395276939_2816', @@ -15016,6 +15283,7 @@ 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462403113447_1_6', @@ -15066,6 +15334,7 @@ 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462403113447_2816', @@ -15161,6 +15430,7 @@ 'original_name': 'Hue ambiance candle Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462403233419_1_6', @@ -15211,6 +15481,7 @@ 'original_name': 'Hue ambiance candle', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462403233419_2816', @@ -15306,6 +15577,7 @@ 'original_name': 'Hue ambiance spot Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462412411853_1_6', @@ -15356,6 +15628,7 @@ 'original_name': 'Hue ambiance spot', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462412411853_2816', @@ -15461,6 +15734,7 @@ 'original_name': 'Hue ambiance spot Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462412413293_1_6', @@ -15511,6 +15785,7 @@ 'original_name': 'Hue ambiance spot', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462412413293_2816', @@ -15616,6 +15891,7 @@ 'original_name': 'Hue dimmer switch Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462389072572_1_22', @@ -15662,6 +15938,7 @@ 'original_name': 'Hue dimmer switch button 1', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00:00:00:00:00:00_6623462389072572_588410585088', @@ -15712,6 +15989,7 @@ 'original_name': 'Hue dimmer switch button 2', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00:00:00:00:00:00_6623462389072572_588410650624', @@ -15762,6 +16040,7 @@ 'original_name': 'Hue dimmer switch button 3', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00:00:00:00:00:00_6623462389072572_588410716160', @@ -15812,6 +16091,7 @@ 'original_name': 'Hue dimmer switch button 4', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00:00:00:00:00:00_6623462389072572_588410781696', @@ -15860,6 +16140,7 @@ 'original_name': 'Hue dimmer switch battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462389072572_644245094400', @@ -15944,6 +16225,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462378982941_1_6', @@ -15990,6 +16272,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462378982941_2816', @@ -16076,6 +16359,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462378983942_1_6', @@ -16122,6 +16406,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462378983942_2816', @@ -16208,6 +16493,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462379122122_1_6', @@ -16254,6 +16540,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462379122122_2816', @@ -16340,6 +16627,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462379123707_1_6', @@ -16386,6 +16674,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462379123707_2816', @@ -16472,6 +16761,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462383114163_1_6', @@ -16518,6 +16808,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462383114163_2816', @@ -16604,6 +16895,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462383114193_1_6', @@ -16650,6 +16942,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462383114193_2816', @@ -16736,6 +17029,7 @@ 'original_name': 'Hue white lamp Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462385996792_1_6', @@ -16782,6 +17076,7 @@ 'original_name': 'Hue white lamp', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_6623462385996792_2816', @@ -16868,6 +17163,7 @@ 'original_name': 'Philips hue - 482544 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -16953,6 +17249,7 @@ 'original_name': 'Koogeek-LS1-20833F Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -17004,6 +17301,7 @@ 'original_name': 'Koogeek-LS1-20833F Light Strip', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_7', @@ -17104,6 +17402,7 @@ 'original_name': 'Koogeek-P1-A00AA0 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -17148,6 +17447,7 @@ 'original_name': 'Koogeek-P1-A00AA0 Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_21_22', @@ -17192,6 +17492,7 @@ 'original_name': 'Koogeek-P1-A00AA0 outlet', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_7', @@ -17277,6 +17578,7 @@ 'original_name': 'Koogeek-SW2-187A91 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -17321,6 +17623,7 @@ 'original_name': 'Koogeek-SW2-187A91 Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_14_18', @@ -17365,6 +17668,7 @@ 'original_name': 'Koogeek-SW2-187A91 Switch 1', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -17406,6 +17710,7 @@ 'original_name': 'Koogeek-SW2-187A91 Switch 2', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_11', @@ -17490,6 +17795,7 @@ 'original_name': 'Lennox Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -17541,6 +17847,7 @@ 'original_name': 'Lennox', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_100', @@ -17602,6 +17909,7 @@ 'original_name': 'Lennox Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_100_105', @@ -17649,6 +17957,7 @@ 'original_name': 'Lennox Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_100_107', @@ -17695,6 +18004,7 @@ 'original_name': 'Lennox Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_100_103', @@ -17782,6 +18092,7 @@ 'original_name': 'LG webOS TV AF80 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -17834,6 +18145,7 @@ 'original_name': 'LG webOS TV AF80', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_48', @@ -17887,6 +18199,7 @@ 'original_name': 'LG webOS TV AF80 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_80_82', @@ -17971,6 +18284,7 @@ 'original_name': 'Caséta® Wireless Fan Speed Control Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_21474836482_1_85899345921', @@ -18016,6 +18330,7 @@ 'original_name': 'Caséta® Wireless Fan Speed Control', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_21474836482_2', @@ -18102,6 +18417,7 @@ 'original_name': 'Smart Bridge 2 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_85899345921', @@ -18187,6 +18503,7 @@ 'original_name': 'MSS425F-15cc Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -18229,6 +18546,7 @@ 'original_name': 'MSS425F-15cc Outlet-1', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_12', @@ -18270,6 +18588,7 @@ 'original_name': 'MSS425F-15cc Outlet-2', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_15', @@ -18311,6 +18630,7 @@ 'original_name': 'MSS425F-15cc Outlet-3', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_18', @@ -18352,6 +18672,7 @@ 'original_name': 'MSS425F-15cc Outlet-4', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_21', @@ -18393,6 +18714,7 @@ 'original_name': 'MSS425F-15cc USB', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_24', @@ -18477,6 +18799,7 @@ 'original_name': 'MSS565-28da Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -18523,6 +18846,7 @@ 'original_name': 'MSS565-28da Dimmer Switch', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_12', @@ -18613,6 +18937,7 @@ 'original_name': 'Mysa-85dda9 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -18664,6 +18989,7 @@ 'original_name': 'Mysa-85dda9 Thermostat', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_20', @@ -18722,6 +19048,7 @@ 'original_name': 'Mysa-85dda9 Display', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_40', @@ -18774,6 +19101,7 @@ 'original_name': 'Mysa-85dda9 Temperature Display Units', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_units', 'unique_id': '00:00:00:00:00:00_1_20_26', @@ -18821,6 +19149,7 @@ 'original_name': 'Mysa-85dda9 Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_20_27', @@ -18867,6 +19196,7 @@ 'original_name': 'Mysa-85dda9 Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_20_25', @@ -18954,6 +19284,7 @@ 'original_name': 'Nanoleaf Strip 3B32 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -19005,6 +19336,7 @@ 'original_name': 'Nanoleaf Strip 3B32 Nanoleaf Light Strip', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_19', @@ -19081,6 +19413,7 @@ 'original_name': 'Nanoleaf Strip 3B32 Thread Capabilities', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thread_node_capabilities', 'unique_id': '00:00:00:00:00:00_1_31_115', @@ -19141,6 +19474,7 @@ 'original_name': 'Nanoleaf Strip 3B32 Thread Status', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thread_status', 'unique_id': '00:00:00:00:00:00_1_31_117', @@ -19235,6 +19569,7 @@ 'original_name': 'Netatmo-Doorbell-g738658 Motion Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_10', @@ -19277,6 +19612,7 @@ 'original_name': 'Netatmo-Doorbell-g738658 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -19319,6 +19655,7 @@ 'original_name': 'Netatmo-Doorbell-g738658', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1', @@ -19367,6 +19704,7 @@ 'original_name': 'Netatmo-Doorbell-g738658', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doorbell', 'unique_id': '00:00:00:00:00:00_1_49', @@ -19415,6 +19753,7 @@ 'original_name': 'Netatmo-Doorbell-g738658 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_51_52', @@ -19456,6 +19795,7 @@ 'original_name': 'Netatmo-Doorbell-g738658 Mute', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mute', 'unique_id': '00:00:00:00:00:00_1_8_9', @@ -19540,6 +19880,7 @@ 'original_name': 'Smart CO Alarm Carbon Monoxide Sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_22', @@ -19582,6 +19923,7 @@ 'original_name': 'Smart CO Alarm Low Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_36', @@ -19624,6 +19966,7 @@ 'original_name': 'Smart CO Alarm Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_7_3', @@ -19709,6 +20052,7 @@ 'original_name': 'Healthy Home Coach Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -19753,6 +20097,7 @@ 'original_name': 'Healthy Home Coach Air Quality', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_24_8', @@ -19798,6 +20143,7 @@ 'original_name': 'Healthy Home Coach Carbon Dioxide sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_10', @@ -19844,6 +20190,7 @@ 'original_name': 'Healthy Home Coach Humidity sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_14', @@ -19890,6 +20237,7 @@ 'original_name': 'Healthy Home Coach Noise', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_20_21', @@ -19936,6 +20284,7 @@ 'original_name': 'Healthy Home Coach Temperature sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_17', @@ -20023,6 +20372,7 @@ 'original_name': 'RainMachine-00ce4a Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -20065,6 +20415,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_512', @@ -20109,6 +20460,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_768', @@ -20153,6 +20505,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_1024', @@ -20197,6 +20550,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_1280', @@ -20241,6 +20595,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_1536', @@ -20285,6 +20640,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_1792', @@ -20329,6 +20685,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_2048', @@ -20373,6 +20730,7 @@ 'original_name': 'RainMachine-00ce4a', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_2304', @@ -20460,6 +20818,7 @@ 'original_name': 'Master Bath South Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_2', @@ -20502,6 +20861,7 @@ 'original_name': 'Master Bath South RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_48', @@ -20547,6 +20907,7 @@ 'original_name': 'Master Bath South RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_64', @@ -20631,6 +20992,7 @@ 'original_name': 'RYSE SmartBridge Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -20712,6 +21074,7 @@ 'original_name': 'RYSE SmartShade Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_2', @@ -20754,6 +21117,7 @@ 'original_name': 'RYSE SmartShade RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_48', @@ -20799,6 +21163,7 @@ 'original_name': 'RYSE SmartShade RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_64', @@ -20887,6 +21252,7 @@ 'original_name': 'BR Left Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_1_2', @@ -20929,6 +21295,7 @@ 'original_name': 'BR Left RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_48', @@ -20974,6 +21341,7 @@ 'original_name': 'BR Left RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_4_64', @@ -21058,6 +21426,7 @@ 'original_name': 'LR Left Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_2', @@ -21100,6 +21469,7 @@ 'original_name': 'LR Left RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_48', @@ -21145,6 +21515,7 @@ 'original_name': 'LR Left RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_64', @@ -21229,6 +21600,7 @@ 'original_name': 'LR Right Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_2', @@ -21271,6 +21643,7 @@ 'original_name': 'LR Right RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_48', @@ -21316,6 +21689,7 @@ 'original_name': 'LR Right RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_64', @@ -21400,6 +21774,7 @@ 'original_name': 'RYSE SmartBridge Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -21481,6 +21856,7 @@ 'original_name': 'RZSS Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_5_1_2', @@ -21523,6 +21899,7 @@ 'original_name': 'RZSS RYSE Shade', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_5_48', @@ -21568,6 +21945,7 @@ 'original_name': 'RZSS RYSE Shade Battery', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_5_64', @@ -21656,6 +22034,7 @@ 'original_name': 'SENSE Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_3', @@ -21698,6 +22077,7 @@ 'original_name': 'SENSE Lock Mechanism', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_30', @@ -21783,6 +22163,7 @@ 'original_name': 'SIMPLEconnect Fan-06F674 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -21828,6 +22209,7 @@ 'original_name': 'SIMPLEconnect Fan-06F674 Hunter Fan', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -21880,6 +22262,7 @@ 'original_name': 'SIMPLEconnect Fan-06F674 Hunter Light', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_29', @@ -21970,6 +22353,7 @@ 'original_name': 'VELUX Internal Cover Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -22012,6 +22396,7 @@ 'original_name': 'VELUX Internal Cover Venetian Blinds', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -22099,6 +22484,7 @@ 'original_name': 'U by Moen-015F44 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -22151,6 +22537,7 @@ 'original_name': 'U by Moen-015F44', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_11', @@ -22207,6 +22594,7 @@ 'original_name': 'U by Moen-015F44 Current Temperature', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_11_13', @@ -22251,6 +22639,7 @@ 'original_name': 'U by Moen-015F44', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -22292,6 +22681,7 @@ 'original_name': 'U by Moen-015F44 Outlet 1', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_17', @@ -22334,6 +22724,7 @@ 'original_name': 'U by Moen-015F44 Outlet 2', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_22', @@ -22376,6 +22767,7 @@ 'original_name': 'U by Moen-015F44 Outlet 3', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_27', @@ -22418,6 +22810,7 @@ 'original_name': 'U by Moen-015F44 Outlet 4', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve', 'unique_id': '00:00:00:00:00:00_1_32', @@ -22503,6 +22896,7 @@ 'original_name': 'VELUX Sensor Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -22547,6 +22941,7 @@ 'original_name': 'VELUX Sensor Carbon Dioxide sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_14', @@ -22593,6 +22988,7 @@ 'original_name': 'VELUX Sensor Humidity sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_11', @@ -22639,6 +23035,7 @@ 'original_name': 'VELUX Sensor Temperature sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -22726,6 +23123,7 @@ 'original_name': 'VELUX Gateway Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_6', @@ -22807,6 +23205,7 @@ 'original_name': 'VELUX Sensor Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_1_7', @@ -22851,6 +23250,7 @@ 'original_name': 'VELUX Sensor Carbon Dioxide sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_14', @@ -22897,6 +23297,7 @@ 'original_name': 'VELUX Sensor Humidity sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_11', @@ -22943,6 +23344,7 @@ 'original_name': 'VELUX Sensor Temperature sensor', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_2_8', @@ -23026,6 +23428,7 @@ 'original_name': 'VELUX Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_1_7', @@ -23068,6 +23471,7 @@ 'original_name': 'VELUX Window Roof Window', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_3_8', @@ -23155,6 +23559,7 @@ 'original_name': 'VELUX Window Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -23197,6 +23602,7 @@ 'original_name': 'VELUX Window Roof Window', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -23284,6 +23690,7 @@ 'original_name': 'VELUX External Cover Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_7', @@ -23326,6 +23733,7 @@ 'original_name': 'VELUX External Cover Awning Blinds', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_8', @@ -23412,6 +23820,7 @@ 'original_name': 'VOCOlinc-Flowerbud-0d324b Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -23461,6 +23870,7 @@ 'original_name': 'VOCOlinc-Flowerbud-0d324b', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_30', @@ -23522,6 +23932,7 @@ 'original_name': 'VOCOlinc-Flowerbud-0d324b Mood Light', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_9', @@ -23594,6 +24005,7 @@ 'original_name': 'VOCOlinc-Flowerbud-0d324b Spray Quantity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spray_quantity', 'unique_id': '00:00:00:00:00:00_1_30_38', @@ -23641,6 +24053,7 @@ 'original_name': 'VOCOlinc-Flowerbud-0d324b Current Humidity', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_30_33', @@ -23728,6 +24141,7 @@ 'original_name': 'VOCOlinc-VP3-123456 Identify', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_1_2', @@ -23772,6 +24186,7 @@ 'original_name': 'VOCOlinc-VP3-123456 Power', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_48_97', @@ -23816,6 +24231,7 @@ 'original_name': 'VOCOlinc-VP3-123456 Outlet', 'platform': 'homekit_controller', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:00:00_1_48', diff --git a/tests/components/homewizard/snapshots/test_button.ambr b/tests/components/homewizard/snapshots/test_button.ambr index 16cc62ad726..a07c0745c45 100644 --- a/tests/components/homewizard/snapshots/test_button.ambr +++ b/tests/components/homewizard/snapshots/test_button.ambr @@ -41,6 +41,7 @@ 'original_name': 'Identify', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_identify', diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 1c901bda6f6..3224a0cc63e 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -50,6 +50,7 @@ 'original_name': 'Status light brightness', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_light_brightness', 'unique_id': 'HWE-P1_5c2fafabcdef_status_light_brightness', @@ -144,6 +145,7 @@ 'original_name': 'Status light brightness', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_light_brightness', 'unique_id': 'HWE-P1_5c2fafabcdef_status_light_brightness', diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index f68b5a57d2e..4e73968d113 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -66,6 +66,7 @@ 'original_name': 'Battery cycles', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cycles', 'unique_id': 'HWE-P1_5c2fafabcdef_cycles', @@ -153,6 +154,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -242,6 +244,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -331,6 +334,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -420,6 +424,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -512,6 +517,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -604,6 +610,7 @@ 'original_name': 'State of charge', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_of_charge_pct', 'unique_id': 'HWE-P1_5c2fafabcdef_state_of_charge_pct', @@ -691,6 +698,7 @@ 'original_name': 'Uptime', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': 'HWE-P1_5c2fafabcdef_uptime', @@ -778,6 +786,7 @@ 'original_name': 'Voltage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_v', @@ -867,6 +876,7 @@ 'original_name': 'Wi-Fi RSSI', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_rssi', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_rssi', @@ -953,6 +963,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -1039,6 +1050,7 @@ 'original_name': 'Apparent power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_va', @@ -1128,6 +1140,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -1217,6 +1230,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -1306,6 +1320,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -1395,6 +1410,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -1487,6 +1503,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -1576,6 +1593,7 @@ 'original_name': 'Power factor', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor', @@ -1665,6 +1683,7 @@ 'original_name': 'Reactive power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_var', @@ -1754,6 +1773,7 @@ 'original_name': 'Voltage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_v', @@ -1841,6 +1861,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -1927,6 +1948,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -2015,6 +2037,7 @@ 'original_name': 'Apparent power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_va', @@ -2104,6 +2127,7 @@ 'original_name': 'Apparent power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l1_va', @@ -2193,6 +2217,7 @@ 'original_name': 'Apparent power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l2_va', @@ -2282,6 +2307,7 @@ 'original_name': 'Apparent power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l3_va', @@ -2371,6 +2397,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -2460,6 +2487,7 @@ 'original_name': 'Current phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l1_a', @@ -2549,6 +2577,7 @@ 'original_name': 'Current phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l2_a', @@ -2638,6 +2667,7 @@ 'original_name': 'Current phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l3_a', @@ -2727,6 +2757,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -2816,6 +2847,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -2905,6 +2937,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -2997,6 +3030,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -3086,6 +3120,7 @@ 'original_name': 'Power factor phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l1', @@ -3175,6 +3210,7 @@ 'original_name': 'Power factor phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l2', @@ -3264,6 +3300,7 @@ 'original_name': 'Power factor phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l3', @@ -3356,6 +3393,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -3448,6 +3486,7 @@ 'original_name': 'Power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l2_w', @@ -3540,6 +3579,7 @@ 'original_name': 'Power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l3_w', @@ -3629,6 +3669,7 @@ 'original_name': 'Reactive power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_var', @@ -3718,6 +3759,7 @@ 'original_name': 'Reactive power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l1_var', @@ -3807,6 +3849,7 @@ 'original_name': 'Reactive power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l2_var', @@ -3896,6 +3939,7 @@ 'original_name': 'Reactive power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l3_var', @@ -3985,6 +4029,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l1_v', @@ -4074,6 +4119,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l2_v', @@ -4163,6 +4209,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l3_v', @@ -4250,6 +4297,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -4336,6 +4384,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -4422,6 +4471,7 @@ 'original_name': 'Average demand', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_average_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_average_w', @@ -4510,6 +4560,7 @@ 'original_name': 'Current phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l1_a', @@ -4599,6 +4650,7 @@ 'original_name': 'Current phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l2_a', @@ -4688,6 +4740,7 @@ 'original_name': 'Current phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l3_a', @@ -4775,6 +4828,7 @@ 'original_name': 'DSMR version', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsmr_version', 'unique_id': 'HWE-P1_5c2fafabcdef_smr_version', @@ -4861,6 +4915,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -4950,6 +5005,7 @@ 'original_name': 'Energy export tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t1_kwh', @@ -5039,6 +5095,7 @@ 'original_name': 'Energy export tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t2_kwh', @@ -5128,6 +5185,7 @@ 'original_name': 'Energy export tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t3_kwh', @@ -5217,6 +5275,7 @@ 'original_name': 'Energy export tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t4_kwh', @@ -5306,6 +5365,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -5395,6 +5455,7 @@ 'original_name': 'Energy import tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t1_kwh', @@ -5484,6 +5545,7 @@ 'original_name': 'Energy import tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t2_kwh', @@ -5573,6 +5635,7 @@ 'original_name': 'Energy import tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t3_kwh', @@ -5662,6 +5725,7 @@ 'original_name': 'Energy import tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t4_kwh', @@ -5751,6 +5815,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -5838,6 +5903,7 @@ 'original_name': 'Long power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'long_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_long_power_fail_count', @@ -5922,6 +5988,7 @@ 'original_name': 'Peak demand current month', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_power_peak_w', 'unique_id': 'HWE-P1_5c2fafabcdef_monthly_power_peak_w', @@ -6013,6 +6080,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -6100,6 +6168,7 @@ 'original_name': 'Power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'any_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_any_power_fail_count', @@ -6189,6 +6258,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -6281,6 +6351,7 @@ 'original_name': 'Power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l2_w', @@ -6373,6 +6444,7 @@ 'original_name': 'Power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l3_w', @@ -6460,6 +6532,7 @@ 'original_name': 'Smart meter identifier', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'unique_meter_id', 'unique_id': 'HWE-P1_5c2fafabcdef_unique_meter_id', @@ -6544,6 +6617,7 @@ 'original_name': 'Smart meter model', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_model', 'unique_id': 'HWE-P1_5c2fafabcdef_meter_model', @@ -6635,6 +6709,7 @@ 'original_name': 'Tariff', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_tariff', 'unique_id': 'HWE-P1_5c2fafabcdef_active_tariff', @@ -6728,6 +6803,7 @@ 'original_name': 'Total water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_liter_m3', 'unique_id': 'HWE-P1_5c2fafabcdef_total_liter_m3', @@ -6817,6 +6893,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l1_v', @@ -6906,6 +6983,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l2_v', @@ -6995,6 +7073,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l3_v', @@ -7082,6 +7161,7 @@ 'original_name': 'Voltage sags detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l1_count', @@ -7166,6 +7246,7 @@ 'original_name': 'Voltage sags detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l2_count', @@ -7250,6 +7331,7 @@ 'original_name': 'Voltage sags detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l3_count', @@ -7334,6 +7416,7 @@ 'original_name': 'Voltage swells detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l1_count', @@ -7418,6 +7501,7 @@ 'original_name': 'Voltage swells detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l2_count', @@ -7502,6 +7586,7 @@ 'original_name': 'Voltage swells detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l3_count', @@ -7588,6 +7673,7 @@ 'original_name': 'Water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_liter_lpm', 'unique_id': 'HWE-P1_5c2fafabcdef_active_liter_lpm', @@ -7674,6 +7760,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -7760,6 +7847,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -7844,6 +7932,7 @@ 'original_name': 'Gas', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_gas_meter_G001', @@ -7929,6 +8018,7 @@ 'original_name': 'Energy', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_heat_meter_H001', @@ -8014,6 +8104,7 @@ 'original_name': None, 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_inlet_heat_meter_IH001', @@ -8098,6 +8189,7 @@ 'original_name': 'Water', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_warm_water_meter_WW001', @@ -8183,6 +8275,7 @@ 'original_name': 'Water', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_water_meter_W001', @@ -8270,6 +8363,7 @@ 'original_name': 'Average demand', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_average_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_average_w', @@ -8358,6 +8452,7 @@ 'original_name': 'Current phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l1_a', @@ -8447,6 +8542,7 @@ 'original_name': 'Current phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l2_a', @@ -8536,6 +8632,7 @@ 'original_name': 'Current phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l3_a', @@ -8623,6 +8720,7 @@ 'original_name': 'DSMR version', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsmr_version', 'unique_id': 'HWE-P1_5c2fafabcdef_smr_version', @@ -8709,6 +8807,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -8798,6 +8897,7 @@ 'original_name': 'Energy export tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t1_kwh', @@ -8887,6 +8987,7 @@ 'original_name': 'Energy export tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t2_kwh', @@ -8976,6 +9077,7 @@ 'original_name': 'Energy export tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t3_kwh', @@ -9065,6 +9167,7 @@ 'original_name': 'Energy export tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t4_kwh', @@ -9154,6 +9257,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -9243,6 +9347,7 @@ 'original_name': 'Energy import tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t1_kwh', @@ -9332,6 +9437,7 @@ 'original_name': 'Energy import tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t2_kwh', @@ -9421,6 +9527,7 @@ 'original_name': 'Energy import tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t3_kwh', @@ -9510,6 +9617,7 @@ 'original_name': 'Energy import tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t4_kwh', @@ -9599,6 +9707,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -9686,6 +9795,7 @@ 'original_name': 'Long power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'long_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_long_power_fail_count', @@ -9770,6 +9880,7 @@ 'original_name': 'Peak demand current month', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_power_peak_w', 'unique_id': 'HWE-P1_5c2fafabcdef_monthly_power_peak_w', @@ -9861,6 +9972,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -9948,6 +10060,7 @@ 'original_name': 'Power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'any_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_any_power_fail_count', @@ -10037,6 +10150,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -10129,6 +10243,7 @@ 'original_name': 'Power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l2_w', @@ -10221,6 +10336,7 @@ 'original_name': 'Power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l3_w', @@ -10308,6 +10424,7 @@ 'original_name': 'Smart meter identifier', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'unique_meter_id', 'unique_id': 'HWE-P1_5c2fafabcdef_unique_meter_id', @@ -10392,6 +10509,7 @@ 'original_name': 'Smart meter model', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_model', 'unique_id': 'HWE-P1_5c2fafabcdef_meter_model', @@ -10483,6 +10601,7 @@ 'original_name': 'Tariff', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_tariff', 'unique_id': 'HWE-P1_5c2fafabcdef_active_tariff', @@ -10576,6 +10695,7 @@ 'original_name': 'Total water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_liter_m3', 'unique_id': 'HWE-P1_5c2fafabcdef_total_liter_m3', @@ -10665,6 +10785,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l1_v', @@ -10754,6 +10875,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l2_v', @@ -10843,6 +10965,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l3_v', @@ -10930,6 +11053,7 @@ 'original_name': 'Voltage sags detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l1_count', @@ -11014,6 +11138,7 @@ 'original_name': 'Voltage sags detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l2_count', @@ -11098,6 +11223,7 @@ 'original_name': 'Voltage sags detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l3_count', @@ -11182,6 +11308,7 @@ 'original_name': 'Voltage swells detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l1_count', @@ -11266,6 +11393,7 @@ 'original_name': 'Voltage swells detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l2_count', @@ -11350,6 +11478,7 @@ 'original_name': 'Voltage swells detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l3_count', @@ -11436,6 +11565,7 @@ 'original_name': 'Water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_liter_lpm', 'unique_id': 'HWE-P1_5c2fafabcdef_active_liter_lpm', @@ -11522,6 +11652,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -11608,6 +11739,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -11692,6 +11824,7 @@ 'original_name': 'Gas', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_gas_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -11777,6 +11910,7 @@ 'original_name': 'Energy', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -11862,6 +11996,7 @@ 'original_name': None, 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_inlet_heat_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -11946,6 +12081,7 @@ 'original_name': 'Water', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_warm_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -12031,6 +12167,7 @@ 'original_name': 'Water', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'homewizard_water_meter_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -12118,6 +12255,7 @@ 'original_name': 'Average demand', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_average_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_average_w', @@ -12206,6 +12344,7 @@ 'original_name': 'Current phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l1_a', @@ -12295,6 +12434,7 @@ 'original_name': 'Current phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l2_a', @@ -12384,6 +12524,7 @@ 'original_name': 'Current phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l3_a', @@ -12473,6 +12614,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -12562,6 +12704,7 @@ 'original_name': 'Energy export tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t1_kwh', @@ -12651,6 +12794,7 @@ 'original_name': 'Energy export tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t2_kwh', @@ -12740,6 +12884,7 @@ 'original_name': 'Energy export tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t3_kwh', @@ -12829,6 +12974,7 @@ 'original_name': 'Energy export tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_t4_kwh', @@ -12918,6 +13064,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -13007,6 +13154,7 @@ 'original_name': 'Energy import tariff 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t1_kwh', @@ -13096,6 +13244,7 @@ 'original_name': 'Energy import tariff 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t2_kwh', @@ -13185,6 +13334,7 @@ 'original_name': 'Energy import tariff 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t3_kwh', @@ -13274,6 +13424,7 @@ 'original_name': 'Energy import tariff 4', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_t4_kwh', @@ -13363,6 +13514,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -13450,6 +13602,7 @@ 'original_name': 'Long power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'long_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_long_power_fail_count', @@ -13539,6 +13692,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -13626,6 +13780,7 @@ 'original_name': 'Power failures detected', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'any_power_fail_count', 'unique_id': 'HWE-P1_5c2fafabcdef_any_power_fail_count', @@ -13715,6 +13870,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -13807,6 +13963,7 @@ 'original_name': 'Power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l2_w', @@ -13899,6 +14056,7 @@ 'original_name': 'Power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l3_w', @@ -13988,6 +14146,7 @@ 'original_name': 'Total water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_liter_m3', 'unique_id': 'HWE-P1_5c2fafabcdef_total_liter_m3', @@ -14077,6 +14236,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l1_v', @@ -14166,6 +14326,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l2_v', @@ -14255,6 +14416,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l3_v', @@ -14342,6 +14504,7 @@ 'original_name': 'Voltage sags detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l1_count', @@ -14426,6 +14589,7 @@ 'original_name': 'Voltage sags detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l2_count', @@ -14510,6 +14674,7 @@ 'original_name': 'Voltage sags detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_sag_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_sag_l3_count', @@ -14594,6 +14759,7 @@ 'original_name': 'Voltage swells detected phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l1_count', @@ -14678,6 +14844,7 @@ 'original_name': 'Voltage swells detected phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l2_count', @@ -14762,6 +14929,7 @@ 'original_name': 'Voltage swells detected phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_swell_phase_count', 'unique_id': 'HWE-P1_5c2fafabcdef_voltage_swell_l3_count', @@ -14848,6 +15016,7 @@ 'original_name': 'Water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_liter_lpm', 'unique_id': 'HWE-P1_5c2fafabcdef_active_liter_lpm', @@ -14934,6 +15103,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -15020,6 +15190,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -15108,6 +15279,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -15197,6 +15369,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -15289,6 +15462,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -15381,6 +15555,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -15468,6 +15643,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -15554,6 +15730,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -15642,6 +15819,7 @@ 'original_name': 'Apparent power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_va', @@ -15731,6 +15909,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -15820,6 +15999,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -15909,6 +16089,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -15998,6 +16179,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -16090,6 +16272,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -16179,6 +16362,7 @@ 'original_name': 'Power factor', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor', @@ -16271,6 +16455,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -16360,6 +16545,7 @@ 'original_name': 'Reactive power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_var', @@ -16449,6 +16635,7 @@ 'original_name': 'Voltage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_v', @@ -16536,6 +16723,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -16622,6 +16810,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -16710,6 +16899,7 @@ 'original_name': 'Total water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_liter_m3', 'unique_id': 'HWE-P1_5c2fafabcdef_total_liter_m3', @@ -16799,6 +16989,7 @@ 'original_name': 'Water usage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_liter_lpm', 'unique_id': 'HWE-P1_5c2fafabcdef_active_liter_lpm', @@ -16885,6 +17076,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -16971,6 +17163,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -17059,6 +17252,7 @@ 'original_name': 'Apparent power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_va', @@ -17148,6 +17342,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -17237,6 +17432,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -17326,6 +17522,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -17415,6 +17612,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -17507,6 +17705,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -17596,6 +17795,7 @@ 'original_name': 'Power factor', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor', @@ -17685,6 +17885,7 @@ 'original_name': 'Reactive power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_var', @@ -17774,6 +17975,7 @@ 'original_name': 'Voltage', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_v', @@ -17861,6 +18063,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -17947,6 +18150,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', @@ -18035,6 +18239,7 @@ 'original_name': 'Apparent power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_va', @@ -18124,6 +18329,7 @@ 'original_name': 'Apparent power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l1_va', @@ -18213,6 +18419,7 @@ 'original_name': 'Apparent power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l2_va', @@ -18302,6 +18509,7 @@ 'original_name': 'Apparent power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_apparent_power_phase_va', 'unique_id': 'HWE-P1_5c2fafabcdef_active_apparent_power_l3_va', @@ -18391,6 +18599,7 @@ 'original_name': 'Current', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_a', @@ -18480,6 +18689,7 @@ 'original_name': 'Current phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l1_a', @@ -18569,6 +18779,7 @@ 'original_name': 'Current phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l2_a', @@ -18658,6 +18869,7 @@ 'original_name': 'Current phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'HWE-P1_5c2fafabcdef_active_current_l3_a', @@ -18747,6 +18959,7 @@ 'original_name': 'Energy export', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_export_kwh', @@ -18836,6 +19049,7 @@ 'original_name': 'Energy import', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'HWE-P1_5c2fafabcdef_total_power_import_kwh', @@ -18925,6 +19139,7 @@ 'original_name': 'Frequency', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_frequency_hz', @@ -19017,6 +19232,7 @@ 'original_name': 'Power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_w', @@ -19106,6 +19322,7 @@ 'original_name': 'Power factor phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l1', @@ -19195,6 +19412,7 @@ 'original_name': 'Power factor phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l2', @@ -19284,6 +19502,7 @@ 'original_name': 'Power factor phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_factor_phase', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_factor_l3', @@ -19376,6 +19595,7 @@ 'original_name': 'Power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l1_w', @@ -19468,6 +19688,7 @@ 'original_name': 'Power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l2_w', @@ -19560,6 +19781,7 @@ 'original_name': 'Power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'HWE-P1_5c2fafabcdef_active_power_l3_w', @@ -19649,6 +19871,7 @@ 'original_name': 'Reactive power', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_var', @@ -19738,6 +19961,7 @@ 'original_name': 'Reactive power phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l1_var', @@ -19827,6 +20051,7 @@ 'original_name': 'Reactive power phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l2_var', @@ -19916,6 +20141,7 @@ 'original_name': 'Reactive power phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_reactive_power_phase_var', 'unique_id': 'HWE-P1_5c2fafabcdef_active_reactive_power_l3_var', @@ -20005,6 +20231,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l1_v', @@ -20094,6 +20321,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l2_v', @@ -20183,6 +20411,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'HWE-P1_5c2fafabcdef_active_voltage_l3_v', @@ -20270,6 +20499,7 @@ 'original_name': 'Wi-Fi SSID', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_ssid', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_ssid', @@ -20356,6 +20586,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': 'HWE-P1_5c2fafabcdef_wifi_strength', diff --git a/tests/components/homewizard/snapshots/test_switch.ambr b/tests/components/homewizard/snapshots/test_switch.ambr index cd21cb92819..c4e67003b58 100644 --- a/tests/components/homewizard/snapshots/test_switch.ambr +++ b/tests/components/homewizard/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -124,6 +125,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -209,6 +211,7 @@ 'original_name': None, 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_power_on', @@ -293,6 +296,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -377,6 +381,7 @@ 'original_name': 'Switch lock', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'switch_lock', 'unique_id': 'HWE-P1_5c2fafabcdef_switch_lock', @@ -462,6 +467,7 @@ 'original_name': None, 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'HWE-P1_5c2fafabcdef_power_on', @@ -546,6 +552,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -630,6 +637,7 @@ 'original_name': 'Switch lock', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'switch_lock', 'unique_id': 'HWE-P1_5c2fafabcdef_switch_lock', @@ -714,6 +722,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -798,6 +807,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', @@ -882,6 +892,7 @@ 'original_name': 'Cloud connection', 'platform': 'homewizard', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': 'HWE-P1_5c2fafabcdef_cloud_connection', diff --git a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr index bac9f187001..6c4e8e9e308 100644 --- a/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charging', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_charging', @@ -75,6 +76,7 @@ 'original_name': 'Leaving dock', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaving_dock', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_leaving_dock', @@ -122,6 +124,7 @@ 'original_name': 'Charging', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_battery_charging', @@ -170,6 +173,7 @@ 'original_name': 'Leaving dock', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaving_dock', 'unique_id': '1234_leaving_dock', diff --git a/tests/components/husqvarna_automower/snapshots/test_button.ambr b/tests/components/husqvarna_automower/snapshots/test_button.ambr index 088850c1e07..3d48125aa9a 100644 --- a/tests/components/husqvarna_automower/snapshots/test_button.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Confirm error', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'confirm_error', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_confirm_error', @@ -74,6 +75,7 @@ 'original_name': 'Sync clock', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sync_clock', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_sync_clock', @@ -121,6 +123,7 @@ 'original_name': 'Sync clock', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sync_clock', 'unique_id': '1234_sync_clock', diff --git a/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr index e94eea4087c..acdf083f52c 100644 --- a/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0', diff --git a/tests/components/husqvarna_automower/snapshots/test_number.ambr b/tests/components/husqvarna_automower/snapshots/test_number.ambr index 291aef83dbf..f0f45110b80 100644 --- a/tests/components/husqvarna_automower/snapshots/test_number.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Back lawn cutting height', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_cutting_height_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_654321_cutting_height_work_area', @@ -89,6 +90,7 @@ 'original_name': 'Cutting height', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cutting_height', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_cutting_height', @@ -145,6 +147,7 @@ 'original_name': 'Front lawn cutting height', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_cutting_height_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_cutting_height_work_area', @@ -202,6 +205,7 @@ 'original_name': 'My lawn cutting height', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'my_lawn_cutting_height_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_cutting_height_work_area', diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 979d40a53d8..526474ec08a 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_battery_percent', @@ -84,6 +85,7 @@ 'original_name': 'Cutting blade usage time', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cutting_blade_usage_time', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_cutting_blade_usage_time', @@ -142,6 +144,7 @@ 'original_name': 'Downtime', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'downtime', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_downtime', @@ -325,6 +328,7 @@ 'original_name': 'Error', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_error', @@ -505,6 +509,7 @@ 'original_name': 'Front lawn last time completed', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_last_time_completed', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_last_time_completed', @@ -555,6 +560,7 @@ 'original_name': 'Front lawn progress', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_progress', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_progress', @@ -612,6 +618,7 @@ 'original_name': 'Mode', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_mode', @@ -667,6 +674,7 @@ 'original_name': 'My lawn last time completed', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'my_lawn_last_time_completed', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_last_time_completed', @@ -717,6 +725,7 @@ 'original_name': 'My lawn progress', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'my_lawn_progress', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_progress', @@ -766,6 +775,7 @@ 'original_name': 'Next start', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_start_timestamp', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_next_start_timestamp', @@ -816,6 +826,7 @@ 'original_name': 'Number of charging cycles', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'number_of_charging_cycles', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_number_of_charging_cycles', @@ -866,6 +877,7 @@ 'original_name': 'Number of collisions', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'number_of_collisions', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_number_of_collisions', @@ -927,6 +939,7 @@ 'original_name': 'Restricted reason', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restricted_reason', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_restricted_reason', @@ -992,6 +1005,7 @@ 'original_name': 'Total charging time', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_charging_time', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_total_charging_time', @@ -1047,6 +1061,7 @@ 'original_name': 'Total cutting time', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_cutting_time', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_total_cutting_time', @@ -1102,6 +1117,7 @@ 'original_name': 'Total drive distance', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_drive_distance', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_total_drive_distance', @@ -1157,6 +1173,7 @@ 'original_name': 'Total running time', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_running_time', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_total_running_time', @@ -1212,6 +1229,7 @@ 'original_name': 'Total searching time', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_searching_time', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_total_searching_time', @@ -1270,6 +1288,7 @@ 'original_name': 'Uptime', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_uptime', @@ -1327,6 +1346,7 @@ 'original_name': 'Work area', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_work_area', @@ -1388,6 +1408,7 @@ 'original_name': 'Battery', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234_battery_percent', @@ -1571,6 +1592,7 @@ 'original_name': 'Error', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'error', 'unique_id': '1234_error', @@ -1759,6 +1781,7 @@ 'original_name': 'Mode', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '1234_mode', @@ -1814,6 +1837,7 @@ 'original_name': 'Next start', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_start_timestamp', 'unique_id': '1234_next_start_timestamp', @@ -1875,6 +1899,7 @@ 'original_name': 'Restricted reason', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'restricted_reason', 'unique_id': '1234_restricted_reason', diff --git a/tests/components/husqvarna_automower/snapshots/test_switch.ambr b/tests/components/husqvarna_automower/snapshots/test_switch.ambr index 5e01694e924..a876fc4c1b6 100644 --- a/tests/components/husqvarna_automower/snapshots/test_switch.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Avoid Danger Zone', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stay_out_zones', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_AAAAAAAA-BBBB-CCCC-DDDD-123456789101_stay_out_zones', @@ -74,6 +75,7 @@ 'original_name': 'Avoid Springflowers', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stay_out_zones', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_81C6EEA2-D139-4FEA-B134-F22A6B3EA403_stay_out_zones', @@ -121,6 +123,7 @@ 'original_name': 'Back lawn', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_654321_work_area', @@ -168,6 +171,7 @@ 'original_name': 'Enable schedule', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'enable_schedule', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_enable_schedule', @@ -215,6 +219,7 @@ 'original_name': 'Front lawn', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'work_area_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_123456_work_area', @@ -262,6 +267,7 @@ 'original_name': 'My lawn', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'my_lawn_work_area', 'unique_id': 'c7233734-b219-4287-a173-08e3643f89f0_0_work_area', @@ -309,6 +315,7 @@ 'original_name': 'Enable schedule', 'platform': 'husqvarna_automower', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'enable_schedule', 'unique_id': '1234_enable_schedule', diff --git a/tests/components/hydrawise/snapshots/test_binary_sensor.ambr b/tests/components/hydrawise/snapshots/test_binary_sensor.ambr index 84e52a7f966..30adfea90be 100644 --- a/tests/components/hydrawise/snapshots/test_binary_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Connectivity', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '52496_status', @@ -76,6 +77,7 @@ 'original_name': 'Rain sensor', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rain_sensor', 'unique_id': '52496_rain_sensor', @@ -125,6 +127,7 @@ 'original_name': 'Watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'watering', 'unique_id': '5965394_is_watering', @@ -174,6 +177,7 @@ 'original_name': 'Watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'watering', 'unique_id': '5965395_is_watering', diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index 3e475b1eeb1..c06442a5269 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Daily active water use', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_use', 'unique_id': '52496_daily_active_water_use', @@ -83,6 +84,7 @@ 'original_name': 'Daily active watering time', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_time', 'unique_id': '52496_daily_active_water_time', @@ -139,6 +141,7 @@ 'original_name': 'Daily inactive water use', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_inactive_water_use', 'unique_id': '52496_daily_inactive_water_use', @@ -195,6 +198,7 @@ 'original_name': 'Daily total water use', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_total_water_use', 'unique_id': '52496_daily_total_water_use', @@ -251,6 +255,7 @@ 'original_name': 'Daily active water use', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_use', 'unique_id': '5965394_daily_active_water_use', @@ -301,6 +306,7 @@ 'original_name': 'Daily active watering time', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_time', 'unique_id': '5965394_daily_active_water_time', @@ -351,6 +357,7 @@ 'original_name': 'Next cycle', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_cycle', 'unique_id': '5965394_next_cycle', @@ -400,6 +407,7 @@ 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'watering_time', 'unique_id': '5965394_watering_time', @@ -455,6 +463,7 @@ 'original_name': 'Daily active water use', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_use', 'unique_id': '5965395_daily_active_water_use', @@ -506,6 +515,7 @@ 'original_name': 'Daily active watering time', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_active_water_time', 'unique_id': '5965395_daily_active_water_time', @@ -556,6 +566,7 @@ 'original_name': 'Next cycle', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_cycle', 'unique_id': '5965395_next_cycle', @@ -605,6 +616,7 @@ 'original_name': 'Remaining watering time', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'watering_time', 'unique_id': '5965395_watering_time', diff --git a/tests/components/hydrawise/snapshots/test_switch.ambr b/tests/components/hydrawise/snapshots/test_switch.ambr index 9ad37ddbfbf..684e1d3ac3e 100644 --- a/tests/components/hydrawise/snapshots/test_switch.ambr +++ b/tests/components/hydrawise/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Automatic watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_watering', 'unique_id': '5965394_auto_watering', @@ -76,6 +77,7 @@ 'original_name': 'Manual watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_watering', 'unique_id': '5965394_manual_watering', @@ -125,6 +127,7 @@ 'original_name': 'Automatic watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_watering', 'unique_id': '5965395_auto_watering', @@ -174,6 +177,7 @@ 'original_name': 'Manual watering', 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_watering', 'unique_id': '5965395_manual_watering', diff --git a/tests/components/hydrawise/snapshots/test_valve.ambr b/tests/components/hydrawise/snapshots/test_valve.ambr index 197e7796a07..558c8f12a56 100644 --- a/tests/components/hydrawise/snapshots/test_valve.ambr +++ b/tests/components/hydrawise/snapshots/test_valve.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '5965394_zone', @@ -77,6 +78,7 @@ 'original_name': None, 'platform': 'hydrawise', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '5965395_zone', diff --git a/tests/components/igloohome/snapshots/test_lock.ambr b/tests/components/igloohome/snapshots/test_lock.ambr index 5d94cf27c6b..1d539049411 100644 --- a/tests/components/igloohome/snapshots/test_lock.ambr +++ b/tests/components/igloohome/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'igloohome', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lock_OE1X123cbb11', diff --git a/tests/components/igloohome/snapshots/test_sensor.ambr b/tests/components/igloohome/snapshots/test_sensor.ambr index 9e17343d4fa..c2954ad5f15 100644 --- a/tests/components/igloohome/snapshots/test_sensor.ambr +++ b/tests/components/igloohome/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'igloohome', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'battery_OE1X123cbb11', diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index 38f50df5407..beead7d251b 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Air temperature', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_air_temperature', 'unique_id': '111111111111111_temp_air_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Battery autonomy', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': '111111111111111_battery_autonomy', @@ -133,6 +135,7 @@ 'original_name': 'Battery charge time', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_charge_time', 'unique_id': '111111111111111_battery_charge_time', @@ -185,6 +188,7 @@ 'original_name': 'Battery power', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_power', 'unique_id': '111111111111111_battery_power', @@ -237,6 +241,7 @@ 'original_name': 'Battery state of charge', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_soc', 'unique_id': '111111111111111_battery_soc', @@ -289,6 +294,7 @@ 'original_name': 'Battery stored', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_stored', 'unique_id': '111111111111111_battery_stored', @@ -341,6 +347,7 @@ 'original_name': 'Charging current limit', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'inverter_charging_current_limit', 'unique_id': '111111111111111_inverter_charging_current_limit', @@ -393,6 +400,7 @@ 'original_name': 'Component temperature', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_component_temperature', 'unique_id': '111111111111111_temp_component_temperature', @@ -445,6 +453,7 @@ 'original_name': 'Grid current L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_current_l1', 'unique_id': '111111111111111_grid_current_l1', @@ -497,6 +506,7 @@ 'original_name': 'Grid current L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_current_l2', 'unique_id': '111111111111111_grid_current_l2', @@ -549,6 +559,7 @@ 'original_name': 'Grid current L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_current_l3', 'unique_id': '111111111111111_grid_current_l3', @@ -601,6 +612,7 @@ 'original_name': 'Grid frequency', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_frequency', 'unique_id': '111111111111111_grid_frequency', @@ -653,6 +665,7 @@ 'original_name': 'Grid voltage L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_voltage_l1', 'unique_id': '111111111111111_grid_voltage_l1', @@ -705,6 +718,7 @@ 'original_name': 'Grid voltage L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_voltage_l2', 'unique_id': '111111111111111_grid_voltage_l2', @@ -757,6 +771,7 @@ 'original_name': 'Grid voltage L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_voltage_l3', 'unique_id': '111111111111111_grid_voltage_l3', @@ -809,6 +824,7 @@ 'original_name': 'Injection power limit', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'inverter_injection_power_limit', 'unique_id': '111111111111111_inverter_injection_power_limit', @@ -861,6 +877,7 @@ 'original_name': 'Input power L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_power_l1', 'unique_id': '111111111111111_input_power_l1', @@ -913,6 +930,7 @@ 'original_name': 'Input power L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_power_l2', 'unique_id': '111111111111111_input_power_l2', @@ -965,6 +983,7 @@ 'original_name': 'Input power L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_power_l3', 'unique_id': '111111111111111_input_power_l3', @@ -1017,6 +1036,7 @@ 'original_name': 'Input power total', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'input_power_total', 'unique_id': '111111111111111_input_power_total', @@ -1069,6 +1089,7 @@ 'original_name': 'Meter power', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_power', 'unique_id': '111111111111111_meter_power', @@ -1121,6 +1142,7 @@ 'original_name': 'Meter power protocol', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_power_protocol', 'unique_id': '111111111111111_meter_power_protocol', @@ -1176,6 +1198,7 @@ 'original_name': 'Monitoring building consumption', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_building_consumption', 'unique_id': '111111111111111_monitoring_building_consumption', @@ -1231,6 +1254,7 @@ 'original_name': 'Monitoring building consumption (minute)', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_minute_building_consumption', 'unique_id': '111111111111111_monitoring_minute_building_consumption', @@ -1286,6 +1310,7 @@ 'original_name': 'Monitoring economy factor', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_economy_factor', 'unique_id': '111111111111111_monitoring_economy_factor', @@ -1340,6 +1365,7 @@ 'original_name': 'Monitoring grid consumption', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_grid_consumption', 'unique_id': '111111111111111_monitoring_grid_consumption', @@ -1395,6 +1421,7 @@ 'original_name': 'Monitoring grid consumption (minute)', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_minute_grid_consumption', 'unique_id': '111111111111111_monitoring_minute_grid_consumption', @@ -1450,6 +1477,7 @@ 'original_name': 'Monitoring grid injection', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_grid_injection', 'unique_id': '111111111111111_monitoring_grid_injection', @@ -1505,6 +1533,7 @@ 'original_name': 'Monitoring grid injection (minute)', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_minute_grid_injection', 'unique_id': '111111111111111_monitoring_minute_grid_injection', @@ -1560,6 +1589,7 @@ 'original_name': 'Monitoring grid power flow', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_grid_power_flow', 'unique_id': '111111111111111_monitoring_grid_power_flow', @@ -1615,6 +1645,7 @@ 'original_name': 'Monitoring grid power flow (minute)', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_minute_grid_power_flow', 'unique_id': '111111111111111_monitoring_minute_grid_power_flow', @@ -1670,6 +1701,7 @@ 'original_name': 'Monitoring self-consumption', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_self_consumption', 'unique_id': '111111111111111_monitoring_self_consumption', @@ -1724,6 +1756,7 @@ 'original_name': 'Monitoring self-sufficiency', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_self_sufficiency', 'unique_id': '111111111111111_monitoring_self_sufficiency', @@ -1778,6 +1811,7 @@ 'original_name': 'Monitoring solar production', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_solar_production', 'unique_id': '111111111111111_monitoring_solar_production', @@ -1833,6 +1867,7 @@ 'original_name': 'Monitoring solar production (minute)', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monitoring_minute_solar_production', 'unique_id': '111111111111111_monitoring_minute_solar_production', @@ -1885,6 +1920,7 @@ 'original_name': 'Output current L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_current_l1', 'unique_id': '111111111111111_output_current_l1', @@ -1937,6 +1973,7 @@ 'original_name': 'Output current L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_current_l2', 'unique_id': '111111111111111_output_current_l2', @@ -1989,6 +2026,7 @@ 'original_name': 'Output current L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_current_l3', 'unique_id': '111111111111111_output_current_l3', @@ -2041,6 +2079,7 @@ 'original_name': 'Output frequency', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_frequency', 'unique_id': '111111111111111_output_frequency', @@ -2093,6 +2132,7 @@ 'original_name': 'Output power L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_power_l1', 'unique_id': '111111111111111_output_power_l1', @@ -2145,6 +2185,7 @@ 'original_name': 'Output power L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_power_l2', 'unique_id': '111111111111111_output_power_l2', @@ -2197,6 +2238,7 @@ 'original_name': 'Output power L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_power_l3', 'unique_id': '111111111111111_output_power_l3', @@ -2249,6 +2291,7 @@ 'original_name': 'Output power total', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_power_total', 'unique_id': '111111111111111_output_power_total', @@ -2301,6 +2344,7 @@ 'original_name': 'Output voltage L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_voltage_l1', 'unique_id': '111111111111111_output_voltage_l1', @@ -2353,6 +2397,7 @@ 'original_name': 'Output voltage L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_voltage_l2', 'unique_id': '111111111111111_output_voltage_l2', @@ -2405,6 +2450,7 @@ 'original_name': 'Output voltage L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_voltage_l3', 'unique_id': '111111111111111_output_voltage_l3', @@ -2457,6 +2503,7 @@ 'original_name': 'PV consumed', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pv_consumed', 'unique_id': '111111111111111_pv_consumed', @@ -2509,6 +2556,7 @@ 'original_name': 'PV injected', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pv_injected', 'unique_id': '111111111111111_pv_injected', @@ -2561,6 +2609,7 @@ 'original_name': 'PV power 1', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pv_power_1', 'unique_id': '111111111111111_pv_power_1', @@ -2613,6 +2662,7 @@ 'original_name': 'PV power 2', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pv_power_2', 'unique_id': '111111111111111_pv_power_2', @@ -2665,6 +2715,7 @@ 'original_name': 'PV power total', 'platform': 'imeon_inverter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pv_power_total', 'unique_id': '111111111111111_pv_power_total', diff --git a/tests/components/imgw_pib/snapshots/test_sensor.ambr b/tests/components/imgw_pib/snapshots/test_sensor.ambr index ccc6e46befa..5b588af4518 100644 --- a/tests/components/imgw_pib/snapshots/test_sensor.ambr +++ b/tests/components/imgw_pib/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Water level', 'platform': 'imgw_pib', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_level', 'unique_id': '123_water_level', @@ -88,6 +89,7 @@ 'original_name': 'Water temperature', 'platform': 'imgw_pib', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_temperature', 'unique_id': '123_water_temperature', diff --git a/tests/components/immich/snapshots/test_sensor.ambr b/tests/components/immich/snapshots/test_sensor.ambr index 7284f98f681..d1ae9a8be8d 100644 --- a/tests/components/immich/snapshots/test_sensor.ambr +++ b/tests/components/immich/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Disk available', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_available', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_available', @@ -93,6 +94,7 @@ 'original_name': 'Disk size', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_size', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_size', @@ -145,6 +147,7 @@ 'original_name': 'Disk usage', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_usage', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_usage', @@ -202,6 +205,7 @@ 'original_name': 'Disk used', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_use', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_disk_use', @@ -260,6 +264,7 @@ 'original_name': 'Disk used by photos', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'usage_by_photos', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_usage_by_photos', @@ -318,6 +323,7 @@ 'original_name': 'Disk used by videos', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'usage_by_videos', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_usage_by_videos', @@ -370,6 +376,7 @@ 'original_name': 'Photos count', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'photos_count', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_photos_count', @@ -421,6 +428,7 @@ 'original_name': 'Videos count', 'platform': 'immich', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'videos_count', 'unique_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e_videos_count', diff --git a/tests/components/incomfort/snapshots/test_binary_sensor.ambr b/tests/components/incomfort/snapshots/test_binary_sensor.ambr index 518ea230705..cb938e5b1b7 100644 --- a/tests/components/incomfort/snapshots/test_binary_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Burner', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_burning', 'unique_id': 'c0ffeec0ffee_is_burning', @@ -75,6 +76,7 @@ 'original_name': 'Fault', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', @@ -124,6 +126,7 @@ 'original_name': 'Hot water tap', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_tapping', 'unique_id': 'c0ffeec0ffee_is_tapping', @@ -172,6 +175,7 @@ 'original_name': 'Pump', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pumping', 'unique_id': 'c0ffeec0ffee_is_pumping', @@ -220,6 +224,7 @@ 'original_name': 'Burner', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_burning', 'unique_id': 'c0ffeec0ffee_is_burning', @@ -268,6 +273,7 @@ 'original_name': 'Fault', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', @@ -317,6 +323,7 @@ 'original_name': 'Hot water tap', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_tapping', 'unique_id': 'c0ffeec0ffee_is_tapping', @@ -365,6 +372,7 @@ 'original_name': 'Pump', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pumping', 'unique_id': 'c0ffeec0ffee_is_pumping', @@ -413,6 +421,7 @@ 'original_name': 'Burner', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_burning', 'unique_id': 'c0ffeec0ffee_is_burning', @@ -461,6 +470,7 @@ 'original_name': 'Fault', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', @@ -510,6 +520,7 @@ 'original_name': 'Hot water tap', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_tapping', 'unique_id': 'c0ffeec0ffee_is_tapping', @@ -558,6 +569,7 @@ 'original_name': 'Pump', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pumping', 'unique_id': 'c0ffeec0ffee_is_pumping', @@ -606,6 +618,7 @@ 'original_name': 'Burner', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_burning', 'unique_id': 'c0ffeec0ffee_is_burning', @@ -654,6 +667,7 @@ 'original_name': 'Fault', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', @@ -703,6 +717,7 @@ 'original_name': 'Hot water tap', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_tapping', 'unique_id': 'c0ffeec0ffee_is_tapping', @@ -751,6 +766,7 @@ 'original_name': 'Pump', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pumping', 'unique_id': 'c0ffeec0ffee_is_pumping', @@ -799,6 +815,7 @@ 'original_name': 'Burner', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_burning', 'unique_id': 'c0ffeec0ffee_is_burning', @@ -847,6 +864,7 @@ 'original_name': 'Fault', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fault', 'unique_id': 'c0ffeec0ffee_failed', @@ -896,6 +914,7 @@ 'original_name': 'Hot water tap', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_tapping', 'unique_id': 'c0ffeec0ffee_is_tapping', @@ -944,6 +963,7 @@ 'original_name': 'Pump', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_pumping', 'unique_id': 'c0ffeec0ffee_is_pumping', diff --git a/tests/components/incomfort/snapshots/test_climate.ambr b/tests/components/incomfort/snapshots/test_climate.ambr index d435bac81eb..dd5c9ca00d7 100644 --- a/tests/components/incomfort/snapshots/test_climate.ambr +++ b/tests/components/incomfort/snapshots/test_climate.ambr @@ -33,6 +33,7 @@ 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c0ffeec0ffee_1', @@ -100,6 +101,7 @@ 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c0ffeec0ffee_1', @@ -167,6 +169,7 @@ 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c0ffeec0ffee_1', @@ -234,6 +237,7 @@ 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c0ffeec0ffee_1', diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr index 294a6094164..c08b7ba9f1e 100644 --- a/tests/components/incomfort/snapshots/test_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Pressure', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c0ffeec0ffee_cv_pressure', @@ -81,6 +82,7 @@ 'original_name': 'Tap temperature', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tap_temperature', 'unique_id': 'c0ffeec0ffee_tap_temp', @@ -134,6 +136,7 @@ 'original_name': 'Temperature', 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c0ffeec0ffee_cv_temp', diff --git a/tests/components/incomfort/snapshots/test_water_heater.ambr b/tests/components/incomfort/snapshots/test_water_heater.ambr index d3fc2b057fc..dd55793290f 100644 --- a/tests/components/incomfort/snapshots/test_water_heater.ambr +++ b/tests/components/incomfort/snapshots/test_water_heater.ambr @@ -30,6 +30,7 @@ 'original_name': None, 'platform': 'incomfort', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boiler', 'unique_id': 'c0ffeec0ffee', diff --git a/tests/components/intellifire/snapshots/test_binary_sensor.ambr b/tests/components/intellifire/snapshots/test_binary_sensor.ambr index c2ed8ff17b0..2c33012488b 100644 --- a/tests/components/intellifire/snapshots/test_binary_sensor.ambr +++ b/tests/components/intellifire/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Accessory error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'accessory_error', 'unique_id': 'error_accessory_mock_serial', @@ -76,6 +77,7 @@ 'original_name': 'Cloud connectivity', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connectivity', 'unique_id': 'cloud_connectivity_mock_serial', @@ -125,6 +127,7 @@ 'original_name': 'Disabled error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disabled_error', 'unique_id': 'error_disabled_mock_serial', @@ -174,6 +177,7 @@ 'original_name': 'ECM offline error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecm_offline_error', 'unique_id': 'error_ecm_offline_mock_serial', @@ -223,6 +227,7 @@ 'original_name': 'Fan delay error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_delay_error', 'unique_id': 'error_fan_delay_mock_serial', @@ -272,6 +277,7 @@ 'original_name': 'Fan error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_error', 'unique_id': 'error_fan_mock_serial', @@ -321,6 +327,7 @@ 'original_name': 'Flame', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flame', 'unique_id': 'on_off_mock_serial', @@ -369,6 +376,7 @@ 'original_name': 'Flame error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flame_error', 'unique_id': 'error_flame_mock_serial', @@ -418,6 +426,7 @@ 'original_name': 'Lights error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lights_error', 'unique_id': 'error_lights_mock_serial', @@ -467,6 +476,7 @@ 'original_name': 'Local connectivity', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'local_connectivity', 'unique_id': 'local_connectivity_mock_serial', @@ -516,6 +526,7 @@ 'original_name': 'Maintenance error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'maintenance_error', 'unique_id': 'error_maintenance_mock_serial', @@ -565,6 +576,7 @@ 'original_name': 'Offline error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'offline_error', 'unique_id': 'error_offline_mock_serial', @@ -614,6 +626,7 @@ 'original_name': 'Pilot flame error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pilot_flame_error', 'unique_id': 'error_pilot_flame_mock_serial', @@ -663,6 +676,7 @@ 'original_name': 'Pilot light on', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pilot_light_on', 'unique_id': 'pilot_light_on_mock_serial', @@ -711,6 +725,7 @@ 'original_name': 'Soft lock out error', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'soft_lock_out_error', 'unique_id': 'error_soft_lock_out_mock_serial', @@ -760,6 +775,7 @@ 'original_name': 'Thermostat on', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_on', 'unique_id': 'thermostat_on_mock_serial', @@ -808,6 +824,7 @@ 'original_name': 'Timer on', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'timer_on', 'unique_id': 'timer_on_mock_serial', diff --git a/tests/components/intellifire/snapshots/test_climate.ambr b/tests/components/intellifire/snapshots/test_climate.ambr index d0744424cff..e13d9c6c0b4 100644 --- a/tests/components/intellifire/snapshots/test_climate.ambr +++ b/tests/components/intellifire/snapshots/test_climate.ambr @@ -35,6 +35,7 @@ 'original_name': 'Thermostat', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'climate_mock_serial', diff --git a/tests/components/intellifire/snapshots/test_sensor.ambr b/tests/components/intellifire/snapshots/test_sensor.ambr index 3826b75a417..c65da4357ef 100644 --- a/tests/components/intellifire/snapshots/test_sensor.ambr +++ b/tests/components/intellifire/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Connection quality', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_quality', 'unique_id': 'connection_quality_mock_serial', @@ -75,6 +76,7 @@ 'original_name': 'Downtime', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'downtime', 'unique_id': 'downtime_mock_serial', @@ -124,6 +126,7 @@ 'original_name': 'ECM latency', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ecm_latency', 'unique_id': 'ecm_latency_mock_serial', @@ -174,6 +177,7 @@ 'original_name': 'Fan speed', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_speed', 'unique_id': 'fan_speed_mock_serial', @@ -225,6 +229,7 @@ 'original_name': 'Flame height', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flame_height', 'unique_id': 'flame_height_mock_serial', @@ -274,6 +279,7 @@ 'original_name': 'IP address', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ipv4_address', 'unique_id': 'ipv4_address_mock_serial', @@ -324,6 +330,7 @@ 'original_name': 'Target temperature', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'target_temp', 'unique_id': 'target_temp_mock_serial', @@ -377,6 +384,7 @@ 'original_name': 'Temperature', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'temperature_mock_serial', @@ -430,6 +438,7 @@ 'original_name': 'Timer end', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'timer_end_timestamp', 'unique_id': 'timer_end_timestamp_mock_serial', @@ -480,6 +489,7 @@ 'original_name': 'Uptime', 'platform': 'intellifire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': 'uptime_mock_serial', diff --git a/tests/components/iometer/snapshots/test_binary_sensor.ambr b/tests/components/iometer/snapshots/test_binary_sensor.ambr index 38aab735a14..7e64f56a1fc 100644 --- a/tests/components/iometer/snapshots/test_binary_sensor.ambr +++ b/tests/components/iometer/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Core attachment status', 'platform': 'iometer', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'attachment_status', 'unique_id': '01JQ6G5395176MAAWKAAPEZHV6_attachment_status', @@ -75,6 +76,7 @@ 'original_name': 'Core/Bridge connection status', 'platform': 'iometer', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connection_status', 'unique_id': '01JQ6G5395176MAAWKAAPEZHV6_connection_status', diff --git a/tests/components/iotty/snapshots/test_switch.ambr b/tests/components/iotty/snapshots/test_switch.ambr index 16913d340f0..058a5d35cd0 100644 --- a/tests/components/iotty/snapshots/test_switch.ambr +++ b/tests/components/iotty/snapshots/test_switch.ambr @@ -78,6 +78,7 @@ 'original_name': None, 'platform': 'iotty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'TestLS', diff --git a/tests/components/ipp/snapshots/test_sensor.ambr b/tests/components/ipp/snapshots/test_sensor.ambr index f8e0578a6b9..5a9669c1afb 100644 --- a/tests/components/ipp/snapshots/test_sensor.ambr +++ b/tests/components/ipp/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': None, 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'printer', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_printer', @@ -95,6 +96,7 @@ 'original_name': 'Black ink', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_0', @@ -149,6 +151,7 @@ 'original_name': 'Cyan ink', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_1', @@ -203,6 +206,7 @@ 'original_name': 'Magenta ink', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_2', @@ -257,6 +261,7 @@ 'original_name': 'Photo black ink', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_3', @@ -309,6 +314,7 @@ 'original_name': 'Uptime', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_uptime', @@ -359,6 +365,7 @@ 'original_name': 'Yellow ink', 'platform': 'ipp', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'marker', 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251_marker_4', diff --git a/tests/components/iron_os/snapshots/test_binary_sensor.ambr b/tests/components/iron_os/snapshots/test_binary_sensor.ambr index c36c1cc42ff..5d866d38786 100644 --- a/tests/components/iron_os/snapshots/test_binary_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Soldering tip', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_connected', diff --git a/tests/components/iron_os/snapshots/test_button.ambr b/tests/components/iron_os/snapshots/test_button.ambr index c9ff9181515..329940d5ca1 100644 --- a/tests/components/iron_os/snapshots/test_button.ambr +++ b/tests/components/iron_os/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Restore default settings', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_reset', @@ -74,6 +75,7 @@ 'original_name': 'Save settings', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_settings_save', diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index b2ec7a70a92..37d8b1f4819 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Boost temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_boost_temp', @@ -90,6 +91,7 @@ 'original_name': 'Calibration offset', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset', @@ -147,6 +149,7 @@ 'original_name': 'Display brightness', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_display_brightness', @@ -203,6 +206,7 @@ 'original_name': 'Hall effect sensitivity', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_hall_sensitivity', @@ -259,6 +263,7 @@ 'original_name': 'Hall sensor sleep timeout', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_hall_effect_sleep_time', @@ -316,6 +321,7 @@ 'original_name': 'Keep-awake pulse delay', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_delay', @@ -373,6 +379,7 @@ 'original_name': 'Keep-awake pulse duration', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_duration', @@ -430,6 +437,7 @@ 'original_name': 'Keep-awake pulse intensity', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_power', @@ -487,6 +495,7 @@ 'original_name': 'Long-press temperature step', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_increment_long', @@ -544,6 +553,7 @@ 'original_name': 'Min. voltage per cell', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_min_voltage_per_cell', @@ -601,6 +611,7 @@ 'original_name': 'Motion sensitivity', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_accel_sensitivity', @@ -657,6 +668,7 @@ 'original_name': 'Power Delivery timeout', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_pd_timeout', @@ -715,6 +727,7 @@ 'original_name': 'Power limit', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_power_limit', @@ -772,6 +785,7 @@ 'original_name': 'Quick Charge voltage', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_qc_max_voltage', @@ -830,6 +844,7 @@ 'original_name': 'Setpoint temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_setpoint_temperature', @@ -888,6 +903,7 @@ 'original_name': 'Short-press temperature step', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_increment_short', @@ -945,6 +961,7 @@ 'original_name': 'Shutdown timeout', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_shutdown_timeout', @@ -1003,6 +1020,7 @@ 'original_name': 'Sleep temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_sleep_temperature', @@ -1061,6 +1079,7 @@ 'original_name': 'Sleep timeout', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_sleep_timeout', @@ -1118,6 +1137,7 @@ 'original_name': 'Voltage divider', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_voltage_div', diff --git a/tests/components/iron_os/snapshots/test_select.ambr b/tests/components/iron_os/snapshots/test_select.ambr index 540cab234a5..41696371411 100644 --- a/tests/components/iron_os/snapshots/test_select.ambr +++ b/tests/components/iron_os/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Animation speed', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_speed', @@ -97,6 +98,7 @@ 'original_name': 'Boot logo duration', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_logo_duration', @@ -159,6 +161,7 @@ 'original_name': 'Button locking mode', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_locking_mode', @@ -217,6 +220,7 @@ 'original_name': 'Display orientation mode', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_orientation_mode', @@ -275,6 +279,7 @@ 'original_name': 'Power Delivery 3.1 EPR', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_usb_pd_mode', @@ -335,6 +340,7 @@ 'original_name': 'Power source', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_min_dc_voltage_cells', @@ -394,6 +400,7 @@ 'original_name': 'Scrolling speed', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_desc_scroll_speed', @@ -452,6 +459,7 @@ 'original_name': 'Soldering tip type', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_type', @@ -512,6 +520,7 @@ 'original_name': 'Start-up behavior', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_autostart_mode', @@ -570,6 +579,7 @@ 'original_name': 'Temperature display unit', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_unit', diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 6a30aa6632b..2d22f48c4a1 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'DC input voltage', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_voltage', @@ -81,6 +82,7 @@ 'original_name': 'Estimated power', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_estimated_power', @@ -133,6 +135,7 @@ 'original_name': 'Hall effect strength', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_hall_sensor', @@ -183,6 +186,7 @@ 'original_name': 'Handle temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_handle_temperature', @@ -235,6 +239,7 @@ 'original_name': 'Last movement time', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_movement_time', @@ -285,6 +290,7 @@ 'original_name': 'Max tip temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_max_tip_temp_ability', @@ -352,6 +358,7 @@ 'original_name': 'Operating mode', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_operating_mode', @@ -422,6 +429,7 @@ 'original_name': 'Power level', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_power_pwm_level', @@ -479,6 +487,7 @@ 'original_name': 'Power source', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_power_source', @@ -538,6 +547,7 @@ 'original_name': 'Raw tip voltage', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', @@ -590,6 +600,7 @@ 'original_name': 'Tip resistance', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_resistance', @@ -641,6 +652,7 @@ 'original_name': 'Tip temperature', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_live_temperature', @@ -693,6 +705,7 @@ 'original_name': 'Uptime', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_uptime', diff --git a/tests/components/iron_os/snapshots/test_switch.ambr b/tests/components/iron_os/snapshots/test_switch.ambr index a3d28e58d63..ff231c4050f 100644 --- a/tests/components/iron_os/snapshots/test_switch.ambr +++ b/tests/components/iron_os/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Animation loop', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_animation_loop', @@ -74,6 +75,7 @@ 'original_name': 'Calibrate CJC', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_calibrate_cjc', @@ -121,6 +123,7 @@ 'original_name': 'Cool down screen flashing', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_cooling_temp_blink', @@ -168,6 +171,7 @@ 'original_name': 'Detailed idle screen', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_idle_screen_details', @@ -215,6 +219,7 @@ 'original_name': 'Detailed solder screen', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_solder_screen_details', @@ -262,6 +267,7 @@ 'original_name': 'Invert screen', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_display_invert', @@ -309,6 +315,7 @@ 'original_name': 'Swap +/- buttons', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'c0:ff:ee:c0:ff:ee_invert_buttons', diff --git a/tests/components/iron_os/snapshots/test_update.ambr b/tests/components/iron_os/snapshots/test_update.ambr index fcd7196a70c..48d702001a4 100644 --- a/tests/components/iron_os/snapshots/test_update.ambr +++ b/tests/components/iron_os/snapshots/test_update.ambr @@ -30,6 +30,7 @@ 'original_name': 'Firmware', 'platform': 'iron_os', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c0:ff:ee:c0:ff:ee_firmware', diff --git a/tests/components/israel_rail/snapshots/test_sensor.ambr b/tests/components/israel_rail/snapshots/test_sensor.ambr index 610c2c53e22..e9c9bec80aa 100644 --- a/tests/components/israel_rail/snapshots/test_sensor.ambr +++ b/tests/components/israel_rail/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Departure', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure0', 'unique_id': 'באר יעקב אשקלון_departure', @@ -76,6 +77,7 @@ 'original_name': 'Departure +1', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure1', 'unique_id': 'באר יעקב אשקלון_departure1', @@ -125,6 +127,7 @@ 'original_name': 'Departure +2', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure2', 'unique_id': 'באר יעקב אשקלון_departure2', @@ -174,6 +177,7 @@ 'original_name': 'Platform', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'platform', 'unique_id': 'באר יעקב אשקלון_platform', @@ -222,6 +226,7 @@ 'original_name': 'Train number', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'train_number', 'unique_id': 'באר יעקב אשקלון_train_number', @@ -270,6 +275,7 @@ 'original_name': 'Trains', 'platform': 'israel_rail', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'trains', 'unique_id': 'באר יעקב אשקלון_trains', diff --git a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr index 296ce26c7f2..1d6cabcd2fa 100644 --- a/tests/components/ista_ecotrend/snapshots/test_sensor.ambr +++ b/tests/components/ista_ecotrend/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Heating', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating', @@ -86,6 +87,7 @@ 'original_name': 'Heating cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating_cost', @@ -141,6 +143,7 @@ 'original_name': 'Heating energy', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_heating_energy', @@ -196,6 +199,7 @@ 'original_name': 'Hot water', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water', @@ -251,6 +255,7 @@ 'original_name': 'Hot water cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water_cost', @@ -306,6 +311,7 @@ 'original_name': 'Hot water energy', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_hot_water_energy', @@ -361,6 +367,7 @@ 'original_name': 'Water', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_water', @@ -416,6 +423,7 @@ 'original_name': 'Water cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'eaf5c5c8-889f-4a3c-b68c-e9a676505762_water_cost', @@ -471,6 +479,7 @@ 'original_name': 'Heating', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating', @@ -525,6 +534,7 @@ 'original_name': 'Heating cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating_cost', @@ -580,6 +590,7 @@ 'original_name': 'Heating energy', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_heating_energy', @@ -635,6 +646,7 @@ 'original_name': 'Hot water', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water', @@ -690,6 +702,7 @@ 'original_name': 'Hot water cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water_cost', @@ -745,6 +758,7 @@ 'original_name': 'Hot water energy', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_hot_water_energy', @@ -800,6 +814,7 @@ 'original_name': 'Water', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_water', @@ -855,6 +870,7 @@ 'original_name': 'Water cost', 'platform': 'ista_ecotrend', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': '26e93f1a-c828-11ea-87d0-0242ac130003_water_cost', diff --git a/tests/components/ituran/snapshots/test_device_tracker.ambr b/tests/components/ituran/snapshots/test_device_tracker.ambr index e73f0cfee24..2bd5286f7e4 100644 --- a/tests/components/ituran/snapshots/test_device_tracker.ambr +++ b/tests/components/ituran/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'car', 'unique_id': '12345678-device_tracker', diff --git a/tests/components/ituran/snapshots/test_sensor.ambr b/tests/components/ituran/snapshots/test_sensor.ambr index f96190fdbc2..5278c657a66 100644 --- a/tests/components/ituran/snapshots/test_sensor.ambr +++ b/tests/components/ituran/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Address', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'address', 'unique_id': '12345678-address', @@ -77,6 +78,7 @@ 'original_name': 'Battery voltage', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': '12345678-battery_voltage', @@ -129,6 +131,7 @@ 'original_name': 'Heading', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heading', 'unique_id': '12345678-heading', @@ -177,6 +180,7 @@ 'original_name': 'Last update from vehicle', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_update_from_vehicle', 'unique_id': '12345678-last_update_from_vehicle', @@ -228,6 +232,7 @@ 'original_name': 'Mileage', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': '12345678-mileage', @@ -280,6 +285,7 @@ 'original_name': 'Speed', 'platform': 'ituran', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345678-speed', diff --git a/tests/components/kitchen_sink/snapshots/test_switch.ambr b/tests/components/kitchen_sink/snapshots/test_switch.ambr index 5535554017f..9c9f31a2544 100644 --- a/tests/components/kitchen_sink/snapshots/test_switch.ambr +++ b/tests/components/kitchen_sink/snapshots/test_switch.ambr @@ -40,6 +40,7 @@ 'original_name': None, 'platform': 'kitchen_sink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet_1', @@ -153,6 +154,7 @@ 'original_name': None, 'platform': 'kitchen_sink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet_2', diff --git a/tests/components/knocki/snapshots/test_event.ambr b/tests/components/knocki/snapshots/test_event.ambr index 65fecd59739..0700e2f48b4 100644 --- a/tests/components/knocki/snapshots/test_event.ambr +++ b/tests/components/knocki/snapshots/test_event.ambr @@ -31,6 +31,7 @@ 'original_name': 'Aaaa', 'platform': 'knocki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'knocki', 'unique_id': 'KNC1-W-00000214_31', diff --git a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr index 0e772fb9653..0c72fd906a8 100644 --- a/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Backflush active', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backflush_enabled', 'unique_id': 'GS012345_backflush_enabled', @@ -75,6 +76,7 @@ 'original_name': 'Brewing active', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brew_active', 'unique_id': 'GS012345_brew_active', @@ -123,6 +125,7 @@ 'original_name': 'Water tank empty', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_tank', 'unique_id': 'GS012345_water_tank', @@ -171,6 +174,7 @@ 'original_name': 'WebSocket connected', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'websocket_connected', 'unique_id': 'GS012345_websocket_connected', diff --git a/tests/components/lamarzocco/snapshots/test_button.ambr b/tests/components/lamarzocco/snapshots/test_button.ambr index 33aace5f97a..2f6d789b1a0 100644 --- a/tests/components/lamarzocco/snapshots/test_button.ambr +++ b/tests/components/lamarzocco/snapshots/test_button.ambr @@ -40,6 +40,7 @@ 'original_name': 'Start backflush', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_backflush', 'unique_id': 'GS012345_start_backflush', diff --git a/tests/components/lamarzocco/snapshots/test_calendar.ambr b/tests/components/lamarzocco/snapshots/test_calendar.ambr index 74847892cfa..60ba292d0f1 100644 --- a/tests/components/lamarzocco/snapshots/test_calendar.ambr +++ b/tests/components/lamarzocco/snapshots/test_calendar.ambr @@ -111,6 +111,7 @@ 'original_name': 'Auto on/off schedule (aXFz5bJ)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off_schedule', 'unique_id': 'GS012345_auto_on_off_schedule_aXFz5bJ', @@ -145,6 +146,7 @@ 'original_name': 'Auto on/off schedule (Os2OswX)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off_schedule', 'unique_id': 'GS012345_auto_on_off_schedule_Os2OswX', diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 8f59ce4a6fa..85892521456 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -51,6 +51,7 @@ 'original_name': 'Coffee target temperature', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'coffee_temp', 'unique_id': 'GS012345_coffee_temp', @@ -109,6 +110,7 @@ 'original_name': 'Smart standby time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_standby_time', 'unique_id': 'GS012345_smart_standby_time', @@ -167,6 +169,7 @@ 'original_name': 'Prebrew off time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'prebrew_time_off', 'unique_id': 'MR012345_prebrew_off', @@ -225,6 +228,7 @@ 'original_name': 'Prebrew on time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'prebrew_time_on', 'unique_id': 'MR012345_prebrew_on', @@ -283,6 +287,7 @@ 'original_name': 'Preinfusion time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'preinfusion_time', 'unique_id': 'MR012345_preinfusion_off', diff --git a/tests/components/lamarzocco/snapshots/test_select.ambr b/tests/components/lamarzocco/snapshots/test_select.ambr index 218b0092a49..701ce6b1cd2 100644 --- a/tests/components/lamarzocco/snapshots/test_select.ambr +++ b/tests/components/lamarzocco/snapshots/test_select.ambr @@ -51,6 +51,7 @@ 'original_name': 'Prebrew/-infusion mode', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'prebrew_infusion_select', 'unique_id': 'GS012345_prebrew_infusion_select', @@ -109,6 +110,7 @@ 'original_name': 'Prebrew/-infusion mode', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'prebrew_infusion_select', 'unique_id': 'MR012345_prebrew_infusion_select', @@ -167,6 +169,7 @@ 'original_name': 'Prebrew/-infusion mode', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'prebrew_infusion_select', 'unique_id': 'LM012345_prebrew_infusion_select', @@ -223,6 +226,7 @@ 'original_name': 'Smart standby mode', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_standby_mode', 'unique_id': 'GS012345_smart_standby_mode', @@ -281,6 +285,7 @@ 'original_name': 'Steam level', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'steam_temp_select', 'unique_id': 'MR012345_steam_temp_select', diff --git a/tests/components/lamarzocco/snapshots/test_sensor.ambr b/tests/components/lamarzocco/snapshots/test_sensor.ambr index 15eda23c094..eea4616d0ff 100644 --- a/tests/components/lamarzocco/snapshots/test_sensor.ambr +++ b/tests/components/lamarzocco/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Brewing start time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brewing_start_time', 'unique_id': 'GS012345_brewing_start_time', @@ -75,6 +76,7 @@ 'original_name': 'Coffee boiler ready time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'coffee_boiler_ready_time', 'unique_id': 'GS012345_coffee_boiler_ready_time', @@ -123,6 +125,7 @@ 'original_name': 'Last cleaning time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_cleaning_time', 'unique_id': 'GS012345_last_cleaning_time', @@ -171,6 +174,7 @@ 'original_name': 'Steam boiler ready time', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'steam_boiler_ready_time', 'unique_id': 'GS012345_steam_boiler_ready_time', @@ -221,6 +225,7 @@ 'original_name': 'Total coffees made', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_coffees_made', 'unique_id': 'GS012345_drink_stats_coffee', @@ -272,6 +277,7 @@ 'original_name': 'Total flushes done', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_flushes_done', 'unique_id': 'GS012345_drink_stats_flushing', diff --git a/tests/components/lamarzocco/snapshots/test_switch.ambr b/tests/components/lamarzocco/snapshots/test_switch.ambr index 085d9a16125..1e36e36ef8b 100644 --- a/tests/components/lamarzocco/snapshots/test_switch.ambr +++ b/tests/components/lamarzocco/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Auto on/off (Os2OswX)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off', 'unique_id': 'GS012345_auto_on_off_Os2OswX', @@ -61,6 +62,7 @@ 'original_name': 'Auto on/off (aXFz5bJ)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off', 'unique_id': 'GS012345_auto_on_off_aXFz5bJ', @@ -121,6 +123,7 @@ 'original_name': None, 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'main', 'unique_id': 'GS012345_main', @@ -168,6 +171,7 @@ 'original_name': 'Auto on/off (aXFz5bJ)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off', 'unique_id': 'GS012345_auto_on_off_aXFz5bJ', @@ -215,6 +219,7 @@ 'original_name': 'Auto on/off (Os2OswX)', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_on_off', 'unique_id': 'GS012345_auto_on_off_Os2OswX', @@ -262,6 +267,7 @@ 'original_name': 'Smart standby enabled', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_standby_enabled', 'unique_id': 'GS012345_smart_standby_enabled', @@ -309,6 +315,7 @@ 'original_name': 'Steam boiler', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'steam_boiler', 'unique_id': 'GS012345_steam_boiler_enable', diff --git a/tests/components/lamarzocco/snapshots/test_update.ambr b/tests/components/lamarzocco/snapshots/test_update.ambr index 508d0d36911..951e8a3d9db 100644 --- a/tests/components/lamarzocco/snapshots/test_update.ambr +++ b/tests/components/lamarzocco/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Gateway firmware', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'gateway_firmware', 'unique_id': 'GS012345_gateway_firmware', @@ -87,6 +88,7 @@ 'original_name': 'Machine firmware', 'platform': 'lamarzocco', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'machine_firmware', 'unique_id': 'GS012345_machine_firmware', diff --git a/tests/components/lcn/snapshots/test_binary_sensor.ambr b/tests/components/lcn/snapshots/test_binary_sensor.ambr index e3f7c9ab404..d1a76b98bf1 100644 --- a/tests/components/lcn/snapshots/test_binary_sensor.ambr +++ b/tests/components/lcn/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Binary_Sensor1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-binsensor1', @@ -74,6 +75,7 @@ 'original_name': 'Sensor_KeyLock', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-a5', @@ -121,6 +123,7 @@ 'original_name': 'Sensor_LockRegulator1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-r1varsetpoint', diff --git a/tests/components/lcn/snapshots/test_climate.ambr b/tests/components/lcn/snapshots/test_climate.ambr index 7393a9a8421..ffc9a2fad4d 100644 --- a/tests/components/lcn/snapshots/test_climate.ambr +++ b/tests/components/lcn/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': 'Climate1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-r1varsetpoint', diff --git a/tests/components/lcn/snapshots/test_cover.ambr b/tests/components/lcn/snapshots/test_cover.ambr index 722261f1432..b5d02b8b43b 100644 --- a/tests/components/lcn/snapshots/test_cover.ambr +++ b/tests/components/lcn/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Cover_Outputs', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-outputs', @@ -76,6 +77,7 @@ 'original_name': 'Cover_Relays', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-motor1', @@ -125,6 +127,7 @@ 'original_name': 'Cover_Relays_BS4', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-motor2', @@ -174,6 +177,7 @@ 'original_name': 'Cover_Relays_Module', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-motor3', diff --git a/tests/components/lcn/snapshots/test_light.ambr b/tests/components/lcn/snapshots/test_light.ambr index 0a9086d1efb..6aaed89818d 100644 --- a/tests/components/lcn/snapshots/test_light.ambr +++ b/tests/components/lcn/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Light_Output1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-output1', @@ -88,6 +89,7 @@ 'original_name': 'Light_Output2', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-output2', @@ -144,6 +146,7 @@ 'original_name': 'Light_Relay1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-relay1', diff --git a/tests/components/lcn/snapshots/test_scene.ambr b/tests/components/lcn/snapshots/test_scene.ambr index 9196e7d8ae0..21ba0894063 100644 --- a/tests/components/lcn/snapshots/test_scene.ambr +++ b/tests/components/lcn/snapshots/test_scene.ambr @@ -27,6 +27,7 @@ 'original_name': 'Romantic', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-00', @@ -74,6 +75,7 @@ 'original_name': 'Romantic Transition', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-01', diff --git a/tests/components/lcn/snapshots/test_sensor.ambr b/tests/components/lcn/snapshots/test_sensor.ambr index 60586a45058..7cec584ca48 100644 --- a/tests/components/lcn/snapshots/test_sensor.ambr +++ b/tests/components/lcn/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Sensor_Led6', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-led6', @@ -74,6 +75,7 @@ 'original_name': 'Sensor_LogicOp1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-logicop1', @@ -121,6 +123,7 @@ 'original_name': 'Sensor_Setpoint1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-r1varsetpoint', @@ -170,6 +173,7 @@ 'original_name': 'Sensor_Var1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-var1', diff --git a/tests/components/lcn/snapshots/test_switch.ambr b/tests/components/lcn/snapshots/test_switch.ambr index b37dd3303db..89d4d12cf35 100644 --- a/tests/components/lcn/snapshots/test_switch.ambr +++ b/tests/components/lcn/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Switch_Group5', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-g000005-relay1', @@ -74,6 +75,7 @@ 'original_name': 'Switch_KeyLock1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-a1', @@ -121,6 +123,7 @@ 'original_name': 'Switch_Output1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-output1', @@ -168,6 +171,7 @@ 'original_name': 'Switch_Output2', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-output2', @@ -215,6 +219,7 @@ 'original_name': 'Switch_Regulator1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-r1varsetpoint', @@ -262,6 +267,7 @@ 'original_name': 'Switch_Relay1', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-relay1', @@ -309,6 +315,7 @@ 'original_name': 'Switch_Relay2', 'platform': 'lcn', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'lcn/config_entry_pchk_json-m000007-relay2', diff --git a/tests/components/lektrico/snapshots/test_binary_sensor.ambr b/tests/components/lektrico/snapshots/test_binary_sensor.ambr index 7d812c0fc67..11fb3aa5a0a 100644 --- a/tests/components/lektrico/snapshots/test_binary_sensor.ambr +++ b/tests/components/lektrico/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'EV diode short', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cp_diode_failure', 'unique_id': '500006_cp_diode_failure', @@ -75,6 +76,7 @@ 'original_name': 'EV error', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state_e_activated', 'unique_id': '500006_state_e_activated', @@ -123,6 +125,7 @@ 'original_name': 'Metering error', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_fault', 'unique_id': '500006_meter_fault', @@ -171,6 +174,7 @@ 'original_name': 'Overcurrent', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overcurrent', 'unique_id': '500006_overcurrent', @@ -219,6 +223,7 @@ 'original_name': 'Overheating', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'critical_temp', 'unique_id': '500006_critical_temp', @@ -267,6 +272,7 @@ 'original_name': 'Overvoltage', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overvoltage', 'unique_id': '500006_overvoltage', @@ -315,6 +321,7 @@ 'original_name': 'RCD error', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rcd_error', 'unique_id': '500006_rcd_error', @@ -363,6 +370,7 @@ 'original_name': 'Relay contacts welded', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'contactor_failure', 'unique_id': '500006_contactor_failure', @@ -411,6 +419,7 @@ 'original_name': 'Thermal throttling', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overtemp', 'unique_id': '500006_overtemp', @@ -459,6 +468,7 @@ 'original_name': 'Undervoltage', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'undervoltage', 'unique_id': '500006_undervoltage', diff --git a/tests/components/lektrico/snapshots/test_button.ambr b/tests/components/lektrico/snapshots/test_button.ambr index 760a2f9fcdd..518b96e8191 100644 --- a/tests/components/lektrico/snapshots/test_button.ambr +++ b/tests/components/lektrico/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge start', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_start', 'unique_id': '500006-charge_start', @@ -74,6 +75,7 @@ 'original_name': 'Charge stop', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_stop', 'unique_id': '500006-charge_stop', @@ -121,6 +123,7 @@ 'original_name': 'Charging schedule override', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_schedule_override', 'unique_id': '500006-charging_schedule_override', @@ -168,6 +171,7 @@ 'original_name': 'Restart', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006-reboot', diff --git a/tests/components/lektrico/snapshots/test_number.ambr b/tests/components/lektrico/snapshots/test_number.ambr index 368479cdd06..1fe5f7613a6 100644 --- a/tests/components/lektrico/snapshots/test_number.ambr +++ b/tests/components/lektrico/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Dynamic limit', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dynamic_limit', 'unique_id': '500006_dynamic_limit', @@ -89,6 +90,7 @@ 'original_name': 'LED brightness', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led_max_brightness', 'unique_id': '500006_led_max_brightness', diff --git a/tests/components/lektrico/snapshots/test_select.ambr b/tests/components/lektrico/snapshots/test_select.ambr index 0f564abb146..e0d3cbbe755 100644 --- a/tests/components/lektrico/snapshots/test_select.ambr +++ b/tests/components/lektrico/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Load balancing mode', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_balancing_mode', 'unique_id': '500006_load_balancing_mode', diff --git a/tests/components/lektrico/snapshots/test_sensor.ambr b/tests/components/lektrico/snapshots/test_sensor.ambr index aa146f55776..e2ae997d423 100644 --- a/tests/components/lektrico/snapshots/test_sensor.ambr +++ b/tests/components/lektrico/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charging time', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_time', 'unique_id': '500006_charging_time', @@ -78,6 +79,7 @@ 'original_name': 'Current', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006_current', @@ -128,6 +130,7 @@ 'original_name': 'Energy', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006_energy', @@ -177,6 +180,7 @@ 'original_name': 'Installation current', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'installation_current', 'unique_id': '500006_installation_current', @@ -228,6 +232,7 @@ 'original_name': 'Lifetime energy', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lifetime_energy', 'unique_id': '500006_lifetime_energy', @@ -292,6 +297,7 @@ 'original_name': 'Limit reason', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'limit_reason', 'unique_id': '500006_limit_reason', @@ -358,6 +364,7 @@ 'original_name': 'Power', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006_power', @@ -420,6 +427,7 @@ 'original_name': 'State', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': '500006_state', @@ -481,6 +489,7 @@ 'original_name': 'Temperature', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006_temperature', @@ -531,6 +540,7 @@ 'original_name': 'Voltage', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '500006_voltage', diff --git a/tests/components/lektrico/snapshots/test_switch.ambr b/tests/components/lektrico/snapshots/test_switch.ambr index c55e96ac9a9..71fb8b599c6 100644 --- a/tests/components/lektrico/snapshots/test_switch.ambr +++ b/tests/components/lektrico/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Authentication', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'authentication', 'unique_id': '500006_authentication', @@ -74,6 +75,7 @@ 'original_name': 'Lock', 'platform': 'lektrico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock', 'unique_id': '500006_lock', diff --git a/tests/components/letpot/snapshots/test_binary_sensor.ambr b/tests/components/letpot/snapshots/test_binary_sensor.ambr index 121cf4e3f82..64596ffcd4b 100644 --- a/tests/components/letpot/snapshots/test_binary_sensor.ambr +++ b/tests/components/letpot/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Low water', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'low_water', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_low_water', @@ -75,6 +76,7 @@ 'original_name': 'Pump', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_pump', @@ -123,6 +125,7 @@ 'original_name': 'Pump error', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_error', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH31ABCD_pump_error', @@ -171,6 +174,7 @@ 'original_name': 'Low nutrients', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'low_nutrients', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_low_nutrients', @@ -219,6 +223,7 @@ 'original_name': 'Low water', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'low_water', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_low_water', @@ -267,6 +272,7 @@ 'original_name': 'Pump', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_pump', @@ -315,6 +321,7 @@ 'original_name': 'Refill error', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'refill_error', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_refill_error', diff --git a/tests/components/letpot/snapshots/test_sensor.ambr b/tests/components/letpot/snapshots/test_sensor.ambr index 5d123cf6ce0..415a1ae8b32 100644 --- a/tests/components/letpot/snapshots/test_sensor.ambr +++ b/tests/components/letpot/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Temperature', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Water level', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_level', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_water_level', diff --git a/tests/components/letpot/snapshots/test_switch.ambr b/tests/components/letpot/snapshots/test_switch.ambr index 1a36e555dd1..d76f943ccaa 100644 --- a/tests/components/letpot/snapshots/test_switch.ambr +++ b/tests/components/letpot/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm sound', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_sound', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_alarm_sound', @@ -74,6 +75,7 @@ 'original_name': 'Auto mode', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_mode', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_auto_mode', @@ -121,6 +123,7 @@ 'original_name': 'Power', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_power', @@ -168,6 +171,7 @@ 'original_name': 'Pump cycling', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_cycling', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_pump_cycling', diff --git a/tests/components/letpot/snapshots/test_time.ambr b/tests/components/letpot/snapshots/test_time.ambr index 9ca75003e56..8c3ba0c8c08 100644 --- a/tests/components/letpot/snapshots/test_time.ambr +++ b/tests/components/letpot/snapshots/test_time.ambr @@ -27,6 +27,7 @@ 'original_name': 'Light off', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_schedule_end', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_end', @@ -74,6 +75,7 @@ 'original_name': 'Light on', 'platform': 'letpot', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_schedule_start', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_light_schedule_start', diff --git a/tests/components/lg_thinq/snapshots/test_climate.ambr b/tests/components/lg_thinq/snapshots/test_climate.ambr index 111d49a2ef3..fd1b31e80bf 100644 --- a/tests/components/lg_thinq/snapshots/test_climate.ambr +++ b/tests/components/lg_thinq/snapshots/test_climate.ambr @@ -52,6 +52,7 @@ 'original_name': None, 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_climate_air_conditioner', diff --git a/tests/components/lg_thinq/snapshots/test_event.ambr b/tests/components/lg_thinq/snapshots/test_event.ambr index dbb43ce0bb9..670ce8985fa 100644 --- a/tests/components/lg_thinq/snapshots/test_event.ambr +++ b/tests/components/lg_thinq/snapshots/test_event.ambr @@ -31,6 +31,7 @@ 'original_name': 'Notification', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_notification', diff --git a/tests/components/lg_thinq/snapshots/test_number.ambr b/tests/components/lg_thinq/snapshots/test_number.ambr index ef4d9a21b86..5fa03b60033 100644 --- a/tests/components/lg_thinq/snapshots/test_number.ambr +++ b/tests/components/lg_thinq/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Schedule turn-off', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_stop', @@ -89,6 +90,7 @@ 'original_name': 'Schedule turn-on', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_hour_to_start', diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr index 5e6eb98ac42..f5e8fb79d06 100644 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter remaining', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_filter_lifetime', @@ -77,6 +78,7 @@ 'original_name': 'Humidity', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_humidity', @@ -129,6 +131,7 @@ 'original_name': 'PM1', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm1', @@ -181,6 +184,7 @@ 'original_name': 'PM10', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm10', @@ -233,6 +237,7 @@ 'original_name': 'PM2.5', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_pm2', @@ -283,6 +288,7 @@ 'original_name': 'Schedule turn-off', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_to_stop', @@ -332,6 +338,7 @@ 'original_name': 'Schedule turn-on', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_relative_to_start', @@ -381,6 +388,7 @@ 'original_name': 'Schedule turn-on', 'platform': 'lg_thinq', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'MW2-2E247F93-B570-46A6-B827-920E9E10F966_absolute_to_start', diff --git a/tests/components/linear_garage_door/snapshots/test_cover.ambr b/tests/components/linear_garage_door/snapshots/test_cover.ambr index a09156c53e0..dc3df6684bc 100644 --- a/tests/components/linear_garage_door/snapshots/test_cover.ambr +++ b/tests/components/linear_garage_door/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'test1-GDO', @@ -76,6 +77,7 @@ 'original_name': None, 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'test2-GDO', @@ -125,6 +127,7 @@ 'original_name': None, 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'test3-GDO', @@ -174,6 +177,7 @@ 'original_name': None, 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'test4-GDO', diff --git a/tests/components/linear_garage_door/snapshots/test_light.ambr b/tests/components/linear_garage_door/snapshots/test_light.ambr index 9e27efc02ec..930d78d4706 100644 --- a/tests/components/linear_garage_door/snapshots/test_light.ambr +++ b/tests/components/linear_garage_door/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Light', 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'test1-Light', @@ -88,6 +89,7 @@ 'original_name': 'Light', 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'test2-Light', @@ -145,6 +147,7 @@ 'original_name': 'Light', 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'test3-Light', @@ -202,6 +205,7 @@ 'original_name': 'Light', 'platform': 'linear_garage_door', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'test4-Light', diff --git a/tests/components/madvr/snapshots/test_binary_sensor.ambr b/tests/components/madvr/snapshots/test_binary_sensor.ambr index 7d665210a6f..8f82914ae25 100644 --- a/tests/components/madvr/snapshots/test_binary_sensor.ambr +++ b/tests/components/madvr/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'HDR flag', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hdr_flag', 'unique_id': '00:11:22:33:44:55_hdr_flag', @@ -74,6 +75,7 @@ 'original_name': 'Outgoing HDR flag', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_hdr_flag', 'unique_id': '00:11:22:33:44:55_outgoing_hdr_flag', @@ -121,6 +123,7 @@ 'original_name': 'Power state', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_state', 'unique_id': '00:11:22:33:44:55_power_state', @@ -168,6 +171,7 @@ 'original_name': 'Signal state', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'signal_state', 'unique_id': '00:11:22:33:44:55_signal_state', diff --git a/tests/components/madvr/snapshots/test_remote.ambr b/tests/components/madvr/snapshots/test_remote.ambr index c90270674c8..876fa81ed0c 100644 --- a/tests/components/madvr/snapshots/test_remote.ambr +++ b/tests/components/madvr/snapshots/test_remote.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:11:22:33:44:55', diff --git a/tests/components/madvr/snapshots/test_sensor.ambr b/tests/components/madvr/snapshots/test_sensor.ambr index 115f6a3f5d7..ac5cbe24d5c 100644 --- a/tests/components/madvr/snapshots/test_sensor.ambr +++ b/tests/components/madvr/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Aspect decimal', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aspect_dec', 'unique_id': '00:11:22:33:44:55_aspect_dec', @@ -74,6 +75,7 @@ 'original_name': 'Aspect integer', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aspect_int', 'unique_id': '00:11:22:33:44:55_aspect_int', @@ -121,6 +123,7 @@ 'original_name': 'Aspect name', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aspect_name', 'unique_id': '00:11:22:33:44:55_aspect_name', @@ -168,6 +171,7 @@ 'original_name': 'Aspect resolution', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'aspect_res', 'unique_id': '00:11:22:33:44:55_aspect_res', @@ -217,6 +221,7 @@ 'original_name': 'CPU temperature', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_cpu', 'unique_id': '00:11:22:33:44:55_temp_cpu', @@ -269,6 +274,7 @@ 'original_name': 'GPU temperature', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_gpu', 'unique_id': '00:11:22:33:44:55_temp_gpu', @@ -321,6 +327,7 @@ 'original_name': 'HDMI temperature', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_hdmi', 'unique_id': '00:11:22:33:44:55_temp_hdmi', @@ -376,6 +383,7 @@ 'original_name': 'Incoming aspect ratio', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_aspect_ratio', 'unique_id': '00:11:22:33:44:55_incoming_aspect_ratio', @@ -434,6 +442,7 @@ 'original_name': 'Incoming bit depth', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_bit_depth', 'unique_id': '00:11:22:33:44:55_incoming_bit_depth', @@ -492,6 +501,7 @@ 'original_name': 'Incoming black levels', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_black_levels', 'unique_id': '00:11:22:33:44:55_incoming_black_levels', @@ -551,6 +561,7 @@ 'original_name': 'Incoming color space', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_color_space', 'unique_id': '00:11:22:33:44:55_incoming_color_space', @@ -615,6 +626,7 @@ 'original_name': 'Incoming colorimetry', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_colorimetry', 'unique_id': '00:11:22:33:44:55_incoming_colorimetry', @@ -672,6 +684,7 @@ 'original_name': 'Incoming frame rate', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_frame_rate', 'unique_id': '00:11:22:33:44:55_incoming_frame_rate', @@ -719,6 +732,7 @@ 'original_name': 'Incoming resolution', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_res', 'unique_id': '00:11:22:33:44:55_incoming_res', @@ -771,6 +785,7 @@ 'original_name': 'Incoming signal type', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'incoming_signal_type', 'unique_id': '00:11:22:33:44:55_incoming_signal_type', @@ -825,6 +840,7 @@ 'original_name': 'Mainboard temperature', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_mainboard', 'unique_id': '00:11:22:33:44:55_temp_mainboard', @@ -875,6 +891,7 @@ 'original_name': 'Masking decimal', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'masking_dec', 'unique_id': '00:11:22:33:44:55_masking_dec', @@ -922,6 +939,7 @@ 'original_name': 'Masking integer', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'masking_int', 'unique_id': '00:11:22:33:44:55_masking_int', @@ -969,6 +987,7 @@ 'original_name': 'Masking resolution', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'masking_res', 'unique_id': '00:11:22:33:44:55_masking_res', @@ -1022,6 +1041,7 @@ 'original_name': 'Outgoing bit depth', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_bit_depth', 'unique_id': '00:11:22:33:44:55_outgoing_bit_depth', @@ -1080,6 +1100,7 @@ 'original_name': 'Outgoing black levels', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_black_levels', 'unique_id': '00:11:22:33:44:55_outgoing_black_levels', @@ -1139,6 +1160,7 @@ 'original_name': 'Outgoing color space', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_color_space', 'unique_id': '00:11:22:33:44:55_outgoing_color_space', @@ -1203,6 +1225,7 @@ 'original_name': 'Outgoing colorimetry', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_colorimetry', 'unique_id': '00:11:22:33:44:55_outgoing_colorimetry', @@ -1260,6 +1283,7 @@ 'original_name': 'Outgoing frame rate', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_frame_rate', 'unique_id': '00:11:22:33:44:55_outgoing_frame_rate', @@ -1307,6 +1331,7 @@ 'original_name': 'Outgoing resolution', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_res', 'unique_id': '00:11:22:33:44:55_outgoing_res', @@ -1359,6 +1384,7 @@ 'original_name': 'Outgoing signal type', 'platform': 'madvr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outgoing_signal_type', 'unique_id': '00:11:22:33:44:55_outgoing_signal_type', diff --git a/tests/components/mastodon/snapshots/test_sensor.ambr b/tests/components/mastodon/snapshots/test_sensor.ambr index 40986210454..db84517b33d 100644 --- a/tests/components/mastodon/snapshots/test_sensor.ambr +++ b/tests/components/mastodon/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Followers', 'platform': 'mastodon', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'followers', 'unique_id': 'trwnh_mastodon_social_followers', @@ -80,6 +81,7 @@ 'original_name': 'Following', 'platform': 'mastodon', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'following', 'unique_id': 'trwnh_mastodon_social_following', @@ -131,6 +133,7 @@ 'original_name': 'Posts', 'platform': 'mastodon', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'posts', 'unique_id': 'trwnh_mastodon_social_posts', diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index e91ea9f7ba9..f13d86c4557 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-BatteryChargeLevel-47-14', @@ -75,6 +76,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-BatteryChargeLevel-47-14', @@ -123,6 +125,7 @@ 'original_name': 'Door', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-LockDoorStateSensor-257-3', @@ -171,6 +174,7 @@ 'original_name': 'Door', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ContactSensor-69-0', @@ -219,6 +223,7 @@ 'original_name': 'Water leak', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_leak', 'unique_id': '00000000000004D2-0000000000000020-MatterNodeDevice-1-WaterLeakDetector-69-0', @@ -267,6 +272,7 @@ 'original_name': 'Occupancy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-OccupancySensor-1030-0', @@ -315,6 +321,7 @@ 'original_name': 'Occupancy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-OccupancySensor-1030-0', @@ -363,6 +370,7 @@ 'original_name': 'Occupancy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-OccupancySensor-1030-0', @@ -411,6 +419,7 @@ 'original_name': 'Problem', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_fault', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpFault-512-16', @@ -459,6 +468,7 @@ 'original_name': 'Running', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_running', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpStatusRunning-512-16', @@ -507,6 +517,7 @@ 'original_name': 'Charging status', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_charging_status', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseChargingStatusSensor-153-0', @@ -555,6 +566,7 @@ 'original_name': 'Plug', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_plug_state', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvsePlugStateSensor-153-0', @@ -603,6 +615,7 @@ 'original_name': 'Supply charging state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_supply_charging_state', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseSupplyStateSensor-153-1', @@ -651,6 +664,7 @@ 'original_name': 'Boost state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boost_state', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementBoostStateSensor-148-5', @@ -698,6 +712,7 @@ 'original_name': 'Battery alert', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_alert', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmBatteryAlertSensor-92-3', @@ -746,6 +761,7 @@ 'original_name': 'End of service', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'end_of_service', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmEndfOfServiceSensor-92-7', @@ -794,6 +810,7 @@ 'original_name': 'Hardware fault', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hardware_fault', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmHardwareFaultAlertSensor-92-6', @@ -842,6 +859,7 @@ 'original_name': 'Muted', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muted', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmDeviceMutedSensor-92-4', @@ -889,6 +907,7 @@ 'original_name': 'Smoke', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmSmokeStateSensor-92-1', @@ -937,6 +956,7 @@ 'original_name': 'Test in progress', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'test_in_progress', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SmokeCoAlarmTestInProgressSensor-92-5', diff --git a/tests/components/matter/snapshots/test_button.ambr b/tests/components/matter/snapshots/test_button.ambr index fe8ddb11aa9..3f18896348e 100644 --- a/tests/components/matter/snapshots/test_button.ambr +++ b/tests/components/matter/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reset filter condition', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter_condition', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-HepaFilterMonitoringResetButton-113-65529', @@ -74,6 +75,7 @@ 'original_name': 'Reset filter condition', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter_condition', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-ActivatedCarbonFilterMonitoringResetButton-114-65529', @@ -121,6 +123,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -169,6 +172,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-IdentifyButton-3-1', @@ -217,6 +221,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -265,6 +270,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-IdentifyButton-3-1', @@ -313,6 +319,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-1-IdentifyButton-3-1', @@ -361,6 +368,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-IdentifyButton-3-1', @@ -409,6 +417,7 @@ 'original_name': 'Identify (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', @@ -457,6 +466,7 @@ 'original_name': 'Identify (2)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-IdentifyButton-3-1', @@ -505,6 +515,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -553,6 +564,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', @@ -601,6 +613,7 @@ 'original_name': 'Pause', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', @@ -648,6 +661,7 @@ 'original_name': 'Resume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'resume', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateResumeButton-96-65529', @@ -695,6 +709,7 @@ 'original_name': 'Start', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateStartButton-96-65529', @@ -742,6 +757,7 @@ 'original_name': 'Stop', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateStopButton-96-65529', @@ -789,6 +805,7 @@ 'original_name': 'Pause', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', @@ -836,6 +853,7 @@ 'original_name': 'Resume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'resume', 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-OperationalStateResumeButton-96-65529', @@ -883,6 +901,7 @@ 'original_name': 'Start', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-OperationalStateStartButton-96-65529', @@ -930,6 +949,7 @@ 'original_name': 'Stop', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-OperationalStateStopButton-96-65529', @@ -977,6 +997,7 @@ 'original_name': 'Config', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-IdentifyButton-3-1', @@ -1025,6 +1046,7 @@ 'original_name': 'Down', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-IdentifyButton-3-1', @@ -1073,6 +1095,7 @@ 'original_name': 'Identify (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1121,6 +1144,7 @@ 'original_name': 'Identify (2)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-IdentifyButton-3-1', @@ -1169,6 +1193,7 @@ 'original_name': 'Identify (6)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-IdentifyButton-3-1', @@ -1217,6 +1242,7 @@ 'original_name': 'Up', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-IdentifyButton-3-1', @@ -1265,6 +1291,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1313,6 +1340,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1361,6 +1389,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1409,6 +1438,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1457,6 +1487,7 @@ 'original_name': 'Pause', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', @@ -1504,6 +1535,7 @@ 'original_name': 'Start', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-OperationalStateStartButton-96-65529', @@ -1551,6 +1583,7 @@ 'original_name': 'Stop', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-OperationalStateStopButton-96-65529', @@ -1598,6 +1631,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1646,6 +1680,7 @@ 'original_name': 'Pause', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStatePauseButton-96-65529', @@ -1693,6 +1728,7 @@ 'original_name': 'Resume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'resume', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateResumeButton-96-65529', @@ -1740,6 +1776,7 @@ 'original_name': 'Start', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateStartButton-96-65529', @@ -1787,6 +1824,7 @@ 'original_name': 'Stop', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateStopButton-96-65529', @@ -1834,6 +1872,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1882,6 +1921,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1930,6 +1970,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -1978,6 +2019,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-0-IdentifyButton-3-1', @@ -2026,6 +2068,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-IdentifyButton-3-1', @@ -2074,6 +2117,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-IdentifyButton-3-1', @@ -2122,6 +2166,7 @@ 'original_name': 'Identify', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-IdentifyButton-3-1', diff --git a/tests/components/matter/snapshots/test_climate.ambr b/tests/components/matter/snapshots/test_climate.ambr index 8aeb1aaafdd..07a5a69d801 100644 --- a/tests/components/matter/snapshots/test_climate.ambr +++ b/tests/components/matter/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-MatterThermostat-513-0', @@ -97,6 +98,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-MatterThermostat-513-0', @@ -164,6 +166,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-MatterThermostat-513-0', @@ -233,6 +236,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterThermostat-513-0', diff --git a/tests/components/matter/snapshots/test_cover.ambr b/tests/components/matter/snapshots/test_cover.ambr index c83dcf63c6b..c8e2c03739a 100644 --- a/tests/components/matter/snapshots/test_cover.ambr +++ b/tests/components/matter/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-MatterCoverPositionAwareLiftAndTilt-258-10', @@ -78,6 +79,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-MatterCover-258-10', @@ -127,6 +129,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterCoverPositionAwareLift-258-10', @@ -177,6 +180,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-MatterCoverPositionAwareTilt-258-10', @@ -227,6 +231,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-MatterCover-258-10', diff --git a/tests/components/matter/snapshots/test_event.ambr b/tests/components/matter/snapshots/test_event.ambr index 153f5751f14..aa4fb483248 100644 --- a/tests/components/matter/snapshots/test_event.ambr +++ b/tests/components/matter/snapshots/test_event.ambr @@ -34,6 +34,7 @@ 'original_name': 'Button', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-GenericSwitch-59-1', @@ -96,6 +97,7 @@ 'original_name': 'Button (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-GenericSwitch-59-1', @@ -160,6 +162,7 @@ 'original_name': 'Fancy Button', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-2-GenericSwitch-59-1', @@ -227,6 +230,7 @@ 'original_name': 'Config', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-GenericSwitch-59-1', @@ -295,6 +299,7 @@ 'original_name': 'Down', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-GenericSwitch-59-1', @@ -363,6 +368,7 @@ 'original_name': 'Up', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-GenericSwitch-59-1', diff --git a/tests/components/matter/snapshots/test_fan.ambr b/tests/components/matter/snapshots/test_fan.ambr index e4dc14967e5..e7ae2647d5b 100644 --- a/tests/components/matter/snapshots/test_fan.ambr +++ b/tests/components/matter/snapshots/test_fan.ambr @@ -36,6 +36,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-MatterFan-514-0', @@ -106,6 +107,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-MatterFan-514-0', @@ -173,6 +175,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-MatterFan-514-0', @@ -238,6 +241,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterFan-514-0', diff --git a/tests/components/matter/snapshots/test_light.ambr b/tests/components/matter/snapshots/test_light.ambr index a56f8f891e9..83b953c9b04 100644 --- a/tests/components/matter/snapshots/test_light.ambr +++ b/tests/components/matter/snapshots/test_light.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -111,6 +112,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -168,6 +170,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-MatterLight-6-0', @@ -231,6 +234,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -309,6 +313,7 @@ 'original_name': 'Light (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'light', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-MatterLight-6-0', @@ -372,6 +377,7 @@ 'original_name': 'Light (6)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'light', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-MatterLight-6-0', @@ -440,6 +446,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -502,6 +509,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -576,6 +584,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLight-6-0', @@ -644,6 +653,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-MatterLight-6-0', diff --git a/tests/components/matter/snapshots/test_lock.ambr b/tests/components/matter/snapshots/test_lock.ambr index 10ba84dd49b..7384449839c 100644 --- a/tests/components/matter/snapshots/test_lock.ambr +++ b/tests/components/matter/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLock-257-0', @@ -75,6 +76,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterLock-257-0', diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index 3240538f0a5..5ba0f275f8d 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -88,6 +89,7 @@ 'original_name': 'Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-off_transition_time-8-19', @@ -145,6 +147,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -201,6 +204,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -258,6 +262,7 @@ 'original_name': 'On transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_transition_time-8-18', @@ -315,6 +320,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-on_level-8-17', @@ -371,6 +377,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -428,6 +435,7 @@ 'original_name': 'Automatic relock timer', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_relock_timer', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-AutoRelockTimer-257-35', @@ -485,6 +493,7 @@ 'original_name': 'Automatic relock timer', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_relock_timer', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-AutoRelockTimer-257-35', @@ -542,6 +551,7 @@ 'original_name': 'Temperature offset', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_offset', 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveTemperatureOffset-513-16', @@ -600,6 +610,7 @@ 'original_name': 'Altitude above sea level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'altitude', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-EveWeatherAltitude-319486977-319422483', @@ -658,6 +669,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -714,6 +726,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-on_level-8-17', @@ -770,6 +783,7 @@ 'original_name': 'Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_transition_time', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-off_transition_time-8-19', @@ -827,6 +841,7 @@ 'original_name': 'On level (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_level-8-17', @@ -883,6 +898,7 @@ 'original_name': 'On level (6)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-on_level-8-17', @@ -939,6 +955,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -996,6 +1013,7 @@ 'original_name': 'On transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_transition_time', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-on_transition_time-8-18', @@ -1053,6 +1071,7 @@ 'original_name': 'Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-off_transition_time-8-19', @@ -1110,6 +1129,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -1166,6 +1186,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -1223,6 +1244,7 @@ 'original_name': 'On transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_transition_time-8-18', @@ -1280,6 +1302,7 @@ 'original_name': 'Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-off_transition_time-8-19', @@ -1337,6 +1360,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -1393,6 +1417,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -1450,6 +1475,7 @@ 'original_name': 'On transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_transition_time-8-18', @@ -1507,6 +1533,7 @@ 'original_name': 'Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-off_transition_time-8-19', @@ -1564,6 +1591,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_level-8-17', @@ -1620,6 +1648,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -1677,6 +1706,7 @@ 'original_name': 'On transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_transition_time', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-on_transition_time-8-18', @@ -1734,6 +1764,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-on_level-8-17', @@ -1790,6 +1821,7 @@ 'original_name': 'On/Off transition time', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_off_transition_time', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-on_off_transition_time-8-16', @@ -1847,6 +1879,7 @@ 'original_name': 'On level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_level', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-on_level-8-17', diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 0ab50d7a7fc..092928ff1d4 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Lighting', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterModeSelect-80-3', @@ -92,6 +93,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -151,6 +153,7 @@ 'original_name': 'Temperature level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_level', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-TemperatureControlSelectedTemperatureLevel-86-4', @@ -219,6 +222,7 @@ 'original_name': 'LED Color', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-6-MatterModeSelect-80-3', @@ -288,6 +292,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -348,6 +353,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -408,6 +414,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -468,6 +475,7 @@ 'original_name': 'Sound volume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_sound_volume', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-DoorLockSoundVolume-257-36', @@ -528,6 +536,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -588,6 +597,7 @@ 'original_name': 'Sound volume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door_lock_sound_volume', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-DoorLockSoundVolume-257-36', @@ -648,6 +658,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -708,6 +719,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -766,6 +778,7 @@ 'original_name': 'Temperature display mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_mode', 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0', @@ -823,6 +836,7 @@ 'original_name': 'Lighting', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterModeSelect-80-3', @@ -882,6 +896,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -941,6 +956,7 @@ 'original_name': 'Temperature level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_level', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-TemperatureControlSelectedTemperatureLevel-86-4', @@ -1000,6 +1016,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1058,6 +1075,7 @@ 'original_name': 'Dimming Edge', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-MatterModeSelect-80-3', @@ -1127,6 +1145,7 @@ 'original_name': 'Dimming Speed', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-MatterModeSelect-80-3', @@ -1207,6 +1226,7 @@ 'original_name': 'LED Color', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-MatterModeSelect-80-3', @@ -1276,6 +1296,7 @@ 'original_name': 'Power-on behavior on startup (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1336,6 +1357,7 @@ 'original_name': 'Power-on behavior on startup (6)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-6-MatterStartUpOnOff-6-16387', @@ -1394,6 +1416,7 @@ 'original_name': 'Relay', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-MatterModeSelect-80-3', @@ -1450,6 +1473,7 @@ 'original_name': 'Smart Bulb Mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-2-MatterModeSelect-80-3', @@ -1511,6 +1535,7 @@ 'original_name': 'Switch Mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-1-MatterModeSelect-80-3', @@ -1574,6 +1599,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1634,6 +1660,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1694,6 +1721,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1754,6 +1782,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1814,6 +1843,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -1879,6 +1909,7 @@ 'original_name': 'Mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-MatterOvenMode-73-1', @@ -1943,6 +1974,7 @@ 'original_name': 'Temperature level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_level', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-TemperatureControlSelectedTemperatureLevel-86-4', @@ -2002,6 +2034,7 @@ 'original_name': 'mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_operation_mode', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpConfigurationAndControlOperationMode-512-32', @@ -2063,6 +2096,7 @@ 'original_name': 'Energy management mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_energy_management_mode', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-MatterDeviceEnergyManagementMode-159-1', @@ -2124,6 +2158,7 @@ 'original_name': 'Mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-MatterEnergyEvseMode-157-1', @@ -2182,6 +2217,7 @@ 'original_name': 'Number of rinses', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'laundry_washer_number_of_rinses', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-MatterLaundryWasherNumberOfRinses-83-2', @@ -2240,6 +2276,7 @@ 'original_name': 'Spin speed', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'laundry_washer_spin_speed', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-LaundryWasherControlsSpinSpeed-83-1', @@ -2299,6 +2336,7 @@ 'original_name': 'Temperature level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_level', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-TemperatureControlSelectedTemperatureLevel-86-4', @@ -2357,6 +2395,7 @@ 'original_name': 'Mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-MatterRefrigeratorAndTemperatureControlledCabinetMode-82-1', @@ -2417,6 +2456,7 @@ 'original_name': 'Energy management mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_energy_management_mode', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-MatterDeviceEnergyManagementMode-159-1', @@ -2478,6 +2518,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', @@ -2536,6 +2577,7 @@ 'original_name': 'Temperature display mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_display_mode', 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-TrvTemperatureDisplayMode-516-0', @@ -2595,6 +2637,7 @@ 'original_name': 'Clean mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clean_mode', 'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterRvcCleanMode-85-1', @@ -2656,6 +2699,7 @@ 'original_name': 'Power-on behavior on startup', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'startup_on_off', 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterStartUpOnOff-6-16387', diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 424511f286e..ec3cb30ea83 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Activated carbon filter condition', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activated_carbon_filter_condition', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-ActivatedCarbonFilterCondition-114-0', @@ -87,6 +88,7 @@ 'original_name': 'Air quality', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-AirQuality-91-0', @@ -145,6 +147,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-CarbonDioxideSensor-1037-0', @@ -197,6 +200,7 @@ 'original_name': 'Carbon monoxide', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-CarbonMonoxideSensor-1036-0', @@ -249,6 +253,7 @@ 'original_name': 'Hepa filter condition', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hepa_filter_condition', 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-1-HepaFilterCondition-113-0', @@ -300,6 +305,7 @@ 'original_name': 'Humidity', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-4-HumiditySensor-1029-0', @@ -352,6 +358,7 @@ 'original_name': 'Nitrogen dioxide', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-NitrogenDioxideSensor-1043-0', @@ -404,6 +411,7 @@ 'original_name': 'Ozone', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-OzoneConcentrationSensor-1045-0', @@ -456,6 +464,7 @@ 'original_name': 'PM1', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM1Sensor-1068-0', @@ -508,6 +517,7 @@ 'original_name': 'PM10', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM10Sensor-1069-0', @@ -560,6 +570,7 @@ 'original_name': 'PM2.5', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-PM25Sensor-1066-0', @@ -612,6 +623,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-3-TemperatureSensor-1026-0', @@ -664,6 +676,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-5-ThermostatLocalTemperature-513-0', @@ -716,6 +729,7 @@ 'original_name': 'Volatile organic compounds parts', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000008F-MatterNodeDevice-2-TotalVolatileOrganicCompoundsSensor-1070-0', @@ -775,6 +789,7 @@ 'original_name': 'Air quality', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-AirQuality-91-0', @@ -833,6 +848,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-CarbonDioxideSensor-1037-0', @@ -885,6 +901,7 @@ 'original_name': 'Humidity', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-HumiditySensor-1029-0', @@ -937,6 +954,7 @@ 'original_name': 'Nitrogen dioxide', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-NitrogenDioxideSensor-1043-0', @@ -989,6 +1007,7 @@ 'original_name': 'PM1', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM1Sensor-1068-0', @@ -1041,6 +1060,7 @@ 'original_name': 'PM10', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM10Sensor-1069-0', @@ -1093,6 +1113,7 @@ 'original_name': 'PM2.5', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PM25Sensor-1066-0', @@ -1145,6 +1166,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TemperatureSensor-1026-0', @@ -1197,6 +1219,7 @@ 'original_name': 'Volatile organic compounds parts', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TotalVolatileOrganicCompoundsSensor-1070-0', @@ -1249,6 +1272,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-TemperatureSensor-1026-0', @@ -1299,6 +1323,7 @@ 'original_name': 'Max PIN code length', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_pin_code_length', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', @@ -1346,6 +1371,7 @@ 'original_name': 'Min PIN code length', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'min_pin_code_length', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', @@ -1393,6 +1419,7 @@ 'original_name': 'Max PIN code length', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_pin_code_length', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', @@ -1440,6 +1467,7 @@ 'original_name': 'Min PIN code length', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'min_pin_code_length', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', @@ -1489,6 +1517,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSource-47-12', @@ -1544,6 +1573,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatVoltage-47-11', @@ -1599,6 +1629,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-EveEnergySensorWattCurrent-319486977-319422473', @@ -1654,6 +1685,7 @@ 'original_name': 'Energy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-EveEnergySensorWattAccumulated-319486977-319422475', @@ -1709,6 +1741,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-EveEnergySensorWatt-319486977-319422474', @@ -1764,6 +1797,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-EveEnergySensorVoltage-319486977-319422472', @@ -1822,6 +1856,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', @@ -1880,6 +1915,7 @@ 'original_name': 'Energy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalEnergyMeasurementCumulativeEnergyImported-145-1', @@ -1938,6 +1974,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', @@ -1996,6 +2033,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', @@ -2048,6 +2086,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-0-PowerSource-47-12', @@ -2100,6 +2139,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-ThermostatLocalTemperature-513-0', @@ -2150,6 +2190,7 @@ 'original_name': 'Valve position', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve_position', 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveThermoValvePosition-319486977-319422488', @@ -2203,6 +2244,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-0-PowerSourceBatVoltage-47-11', @@ -2255,6 +2297,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-0-PowerSource-47-12', @@ -2307,6 +2350,7 @@ 'original_name': 'Humidity', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-HumiditySensor-1029-0', @@ -2362,6 +2406,7 @@ 'original_name': 'Pressure', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-EveWeatherPressure-319486977-319422484', @@ -2414,6 +2459,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-TemperatureSensor-1026-0', @@ -2469,6 +2515,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-0-PowerSourceBatVoltage-47-11', @@ -2521,6 +2568,7 @@ 'original_name': 'Flow', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flow', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-FlowSensor-1028-0', @@ -2572,6 +2620,7 @@ 'original_name': 'Humidity', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-HumiditySensor-1029-0', @@ -2628,6 +2677,7 @@ 'original_name': 'Current phase', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalStateCurrentPhase-96-1', @@ -2688,6 +2738,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-OperationalState-96-4', @@ -2744,6 +2795,7 @@ 'original_name': 'Illuminance', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-LightSensor-1024-0', @@ -2801,6 +2853,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-000000000000009D-MatterNodeDevice-1-OperationalState-96-4', @@ -2861,6 +2914,7 @@ 'original_name': 'Current phase', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-OvenCavityOperationalStateCurrentPhase-72-1', @@ -2920,6 +2974,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-OvenCavityOperationalState-72-4', @@ -2975,6 +3030,7 @@ 'original_name': 'Temperature (2)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-2-TemperatureSensor-1026-0', @@ -3027,6 +3083,7 @@ 'original_name': 'Temperature (4)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-TemperatureSensor-1026-0', @@ -3079,6 +3136,7 @@ 'original_name': 'Pressure', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PressureSensor-1027-0', @@ -3138,6 +3196,7 @@ 'original_name': 'Control mode', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_control_mode', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpControlMode-512-33', @@ -3196,6 +3255,7 @@ 'original_name': 'Flow', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flow', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-FlowSensor-1028-0', @@ -3247,6 +3307,7 @@ 'original_name': 'Pressure', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PressureSensor-1027-0', @@ -3299,6 +3360,7 @@ 'original_name': 'Rotation speed', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pump_speed', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-PumpSpeed-512-20', @@ -3350,6 +3412,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-TemperatureSensor-1026-0', @@ -3402,6 +3465,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-2-TemperatureSensor-1026-0', @@ -3460,6 +3524,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', @@ -3518,6 +3583,7 @@ 'original_name': 'Energy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalEnergyMeasurementCumulativeEnergyImported-145-1', @@ -3576,6 +3642,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-1-OperationalState-96-4', @@ -3639,6 +3706,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', @@ -3697,6 +3765,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000036-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', @@ -3755,6 +3824,7 @@ 'original_name': 'Appliance energy state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'esa_state', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-ESAState-152-2', @@ -3818,6 +3888,7 @@ 'original_name': 'Circuit capacity', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_circuit_capacity', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseCircuitCapacity-153-5', @@ -3887,6 +3958,7 @@ 'original_name': 'Fault state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_fault_state', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseFaultState-153-2', @@ -3961,6 +4033,7 @@ 'original_name': 'Max charge current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_max_charge_current', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseMaximumChargeCurrent-153-7', @@ -4019,6 +4092,7 @@ 'original_name': 'Min charge current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_min_charge_current', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseMinimumChargeCurrent-153-6', @@ -4077,6 +4151,7 @@ 'original_name': 'User max charge current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_user_max_charge_current', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseUserMaximumChargeCurrent-153-9', @@ -4135,6 +4210,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', @@ -4191,6 +4267,7 @@ 'original_name': 'Current phase', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalStateCurrentPhase-96-1', @@ -4252,6 +4329,7 @@ 'original_name': 'Energy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalEnergyMeasurementCumulativeEnergyImported-145-1', @@ -4309,6 +4387,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-1-OperationalState-96-4', @@ -4371,6 +4450,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', @@ -4429,6 +4509,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000001D-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', @@ -4487,6 +4568,7 @@ 'original_name': 'Appliance energy state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'esa_state', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ESAState-152-2', @@ -4550,6 +4632,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementActiveCurrent-144-5', @@ -4602,6 +4685,7 @@ 'original_name': 'Hot water level', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tank_percentage', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementTankPercentage-148-4', @@ -4659,6 +4743,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementWatt-144-8', @@ -4717,6 +4802,7 @@ 'original_name': 'Required heating energy', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'estimated_heat_required', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementEstimatedHeatRequired-148-3', @@ -4769,6 +4855,7 @@ 'original_name': 'Tank volume', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tank_volume', 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-WaterHeaterManagementTankVolume-148-2', @@ -4827,6 +4914,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-ElectricalPowerMeasurementVoltage-144-4', @@ -4879,6 +4967,7 @@ 'original_name': 'Battery', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSource-47-12', @@ -4929,6 +5018,7 @@ 'original_name': 'Battery type', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_replacement_description', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatReplacementDescription-47-19', @@ -4981,6 +5071,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-PowerSourceBatVoltage-47-11', @@ -5039,6 +5130,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementActiveCurrent-144-5', @@ -5097,6 +5189,7 @@ 'original_name': 'Energy exported', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_exported', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalEnergyMeasurementCumulativeEnergyExported-145-2', @@ -5155,6 +5248,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementWatt-144-8', @@ -5213,6 +5307,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-ElectricalPowerMeasurementVoltage-144-4', @@ -5265,6 +5360,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TemperatureSensor-1026-0', @@ -5317,6 +5413,7 @@ 'original_name': 'Temperature', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ThermostatLocalTemperature-513-0', @@ -5377,6 +5474,7 @@ 'original_name': 'Operational state', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_state', 'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-RvcOperationalState-97-4', @@ -5434,6 +5532,7 @@ 'original_name': 'Target opening position', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'window_covering_target_position', 'unique_id': '00000000000004D2-0000000000000032-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11', @@ -5482,6 +5581,7 @@ 'original_name': 'Target opening position', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'window_covering_target_position', 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-TargetPositionLiftPercent100ths-258-11', @@ -5535,6 +5635,7 @@ 'original_name': 'Current', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementRmsCurrent-2820-1288', @@ -5590,6 +5691,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementActivePower-2820-1291', @@ -5645,6 +5747,7 @@ 'original_name': 'Voltage', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-ElectricalMeasurementRmsVoltage-2820-1285', diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index 08a3e0290c8..01881448e13 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Power (1)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-MatterPowerToggle-6-0', @@ -75,6 +76,7 @@ 'original_name': 'Power (2)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-2-MatterPowerToggle-6-0', @@ -123,6 +125,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterSwitch-6-0', @@ -171,6 +174,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterSwitch-6-0', @@ -219,6 +223,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000053-MatterNodeDevice-1-MatterPlug-6-0', @@ -267,6 +272,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-00000000000000B7-MatterNodeDevice-1-MatterPlug-6-0', @@ -315,6 +321,7 @@ 'original_name': 'Child lock', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-EveTrvChildLock-516-1', @@ -362,6 +369,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000008-MatterNodeDevice-1-MatterPowerToggle-6-0', @@ -410,6 +418,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-000000000000000E-MatterNodeDevice-1-MatterSwitch-6-0', @@ -458,6 +467,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterPlug-6-0', @@ -506,6 +516,7 @@ 'original_name': 'Power (3)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-3-MatterPowerToggle-6-0', @@ -554,6 +565,7 @@ 'original_name': 'Power (4)', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000002-MatterNodeDevice-4-MatterPowerToggle-6-0', @@ -602,6 +614,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000003-MatterNodeDevice-1-MatterPowerToggle-6-0', @@ -650,6 +663,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-0000000000000024-MatterNodeDevice-1-MatterPowerToggle-6-0', @@ -698,6 +712,7 @@ 'original_name': 'Enable charging', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'evse_charging_switch', 'unique_id': '00000000000004D2-0000000000000017-MatterNodeDevice-1-EnergyEvseChargingSwitch-153-1', @@ -745,6 +760,7 @@ 'original_name': 'Power', 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': '00000000000004D2-000000000000003A-MatterNodeDevice-1-MatterPowerToggle-6-0', @@ -793,6 +809,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MatterSwitch-6-0', @@ -841,6 +858,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterSwitch-6-0', @@ -889,6 +907,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000004-MatterNodeDevice-1-MatterPlug-6-0', diff --git a/tests/components/matter/snapshots/test_vacuum.ambr b/tests/components/matter/snapshots/test_vacuum.ambr index 0703a1af4c7..cb859147d75 100644 --- a/tests/components/matter/snapshots/test_vacuum.ambr +++ b/tests/components/matter/snapshots/test_vacuum.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000042-MatterNodeDevice-1-MatterVacuumCleaner-84-1', diff --git a/tests/components/matter/snapshots/test_valve.ambr b/tests/components/matter/snapshots/test_valve.ambr index 99da4c2d0f6..6c178449083 100644 --- a/tests/components/matter/snapshots/test_valve.ambr +++ b/tests/components/matter/snapshots/test_valve.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-MatterValve-129-4', diff --git a/tests/components/matter/snapshots/test_water_heater.ambr b/tests/components/matter/snapshots/test_water_heater.ambr index fcf9a7665fd..6dd483fb1d7 100644 --- a/tests/components/matter/snapshots/test_water_heater.ambr +++ b/tests/components/matter/snapshots/test_water_heater.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'matter', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00000000000004D2-0000000000000019-MatterNodeDevice-2-MatterWaterHeater-513-18', diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr index 7587a7a55b7..48f5aaa7d75 100644 --- a/tests/components/mealie/snapshots/test_calendar.ambr +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -191,6 +191,7 @@ 'original_name': 'Breakfast', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'breakfast', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_breakfast', @@ -244,6 +245,7 @@ 'original_name': 'Dinner', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dinner', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_dinner', @@ -297,6 +299,7 @@ 'original_name': 'Lunch', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lunch', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_lunch', @@ -350,6 +353,7 @@ 'original_name': 'Side', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'side', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_side', diff --git a/tests/components/mealie/snapshots/test_sensor.ambr b/tests/components/mealie/snapshots/test_sensor.ambr index 19219c01c1c..9dea508df39 100644 --- a/tests/components/mealie/snapshots/test_sensor.ambr +++ b/tests/components/mealie/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Categories', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'categories', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_categories', @@ -80,6 +81,7 @@ 'original_name': 'Recipes', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'recipes', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_recipes', @@ -131,6 +133,7 @@ 'original_name': 'Tags', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tags', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tags', @@ -182,6 +185,7 @@ 'original_name': 'Tools', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tools', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_tools', @@ -233,6 +237,7 @@ 'original_name': 'Users', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'users', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_users', diff --git a/tests/components/mealie/snapshots/test_todo.ambr b/tests/components/mealie/snapshots/test_todo.ambr index 88c677de581..26cfb1ced68 100644 --- a/tests/components/mealie/snapshots/test_todo.ambr +++ b/tests/components/mealie/snapshots/test_todo.ambr @@ -27,6 +27,7 @@ 'original_name': 'Freezer', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'shopping_list', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_e9d78ff2-4b23-4b77-a3a8-464827100b46', @@ -75,6 +76,7 @@ 'original_name': 'Special groceries', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'shopping_list', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_f8438635-8211-4be8-80d0-0aa42e37a5f2', @@ -123,6 +125,7 @@ 'original_name': 'Supermarket', 'platform': 'mealie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'shopping_list', 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_27edbaab-2ec6-441f-8490-0283ea77585f', diff --git a/tests/components/meteo_france/snapshots/test_sensor.ambr b/tests/components/meteo_france/snapshots/test_sensor.ambr index 35b6a9d19f7..553f82c2a8e 100644 --- a/tests/components/meteo_france/snapshots/test_sensor.ambr +++ b/tests/components/meteo_france/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': '32 Weather alert', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '32 Weather alert', @@ -82,6 +83,7 @@ 'original_name': 'La Clusaz Cloud cover', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_cloud', @@ -132,6 +134,7 @@ 'original_name': 'La Clusaz Daily original condition', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_daily_original_condition', @@ -180,6 +183,7 @@ 'original_name': 'La Clusaz Daily precipitation', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_precipitation', @@ -230,6 +234,7 @@ 'original_name': 'La Clusaz Freeze chance', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_freeze_chance', @@ -282,6 +287,7 @@ 'original_name': 'La Clusaz Humidity', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_humidity', @@ -333,6 +339,7 @@ 'original_name': 'La Clusaz Original condition', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_original_condition', @@ -383,6 +390,7 @@ 'original_name': 'La Clusaz Pressure', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_pressure', @@ -434,6 +442,7 @@ 'original_name': 'La Clusaz Rain chance', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_rain_chance', @@ -484,6 +493,7 @@ 'original_name': 'La Clusaz Snow chance', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_snow_chance', @@ -536,6 +546,7 @@ 'original_name': 'La Clusaz Temperature', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_temperature', @@ -587,6 +598,7 @@ 'original_name': 'La Clusaz UV', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_uv', @@ -639,6 +651,7 @@ 'original_name': 'La Clusaz Wind gust', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_wind_gust', @@ -693,6 +706,7 @@ 'original_name': 'La Clusaz Wind speed', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '45.90417,6.42306_wind_speed', @@ -744,6 +758,7 @@ 'original_name': 'Meudon Next rain', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '48.807166,2.239895_next_rain', diff --git a/tests/components/meteo_france/snapshots/test_weather.ambr b/tests/components/meteo_france/snapshots/test_weather.ambr index d5e03c95de2..4fdc22cd427 100644 --- a/tests/components/meteo_france/snapshots/test_weather.ambr +++ b/tests/components/meteo_france/snapshots/test_weather.ambr @@ -27,6 +27,7 @@ 'original_name': 'La Clusaz', 'platform': 'meteo_france', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '45.90417,6.42306', diff --git a/tests/components/miele/snapshots/test_binary_sensor.ambr b/tests/components/miele/snapshots/test_binary_sensor.ambr index 423a4639ffb..f102c925c98 100644 --- a/tests/components/miele/snapshots/test_binary_sensor.ambr +++ b/tests/components/miele/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_signal_door', @@ -75,6 +76,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_1-state_mobile_start', @@ -122,6 +124,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_1-state_signal_info', @@ -170,6 +173,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_signal_failure', @@ -218,6 +222,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_1-state_full_remote_control', @@ -265,6 +270,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_1-state_smart_grid', @@ -312,6 +318,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'DummyAppliance_18-state_mobile_start', @@ -359,6 +366,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'DummyAppliance_18-state_signal_info', @@ -407,6 +415,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DummyAppliance_18-state_signal_failure', @@ -455,6 +464,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'DummyAppliance_18-state_full_remote_control', @@ -502,6 +512,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'DummyAppliance_18-state_smart_grid', @@ -549,6 +560,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_signal_door', @@ -597,6 +609,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_2-state_mobile_start', @@ -644,6 +657,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_2-state_signal_info', @@ -692,6 +706,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_signal_failure', @@ -740,6 +755,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_2-state_full_remote_control', @@ -787,6 +803,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_2-state_smart_grid', @@ -834,6 +851,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_3-state_signal_door', @@ -882,6 +900,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_3-state_mobile_start', @@ -929,6 +948,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_3-state_signal_info', @@ -977,6 +997,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_3-state_signal_failure', @@ -1025,6 +1046,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_3-state_full_remote_control', @@ -1072,6 +1094,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_3-state_smart_grid', @@ -1119,6 +1142,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_signal_door', @@ -1167,6 +1191,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_1-state_mobile_start', @@ -1214,6 +1239,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_1-state_signal_info', @@ -1262,6 +1288,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_signal_failure', @@ -1310,6 +1337,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_1-state_full_remote_control', @@ -1357,6 +1385,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_1-state_smart_grid', @@ -1404,6 +1433,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'DummyAppliance_18-state_mobile_start', @@ -1451,6 +1481,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'DummyAppliance_18-state_signal_info', @@ -1499,6 +1530,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'DummyAppliance_18-state_signal_failure', @@ -1547,6 +1579,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'DummyAppliance_18-state_full_remote_control', @@ -1594,6 +1627,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'DummyAppliance_18-state_smart_grid', @@ -1641,6 +1675,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_signal_door', @@ -1689,6 +1724,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_2-state_mobile_start', @@ -1736,6 +1772,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_2-state_signal_info', @@ -1784,6 +1821,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_signal_failure', @@ -1832,6 +1870,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_2-state_full_remote_control', @@ -1879,6 +1918,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_2-state_smart_grid', @@ -1926,6 +1966,7 @@ 'original_name': 'Door', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_3-state_signal_door', @@ -1974,6 +2015,7 @@ 'original_name': 'Mobile start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mobile_start', 'unique_id': 'Dummy_Appliance_3-state_mobile_start', @@ -2021,6 +2063,7 @@ 'original_name': 'Notification active', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'notification_active', 'unique_id': 'Dummy_Appliance_3-state_signal_info', @@ -2069,6 +2112,7 @@ 'original_name': 'Problem', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_3-state_signal_failure', @@ -2117,6 +2161,7 @@ 'original_name': 'Remote control', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'Dummy_Appliance_3-state_full_remote_control', @@ -2164,6 +2209,7 @@ 'original_name': 'Smart grid', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_grid', 'unique_id': 'Dummy_Appliance_3-state_smart_grid', diff --git a/tests/components/miele/snapshots/test_button.ambr b/tests/components/miele/snapshots/test_button.ambr index a7683caac24..6e6f3cbb72d 100644 --- a/tests/components/miele/snapshots/test_button.ambr +++ b/tests/components/miele/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Stop', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': 'DummyAppliance_18-stop', @@ -74,6 +75,7 @@ 'original_name': 'Pause', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': 'Dummy_Appliance_3-pause', @@ -121,6 +123,7 @@ 'original_name': 'Start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': 'Dummy_Appliance_3-start', @@ -168,6 +171,7 @@ 'original_name': 'Stop', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': 'Dummy_Appliance_3-stop', @@ -215,6 +219,7 @@ 'original_name': 'Stop', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': 'DummyAppliance_18-stop', @@ -262,6 +267,7 @@ 'original_name': 'Pause', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': 'Dummy_Appliance_3-pause', @@ -309,6 +315,7 @@ 'original_name': 'Start', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start', 'unique_id': 'Dummy_Appliance_3-start', @@ -356,6 +363,7 @@ 'original_name': 'Stop', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': 'Dummy_Appliance_3-stop', diff --git a/tests/components/miele/snapshots/test_climate.ambr b/tests/components/miele/snapshots/test_climate.ambr index 5739f853d94..0fb24c893c4 100644 --- a/tests/components/miele/snapshots/test_climate.ambr +++ b/tests/components/miele/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'freezer', 'unique_id': 'Dummy_Appliance_1-thermostat-1', @@ -97,6 +98,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'refrigerator', 'unique_id': 'Dummy_Appliance_2-thermostat-1', @@ -160,6 +162,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'freezer', 'unique_id': 'Dummy_Appliance_1-thermostat-1', @@ -223,6 +226,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'refrigerator', 'unique_id': 'Dummy_Appliance_2-thermostat-1', diff --git a/tests/components/miele/snapshots/test_fan.ambr b/tests/components/miele/snapshots/test_fan.ambr index 8f30b785bc9..8e5b3afd072 100644 --- a/tests/components/miele/snapshots/test_fan.ambr +++ b/tests/components/miele/snapshots/test_fan.ambr @@ -28,6 +28,7 @@ 'original_name': 'Fan', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan', 'unique_id': 'DummyAppliance_74-fan_readonly', @@ -77,6 +78,7 @@ 'original_name': 'Fan', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan', 'unique_id': 'DummyAppliance_74_off-fan_readonly', @@ -127,6 +129,7 @@ 'original_name': 'Fan', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'fan', 'unique_id': 'DummyAppliance_18-fan', @@ -181,6 +184,7 @@ 'original_name': 'Fan', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'fan', 'unique_id': 'DummyAppliance_18-fan', diff --git a/tests/components/miele/snapshots/test_light.ambr b/tests/components/miele/snapshots/test_light.ambr index 9cfc228873f..8c4a4f4bff9 100644 --- a/tests/components/miele/snapshots/test_light.ambr +++ b/tests/components/miele/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Ambient light', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ambient_light', 'unique_id': 'DummyAppliance_18-ambient_light', @@ -87,6 +88,7 @@ 'original_name': 'Light', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'DummyAppliance_18-light', @@ -143,6 +145,7 @@ 'original_name': 'Ambient light', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ambient_light', 'unique_id': 'DummyAppliance_18-ambient_light', @@ -199,6 +202,7 @@ 'original_name': 'Light', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'DummyAppliance_18-light', diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 2c3c4dfd506..488996cf363 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -48,6 +48,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'DummyAppliance_hob_w_extr-state_status', @@ -144,6 +145,7 @@ 'original_name': 'Plate 1', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plate', 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-1', @@ -245,6 +247,7 @@ 'original_name': 'Plate 2', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plate', 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-2', @@ -346,6 +349,7 @@ 'original_name': 'Plate 3', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plate', 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-3', @@ -447,6 +451,7 @@ 'original_name': 'Plate 4', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plate', 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-4', @@ -548,6 +553,7 @@ 'original_name': 'Plate 5', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plate', 'unique_id': 'DummyAppliance_hob_w_extr-state_plate_step-5', @@ -643,6 +649,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_1-state_status', @@ -714,6 +721,7 @@ 'original_name': 'Temperature', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_temperature_1', @@ -785,6 +793,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'DummyAppliance_18-state_status', @@ -875,6 +884,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_2-state_status', @@ -946,6 +956,7 @@ 'original_name': 'Temperature', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_temperature_1', @@ -1017,6 +1028,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_3-state_status', @@ -1086,6 +1098,7 @@ 'original_name': 'Elapsed time', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elapsed_time', 'unique_id': 'Dummy_Appliance_3-state_elapsed_time', @@ -1137,6 +1150,7 @@ 'original_name': 'Energy consumption', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_consumption', 'unique_id': 'Dummy_Appliance_3-current_energy_consumption', @@ -1187,6 +1201,7 @@ 'original_name': 'Energy forecast', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_forecast', 'unique_id': 'Dummy_Appliance_3-energy_forecast', @@ -1272,6 +1287,7 @@ 'original_name': 'Program', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_id', 'unique_id': 'Dummy_Appliance_3-state_program_id', @@ -1378,6 +1394,7 @@ 'original_name': 'Program phase', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_phase', 'unique_id': 'Dummy_Appliance_3-state_program_phase', @@ -1455,6 +1472,7 @@ 'original_name': 'Program type', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_type', 'unique_id': 'Dummy_Appliance_3-state_program_type', @@ -1510,6 +1528,7 @@ 'original_name': 'Remaining time', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_time', 'unique_id': 'Dummy_Appliance_3-state_remaining_time', @@ -1559,6 +1578,7 @@ 'original_name': 'Spin speed', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spin_speed', 'unique_id': 'Dummy_Appliance_3-state_spinning_speed', @@ -1613,6 +1633,7 @@ 'original_name': 'Start in', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_time', 'unique_id': 'Dummy_Appliance_3-state_start_time', @@ -1664,6 +1685,7 @@ 'original_name': 'Water consumption', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_consumption', 'unique_id': 'Dummy_Appliance_3-current_water_consumption', @@ -1714,6 +1736,7 @@ 'original_name': 'Water forecast', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_forecast', 'unique_id': 'Dummy_Appliance_3-water_forecast', @@ -1783,6 +1806,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_1-state_status', @@ -1854,6 +1878,7 @@ 'original_name': 'Temperature', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_1-state_temperature_1', @@ -1925,6 +1950,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'DummyAppliance_18-state_status', @@ -2015,6 +2041,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_2-state_status', @@ -2086,6 +2113,7 @@ 'original_name': 'Temperature', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Dummy_Appliance_2-state_temperature_1', @@ -2157,6 +2185,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'Dummy_Appliance_3-state_status', @@ -2226,6 +2255,7 @@ 'original_name': 'Elapsed time', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elapsed_time', 'unique_id': 'Dummy_Appliance_3-state_elapsed_time', @@ -2277,6 +2307,7 @@ 'original_name': 'Energy consumption', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_consumption', 'unique_id': 'Dummy_Appliance_3-current_energy_consumption', @@ -2327,6 +2358,7 @@ 'original_name': 'Energy forecast', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_forecast', 'unique_id': 'Dummy_Appliance_3-energy_forecast', @@ -2412,6 +2444,7 @@ 'original_name': 'Program', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_id', 'unique_id': 'Dummy_Appliance_3-state_program_id', @@ -2518,6 +2551,7 @@ 'original_name': 'Program phase', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_phase', 'unique_id': 'Dummy_Appliance_3-state_program_phase', @@ -2595,6 +2629,7 @@ 'original_name': 'Program type', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'program_type', 'unique_id': 'Dummy_Appliance_3-state_program_type', @@ -2650,6 +2685,7 @@ 'original_name': 'Remaining time', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_time', 'unique_id': 'Dummy_Appliance_3-state_remaining_time', @@ -2699,6 +2735,7 @@ 'original_name': 'Spin speed', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spin_speed', 'unique_id': 'Dummy_Appliance_3-state_spinning_speed', @@ -2753,6 +2790,7 @@ 'original_name': 'Start in', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_time', 'unique_id': 'Dummy_Appliance_3-state_start_time', @@ -2804,6 +2842,7 @@ 'original_name': 'Water consumption', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_consumption', 'unique_id': 'Dummy_Appliance_3-current_water_consumption', @@ -2854,6 +2893,7 @@ 'original_name': 'Water forecast', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_forecast', 'unique_id': 'Dummy_Appliance_3-water_forecast', diff --git a/tests/components/miele/snapshots/test_switch.ambr b/tests/components/miele/snapshots/test_switch.ambr index 24166e379e7..c8ca88c5b59 100644 --- a/tests/components/miele/snapshots/test_switch.ambr +++ b/tests/components/miele/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Superfreezing', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'superfreezing', 'unique_id': 'Dummy_Appliance_1-superfreezing', @@ -74,6 +75,7 @@ 'original_name': 'Power', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': 'DummyAppliance_18-poweronoff', @@ -121,6 +123,7 @@ 'original_name': 'Supercooling', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supercooling', 'unique_id': 'Dummy_Appliance_2-supercooling', @@ -168,6 +171,7 @@ 'original_name': 'Power', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': 'Dummy_Appliance_3-poweronoff', @@ -215,6 +219,7 @@ 'original_name': 'Superfreezing', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'superfreezing', 'unique_id': 'Dummy_Appliance_1-superfreezing', @@ -262,6 +267,7 @@ 'original_name': 'Power', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': 'DummyAppliance_18-poweronoff', @@ -309,6 +315,7 @@ 'original_name': 'Supercooling', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supercooling', 'unique_id': 'Dummy_Appliance_2-supercooling', @@ -356,6 +363,7 @@ 'original_name': 'Power', 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power', 'unique_id': 'Dummy_Appliance_3-poweronoff', diff --git a/tests/components/miele/snapshots/test_vacuum.ambr b/tests/components/miele/snapshots/test_vacuum.ambr index c99a6f9b39f..9f96db7b05a 100644 --- a/tests/components/miele/snapshots/test_vacuum.ambr +++ b/tests/components/miele/snapshots/test_vacuum.ambr @@ -33,6 +33,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vacuum', 'unique_id': 'Dummy_Vacuum_1-vacuum', @@ -95,6 +96,7 @@ 'original_name': None, 'platform': 'miele', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vacuum', 'unique_id': 'Dummy_Vacuum_1-vacuum', diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr index 461cb33d776..5ea055b5347 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Büro IO device 1 battery', 'platform': 'moehlenhoff_alpha2', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Alpha2Test:1:battery', diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr index 27244d781df..9104b7473b4 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Sync time', 'platform': 'moehlenhoff_alpha2', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '6fa019921cf8e7a3f57a3c2ed001a10d:sync_time', diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr index 0708137e1cf..57f1b2fdc25 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': 'Büro', 'platform': 'moehlenhoff_alpha2', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'Alpha2Test:1', diff --git a/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr b/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr index 4b1c702591d..28df23dd089 100644 --- a/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr +++ b/tests/components/moehlenhoff_alpha2/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Büro heat control 1 valve opening', 'platform': 'moehlenhoff_alpha2', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Alpha2Test:1:valve_opening', diff --git a/tests/components/monarch_money/snapshots/test_sensor.ambr b/tests/components/monarch_money/snapshots/test_sensor.ambr index b70302188ed..65f85925114 100644 --- a/tests/components/monarch_money/snapshots/test_sensor.ambr +++ b/tests/components/monarch_money/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Expense year to date', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_expense', 'unique_id': '222260252323873333_cashflow_sum_expense', @@ -81,6 +82,7 @@ 'original_name': 'Income year to date', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_income', 'unique_id': '222260252323873333_cashflow_sum_income', @@ -134,6 +136,7 @@ 'original_name': 'Savings rate', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'savings_rate', 'unique_id': '222260252323873333_cashflow_savings_rate', @@ -184,6 +187,7 @@ 'original_name': 'Savings year to date', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'savings', 'unique_id': '222260252323873333_cashflow_savings', @@ -236,6 +240,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_186321412999033223_balance', @@ -287,6 +292,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_186321412999033223_age', @@ -338,6 +344,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_900000002_balance', @@ -390,6 +397,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_900000002_age', @@ -441,6 +449,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_900000000_balance', @@ -493,6 +502,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_900000000_age', @@ -544,6 +554,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_9000000007_balance', @@ -596,6 +607,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_9000000007_age', @@ -647,6 +659,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_90000000022_balance', @@ -699,6 +712,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_90000000022_age', @@ -750,6 +764,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_900000000012_balance', @@ -802,6 +817,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_900000000012_age', @@ -853,6 +869,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_90000000030_balance', @@ -905,6 +922,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_90000000030_age', @@ -954,6 +972,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_121212192626186051_age', @@ -1005,6 +1024,7 @@ 'original_name': 'Value', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'value', 'unique_id': '222260252323873333_121212192626186051_value', @@ -1059,6 +1079,7 @@ 'original_name': 'Balance', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': '222260252323873333_90000000020_balance', @@ -1111,6 +1132,7 @@ 'original_name': 'Data age', 'platform': 'monarch_money', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': '222260252323873333_90000000020_age', diff --git a/tests/components/monzo/snapshots/test_sensor.ambr b/tests/components/monzo/snapshots/test_sensor.ambr index 8d3f83ed4f1..bd6fd4c5daf 100644 --- a/tests/components/monzo/snapshots/test_sensor.ambr +++ b/tests/components/monzo/snapshots/test_sensor.ambr @@ -30,6 +30,7 @@ 'original_name': 'Balance', 'platform': 'monzo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'acc_curr_balance', @@ -83,6 +84,7 @@ 'original_name': 'Total balance', 'platform': 'monzo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_balance', 'unique_id': 'acc_curr_total_balance', @@ -136,6 +138,7 @@ 'original_name': 'Balance', 'platform': 'monzo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'acc_flex_balance', @@ -189,6 +192,7 @@ 'original_name': 'Total balance', 'platform': 'monzo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_balance', 'unique_id': 'acc_flex_total_balance', @@ -242,6 +246,7 @@ 'original_name': 'Balance', 'platform': 'monzo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pot_balance', 'unique_id': 'pot_savings_pot_balance', diff --git a/tests/components/music_assistant/snapshots/test_media_player.ambr b/tests/components/music_assistant/snapshots/test_media_player.ambr index e7c2eec6f4b..d530406ff88 100644 --- a/tests/components/music_assistant/snapshots/test_media_player.ambr +++ b/tests/components/music_assistant/snapshots/test_media_player.ambr @@ -28,6 +28,7 @@ 'original_name': None, 'platform': 'music_assistant', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:02', @@ -95,6 +96,7 @@ 'original_name': None, 'platform': 'music_assistant', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'test_group_player_1', @@ -170,6 +172,7 @@ 'original_name': None, 'platform': 'music_assistant', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '00:00:00:00:00:01', diff --git a/tests/components/myuplink/snapshots/test_binary_sensor.ambr b/tests/components/myuplink/snapshots/test_binary_sensor.ambr index 478c5a55b80..52b3f2314f8 100644 --- a/tests/components/myuplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/myuplink/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm', 'unique_id': '123456-7890-1234-has_alarm', @@ -75,6 +76,7 @@ 'original_name': 'Connectivity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-connection_state', @@ -123,6 +125,7 @@ 'original_name': 'Connectivity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-connection_state', @@ -171,6 +174,7 @@ 'original_name': 'Extern. adjust\xadment climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43161', @@ -218,6 +222,7 @@ 'original_name': 'Extern. adjust\xadment climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43161', @@ -265,6 +270,7 @@ 'original_name': 'Pump: Heating medium (GP1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49995', @@ -312,6 +318,7 @@ 'original_name': 'Pump: Heating medium (GP1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49995', diff --git a/tests/components/myuplink/snapshots/test_number.ambr b/tests/components/myuplink/snapshots/test_number.ambr index f2c89663879..f8a290f89e3 100644 --- a/tests/components/myuplink/snapshots/test_number.ambr +++ b/tests/components/myuplink/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Degree minutes', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'degree_minutes', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940', @@ -89,6 +90,7 @@ 'original_name': 'Degree minutes', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'degree_minutes', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940', @@ -146,6 +148,7 @@ 'original_name': 'Heating offset climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47011', @@ -202,6 +205,7 @@ 'original_name': 'Heating offset climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47011', @@ -258,6 +262,7 @@ 'original_name': 'Room sensor set point value heating climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47398', @@ -314,6 +319,7 @@ 'original_name': 'Room sensor set point value heating climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47398', @@ -370,6 +376,7 @@ 'original_name': 'start diff additional heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'degree_minutes', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-148072', @@ -427,6 +434,7 @@ 'original_name': 'start diff additional heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'degree_minutes', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-148072', diff --git a/tests/components/myuplink/snapshots/test_select.ambr b/tests/components/myuplink/snapshots/test_select.ambr index 032fd2ef455..08c4244d0f6 100644 --- a/tests/components/myuplink/snapshots/test_select.ambr +++ b/tests/components/myuplink/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'comfort mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47041', @@ -94,6 +95,7 @@ 'original_name': 'comfort mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-47041', diff --git a/tests/components/myuplink/snapshots/test_sensor.ambr b/tests/components/myuplink/snapshots/test_sensor.ambr index f9249651208..dc5b4c9fb0d 100644 --- a/tests/components/myuplink/snapshots/test_sensor.ambr +++ b/tests/components/myuplink/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Average outdoor temp (BT1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40067', @@ -81,6 +82,7 @@ 'original_name': 'Average outdoor temp (BT1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40067', @@ -133,6 +135,7 @@ 'original_name': 'Calculated supply climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43009', @@ -185,6 +188,7 @@ 'original_name': 'Calculated supply climate system 1', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43009', @@ -237,6 +241,7 @@ 'original_name': 'Condenser (BT12)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40017', @@ -289,6 +294,7 @@ 'original_name': 'Condenser (BT12)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40017', @@ -341,6 +347,7 @@ 'original_name': 'Current (BE1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40079', @@ -393,6 +400,7 @@ 'original_name': 'Current (BE1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40079', @@ -445,6 +453,7 @@ 'original_name': 'Current (BE2)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40081', @@ -497,6 +506,7 @@ 'original_name': 'Current (BE2)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40081', @@ -549,6 +559,7 @@ 'original_name': 'Current (BE3)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40083', @@ -601,6 +612,7 @@ 'original_name': 'Current (BE3)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40083', @@ -653,6 +665,7 @@ 'original_name': 'Current compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-41778', @@ -705,6 +718,7 @@ 'original_name': 'Current compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-41778', @@ -755,6 +769,7 @@ 'original_name': 'Current fan mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_mode', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43108', @@ -802,6 +817,7 @@ 'original_name': 'Current fan mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_mode', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43108', @@ -849,6 +865,7 @@ 'original_name': 'Current hot water mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43109', @@ -897,6 +914,7 @@ 'original_name': 'Current hot water mode', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43109', @@ -947,6 +965,7 @@ 'original_name': 'Current outd temp (BT1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40004', @@ -999,6 +1018,7 @@ 'original_name': 'Current outd temp (BT1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40004', @@ -1049,6 +1069,7 @@ 'original_name': 'Decrease from reference value', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43125', @@ -1097,6 +1118,7 @@ 'original_name': 'Decrease from reference value', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43125', @@ -1150,6 +1172,7 @@ 'original_name': 'Defrosting time', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43066', @@ -1205,6 +1228,7 @@ 'original_name': 'Defrosting time', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43066', @@ -1255,6 +1279,7 @@ 'original_name': 'Degree minutes', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940', @@ -1303,6 +1328,7 @@ 'original_name': 'Degree minutes', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40940', @@ -1351,6 +1377,7 @@ 'original_name': 'Desired humidity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-42770', @@ -1399,6 +1426,7 @@ 'original_name': 'Desired humidity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49633', @@ -1447,6 +1475,7 @@ 'original_name': 'Desired humidity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-42770', @@ -1495,6 +1524,7 @@ 'original_name': 'Desired humidity', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49633', @@ -1545,6 +1575,7 @@ 'original_name': 'Discharge (BT14)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40018', @@ -1597,6 +1628,7 @@ 'original_name': 'Discharge (BT14)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40018', @@ -1649,6 +1681,7 @@ 'original_name': 'dT Inverter - exh air (BT20)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43146', @@ -1701,6 +1734,7 @@ 'original_name': 'dT Inverter - exh air (BT20)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43146', @@ -1753,6 +1787,7 @@ 'original_name': 'Evaporator (BT16)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40020', @@ -1805,6 +1840,7 @@ 'original_name': 'Evaporator (BT16)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40020', @@ -1857,6 +1893,7 @@ 'original_name': 'Exhaust air (BT20)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40025', @@ -1909,6 +1946,7 @@ 'original_name': 'Exhaust air (BT20)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40025', @@ -1961,6 +1999,7 @@ 'original_name': 'Extract air (BT21)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40026', @@ -2013,6 +2052,7 @@ 'original_name': 'Extract air (BT21)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40026', @@ -2063,6 +2103,7 @@ 'original_name': 'Heating medium pump speed (GP1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43437', @@ -2111,6 +2152,7 @@ 'original_name': 'Heating medium pump speed (GP1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43437', @@ -2161,6 +2203,7 @@ 'original_name': 'Hot water: charge current value ((BT12 | BT63))', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43116', @@ -2213,6 +2256,7 @@ 'original_name': 'Hot water: charge current value ((BT12 | BT63))', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43116', @@ -2265,6 +2309,7 @@ 'original_name': 'Hot water: charge set point value', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43115', @@ -2317,6 +2362,7 @@ 'original_name': 'Hot water: charge set point value', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43115', @@ -2369,6 +2415,7 @@ 'original_name': 'Hot water charging (BT6)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40014', @@ -2421,6 +2468,7 @@ 'original_name': 'Hot water charging (BT6)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40014', @@ -2473,6 +2521,7 @@ 'original_name': 'Hot water top (BT7)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40013', @@ -2525,6 +2574,7 @@ 'original_name': 'Hot water top (BT7)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40013', @@ -2585,6 +2635,7 @@ 'original_name': 'Int elec add heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49993', @@ -2652,6 +2703,7 @@ 'original_name': 'Int elec add heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49993', @@ -2709,6 +2761,7 @@ 'original_name': 'Int elec add heat raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49993-raw', @@ -2756,6 +2809,7 @@ 'original_name': 'Int elec add heat raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'elect_add', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49993-raw', @@ -2805,6 +2859,7 @@ 'original_name': 'Inverter temperature', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43140', @@ -2857,6 +2912,7 @@ 'original_name': 'Inverter temperature', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43140', @@ -2909,6 +2965,7 @@ 'original_name': 'Liquid line (BT15)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40019', @@ -2961,6 +3018,7 @@ 'original_name': 'Liquid line (BT15)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40019', @@ -3013,6 +3071,7 @@ 'original_name': 'Max compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43123', @@ -3065,6 +3124,7 @@ 'original_name': 'Max compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43123', @@ -3117,6 +3177,7 @@ 'original_name': 'Min compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43122', @@ -3169,6 +3230,7 @@ 'original_name': 'Min compressor frequency', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43122', @@ -3221,6 +3283,7 @@ 'original_name': 'Oil temperature (BT29)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40146', @@ -3273,6 +3336,7 @@ 'original_name': 'Oil temperature (BT29)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40146', @@ -3325,6 +3389,7 @@ 'original_name': 'Oil temperature (EP15-BT29)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40145', @@ -3377,6 +3442,7 @@ 'original_name': 'Oil temperature (EP15-BT29)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40145', @@ -3437,6 +3503,7 @@ 'original_name': 'Priority', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'priority', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49994', @@ -3504,6 +3571,7 @@ 'original_name': 'Priority', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'priority', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49994', @@ -3561,6 +3629,7 @@ 'original_name': 'Prior\xadity raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'priority', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49994-raw', @@ -3608,6 +3677,7 @@ 'original_name': 'Prior\xadity raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'priority', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-49994-raw', @@ -3655,6 +3725,7 @@ 'original_name': 'r start diff additional heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-148072r', @@ -3703,6 +3774,7 @@ 'original_name': 'r start diff additional heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-148072r', @@ -3753,6 +3825,7 @@ 'original_name': 'Reference, air speed sensor', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'airflow', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43124', @@ -3805,6 +3878,7 @@ 'original_name': 'Reference, air speed sensor', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'airflow', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43124', @@ -3857,6 +3931,7 @@ 'original_name': 'Return line (BT3)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40012', @@ -3909,6 +3984,7 @@ 'original_name': 'Return line (BT3)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40012', @@ -3961,6 +4037,7 @@ 'original_name': 'Return line (BT62)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40048', @@ -4013,6 +4090,7 @@ 'original_name': 'Return line (BT62)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40048', @@ -4065,6 +4143,7 @@ 'original_name': 'Room temperature (BT50)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40033', @@ -4117,6 +4196,7 @@ 'original_name': 'Room temperature (BT50)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40033', @@ -4174,6 +4254,7 @@ 'original_name': 'Status compressor', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_compressor', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43427', @@ -4235,6 +4316,7 @@ 'original_name': 'Status compressor', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_compressor', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43427', @@ -4289,6 +4371,7 @@ 'original_name': 'Status com\xadpressor raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_compressor', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43427-raw', @@ -4336,6 +4419,7 @@ 'original_name': 'Status com\xadpressor raw', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status_compressor', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43427-raw', @@ -4385,6 +4469,7 @@ 'original_name': 'Suction gas (BT17)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40022', @@ -4437,6 +4522,7 @@ 'original_name': 'Suction gas (BT17)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40022', @@ -4489,6 +4575,7 @@ 'original_name': 'Supply line (BT2)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40008', @@ -4541,6 +4628,7 @@ 'original_name': 'Supply line (BT2)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40008', @@ -4593,6 +4681,7 @@ 'original_name': 'Supply line (BT61)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40047', @@ -4645,6 +4734,7 @@ 'original_name': 'Supply line (BT61)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40047', @@ -4695,6 +4785,7 @@ 'original_name': 'Time factor add heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43081', @@ -4743,6 +4834,7 @@ 'original_name': 'Time factor add heat', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-43081', @@ -4791,6 +4883,7 @@ 'original_name': 'Value, air velocity sensor (BS1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40050', @@ -4839,6 +4932,7 @@ 'original_name': 'Value, air velocity sensor (BS1)', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-40050', diff --git a/tests/components/myuplink/snapshots/test_switch.ambr b/tests/components/myuplink/snapshots/test_switch.ambr index 142d4caa455..4f8d690ada6 100644 --- a/tests/components/myuplink/snapshots/test_switch.ambr +++ b/tests/components/myuplink/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'In\xadcreased venti\xadlation', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boost_ventilation', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50005', @@ -74,6 +75,7 @@ 'original_name': 'In\xadcreased venti\xadlation', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boost_ventilation', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50005', @@ -121,6 +123,7 @@ 'original_name': 'Tempo\xadrary lux', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temporary_lux', 'unique_id': 'robin-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50004', @@ -168,6 +171,7 @@ 'original_name': 'Tempo\xadrary lux', 'platform': 'myuplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temporary_lux', 'unique_id': 'batman-r-1234-20240201-123456-aa-bb-cc-dd-ee-ff-50004', diff --git a/tests/components/nam/snapshots/test_sensor.ambr b/tests/components/nam/snapshots/test_sensor.ambr index c6c32737a31..cc6bc9bc7b6 100644 --- a/tests/components/nam/snapshots/test_sensor.ambr +++ b/tests/components/nam/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'BH1750 illuminance', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bh1750_illuminance', 'unique_id': 'aa:bb:cc:dd:ee:ff-bh1750_illuminance', @@ -87,6 +88,7 @@ 'original_name': 'BME280 humidity', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bme280_humidity', 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_humidity', @@ -142,6 +144,7 @@ 'original_name': 'BME280 pressure', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bme280_pressure', 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_pressure', @@ -197,6 +200,7 @@ 'original_name': 'BME280 temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bme280_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_temperature', @@ -252,6 +256,7 @@ 'original_name': 'BMP180 pressure', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bmp180_pressure', 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_pressure', @@ -307,6 +312,7 @@ 'original_name': 'BMP180 temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bmp180_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp180_temperature', @@ -362,6 +368,7 @@ 'original_name': 'BMP280 pressure', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bmp280_pressure', 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_pressure', @@ -417,6 +424,7 @@ 'original_name': 'BMP280 temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bmp280_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-bmp280_temperature', @@ -472,6 +480,7 @@ 'original_name': 'DHT22 humidity', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dht22_humidity', 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_humidity', @@ -527,6 +536,7 @@ 'original_name': 'DHT22 temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dht22_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_temperature', @@ -582,6 +592,7 @@ 'original_name': 'DS18B20 temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ds18b20_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-ds18b20_temperature', @@ -637,6 +648,7 @@ 'original_name': 'HECA humidity', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heca_humidity', 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_humidity', @@ -692,6 +704,7 @@ 'original_name': 'HECA temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heca_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-heca_temperature', @@ -742,6 +755,7 @@ 'original_name': 'Last restart', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_restart', 'unique_id': 'aa:bb:cc:dd:ee:ff-uptime', @@ -795,6 +809,7 @@ 'original_name': 'MH-Z14A carbon dioxide', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mhz14a_carbon_dioxide', 'unique_id': 'aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide', @@ -845,6 +860,7 @@ 'original_name': 'PMSx003 common air quality index', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pmsx003_caqi', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi', @@ -900,6 +916,7 @@ 'original_name': 'PMSx003 common air quality index level', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pmsx003_caqi_level', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_caqi_level', @@ -960,6 +977,7 @@ 'original_name': 'PMSx003 PM1', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pmsx003_pm1', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0', @@ -1015,6 +1033,7 @@ 'original_name': 'PMSx003 PM10', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pmsx003_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1', @@ -1070,6 +1089,7 @@ 'original_name': 'PMSx003 PM2.5', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pmsx003_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2', @@ -1120,6 +1140,7 @@ 'original_name': 'SDS011 common air quality index', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sds011_caqi', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi', @@ -1175,6 +1196,7 @@ 'original_name': 'SDS011 common air quality index level', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sds011_caqi_level', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_caqi_level', @@ -1235,6 +1257,7 @@ 'original_name': 'SDS011 PM10', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sds011_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1', @@ -1290,6 +1313,7 @@ 'original_name': 'SDS011 PM2.5', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sds011_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2', @@ -1345,6 +1369,7 @@ 'original_name': 'SHT3X humidity', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sht3x_humidity', 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_humidity', @@ -1400,6 +1425,7 @@ 'original_name': 'SHT3X temperature', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sht3x_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_temperature', @@ -1455,6 +1481,7 @@ 'original_name': 'Signal strength', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff-signal', @@ -1505,6 +1532,7 @@ 'original_name': 'SPS30 common air quality index', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_caqi', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi', @@ -1560,6 +1588,7 @@ 'original_name': 'SPS30 common air quality index level', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_caqi_level', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_caqi_level', @@ -1620,6 +1649,7 @@ 'original_name': 'SPS30 PM1', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_pm1', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0', @@ -1675,6 +1705,7 @@ 'original_name': 'SPS30 PM10', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_pm10', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1', @@ -1730,6 +1761,7 @@ 'original_name': 'SPS30 PM2.5', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_pm25', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2', @@ -1785,6 +1817,7 @@ 'original_name': 'SPS30 PM4', 'platform': 'nam', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sps30_pm4', 'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4', diff --git a/tests/components/nanoleaf/snapshots/test_light.ambr b/tests/components/nanoleaf/snapshots/test_light.ambr index 277c24a7365..19d857026dd 100644 --- a/tests/components/nanoleaf/snapshots/test_light.ambr +++ b/tests/components/nanoleaf/snapshots/test_light.ambr @@ -41,6 +41,7 @@ 'original_name': None, 'platform': 'nanoleaf', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'light', 'unique_id': 'ABCDEF123456', diff --git a/tests/components/netatmo/snapshots/test_binary_sensor.ambr b/tests/components/netatmo/snapshots/test_binary_sensor.ambr index 3066c999655..0cf44637a77 100644 --- a/tests/components/netatmo/snapshots/test_binary_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:26:68:92-reachable', @@ -78,6 +79,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:26:69:0c-reachable', @@ -129,6 +131,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:25:cf:a8-reachable', @@ -180,6 +183,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:26:65:14-reachable', @@ -231,6 +235,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:3e:c5:46-reachable', @@ -282,6 +287,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:7e:18-reachable', @@ -331,6 +337,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:44:92-reachable', @@ -380,6 +387,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:bb:26-reachable', @@ -431,6 +439,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:03:1b:e4-reachable', @@ -480,6 +489,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:1c:42-reachable', @@ -529,6 +539,7 @@ 'original_name': 'Connectivity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:c1:ea-reachable', diff --git a/tests/components/netatmo/snapshots/test_button.ambr b/tests/components/netatmo/snapshots/test_button.ambr index 086403c3b69..e43d58ee962 100644 --- a/tests/components/netatmo/snapshots/test_button.ambr +++ b/tests/components/netatmo/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Preferred position', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'preferred_position', 'unique_id': '0009999993-DeviceType.NBO-preferred_position', @@ -75,6 +76,7 @@ 'original_name': 'Preferred position', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'preferred_position', 'unique_id': '0009999992-DeviceType.NBR-preferred_position', diff --git a/tests/components/netatmo/snapshots/test_camera.ambr b/tests/components/netatmo/snapshots/test_camera.ambr index 7f38e261768..0b9bb4e948d 100644 --- a/tests/components/netatmo/snapshots/test_camera.ambr +++ b/tests/components/netatmo/snapshots/test_camera.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12:34:56:10:b9:0e-DeviceType.NOC', @@ -88,6 +89,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12:34:56:00:f1:62-DeviceType.NACamera', @@ -149,6 +151,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12:34:56:10:f1:66-DeviceType.NDB', diff --git a/tests/components/netatmo/snapshots/test_climate.ambr b/tests/components/netatmo/snapshots/test_climate.ambr index 506e0fb5590..22a50213306 100644 --- a/tests/components/netatmo/snapshots/test_climate.ambr +++ b/tests/components/netatmo/snapshots/test_climate.ambr @@ -41,6 +41,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '222452125-DeviceType.OTM', @@ -117,6 +118,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '2940411577-DeviceType.NRV', @@ -199,6 +201,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '1002003001-DeviceType.BNS', @@ -280,6 +283,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '2833524037-DeviceType.NRV', @@ -363,6 +367,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'thermostat', 'unique_id': '2746182631-DeviceType.NATherm1', diff --git a/tests/components/netatmo/snapshots/test_cover.ambr b/tests/components/netatmo/snapshots/test_cover.ambr index 46aafb32e8e..1f83fcba615 100644 --- a/tests/components/netatmo/snapshots/test_cover.ambr +++ b/tests/components/netatmo/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '0009999993-DeviceType.NBO', @@ -78,6 +79,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '0009999992-DeviceType.NBR', diff --git a/tests/components/netatmo/snapshots/test_fan.ambr b/tests/components/netatmo/snapshots/test_fan.ambr index f850f7ada3b..51136218734 100644 --- a/tests/components/netatmo/snapshots/test_fan.ambr +++ b/tests/components/netatmo/snapshots/test_fan.ambr @@ -32,6 +32,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12:34:56:00:01:01:01:b1-DeviceType.NLLF', diff --git a/tests/components/netatmo/snapshots/test_light.ambr b/tests/components/netatmo/snapshots/test_light.ambr index cc7da6e8712..21fdc11842a 100644 --- a/tests/components/netatmo/snapshots/test_light.ambr +++ b/tests/components/netatmo/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:00:01:01:01:a1-light', @@ -88,6 +89,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:10:b9:0e-light', @@ -144,6 +146,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:11:22:33:00:11:45:fe-light', diff --git a/tests/components/netatmo/snapshots/test_select.ambr b/tests/components/netatmo/snapshots/test_select.ambr index d98d9adb87f..f7c6303cead 100644 --- a/tests/components/netatmo/snapshots/test_select.ambr +++ b/tests/components/netatmo/snapshots/test_select.ambr @@ -32,6 +32,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '91763b24c43d3e344f424e8b-schedule-select', diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 8b974027116..1016a889155 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:26:68:92-pressure', @@ -90,6 +91,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:26:68:92-co2', @@ -151,6 +153,7 @@ 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:68:92-health_idx', @@ -211,6 +214,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:26:68:92-humidity', @@ -266,6 +270,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:26:68:92-noise', @@ -319,6 +324,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:68:92-pressure_trend', @@ -369,6 +375,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:26:68:92-reachable', @@ -424,6 +431,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:26:68:92-temperature', @@ -477,6 +485,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:68:92-temp_trend', @@ -527,6 +536,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:68:92-wifi_status', @@ -585,6 +595,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:26:69:0c-pressure', @@ -638,6 +649,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:26:69:0c-co2', @@ -697,6 +709,7 @@ 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:69:0c-health_idx', @@ -755,6 +768,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:26:69:0c-humidity', @@ -808,6 +822,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:26:69:0c-noise', @@ -859,6 +874,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:69:0c-pressure_trend', @@ -907,6 +923,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:26:69:0c-reachable', @@ -962,6 +979,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:26:69:0c-temperature', @@ -1013,6 +1031,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:69:0c-temp_trend', @@ -1061,6 +1080,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:69:0c-wifi_status', @@ -1113,6 +1133,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '222452125-12:34:56:20:f5:8c-battery', @@ -1164,6 +1185,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -1212,6 +1234,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -1262,6 +1285,7 @@ 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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-power', @@ -1315,6 +1339,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1002003001-1002003001-humidity', @@ -1366,6 +1391,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:00:16:0e-12:34:56:00:16:0e-reachable', @@ -1414,6 +1440,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -1470,6 +1497,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-pressure', @@ -1525,6 +1553,7 @@ 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_angle', 'unique_id': 'Home-avg-gustangle_value', @@ -1580,6 +1609,7 @@ 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_strength', 'unique_id': 'Home-avg-guststrength', @@ -1635,6 +1665,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-humidity', @@ -1690,6 +1721,7 @@ 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-rain', @@ -1748,6 +1780,7 @@ 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_1', 'unique_id': 'Home-avg-sum_rain_1', @@ -1803,6 +1836,7 @@ 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_24', 'unique_id': 'Home-avg-sum_rain_24', @@ -1861,6 +1895,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-temperature', @@ -1916,6 +1951,7 @@ 'original_name': 'Wind direction', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-windangle_value', @@ -1971,6 +2007,7 @@ 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-avg-windstrength', @@ -2032,6 +2069,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-pressure', @@ -2087,6 +2125,7 @@ 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_angle', 'unique_id': 'Home-max-gustangle_value', @@ -2142,6 +2181,7 @@ 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_strength', 'unique_id': 'Home-max-guststrength', @@ -2197,6 +2237,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-humidity', @@ -2252,6 +2293,7 @@ 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-rain', @@ -2310,6 +2352,7 @@ 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_1', 'unique_id': 'Home-max-sum_rain_1', @@ -2365,6 +2408,7 @@ 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_24', 'unique_id': 'Home-max-sum_rain_24', @@ -2423,6 +2467,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-temperature', @@ -2478,6 +2523,7 @@ 'original_name': 'Wind direction', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-windangle_value', @@ -2533,6 +2579,7 @@ 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-max-windstrength', @@ -2594,6 +2641,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-pressure', @@ -2649,6 +2697,7 @@ 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_angle', 'unique_id': 'Home-min-gustangle_value', @@ -2704,6 +2753,7 @@ 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_strength', 'unique_id': 'Home-min-guststrength', @@ -2759,6 +2809,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-humidity', @@ -2814,6 +2865,7 @@ 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-rain', @@ -2872,6 +2924,7 @@ 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_1', 'unique_id': 'Home-min-sum_rain_1', @@ -2927,6 +2980,7 @@ 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_24', 'unique_id': 'Home-min-sum_rain_24', @@ -2985,6 +3039,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-temperature', @@ -3040,6 +3095,7 @@ 'original_name': 'Wind direction', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-windangle_value', @@ -3095,6 +3151,7 @@ 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'Home-min-windstrength', @@ -3148,6 +3205,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3204,6 +3262,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:25:cf:a8-pressure', @@ -3259,6 +3318,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:25:cf:a8-co2', @@ -3320,6 +3380,7 @@ 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_idx', 'unique_id': '12:34:56:25:cf:a8-health_idx', @@ -3380,6 +3441,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:25:cf:a8-humidity', @@ -3435,6 +3497,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:25:cf:a8-noise', @@ -3488,6 +3551,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:25:cf:a8-pressure_trend', @@ -3538,6 +3602,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:25:cf:a8-reachable', @@ -3593,6 +3658,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:25:cf:a8-temperature', @@ -3646,6 +3712,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:25:cf:a8-temp_trend', @@ -3696,6 +3763,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:25:cf:a8-wifi_status', @@ -3746,6 +3814,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3794,6 +3863,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3842,6 +3912,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3890,6 +3961,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3938,6 +4010,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -3994,6 +4067,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:26:65:14-pressure', @@ -4049,6 +4123,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2746182631-12:34:56:00:01:ae-battery', @@ -4102,6 +4177,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:26:65:14-co2', @@ -4163,6 +4239,7 @@ 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_idx', 'unique_id': '12:34:56:26:65:14-health_idx', @@ -4223,6 +4300,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:26:65:14-humidity', @@ -4278,6 +4356,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:26:65:14-noise', @@ -4331,6 +4410,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:26:65:14-pressure_trend', @@ -4381,6 +4461,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:26:65:14-reachable', @@ -4436,6 +4517,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:26:65:14-temperature', @@ -4489,6 +4571,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:26:65:14-temp_trend', @@ -4539,6 +4622,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:26:65:14-wifi_status', @@ -4597,6 +4681,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:3e:c5:46-pressure', @@ -4652,6 +4737,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:3e:c5:46-co2', @@ -4713,6 +4799,7 @@ 'original_name': 'Health index', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'health_idx', 'unique_id': '12:34:56:3e:c5:46-health_idx', @@ -4773,6 +4860,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:3e:c5:46-humidity', @@ -4828,6 +4916,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:3e:c5:46-noise', @@ -4881,6 +4970,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:3e:c5:46-pressure_trend', @@ -4931,6 +5021,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:3e:c5:46-reachable', @@ -4986,6 +5077,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:3e:c5:46-temperature', @@ -5039,6 +5131,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:3e:c5:46-temp_trend', @@ -5089,6 +5182,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:3e:c5:46-wifi_status', @@ -5139,6 +5233,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -5189,6 +5284,7 @@ 'original_name': 'Power', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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-power', @@ -5240,6 +5336,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_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', @@ -5290,6 +5387,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2833524037-12:34:56:03:a5:54-battery', @@ -5343,6 +5441,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2940411577-12:34:56:03:a0:ac-battery', @@ -5402,6 +5501,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure', 'unique_id': '12:34:56:80:bb:26-pressure', @@ -5457,6 +5557,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '12:34:56:80:7e:18-battery_percent', @@ -5510,6 +5611,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:80:7e:18-co2', @@ -5563,6 +5665,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:80:7e:18-humidity', @@ -5614,6 +5717,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:80:7e:18-reachable', @@ -5662,6 +5766,7 @@ 'original_name': 'RF strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:7e:18-rf_status', @@ -5715,6 +5820,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:80:7e:18-temperature', @@ -5766,6 +5872,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:7e:18-temp_trend', @@ -5816,6 +5923,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '12:34:56:80:44:92-battery_percent', @@ -5869,6 +5977,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:80:44:92-co2', @@ -5922,6 +6031,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:80:44:92-humidity', @@ -5973,6 +6083,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:80:44:92-reachable', @@ -6021,6 +6132,7 @@ 'original_name': 'RF strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:44:92-rf_status', @@ -6074,6 +6186,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:80:44:92-temperature', @@ -6125,6 +6238,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:44:92-temp_trend', @@ -6175,6 +6289,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'co2', 'unique_id': '12:34:56:80:bb:26-co2', @@ -6230,6 +6345,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '12:34:56:03:1b:e4-battery_percent', @@ -6283,6 +6399,7 @@ 'original_name': 'Gust angle', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_angle', 'unique_id': '12:34:56:03:1b:e4-gustangle_value', @@ -6345,6 +6462,7 @@ 'original_name': 'Gust direction', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_direction', 'unique_id': '12:34:56:03:1b:e4-gustangle', @@ -6406,6 +6524,7 @@ 'original_name': 'Gust strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gust_strength', 'unique_id': '12:34:56:03:1b:e4-guststrength', @@ -6457,6 +6576,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:03:1b:e4-reachable', @@ -6505,6 +6625,7 @@ 'original_name': 'RF strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rf_strength', 'unique_id': '12:34:56:03:1b:e4-rf_status', @@ -6555,6 +6676,7 @@ 'original_name': 'Wind angle', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_angle', 'unique_id': '12:34:56:03:1b:e4-windangle_value', @@ -6617,6 +6739,7 @@ 'original_name': 'Wind direction', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_direction', 'unique_id': '12:34:56:03:1b:e4-windangle', @@ -6678,6 +6801,7 @@ 'original_name': 'Wind speed', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_strength', 'unique_id': '12:34:56:03:1b:e4-windstrength', @@ -6731,6 +6855,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:80:bb:26-humidity', @@ -6786,6 +6911,7 @@ 'original_name': 'Noise', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'noise', 'unique_id': '12:34:56:80:bb:26-noise', @@ -6841,6 +6967,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '12:34:56:80:1c:42-battery_percent', @@ -6894,6 +7021,7 @@ 'original_name': 'Humidity', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '12:34:56:80:1c:42-humidity', @@ -6945,6 +7073,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:80:1c:42-reachable', @@ -6993,6 +7122,7 @@ 'original_name': 'RF strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:1c:42-rf_status', @@ -7046,6 +7176,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:80:1c:42-temperature', @@ -7097,6 +7228,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:1c:42-temp_trend', @@ -7145,6 +7277,7 @@ 'original_name': 'Pressure trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pressure_trend', 'unique_id': '12:34:56:80:bb:26-pressure_trend', @@ -7197,6 +7330,7 @@ 'original_name': 'Battery', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '12:34:56:80:c1:ea-battery_percent', @@ -7250,6 +7384,7 @@ 'original_name': 'Precipitation', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rain', 'unique_id': '12:34:56:80:c1:ea-rain', @@ -7306,6 +7441,7 @@ 'original_name': 'Precipitation last hour', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_1', 'unique_id': '12:34:56:80:c1:ea-sum_rain_1', @@ -7359,6 +7495,7 @@ 'original_name': 'Precipitation today', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sum_rain_24', 'unique_id': '12:34:56:80:c1:ea-sum_rain_24', @@ -7410,6 +7547,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:80:c1:ea-reachable', @@ -7458,6 +7596,7 @@ 'original_name': 'RF strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rf_strength', 'unique_id': '12:34:56:80:c1:ea-rf_status', @@ -7506,6 +7645,7 @@ 'original_name': 'Reachability', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reachable', 'unique_id': '12:34:56:80:bb:26-reachable', @@ -7561,6 +7701,7 @@ 'original_name': 'Temperature', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '12:34:56:80:bb:26-temperature', @@ -7614,6 +7755,7 @@ 'original_name': 'Temperature trend', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temp_trend', 'unique_id': '12:34:56:80:bb:26-temp_trend', @@ -7664,6 +7806,7 @@ 'original_name': 'Wi-Fi strength', 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_strength', 'unique_id': '12:34:56:80:bb:26-wifi_status', diff --git a/tests/components/netatmo/snapshots/test_switch.ambr b/tests/components/netatmo/snapshots/test_switch.ambr index f44cbcd22a5..3dd2d5658ac 100644 --- a/tests/components/netatmo/snapshots/test_switch.ambr +++ b/tests/components/netatmo/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'netatmo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12:34:56:80:00:12:ac:f2-DeviceType.NLP', diff --git a/tests/components/nextcloud/snapshots/test_binary_sensor.ambr b/tests/components/nextcloud/snapshots/test_binary_sensor.ambr index 578659d411d..1037147469f 100644 --- a/tests/components/nextcloud/snapshots/test_binary_sensor.ambr +++ b/tests/components/nextcloud/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Avatars enabled', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_enable_avatars', 'unique_id': '1234567890abcdef#system_enable_avatars', @@ -74,6 +75,7 @@ 'original_name': 'Debug enabled', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_debug', 'unique_id': '1234567890abcdef#system_debug', @@ -121,6 +123,7 @@ 'original_name': 'Filelocking enabled', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_filelocking_enabled', 'unique_id': '1234567890abcdef#system_filelocking.enabled', @@ -168,6 +171,7 @@ 'original_name': 'JIT active', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_on', 'unique_id': '1234567890abcdef#jit_on', @@ -215,6 +219,7 @@ 'original_name': 'JIT enabled', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_enabled', 'unique_id': '1234567890abcdef#jit_enabled', @@ -262,6 +267,7 @@ 'original_name': 'Previews enabled', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_enable_previews', 'unique_id': '1234567890abcdef#system_enable_previews', diff --git a/tests/components/nextcloud/snapshots/test_sensor.ambr b/tests/components/nextcloud/snapshots/test_sensor.ambr index e6154841a28..4aebb1f21f8 100644 --- a/tests/components/nextcloud/snapshots/test_sensor.ambr +++ b/tests/components/nextcloud/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Amount of active users last 5 minutes', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_activeusers_last5minutes', 'unique_id': '1234567890abcdef#activeUsers_last5minutes', @@ -79,6 +80,7 @@ 'original_name': 'Amount of active users last day', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_activeusers_last24hours', 'unique_id': '1234567890abcdef#activeUsers_last24hours', @@ -129,6 +131,7 @@ 'original_name': 'Amount of active users last hour', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_activeusers_last1hour', 'unique_id': '1234567890abcdef#activeUsers_last1hour', @@ -179,6 +182,7 @@ 'original_name': 'Amount of files', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_files', 'unique_id': '1234567890abcdef#storage_num_files', @@ -229,6 +233,7 @@ 'original_name': 'Amount of group shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_groups', 'unique_id': '1234567890abcdef#shares_num_shares_groups', @@ -279,6 +284,7 @@ 'original_name': 'Amount of link shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_link', 'unique_id': '1234567890abcdef#shares_num_shares_link', @@ -329,6 +335,7 @@ 'original_name': 'Amount of local storages', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_storages_local', 'unique_id': '1234567890abcdef#storage_num_storages_local', @@ -379,6 +386,7 @@ 'original_name': 'Amount of mail shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_mail', 'unique_id': '1234567890abcdef#shares_num_shares_mail', @@ -429,6 +437,7 @@ 'original_name': 'Amount of other storages', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_storages_other', 'unique_id': '1234567890abcdef#storage_num_storages_other', @@ -479,6 +488,7 @@ 'original_name': 'Amount of passwordless link shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_link_no_password', 'unique_id': '1234567890abcdef#shares_num_shares_link_no_password', @@ -529,6 +539,7 @@ 'original_name': 'Amount of room shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_room', 'unique_id': '1234567890abcdef#shares_num_shares_room', @@ -579,6 +590,7 @@ 'original_name': 'Amount of shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares', 'unique_id': '1234567890abcdef#shares_num_shares', @@ -629,6 +641,7 @@ 'original_name': 'Amount of shares received', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_fed_shares_received', 'unique_id': '1234567890abcdef#shares_num_fed_shares_received', @@ -679,6 +692,7 @@ 'original_name': 'Amount of shares sent', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_fed_shares_sent', 'unique_id': '1234567890abcdef#shares_num_fed_shares_sent', @@ -729,6 +743,7 @@ 'original_name': 'Amount of storages', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_storages', 'unique_id': '1234567890abcdef#storage_num_storages', @@ -779,6 +794,7 @@ 'original_name': 'Amount of storages at home', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_storages_home', 'unique_id': '1234567890abcdef#storage_num_storages_home', @@ -829,6 +845,7 @@ 'original_name': 'Amount of user', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_storage_num_users', 'unique_id': '1234567890abcdef#storage_num_users', @@ -879,6 +896,7 @@ 'original_name': 'Amount of user shares', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_shares_num_shares_user', 'unique_id': '1234567890abcdef#shares_num_shares_user', @@ -929,6 +947,7 @@ 'original_name': 'Apps installed', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_apps_num_installed', 'unique_id': '1234567890abcdef#system_apps_num_installed', @@ -979,6 +998,7 @@ 'original_name': 'Cache expunges', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_expunges', 'unique_id': '1234567890abcdef#cache_expunges', @@ -1027,6 +1047,7 @@ 'original_name': 'Cache memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_memory_type', 'unique_id': '1234567890abcdef#cache_memory_type', @@ -1080,6 +1101,7 @@ 'original_name': 'Cache memory size', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_mem_size', 'unique_id': '1234567890abcdef#cache_mem_size', @@ -1131,6 +1153,7 @@ 'original_name': 'Cache number of entries', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_num_entries', 'unique_id': '1234567890abcdef#cache_num_entries', @@ -1181,6 +1204,7 @@ 'original_name': 'Cache number of hits', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_num_hits', 'unique_id': '1234567890abcdef#cache_num_hits', @@ -1231,6 +1255,7 @@ 'original_name': 'Cache number of inserts', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_num_inserts', 'unique_id': '1234567890abcdef#cache_num_inserts', @@ -1281,6 +1306,7 @@ 'original_name': 'Cache number of misses', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_num_misses', 'unique_id': '1234567890abcdef#cache_num_misses', @@ -1331,6 +1357,7 @@ 'original_name': 'Cache number of slots', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_num_slots', 'unique_id': '1234567890abcdef#cache_num_slots', @@ -1379,6 +1406,7 @@ 'original_name': 'Cache start time', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_start_time', 'unique_id': '1234567890abcdef#cache_start_time', @@ -1427,6 +1455,7 @@ 'original_name': 'Cache TTL', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_cache_ttl', 'unique_id': '1234567890abcdef#cache_ttl', @@ -1477,6 +1506,7 @@ 'original_name': 'CPU load last 15 minutes', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_cpuload_15', 'unique_id': '1234567890abcdef#system_cpuload_15', @@ -1528,6 +1558,7 @@ 'original_name': 'CPU load last 1 minute', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_cpuload_1', 'unique_id': '1234567890abcdef#system_cpuload_1', @@ -1579,6 +1610,7 @@ 'original_name': 'CPU load last 5 minutes', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_cpuload_5', 'unique_id': '1234567890abcdef#system_cpuload_5', @@ -1633,6 +1665,7 @@ 'original_name': 'Database size', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_database_size', 'unique_id': '1234567890abcdef#database_size', @@ -1682,6 +1715,7 @@ 'original_name': 'Database type', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_database_type', 'unique_id': '1234567890abcdef#database_type', @@ -1729,6 +1763,7 @@ 'original_name': 'Database version', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_database_version', 'unique_id': '1234567890abcdef#database_version', @@ -1782,6 +1817,7 @@ 'original_name': 'Free memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_mem_free', 'unique_id': '1234567890abcdef#system_mem_free', @@ -1837,6 +1873,7 @@ 'original_name': 'Free space', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_freespace', 'unique_id': '1234567890abcdef#system_freespace', @@ -1892,6 +1929,7 @@ 'original_name': 'Free swap memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_swap_free', 'unique_id': '1234567890abcdef#system_swap_free', @@ -1947,6 +1985,7 @@ 'original_name': 'Interned buffer size', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_interned_strings_usage_buffer_size', 'unique_id': '1234567890abcdef#interned_strings_usage_buffer_size', @@ -2002,6 +2041,7 @@ 'original_name': 'Interned free memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_interned_strings_usage_free_memory', 'unique_id': '1234567890abcdef#interned_strings_usage_free_memory', @@ -2053,6 +2093,7 @@ 'original_name': 'Interned number of strings', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_interned_strings_usage_number_of_strings', 'unique_id': '1234567890abcdef#interned_strings_usage_number_of_strings', @@ -2107,6 +2148,7 @@ 'original_name': 'Interned used memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_interned_strings_usage_used_memory', 'unique_id': '1234567890abcdef#interned_strings_usage_used_memory', @@ -2162,6 +2204,7 @@ 'original_name': 'JIT buffer free', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_buffer_free', 'unique_id': '1234567890abcdef#jit_buffer_free', @@ -2217,6 +2260,7 @@ 'original_name': 'JIT buffer size', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_buffer_size', 'unique_id': '1234567890abcdef#jit_buffer_size', @@ -2266,6 +2310,7 @@ 'original_name': 'JIT kind', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_kind', 'unique_id': '1234567890abcdef#jit_kind', @@ -2313,6 +2358,7 @@ 'original_name': 'JIT opt flags', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_opt_flags', 'unique_id': '1234567890abcdef#jit_opt_flags', @@ -2360,6 +2406,7 @@ 'original_name': 'JIT opt level', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_jit_opt_level', 'unique_id': '1234567890abcdef#jit_opt_level', @@ -2409,6 +2456,7 @@ 'original_name': 'Opcache blacklist miss ratio', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_blacklist_miss_ratio', 'unique_id': '1234567890abcdef#opcache_statistics_blacklist_miss_ratio', @@ -2460,6 +2508,7 @@ 'original_name': 'Opcache blacklist misses', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_blacklist_misses', 'unique_id': '1234567890abcdef#opcache_statistics_blacklist_misses', @@ -2510,6 +2559,7 @@ 'original_name': 'Opcache cached keys', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_num_cached_keys', 'unique_id': '1234567890abcdef#opcache_statistics_num_cached_keys', @@ -2560,6 +2610,7 @@ 'original_name': 'Opcache cached scripts', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_num_cached_scripts', 'unique_id': '1234567890abcdef#opcache_statistics_num_cached_scripts', @@ -2611,6 +2662,7 @@ 'original_name': 'Opcache current wasted percentage', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_opcache_memory_usage_current_wasted_percentage', 'unique_id': '1234567890abcdef#server_php_opcache_memory_usage_current_wasted_percentage', @@ -2665,6 +2717,7 @@ 'original_name': 'Opcache free memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_opcache_memory_usage_free_memory', 'unique_id': '1234567890abcdef#server_php_opcache_memory_usage_free_memory', @@ -2716,6 +2769,7 @@ 'original_name': 'Opcache hash restarts', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_hash_restarts', 'unique_id': '1234567890abcdef#opcache_statistics_hash_restarts', @@ -2767,6 +2821,7 @@ 'original_name': 'Opcache hit rate', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_opcache_hit_rate', 'unique_id': '1234567890abcdef#opcache_statistics_opcache_hit_rate', @@ -2817,6 +2872,7 @@ 'original_name': 'Opcache hits', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_hits', 'unique_id': '1234567890abcdef#opcache_statistics_hits', @@ -2865,6 +2921,7 @@ 'original_name': 'Opcache last restart time', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_last_restart_time', 'unique_id': '1234567890abcdef#opcache_statistics_last_restart_time', @@ -2915,6 +2972,7 @@ 'original_name': 'Opcache manual restarts', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_manual_restarts', 'unique_id': '1234567890abcdef#opcache_statistics_manual_restarts', @@ -2965,6 +3023,7 @@ 'original_name': 'Opcache max cached keys', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_max_cached_keys', 'unique_id': '1234567890abcdef#opcache_statistics_max_cached_keys', @@ -3015,6 +3074,7 @@ 'original_name': 'Opcache misses', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_misses', 'unique_id': '1234567890abcdef#opcache_statistics_misses', @@ -3065,6 +3125,7 @@ 'original_name': 'Opcache out of memory restarts', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_oom_restarts', 'unique_id': '1234567890abcdef#opcache_statistics_oom_restarts', @@ -3113,6 +3174,7 @@ 'original_name': 'Opcache start time', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_opcache_statistics_start_time', 'unique_id': '1234567890abcdef#opcache_statistics_start_time', @@ -3167,6 +3229,7 @@ 'original_name': 'Opcache used memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_opcache_memory_usage_used_memory', 'unique_id': '1234567890abcdef#server_php_opcache_memory_usage_used_memory', @@ -3222,6 +3285,7 @@ 'original_name': 'Opcache wasted memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_opcache_memory_usage_wasted_memory', 'unique_id': '1234567890abcdef#server_php_opcache_memory_usage_wasted_memory', @@ -3271,6 +3335,7 @@ 'original_name': 'PHP max execution time', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_max_execution_time', 'unique_id': '1234567890abcdef#server_php_max_execution_time', @@ -3326,6 +3391,7 @@ 'original_name': 'PHP memory limit', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_memory_limit', 'unique_id': '1234567890abcdef#server_php_memory_limit', @@ -3381,6 +3447,7 @@ 'original_name': 'PHP upload maximum filesize', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_upload_max_filesize', 'unique_id': '1234567890abcdef#server_php_upload_max_filesize', @@ -3430,6 +3497,7 @@ 'original_name': 'PHP version', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_php_version', 'unique_id': '1234567890abcdef#server_php_version', @@ -3483,6 +3551,7 @@ 'original_name': 'SMA available memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_sma_avail_mem', 'unique_id': '1234567890abcdef#sma_avail_mem', @@ -3534,6 +3603,7 @@ 'original_name': 'SMA number of segments', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_sma_num_seg', 'unique_id': '1234567890abcdef#sma_num_seg', @@ -3588,6 +3658,7 @@ 'original_name': 'SMA segment size', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_sma_seg_size', 'unique_id': '1234567890abcdef#sma_seg_size', @@ -3637,6 +3708,7 @@ 'original_name': 'System memcache distributed', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_memcache_distributed', 'unique_id': '1234567890abcdef#system_memcache.distributed', @@ -3684,6 +3756,7 @@ 'original_name': 'System memcache local', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_memcache_local', 'unique_id': '1234567890abcdef#system_memcache.local', @@ -3731,6 +3804,7 @@ 'original_name': 'System memcache locking', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_memcache_locking', 'unique_id': '1234567890abcdef#system_memcache.locking', @@ -3778,6 +3852,7 @@ 'original_name': 'System theme', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_theme', 'unique_id': '1234567890abcdef#system_theme', @@ -3825,6 +3900,7 @@ 'original_name': 'System version', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_version', 'unique_id': '1234567890abcdef#system_version', @@ -3878,6 +3954,7 @@ 'original_name': 'Total memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_mem_total', 'unique_id': '1234567890abcdef#system_mem_total', @@ -3933,6 +4010,7 @@ 'original_name': 'Total swap memory', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_swap_total', 'unique_id': '1234567890abcdef#system_swap_total', @@ -3984,6 +4062,7 @@ 'original_name': 'Updates available', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_system_apps_num_updates_available', 'unique_id': '1234567890abcdef#system_apps_num_updates_available', @@ -4032,6 +4111,7 @@ 'original_name': 'Webserver', 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nextcloud_server_webserver', 'unique_id': '1234567890abcdef#server_webserver', diff --git a/tests/components/nextcloud/snapshots/test_update.ambr b/tests/components/nextcloud/snapshots/test_update.ambr index a8acd2f5294..0a3ae568a44 100644 --- a/tests/components/nextcloud/snapshots/test_update.ambr +++ b/tests/components/nextcloud/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'nextcloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234567890abcdef#update', diff --git a/tests/components/nextdns/snapshots/test_binary_sensor.ambr b/tests/components/nextdns/snapshots/test_binary_sensor.ambr index 65a477f50f3..f8a05ad00ad 100644 --- a/tests/components/nextdns/snapshots/test_binary_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Device connection status', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_connection_status', 'unique_id': 'xyz12_this_device_nextdns_connection_status', @@ -75,6 +76,7 @@ 'original_name': 'Device profile connection status', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_profile_connection_status', 'unique_id': 'xyz12_this_device_profile_connection_status', diff --git a/tests/components/nextdns/snapshots/test_button.ambr b/tests/components/nextdns/snapshots/test_button.ambr index 3f1f75d1783..d416f9ef47e 100644 --- a/tests/components/nextdns/snapshots/test_button.ambr +++ b/tests/components/nextdns/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Clear logs', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clear_logs', 'unique_id': 'xyz12_clear_logs', diff --git a/tests/components/nextdns/snapshots/test_sensor.ambr b/tests/components/nextdns/snapshots/test_sensor.ambr index 48c3b0894db..6aa061d1a9a 100644 --- a/tests/components/nextdns/snapshots/test_sensor.ambr +++ b/tests/components/nextdns/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'DNS-over-HTTP/3 queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doh3_queries', 'unique_id': 'xyz12_doh3_queries', @@ -80,6 +81,7 @@ 'original_name': 'DNS-over-HTTP/3 queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doh3_queries_ratio', 'unique_id': 'xyz12_doh3_queries_ratio', @@ -131,6 +133,7 @@ 'original_name': 'DNS-over-HTTPS queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doh_queries', 'unique_id': 'xyz12_doh_queries', @@ -182,6 +185,7 @@ 'original_name': 'DNS-over-HTTPS queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doh_queries_ratio', 'unique_id': 'xyz12_doh_queries_ratio', @@ -233,6 +237,7 @@ 'original_name': 'DNS-over-QUIC queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doq_queries', 'unique_id': 'xyz12_doq_queries', @@ -284,6 +289,7 @@ 'original_name': 'DNS-over-QUIC queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doq_queries_ratio', 'unique_id': 'xyz12_doq_queries_ratio', @@ -335,6 +341,7 @@ 'original_name': 'DNS-over-TLS queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dot_queries', 'unique_id': 'xyz12_dot_queries', @@ -386,6 +393,7 @@ 'original_name': 'DNS-over-TLS queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dot_queries_ratio', 'unique_id': 'xyz12_dot_queries_ratio', @@ -437,6 +445,7 @@ 'original_name': 'DNS queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'all_queries', 'unique_id': 'xyz12_all_queries', @@ -488,6 +497,7 @@ 'original_name': 'DNS queries blocked', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'blocked_queries', 'unique_id': 'xyz12_blocked_queries', @@ -539,6 +549,7 @@ 'original_name': 'DNS queries blocked ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'blocked_queries_ratio', 'unique_id': 'xyz12_blocked_queries_ratio', @@ -590,6 +601,7 @@ 'original_name': 'DNS queries relayed', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'relayed_queries', 'unique_id': 'xyz12_relayed_queries', @@ -641,6 +653,7 @@ 'original_name': 'DNSSEC not validated queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'not_validated_queries', 'unique_id': 'xyz12_not_validated_queries', @@ -692,6 +705,7 @@ 'original_name': 'DNSSEC validated queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'validated_queries', 'unique_id': 'xyz12_validated_queries', @@ -743,6 +757,7 @@ 'original_name': 'DNSSEC validated queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'validated_queries_ratio', 'unique_id': 'xyz12_validated_queries_ratio', @@ -794,6 +809,7 @@ 'original_name': 'Encrypted queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'encrypted_queries', 'unique_id': 'xyz12_encrypted_queries', @@ -845,6 +861,7 @@ 'original_name': 'Encrypted queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'encrypted_queries_ratio', 'unique_id': 'xyz12_encrypted_queries_ratio', @@ -896,6 +913,7 @@ 'original_name': 'IPv4 queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ipv4_queries', 'unique_id': 'xyz12_ipv4_queries', @@ -947,6 +965,7 @@ 'original_name': 'IPv6 queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ipv6_queries', 'unique_id': 'xyz12_ipv6_queries', @@ -998,6 +1017,7 @@ 'original_name': 'IPv6 queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ipv6_queries_ratio', 'unique_id': 'xyz12_ipv6_queries_ratio', @@ -1049,6 +1069,7 @@ 'original_name': 'TCP queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tcp_queries', 'unique_id': 'xyz12_tcp_queries', @@ -1100,6 +1121,7 @@ 'original_name': 'TCP queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tcp_queries_ratio', 'unique_id': 'xyz12_tcp_queries_ratio', @@ -1151,6 +1173,7 @@ 'original_name': 'UDP queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'udp_queries', 'unique_id': 'xyz12_udp_queries', @@ -1202,6 +1225,7 @@ 'original_name': 'UDP queries ratio', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'udp_queries_ratio', 'unique_id': 'xyz12_udp_queries_ratio', @@ -1253,6 +1277,7 @@ 'original_name': 'Unencrypted queries', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'unencrypted_queries', 'unique_id': 'xyz12_unencrypted_queries', diff --git a/tests/components/nextdns/snapshots/test_switch.ambr b/tests/components/nextdns/snapshots/test_switch.ambr index e6d63b7f542..0b25baecd20 100644 --- a/tests/components/nextdns/snapshots/test_switch.ambr +++ b/tests/components/nextdns/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'AI-Driven threat detection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ai_threat_detection', 'unique_id': 'xyz12_ai_threat_detection', @@ -74,6 +75,7 @@ 'original_name': 'Allow affiliate & tracking links', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'allow_affiliate', 'unique_id': 'xyz12_allow_affiliate', @@ -121,6 +123,7 @@ 'original_name': 'Anonymized EDNS client subnet', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'anonymized_ecs', 'unique_id': 'xyz12_anonymized_ecs', @@ -168,6 +171,7 @@ 'original_name': 'Block 9GAG', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_9gag', 'unique_id': 'xyz12_block_9gag', @@ -215,6 +219,7 @@ 'original_name': 'Block Amazon', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_amazon', 'unique_id': 'xyz12_block_amazon', @@ -262,6 +267,7 @@ 'original_name': 'Block BeReal', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_bereal', 'unique_id': 'xyz12_block_bereal', @@ -309,6 +315,7 @@ 'original_name': 'Block Blizzard', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_blizzard', 'unique_id': 'xyz12_block_blizzard', @@ -356,6 +363,7 @@ 'original_name': 'Block bypass methods', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_bypass_methods', 'unique_id': 'xyz12_block_bypass_methods', @@ -403,6 +411,7 @@ 'original_name': 'Block ChatGPT', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_chatgpt', 'unique_id': 'xyz12_block_chatgpt', @@ -450,6 +459,7 @@ 'original_name': 'Block child sexual abuse material', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_csam', 'unique_id': 'xyz12_block_csam', @@ -497,6 +507,7 @@ 'original_name': 'Block Dailymotion', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_dailymotion', 'unique_id': 'xyz12_block_dailymotion', @@ -544,6 +555,7 @@ 'original_name': 'Block dating', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_dating', 'unique_id': 'xyz12_block_dating', @@ -591,6 +603,7 @@ 'original_name': 'Block Discord', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_discord', 'unique_id': 'xyz12_block_discord', @@ -638,6 +651,7 @@ 'original_name': 'Block disguised third-party trackers', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_disguised_trackers', 'unique_id': 'xyz12_block_disguised_trackers', @@ -685,6 +699,7 @@ 'original_name': 'Block Disney Plus', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_disneyplus', 'unique_id': 'xyz12_block_disneyplus', @@ -732,6 +747,7 @@ 'original_name': 'Block dynamic DNS hostnames', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_ddns', 'unique_id': 'xyz12_block_ddns', @@ -779,6 +795,7 @@ 'original_name': 'Block eBay', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_ebay', 'unique_id': 'xyz12_block_ebay', @@ -826,6 +843,7 @@ 'original_name': 'Block Facebook', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_facebook', 'unique_id': 'xyz12_block_facebook', @@ -873,6 +891,7 @@ 'original_name': 'Block Fortnite', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_fortnite', 'unique_id': 'xyz12_block_fortnite', @@ -920,6 +939,7 @@ 'original_name': 'Block gambling', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_gambling', 'unique_id': 'xyz12_block_gambling', @@ -967,6 +987,7 @@ 'original_name': 'Block Google Chat', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_google_chat', 'unique_id': 'xyz12_block_google_chat', @@ -1014,6 +1035,7 @@ 'original_name': 'Block HBO Max', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_hbomax', 'unique_id': 'xyz12_block_hbomax', @@ -1061,6 +1083,7 @@ 'original_name': 'Block Hulu', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'xyz12_block_hulu', @@ -1108,6 +1131,7 @@ 'original_name': 'Block Imgur', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_imgur', 'unique_id': 'xyz12_block_imgur', @@ -1155,6 +1179,7 @@ 'original_name': 'Block Instagram', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_instagram', 'unique_id': 'xyz12_block_instagram', @@ -1202,6 +1227,7 @@ 'original_name': 'Block League of Legends', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_leagueoflegends', 'unique_id': 'xyz12_block_leagueoflegends', @@ -1249,6 +1275,7 @@ 'original_name': 'Block Mastodon', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_mastodon', 'unique_id': 'xyz12_block_mastodon', @@ -1296,6 +1323,7 @@ 'original_name': 'Block Messenger', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_messenger', 'unique_id': 'xyz12_block_messenger', @@ -1343,6 +1371,7 @@ 'original_name': 'Block Minecraft', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_minecraft', 'unique_id': 'xyz12_block_minecraft', @@ -1390,6 +1419,7 @@ 'original_name': 'Block Netflix', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_netflix', 'unique_id': 'xyz12_block_netflix', @@ -1437,6 +1467,7 @@ 'original_name': 'Block newly registered domains', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_nrd', 'unique_id': 'xyz12_block_nrd', @@ -1484,6 +1515,7 @@ 'original_name': 'Block online gaming', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_online_gaming', 'unique_id': 'xyz12_block_online_gaming', @@ -1531,6 +1563,7 @@ 'original_name': 'Block page', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_page', 'unique_id': 'xyz12_block_page', @@ -1578,6 +1611,7 @@ 'original_name': 'Block parked domains', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_parked_domains', 'unique_id': 'xyz12_block_parked_domains', @@ -1625,6 +1659,7 @@ 'original_name': 'Block Pinterest', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_pinterest', 'unique_id': 'xyz12_block_pinterest', @@ -1672,6 +1707,7 @@ 'original_name': 'Block piracy', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_piracy', 'unique_id': 'xyz12_block_piracy', @@ -1719,6 +1755,7 @@ 'original_name': 'Block PlayStation Network', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_playstation_network', 'unique_id': 'xyz12_block_playstation_network', @@ -1766,6 +1803,7 @@ 'original_name': 'Block porn', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_porn', 'unique_id': 'xyz12_block_porn', @@ -1813,6 +1851,7 @@ 'original_name': 'Block Prime Video', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_primevideo', 'unique_id': 'xyz12_block_primevideo', @@ -1860,6 +1899,7 @@ 'original_name': 'Block Reddit', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_reddit', 'unique_id': 'xyz12_block_reddit', @@ -1907,6 +1947,7 @@ 'original_name': 'Block Roblox', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_roblox', 'unique_id': 'xyz12_block_roblox', @@ -1954,6 +1995,7 @@ 'original_name': 'Block Signal', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_signal', 'unique_id': 'xyz12_block_signal', @@ -2001,6 +2043,7 @@ 'original_name': 'Block Skype', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_skype', 'unique_id': 'xyz12_block_skype', @@ -2048,6 +2091,7 @@ 'original_name': 'Block Snapchat', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_snapchat', 'unique_id': 'xyz12_block_snapchat', @@ -2095,6 +2139,7 @@ 'original_name': 'Block social networks', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_social_networks', 'unique_id': 'xyz12_block_social_networks', @@ -2142,6 +2187,7 @@ 'original_name': 'Block Spotify', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_spotify', 'unique_id': 'xyz12_block_spotify', @@ -2189,6 +2235,7 @@ 'original_name': 'Block Steam', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_steam', 'unique_id': 'xyz12_block_steam', @@ -2236,6 +2283,7 @@ 'original_name': 'Block Telegram', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_telegram', 'unique_id': 'xyz12_block_telegram', @@ -2283,6 +2331,7 @@ 'original_name': 'Block TikTok', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_tiktok', 'unique_id': 'xyz12_block_tiktok', @@ -2330,6 +2379,7 @@ 'original_name': 'Block Tinder', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_tinder', 'unique_id': 'xyz12_block_tinder', @@ -2377,6 +2427,7 @@ 'original_name': 'Block Tumblr', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_tumblr', 'unique_id': 'xyz12_block_tumblr', @@ -2424,6 +2475,7 @@ 'original_name': 'Block Twitch', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_twitch', 'unique_id': 'xyz12_block_twitch', @@ -2471,6 +2523,7 @@ 'original_name': 'Block video streaming', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_video_streaming', 'unique_id': 'xyz12_block_video_streaming', @@ -2518,6 +2571,7 @@ 'original_name': 'Block Vimeo', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_vimeo', 'unique_id': 'xyz12_block_vimeo', @@ -2565,6 +2619,7 @@ 'original_name': 'Block VK', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_vk', 'unique_id': 'xyz12_block_vk', @@ -2612,6 +2667,7 @@ 'original_name': 'Block WhatsApp', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_whatsapp', 'unique_id': 'xyz12_block_whatsapp', @@ -2659,6 +2715,7 @@ 'original_name': 'Block X (formerly Twitter)', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_twitter', 'unique_id': 'xyz12_block_twitter', @@ -2706,6 +2763,7 @@ 'original_name': 'Block Xbox Live', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_xboxlive', 'unique_id': 'xyz12_block_xboxlive', @@ -2753,6 +2811,7 @@ 'original_name': 'Block YouTube', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_youtube', 'unique_id': 'xyz12_block_youtube', @@ -2800,6 +2859,7 @@ 'original_name': 'Block Zoom', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_zoom', 'unique_id': 'xyz12_block_zoom', @@ -2847,6 +2907,7 @@ 'original_name': 'Cache boost', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cache_boost', 'unique_id': 'xyz12_cache_boost', @@ -2894,6 +2955,7 @@ 'original_name': 'CNAME flattening', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cname_flattening', 'unique_id': 'xyz12_cname_flattening', @@ -2941,6 +3003,7 @@ 'original_name': 'Cryptojacking protection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cryptojacking_protection', 'unique_id': 'xyz12_cryptojacking_protection', @@ -2988,6 +3051,7 @@ 'original_name': 'DNS rebinding protection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dns_rebinding_protection', 'unique_id': 'xyz12_dns_rebinding_protection', @@ -3035,6 +3099,7 @@ 'original_name': 'Domain generation algorithms protection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dga_protection', 'unique_id': 'xyz12_dga_protection', @@ -3082,6 +3147,7 @@ 'original_name': 'Force SafeSearch', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'safesearch', 'unique_id': 'xyz12_safesearch', @@ -3129,6 +3195,7 @@ 'original_name': 'Force YouTube restricted mode', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'youtube_restricted_mode', 'unique_id': 'xyz12_youtube_restricted_mode', @@ -3176,6 +3243,7 @@ 'original_name': 'Google safe browsing', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'google_safe_browsing', 'unique_id': 'xyz12_google_safe_browsing', @@ -3223,6 +3291,7 @@ 'original_name': 'IDN homograph attacks protection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'idn_homograph_attacks_protection', 'unique_id': 'xyz12_idn_homograph_attacks_protection', @@ -3270,6 +3339,7 @@ 'original_name': 'Logs', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'logs', 'unique_id': 'xyz12_logs', @@ -3317,6 +3387,7 @@ 'original_name': 'Threat intelligence feeds', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'threat_intelligence_feeds', 'unique_id': 'xyz12_threat_intelligence_feeds', @@ -3364,6 +3435,7 @@ 'original_name': 'Typosquatting protection', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'typosquatting_protection', 'unique_id': 'xyz12_typosquatting_protection', @@ -3411,6 +3483,7 @@ 'original_name': 'Web3', 'platform': 'nextdns', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'web3', 'unique_id': 'xyz12_web3', diff --git a/tests/components/nice_go/snapshots/test_cover.ambr b/tests/components/nice_go/snapshots/test_cover.ambr index 0e1f9013a94..31ae154422d 100644 --- a/tests/components/nice_go/snapshots/test_cover.ambr +++ b/tests/components/nice_go/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1', @@ -76,6 +77,7 @@ 'original_name': None, 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '2', @@ -125,6 +127,7 @@ 'original_name': None, 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '3', @@ -174,6 +177,7 @@ 'original_name': None, 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '4', diff --git a/tests/components/nice_go/snapshots/test_light.ambr b/tests/components/nice_go/snapshots/test_light.ambr index 2b88b7d8d74..ffb5b8bff8d 100644 --- a/tests/components/nice_go/snapshots/test_light.ambr +++ b/tests/components/nice_go/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Light', 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': '1', @@ -87,6 +88,7 @@ 'original_name': 'Light', 'platform': 'nice_go', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': '2', diff --git a/tests/components/niko_home_control/snapshots/test_cover.ambr b/tests/components/niko_home_control/snapshots/test_cover.ambr index 5fe89497298..dc7cb0f4bce 100644 --- a/tests/components/niko_home_control/snapshots/test_cover.ambr +++ b/tests/components/niko_home_control/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'niko_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-3', diff --git a/tests/components/niko_home_control/snapshots/test_light.ambr b/tests/components/niko_home_control/snapshots/test_light.ambr index adb0e743786..8cf1c0e97d7 100644 --- a/tests/components/niko_home_control/snapshots/test_light.ambr +++ b/tests/components/niko_home_control/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': None, 'platform': 'niko_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-2', @@ -88,6 +89,7 @@ 'original_name': None, 'platform': 'niko_home_control', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-1', diff --git a/tests/components/nordpool/snapshots/test_sensor.ambr b/tests/components/nordpool/snapshots/test_sensor.ambr index be2b04cc520..232836d1cc9 100644 --- a/tests/components/nordpool/snapshots/test_sensor.ambr +++ b/tests/components/nordpool/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Currency', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'currency', 'unique_id': 'SE3-currency', @@ -79,6 +80,7 @@ 'original_name': 'Current price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_price', 'unique_id': 'SE3-current_price', @@ -133,6 +135,7 @@ 'original_name': 'Daily average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_average', 'unique_id': 'SE3-daily_average', @@ -184,6 +187,7 @@ 'original_name': 'Exchange rate', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exchange_rate', 'unique_id': 'SE3-exchange_rate', @@ -235,6 +239,7 @@ 'original_name': 'Highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'highest_price', 'unique_id': 'SE3-highest_price', @@ -285,6 +290,7 @@ 'original_name': 'Last updated', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'updated_at', 'unique_id': 'SE3-updated_at', @@ -336,6 +342,7 @@ 'original_name': 'Lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lowest_price', 'unique_id': 'SE3-lowest_price', @@ -389,6 +396,7 @@ 'original_name': 'Next price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_price', 'unique_id': 'SE3-next_price', @@ -442,6 +450,7 @@ 'original_name': 'Off-peak 1 average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'off_peak_1-SE3-block_average', @@ -496,6 +505,7 @@ 'original_name': 'Off-peak 1 highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'off_peak_1-SE3-block_max', @@ -550,6 +560,7 @@ 'original_name': 'Off-peak 1 lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'off_peak_1-SE3-block_min', @@ -599,6 +610,7 @@ 'original_name': 'Off-peak 1 time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'off_peak_1-SE3-block_start_time', @@ -647,6 +659,7 @@ 'original_name': 'Off-peak 1 time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'off_peak_1-SE3-block_end_time', @@ -700,6 +713,7 @@ 'original_name': 'Off-peak 2 average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'off_peak_2-SE3-block_average', @@ -754,6 +768,7 @@ 'original_name': 'Off-peak 2 highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'off_peak_2-SE3-block_max', @@ -808,6 +823,7 @@ 'original_name': 'Off-peak 2 lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'off_peak_2-SE3-block_min', @@ -857,6 +873,7 @@ 'original_name': 'Off-peak 2 time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'off_peak_2-SE3-block_start_time', @@ -905,6 +922,7 @@ 'original_name': 'Off-peak 2 time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'off_peak_2-SE3-block_end_time', @@ -958,6 +976,7 @@ 'original_name': 'Peak average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'peak-SE3-block_average', @@ -1012,6 +1031,7 @@ 'original_name': 'Peak highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'peak-SE3-block_max', @@ -1066,6 +1086,7 @@ 'original_name': 'Peak lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'peak-SE3-block_min', @@ -1115,6 +1136,7 @@ 'original_name': 'Peak time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'peak-SE3-block_start_time', @@ -1163,6 +1185,7 @@ 'original_name': 'Peak time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'peak-SE3-block_end_time', @@ -1214,6 +1237,7 @@ 'original_name': 'Previous price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_price', 'unique_id': 'SE3-last_price', @@ -1262,6 +1286,7 @@ 'original_name': 'Currency', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'currency', 'unique_id': 'SE4-currency', @@ -1314,6 +1339,7 @@ 'original_name': 'Current price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_price', 'unique_id': 'SE4-current_price', @@ -1368,6 +1394,7 @@ 'original_name': 'Daily average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_average', 'unique_id': 'SE4-daily_average', @@ -1419,6 +1446,7 @@ 'original_name': 'Exchange rate', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'exchange_rate', 'unique_id': 'SE4-exchange_rate', @@ -1470,6 +1498,7 @@ 'original_name': 'Highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'highest_price', 'unique_id': 'SE4-highest_price', @@ -1520,6 +1549,7 @@ 'original_name': 'Last updated', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'updated_at', 'unique_id': 'SE4-updated_at', @@ -1571,6 +1601,7 @@ 'original_name': 'Lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lowest_price', 'unique_id': 'SE4-lowest_price', @@ -1624,6 +1655,7 @@ 'original_name': 'Next price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_price', 'unique_id': 'SE4-next_price', @@ -1677,6 +1709,7 @@ 'original_name': 'Off-peak 1 average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'off_peak_1-SE4-block_average', @@ -1731,6 +1764,7 @@ 'original_name': 'Off-peak 1 highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'off_peak_1-SE4-block_max', @@ -1785,6 +1819,7 @@ 'original_name': 'Off-peak 1 lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'off_peak_1-SE4-block_min', @@ -1834,6 +1869,7 @@ 'original_name': 'Off-peak 1 time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'off_peak_1-SE4-block_start_time', @@ -1882,6 +1918,7 @@ 'original_name': 'Off-peak 1 time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'off_peak_1-SE4-block_end_time', @@ -1935,6 +1972,7 @@ 'original_name': 'Off-peak 2 average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'off_peak_2-SE4-block_average', @@ -1989,6 +2027,7 @@ 'original_name': 'Off-peak 2 highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'off_peak_2-SE4-block_max', @@ -2043,6 +2082,7 @@ 'original_name': 'Off-peak 2 lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'off_peak_2-SE4-block_min', @@ -2092,6 +2132,7 @@ 'original_name': 'Off-peak 2 time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'off_peak_2-SE4-block_start_time', @@ -2140,6 +2181,7 @@ 'original_name': 'Off-peak 2 time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'off_peak_2-SE4-block_end_time', @@ -2193,6 +2235,7 @@ 'original_name': 'Peak average', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_average', 'unique_id': 'peak-SE4-block_average', @@ -2247,6 +2290,7 @@ 'original_name': 'Peak highest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_max', 'unique_id': 'peak-SE4-block_max', @@ -2301,6 +2345,7 @@ 'original_name': 'Peak lowest price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_min', 'unique_id': 'peak-SE4-block_min', @@ -2350,6 +2395,7 @@ 'original_name': 'Peak time from', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_start_time', 'unique_id': 'peak-SE4-block_start_time', @@ -2398,6 +2444,7 @@ 'original_name': 'Peak time until', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_end_time', 'unique_id': 'peak-SE4-block_end_time', @@ -2449,6 +2496,7 @@ 'original_name': 'Previous price', 'platform': 'nordpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_price', 'unique_id': 'SE4-last_price', diff --git a/tests/components/ntfy/snapshots/test_notify.ambr b/tests/components/ntfy/snapshots/test_notify.ambr index 619ae59cc2f..34320ed5655 100644 --- a/tests/components/ntfy/snapshots/test_notify.ambr +++ b/tests/components/ntfy/snapshots/test_notify.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'ntfy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'publish', 'unique_id': '123456789_ABCDEF_publish', diff --git a/tests/components/nuki/snapshots/test_binary_sensor.ambr b/tests/components/nuki/snapshots/test_binary_sensor.ambr index e48cc55bfb3..88e803115bc 100644 --- a/tests/components/nuki/snapshots/test_binary_sensor.ambr +++ b/tests/components/nuki/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2_battery_critical', @@ -75,6 +76,7 @@ 'original_name': 'Ring Action', 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ring_action', 'unique_id': '2_ringaction', @@ -122,6 +124,7 @@ 'original_name': None, 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1_doorsensor', @@ -170,6 +173,7 @@ 'original_name': 'Battery', 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1_battery_critical', @@ -218,6 +222,7 @@ 'original_name': 'Charging', 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1_battery_charging', diff --git a/tests/components/nuki/snapshots/test_lock.ambr b/tests/components/nuki/snapshots/test_lock.ambr index 2d80110a5cc..07a0f048fe1 100644 --- a/tests/components/nuki/snapshots/test_lock.ambr +++ b/tests/components/nuki/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'nuki_lock', 'unique_id': 2, @@ -75,6 +76,7 @@ 'original_name': None, 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'nuki_lock', 'unique_id': 1, diff --git a/tests/components/nuki/snapshots/test_sensor.ambr b/tests/components/nuki/snapshots/test_sensor.ambr index 5be025727be..55f2d1aac3c 100644 --- a/tests/components/nuki/snapshots/test_sensor.ambr +++ b/tests/components/nuki/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'nuki', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1_battery_level', diff --git a/tests/components/nyt_games/snapshots/test_sensor.ambr b/tests/components/nyt_games/snapshots/test_sensor.ambr index 8201c26739c..261127064f4 100644 --- a/tests/components/nyt_games/snapshots/test_sensor.ambr +++ b/tests/components/nyt_games/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Current streak', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'streak', 'unique_id': '218886794-connections-connections_streak', @@ -81,6 +82,7 @@ 'original_name': 'Highest streak', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_streak', 'unique_id': '218886794-connections-connections_max_streak', @@ -131,6 +133,7 @@ 'original_name': 'Last played', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_played', 'unique_id': '218886794-connections-connections_last_played', @@ -181,6 +184,7 @@ 'original_name': 'Played', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'connections_played', 'unique_id': '218886794-connections-connections_played', @@ -232,6 +236,7 @@ 'original_name': 'Won', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'won', 'unique_id': '218886794-connections-connections_won', @@ -283,6 +288,7 @@ 'original_name': 'Played', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spelling_bees_played', 'unique_id': '218886794-spelling_bee-spelling_bees_played', @@ -334,6 +340,7 @@ 'original_name': 'Total pangrams found', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_pangrams', 'unique_id': '218886794-spelling_bee-spelling_bees_total_pangrams', @@ -385,6 +392,7 @@ 'original_name': 'Total words found', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_words', 'unique_id': '218886794-spelling_bee-spelling_bees_total_words', @@ -436,6 +444,7 @@ 'original_name': 'Current streak', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'streak', 'unique_id': '218886794-wordle-wordles_streak', @@ -488,6 +497,7 @@ 'original_name': 'Highest streak', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_streak', 'unique_id': '218886794-wordle-wordles_max_streak', @@ -540,6 +550,7 @@ 'original_name': 'Played', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wordles_played', 'unique_id': '218886794-wordle-wordles_played', @@ -591,6 +602,7 @@ 'original_name': 'Won', 'platform': 'nyt_games', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'won', 'unique_id': '218886794-wordle-wordles_won', diff --git a/tests/components/ohme/snapshots/test_button.ambr b/tests/components/ohme/snapshots/test_button.ambr index b276e8c3c42..88cf6327bcf 100644 --- a/tests/components/ohme/snapshots/test_button.ambr +++ b/tests/components/ohme/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Approve charge', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'approve', 'unique_id': 'chargerid_approve', diff --git a/tests/components/ohme/snapshots/test_number.ambr b/tests/components/ohme/snapshots/test_number.ambr index 69e18d0b2a7..80ee4d30d9c 100644 --- a/tests/components/ohme/snapshots/test_number.ambr +++ b/tests/components/ohme/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Preconditioning duration', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'preconditioning_duration', 'unique_id': 'chargerid_preconditioning_duration', @@ -89,6 +90,7 @@ 'original_name': 'Target percentage', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'target_percentage', 'unique_id': 'chargerid_target_percentage', diff --git a/tests/components/ohme/snapshots/test_select.ambr b/tests/components/ohme/snapshots/test_select.ambr index 063a9616588..1897e146c01 100644 --- a/tests/components/ohme/snapshots/test_select.ambr +++ b/tests/components/ohme/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Charge mode', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'chargerid_charge_mode', @@ -90,6 +91,7 @@ 'original_name': 'Vehicle', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle', 'unique_id': 'chargerid_vehicle', diff --git a/tests/components/ohme/snapshots/test_sensor.ambr b/tests/components/ohme/snapshots/test_sensor.ambr index 9cef4bfffd9..20c4e7829c9 100644 --- a/tests/components/ohme/snapshots/test_sensor.ambr +++ b/tests/components/ohme/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge slots', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'slot_list', 'unique_id': 'chargerid_slot_list', @@ -74,6 +75,7 @@ 'original_name': 'CT current', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ct_current', 'unique_id': 'chargerid_ct_current', @@ -123,6 +125,7 @@ 'original_name': 'Current', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'chargerid_current', @@ -180,6 +183,7 @@ 'original_name': 'Energy', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'chargerid_energy', @@ -236,6 +240,7 @@ 'original_name': 'Power', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'chargerid_power', @@ -294,6 +299,7 @@ 'original_name': 'Status', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'chargerid_status', @@ -353,6 +359,7 @@ 'original_name': 'Vehicle battery', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_battery', 'unique_id': 'chargerid_battery', @@ -404,6 +411,7 @@ 'original_name': 'Voltage', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'chargerid_voltage', diff --git a/tests/components/ohme/snapshots/test_switch.ambr b/tests/components/ohme/snapshots/test_switch.ambr index 4790d96c551..ef91187f160 100644 --- a/tests/components/ohme/snapshots/test_switch.ambr +++ b/tests/components/ohme/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Lock buttons', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lock_buttons', 'unique_id': 'chargerid_lock_buttons', @@ -74,6 +75,7 @@ 'original_name': 'Price cap', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'price_cap', 'unique_id': 'chargerid_price_cap', @@ -121,6 +123,7 @@ 'original_name': 'Require approval', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'require_approval', 'unique_id': 'chargerid_require_approval', @@ -168,6 +171,7 @@ 'original_name': 'Sleep when inactive', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sleep_when_inactive', 'unique_id': 'chargerid_sleep_when_inactive', diff --git a/tests/components/ohme/snapshots/test_time.ambr b/tests/components/ohme/snapshots/test_time.ambr index 8c85fc2298e..1f77bb1f17a 100644 --- a/tests/components/ohme/snapshots/test_time.ambr +++ b/tests/components/ohme/snapshots/test_time.ambr @@ -27,6 +27,7 @@ 'original_name': 'Target time', 'platform': 'ohme', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'target_time', 'unique_id': 'chargerid_target_time', diff --git a/tests/components/omnilogic/snapshots/test_sensor.ambr b/tests/components/omnilogic/snapshots/test_sensor.ambr index b6eb07dbe26..2bfdc00d6ea 100644 --- a/tests/components/omnilogic/snapshots/test_sensor.ambr +++ b/tests/components/omnilogic/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'SCRUBBED Air Temperature', 'platform': 'omnilogic', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'SCRUBBED_SCRUBBED_air_temperature', @@ -78,6 +79,7 @@ 'original_name': 'SCRUBBED Spa Water Temperature', 'platform': 'omnilogic', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'SCRUBBED_1_water_temperature', diff --git a/tests/components/omnilogic/snapshots/test_switch.ambr b/tests/components/omnilogic/snapshots/test_switch.ambr index cc1a2e226fc..34cd555edf8 100644 --- a/tests/components/omnilogic/snapshots/test_switch.ambr +++ b/tests/components/omnilogic/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'SCRUBBED Spa Filter Pump ', 'platform': 'omnilogic', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'SCRUBBED_1_2_pump', @@ -74,6 +75,7 @@ 'original_name': 'SCRUBBED Spa Spa Jets ', 'platform': 'omnilogic', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'SCRUBBED_1_5_pump', diff --git a/tests/components/ondilo_ico/snapshots/test_sensor.ambr b/tests/components/ondilo_ico/snapshots/test_sensor.ambr index 7df2bfc22ce..7f8b9374aab 100644 --- a/tests/components/ondilo_ico/snapshots/test_sensor.ambr +++ b/tests/components/ondilo_ico/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W1122333044455-battery', @@ -81,6 +82,7 @@ 'original_name': 'Oxydo reduction potential', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oxydo_reduction_potential', 'unique_id': 'W1122333044455-orp', @@ -132,6 +134,7 @@ 'original_name': 'pH', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W1122333044455-ph', @@ -183,6 +186,7 @@ 'original_name': 'RSSI', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rssi', 'unique_id': 'W1122333044455-rssi', @@ -234,6 +238,7 @@ 'original_name': 'Salt', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt', 'unique_id': 'W1122333044455-salt', @@ -285,6 +290,7 @@ 'original_name': 'TDS', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tds', 'unique_id': 'W1122333044455-tds', @@ -336,6 +342,7 @@ 'original_name': 'Temperature', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W1122333044455-temperature', @@ -388,6 +395,7 @@ 'original_name': 'Battery', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W2233304445566-battery', @@ -440,6 +448,7 @@ 'original_name': 'Oxydo reduction potential', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oxydo_reduction_potential', 'unique_id': 'W2233304445566-orp', @@ -491,6 +500,7 @@ 'original_name': 'pH', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W2233304445566-ph', @@ -542,6 +552,7 @@ 'original_name': 'RSSI', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rssi', 'unique_id': 'W2233304445566-rssi', @@ -593,6 +604,7 @@ 'original_name': 'Salt', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'salt', 'unique_id': 'W2233304445566-salt', @@ -644,6 +656,7 @@ 'original_name': 'TDS', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tds', 'unique_id': 'W2233304445566-tds', @@ -695,6 +708,7 @@ 'original_name': 'Temperature', 'platform': 'ondilo_ico', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'W2233304445566-temperature', diff --git a/tests/components/onedrive/snapshots/test_sensor.ambr b/tests/components/onedrive/snapshots/test_sensor.ambr index 742c069f206..53bcf39eeeb 100644 --- a/tests/components/onedrive/snapshots/test_sensor.ambr +++ b/tests/components/onedrive/snapshots/test_sensor.ambr @@ -34,6 +34,7 @@ 'original_name': 'Drive state', 'platform': 'onedrive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state', 'unique_id': 'mock_drive_id_drive_state', @@ -94,6 +95,7 @@ 'original_name': 'Remaining storage', 'platform': 'onedrive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remaining_size', 'unique_id': 'mock_drive_id_remaining_size', @@ -149,6 +151,7 @@ 'original_name': 'Total available storage', 'platform': 'onedrive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_size', 'unique_id': 'mock_drive_id_total_size', @@ -204,6 +207,7 @@ 'original_name': 'Used storage', 'platform': 'onedrive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'used_size', 'unique_id': 'mock_drive_id_used_size', diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 10122ba8685..6309b80b28d 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Sensed A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.A', @@ -76,6 +77,7 @@ 'original_name': 'Sensed B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/12.111111111111/sensed.B', @@ -125,6 +127,7 @@ 'original_name': 'Sensed 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.0', @@ -174,6 +177,7 @@ 'original_name': 'Sensed 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.1', @@ -223,6 +227,7 @@ 'original_name': 'Sensed 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.2', @@ -272,6 +277,7 @@ 'original_name': 'Sensed 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.3', @@ -321,6 +327,7 @@ 'original_name': 'Sensed 4', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.4', @@ -370,6 +377,7 @@ 'original_name': 'Sensed 5', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.5', @@ -419,6 +427,7 @@ 'original_name': 'Sensed 6', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.6', @@ -468,6 +477,7 @@ 'original_name': 'Sensed 7', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/29.111111111111/sensed.7', @@ -517,6 +527,7 @@ 'original_name': 'Sensed A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.A', @@ -566,6 +577,7 @@ 'original_name': 'Sensed B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensed_id', 'unique_id': '/3A.111111111111/sensed.B', @@ -615,6 +627,7 @@ 'original_name': 'Hub short on branch 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.0', @@ -665,6 +678,7 @@ 'original_name': 'Hub short on branch 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.1', @@ -715,6 +729,7 @@ 'original_name': 'Hub short on branch 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.2', @@ -765,6 +780,7 @@ 'original_name': 'Hub short on branch 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_short_id', 'unique_id': '/EF.111111111113/hub/short.3', diff --git a/tests/components/onewire/snapshots/test_select.ambr b/tests/components/onewire/snapshots/test_select.ambr index a896d946841..9861a7d2f5e 100644 --- a/tests/components/onewire/snapshots/test_select.ambr +++ b/tests/components/onewire/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Temperature resolution', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tempres', 'unique_id': '/28.111111111111/tempres', diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index eca459b4c57..4d9ce5c0f07 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/10.111111111111/temperature', @@ -83,6 +84,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/12.111111111111/TAI8570/pressure', @@ -137,6 +139,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/12.111111111111/TAI8570/temperature', @@ -191,6 +194,7 @@ 'original_name': 'Counter A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.A', @@ -243,6 +247,7 @@ 'original_name': 'Counter B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'counter_id', 'unique_id': '/1D.111111111111/counter.B', @@ -295,6 +300,7 @@ 'original_name': 'Latest voltage A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latest_voltage_id', 'unique_id': '/20.111111111111/latestvolt.A', @@ -349,6 +355,7 @@ 'original_name': 'Latest voltage B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latest_voltage_id', 'unique_id': '/20.111111111111/latestvolt.B', @@ -403,6 +410,7 @@ 'original_name': 'Latest voltage C', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latest_voltage_id', 'unique_id': '/20.111111111111/latestvolt.C', @@ -457,6 +465,7 @@ 'original_name': 'Latest voltage D', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latest_voltage_id', 'unique_id': '/20.111111111111/latestvolt.D', @@ -511,6 +520,7 @@ 'original_name': 'Voltage A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_id', 'unique_id': '/20.111111111111/volt.A', @@ -565,6 +575,7 @@ 'original_name': 'Voltage B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_id', 'unique_id': '/20.111111111111/volt.B', @@ -619,6 +630,7 @@ 'original_name': 'Voltage C', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_id', 'unique_id': '/20.111111111111/volt.C', @@ -673,6 +685,7 @@ 'original_name': 'Voltage D', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_id', 'unique_id': '/20.111111111111/volt.D', @@ -727,6 +740,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/22.111111111111/temperature', @@ -781,6 +795,7 @@ 'original_name': 'HIH3600 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih3600', 'unique_id': '/26.111111111111/HIH3600/humidity', @@ -835,6 +850,7 @@ 'original_name': 'HIH4000 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih4000', 'unique_id': '/26.111111111111/HIH4000/humidity', @@ -889,6 +905,7 @@ 'original_name': 'HIH5030 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih5030', 'unique_id': '/26.111111111111/HIH5030/humidity', @@ -943,6 +960,7 @@ 'original_name': 'HTM1735 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_htm1735', 'unique_id': '/26.111111111111/HTM1735/humidity', @@ -997,6 +1015,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/26.111111111111/humidity', @@ -1051,6 +1070,7 @@ 'original_name': 'Illuminance', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/26.111111111111/S3-R1-A/illuminance', @@ -1105,6 +1125,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/26.111111111111/B1-R1-A/pressure', @@ -1159,6 +1180,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/26.111111111111/temperature', @@ -1213,6 +1235,7 @@ 'original_name': 'VAD voltage', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vad', 'unique_id': '/26.111111111111/VAD', @@ -1267,6 +1290,7 @@ 'original_name': 'VDD voltage', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vdd', 'unique_id': '/26.111111111111/VDD', @@ -1321,6 +1345,7 @@ 'original_name': 'VIS voltage difference', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vis', 'unique_id': '/26.111111111111/vis', @@ -1375,6 +1400,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/28.111111111111/temperature', @@ -1429,6 +1455,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/28.222222222222/temperature', @@ -1483,6 +1510,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/28.222222222223/temperature', @@ -1537,6 +1565,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/30.111111111111/temperature', @@ -1591,6 +1620,7 @@ 'original_name': 'Thermocouple K temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermocouple_temperature_k', 'unique_id': '/30.111111111111/typeX/temperature', @@ -1645,6 +1675,7 @@ 'original_name': 'VIS voltage gradient', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vis_gradient', 'unique_id': '/30.111111111111/vis', @@ -1699,6 +1730,7 @@ 'original_name': 'Voltage', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/30.111111111111/volt', @@ -1753,6 +1785,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/3B.111111111111/temperature', @@ -1807,6 +1840,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/42.111111111111/temperature', @@ -1861,6 +1895,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/humidity', @@ -1915,6 +1950,7 @@ 'original_name': 'Illuminance', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/light', @@ -1969,6 +2005,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/pressure', @@ -2023,6 +2060,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.111111111111/EDS0068/temperature', @@ -2077,6 +2115,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.222222222222/EDS0066/pressure', @@ -2131,6 +2170,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/7E.222222222222/EDS0066/temperature', @@ -2185,6 +2225,7 @@ 'original_name': 'HIH3600 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih3600', 'unique_id': '/A6.111111111111/HIH3600/humidity', @@ -2239,6 +2280,7 @@ 'original_name': 'HIH4000 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih4000', 'unique_id': '/A6.111111111111/HIH4000/humidity', @@ -2293,6 +2335,7 @@ 'original_name': 'HIH5030 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_hih5030', 'unique_id': '/A6.111111111111/HIH5030/humidity', @@ -2347,6 +2390,7 @@ 'original_name': 'HTM1735 humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_htm1735', 'unique_id': '/A6.111111111111/HTM1735/humidity', @@ -2401,6 +2445,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/A6.111111111111/humidity', @@ -2455,6 +2500,7 @@ 'original_name': 'Illuminance', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/A6.111111111111/S3-R1-A/illuminance', @@ -2509,6 +2555,7 @@ 'original_name': 'Pressure', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/A6.111111111111/B1-R1-A/pressure', @@ -2563,6 +2610,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/A6.111111111111/temperature', @@ -2617,6 +2665,7 @@ 'original_name': 'VAD voltage', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vad', 'unique_id': '/A6.111111111111/VAD', @@ -2671,6 +2720,7 @@ 'original_name': 'VDD voltage', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vdd', 'unique_id': '/A6.111111111111/VDD', @@ -2725,6 +2775,7 @@ 'original_name': 'VIS voltage difference', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_vis', 'unique_id': '/A6.111111111111/vis', @@ -2779,6 +2830,7 @@ 'original_name': 'Humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/EF.111111111111/humidity/humidity_corrected', @@ -2833,6 +2885,7 @@ 'original_name': 'Raw humidity', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_raw', 'unique_id': '/EF.111111111111/humidity/humidity_raw', @@ -2887,6 +2940,7 @@ 'original_name': 'Temperature', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '/EF.111111111111/humidity/temperature', @@ -2941,6 +2995,7 @@ 'original_name': 'Moisture 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.2', @@ -2995,6 +3050,7 @@ 'original_name': 'Moisture 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_id', 'unique_id': '/EF.111111111112/moisture/sensor.3', @@ -3049,6 +3105,7 @@ 'original_name': 'Wetness 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.0', @@ -3103,6 +3160,7 @@ 'original_name': 'Wetness 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wetness_id', 'unique_id': '/EF.111111111112/moisture/sensor.1', diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8be414c7c1e..d819fdd0d54 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Programmed input-output', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio', 'unique_id': '/05.111111111111/PIO', @@ -76,6 +77,7 @@ 'original_name': 'Latch A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.A', @@ -125,6 +127,7 @@ 'original_name': 'Latch B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/12.111111111111/latch.B', @@ -174,6 +177,7 @@ 'original_name': 'Programmed input-output A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.A', @@ -223,6 +227,7 @@ 'original_name': 'Programmed input-output B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/12.111111111111/PIO.B', @@ -272,6 +277,7 @@ 'original_name': 'Current A/D control', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'iad', 'unique_id': '/26.111111111111/IAD', @@ -321,6 +327,7 @@ 'original_name': 'Latch 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.0', @@ -370,6 +377,7 @@ 'original_name': 'Latch 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.1', @@ -419,6 +427,7 @@ 'original_name': 'Latch 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.2', @@ -468,6 +477,7 @@ 'original_name': 'Latch 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.3', @@ -517,6 +527,7 @@ 'original_name': 'Latch 4', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.4', @@ -566,6 +577,7 @@ 'original_name': 'Latch 5', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.5', @@ -615,6 +627,7 @@ 'original_name': 'Latch 6', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.6', @@ -664,6 +677,7 @@ 'original_name': 'Latch 7', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'latch_id', 'unique_id': '/29.111111111111/latch.7', @@ -713,6 +727,7 @@ 'original_name': 'Programmed input-output 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.0', @@ -762,6 +777,7 @@ 'original_name': 'Programmed input-output 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.1', @@ -811,6 +827,7 @@ 'original_name': 'Programmed input-output 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.2', @@ -860,6 +877,7 @@ 'original_name': 'Programmed input-output 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.3', @@ -909,6 +927,7 @@ 'original_name': 'Programmed input-output 4', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.4', @@ -958,6 +977,7 @@ 'original_name': 'Programmed input-output 5', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.5', @@ -1007,6 +1027,7 @@ 'original_name': 'Programmed input-output 6', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.6', @@ -1056,6 +1077,7 @@ 'original_name': 'Programmed input-output 7', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/29.111111111111/PIO.7', @@ -1105,6 +1127,7 @@ 'original_name': 'Programmed input-output A', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.A', @@ -1154,6 +1177,7 @@ 'original_name': 'Programmed input-output B', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pio_id', 'unique_id': '/3A.111111111111/PIO.B', @@ -1203,6 +1227,7 @@ 'original_name': 'Current A/D control', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'iad', 'unique_id': '/A6.111111111111/IAD', @@ -1252,6 +1277,7 @@ 'original_name': 'Leaf sensor 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.0', @@ -1301,6 +1327,7 @@ 'original_name': 'Leaf sensor 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.1', @@ -1350,6 +1377,7 @@ 'original_name': 'Leaf sensor 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.2', @@ -1399,6 +1427,7 @@ 'original_name': 'Leaf sensor 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'leaf_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_leaf.3', @@ -1448,6 +1477,7 @@ 'original_name': 'Moisture sensor 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.0', @@ -1497,6 +1527,7 @@ 'original_name': 'Moisture sensor 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.1', @@ -1546,6 +1577,7 @@ 'original_name': 'Moisture sensor 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.2', @@ -1595,6 +1627,7 @@ 'original_name': 'Moisture sensor 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'moisture_sensor_id', 'unique_id': '/EF.111111111112/moisture/is_moisture.3', @@ -1644,6 +1677,7 @@ 'original_name': 'Hub branch 0', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.0', @@ -1693,6 +1727,7 @@ 'original_name': 'Hub branch 1', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.1', @@ -1742,6 +1777,7 @@ 'original_name': 'Hub branch 2', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.2', @@ -1791,6 +1827,7 @@ 'original_name': 'Hub branch 3', 'platform': 'onewire', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hub_branch_id', 'unique_id': '/EF.111111111113/hub/branch.3', diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index 7b0cf4fbf99..57a278a498b 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Cloud coverage', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-clouds', @@ -79,6 +80,7 @@ 'original_name': 'Condition', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-condition', @@ -129,6 +131,7 @@ 'original_name': 'Dew Point', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-dew_point', @@ -182,6 +185,7 @@ 'original_name': 'Feels like temperature', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-feels_like_temperature', @@ -235,6 +239,7 @@ 'original_name': 'Humidity', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-humidity', @@ -286,6 +291,7 @@ 'original_name': 'Precipitation kind', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-precipitation_kind', @@ -336,6 +342,7 @@ 'original_name': 'Pressure', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-pressure', @@ -389,6 +396,7 @@ 'original_name': 'Rain', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-rain', @@ -442,6 +450,7 @@ 'original_name': 'Snow', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-snow', @@ -495,6 +504,7 @@ 'original_name': 'Temperature', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-temperature', @@ -548,6 +558,7 @@ 'original_name': 'UV Index', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-uv_index', @@ -600,6 +611,7 @@ 'original_name': 'Visibility', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-visibility_distance', @@ -651,6 +663,7 @@ 'original_name': 'Weather', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-weather', @@ -699,6 +712,7 @@ 'original_name': 'Weather Code', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-weather_code', @@ -749,6 +763,7 @@ 'original_name': 'Wind bearing', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-wind_bearing', @@ -805,6 +820,7 @@ 'original_name': 'Wind speed', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-wind_speed', @@ -858,6 +874,7 @@ 'original_name': 'Cloud coverage', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-clouds', @@ -908,6 +925,7 @@ 'original_name': 'Condition', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-condition', @@ -958,6 +976,7 @@ 'original_name': 'Dew Point', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-dew_point', @@ -1011,6 +1030,7 @@ 'original_name': 'Feels like temperature', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-feels_like_temperature', @@ -1064,6 +1084,7 @@ 'original_name': 'Humidity', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-humidity', @@ -1115,6 +1136,7 @@ 'original_name': 'Precipitation kind', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-precipitation_kind', @@ -1165,6 +1187,7 @@ 'original_name': 'Pressure', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-pressure', @@ -1218,6 +1241,7 @@ 'original_name': 'Rain', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-rain', @@ -1271,6 +1295,7 @@ 'original_name': 'Snow', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-snow', @@ -1324,6 +1349,7 @@ 'original_name': 'Temperature', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-temperature', @@ -1377,6 +1403,7 @@ 'original_name': 'UV Index', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-uv_index', @@ -1429,6 +1456,7 @@ 'original_name': 'Visibility', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-visibility_distance', @@ -1480,6 +1508,7 @@ 'original_name': 'Weather', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-weather', @@ -1528,6 +1557,7 @@ 'original_name': 'Weather Code', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-weather_code', @@ -1578,6 +1608,7 @@ 'original_name': 'Wind bearing', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-wind_bearing', @@ -1634,6 +1665,7 @@ 'original_name': 'Wind speed', 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-wind_speed', diff --git a/tests/components/openweathermap/snapshots/test_weather.ambr b/tests/components/openweathermap/snapshots/test_weather.ambr index 1d77d9179a5..760160a96f4 100644 --- a/tests/components/openweathermap/snapshots/test_weather.ambr +++ b/tests/components/openweathermap/snapshots/test_weather.ambr @@ -51,6 +51,7 @@ 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78', @@ -112,6 +113,7 @@ 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12.34-56.78', @@ -174,6 +176,7 @@ 'original_name': None, 'platform': 'openweathermap', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12.34-56.78', diff --git a/tests/components/osoenergy/snapshots/test_water_heater.ambr b/tests/components/osoenergy/snapshots/test_water_heater.ambr index 92b3a7aa099..18c434d133b 100644 --- a/tests/components/osoenergy/snapshots/test_water_heater.ambr +++ b/tests/components/osoenergy/snapshots/test_water_heater.ambr @@ -30,6 +30,7 @@ 'original_name': None, 'platform': 'osoenergy', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'osoenergy_water_heater', diff --git a/tests/components/overseerr/snapshots/test_event.ambr b/tests/components/overseerr/snapshots/test_event.ambr index 8a7be6c463d..bfa03d9a2e8 100644 --- a/tests/components/overseerr/snapshots/test_event.ambr +++ b/tests/components/overseerr/snapshots/test_event.ambr @@ -36,6 +36,7 @@ 'original_name': 'Last media event', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_media_event', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-media', diff --git a/tests/components/overseerr/snapshots/test_sensor.ambr b/tests/components/overseerr/snapshots/test_sensor.ambr index bbee260b782..44613d6117c 100644 --- a/tests/components/overseerr/snapshots/test_sensor.ambr +++ b/tests/components/overseerr/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Available requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'available_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-available_requests', @@ -80,6 +81,7 @@ 'original_name': 'Declined requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'declined_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-declined_requests', @@ -131,6 +133,7 @@ 'original_name': 'Movie requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'movie_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-movie_requests', @@ -182,6 +185,7 @@ 'original_name': 'Pending requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pending_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-pending_requests', @@ -233,6 +237,7 @@ 'original_name': 'Processing requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'processing_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-processing_requests', @@ -284,6 +289,7 @@ 'original_name': 'Total requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-total_requests', @@ -335,6 +341,7 @@ 'original_name': 'TV requests', 'platform': 'overseerr', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tv_requests', 'unique_id': '01JG00V55WEVTJ0CJHM0GAD7PC-tv_requests', diff --git a/tests/components/palazzetti/snapshots/test_button.ambr b/tests/components/palazzetti/snapshots/test_button.ambr index 8130f0a0ec7..bc711cd8cde 100644 --- a/tests/components/palazzetti/snapshots/test_button.ambr +++ b/tests/components/palazzetti/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Silent', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'silent', 'unique_id': '11:22:33:44:55:66-silent', diff --git a/tests/components/palazzetti/snapshots/test_climate.ambr b/tests/components/palazzetti/snapshots/test_climate.ambr index cf23cb87ccb..4ef71fe4e57 100644 --- a/tests/components/palazzetti/snapshots/test_climate.ambr +++ b/tests/components/palazzetti/snapshots/test_climate.ambr @@ -44,6 +44,7 @@ 'original_name': None, 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'palazzetti', 'unique_id': '11:22:33:44:55:66', diff --git a/tests/components/palazzetti/snapshots/test_number.ambr b/tests/components/palazzetti/snapshots/test_number.ambr index 1d40e9e4b6b..c700f08a69c 100644 --- a/tests/components/palazzetti/snapshots/test_number.ambr +++ b/tests/components/palazzetti/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Combustion power', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'combustion_power', 'unique_id': '11:22:33:44:55:66-combustion_power', @@ -89,6 +90,7 @@ 'original_name': 'Left fan speed', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_left_speed', 'unique_id': '11:22:33:44:55:66-fan_left_speed', @@ -146,6 +148,7 @@ 'original_name': 'Right fan speed', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_right_speed', 'unique_id': '11:22:33:44:55:66-fan_right_speed', diff --git a/tests/components/palazzetti/snapshots/test_sensor.ambr b/tests/components/palazzetti/snapshots/test_sensor.ambr index 6bf4f68c1fa..42f42371dfc 100644 --- a/tests/components/palazzetti/snapshots/test_sensor.ambr +++ b/tests/components/palazzetti/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Air outlet temperature', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_outlet_temperature', 'unique_id': '11:22:33:44:55:66-air_outlet_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Hydro temperature 1', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 't1_hydro', 'unique_id': '11:22:33:44:55:66-t1_hydro', @@ -133,6 +135,7 @@ 'original_name': 'Hydro temperature 2', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 't2_hydro', 'unique_id': '11:22:33:44:55:66-t2_hydro', @@ -185,6 +188,7 @@ 'original_name': 'Pellet quantity', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pellet_quantity', 'unique_id': '11:22:33:44:55:66-pellet_quantity', @@ -237,6 +241,7 @@ 'original_name': 'Return water temperature', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'return_water_temperature', 'unique_id': '11:22:33:44:55:66-return_water_temperature', @@ -289,6 +294,7 @@ 'original_name': 'Room temperature', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'room_temperature', 'unique_id': '11:22:33:44:55:66-room_temperature', @@ -389,6 +395,7 @@ 'original_name': 'Status', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': '11:22:33:44:55:66-status', @@ -488,6 +495,7 @@ 'original_name': 'Tank water temperature', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tank_water_temperature', 'unique_id': '11:22:33:44:55:66-tank_water_temperature', @@ -540,6 +548,7 @@ 'original_name': 'Wood combustion temperature', 'platform': 'palazzetti', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wood_combustion_temperature', 'unique_id': '11:22:33:44:55:66-wood_combustion_temperature', diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index 1f7c7b09d9c..ed59c21276b 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Available storage', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_available', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_available', @@ -81,6 +82,7 @@ 'original_name': 'Correspondents', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'correspondent_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_correspondent_count', @@ -132,6 +134,7 @@ 'original_name': 'Document types', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'document_type_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_document_type_count', @@ -183,6 +186,7 @@ 'original_name': 'Documents in inbox', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'documents_inbox', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_inbox', @@ -238,6 +242,7 @@ 'original_name': 'Status celery', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'celery_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_celery_status', @@ -297,6 +302,7 @@ 'original_name': 'Status classifier', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'classifier_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_classifier_status', @@ -356,6 +362,7 @@ 'original_name': 'Status database', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'database_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_database_status', @@ -415,6 +422,7 @@ 'original_name': 'Status index', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'index_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_index_status', @@ -474,6 +482,7 @@ 'original_name': 'Status redis', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'redis_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_redis_status', @@ -533,6 +542,7 @@ 'original_name': 'Status sanity', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sanity_check_status', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_sanity_check_status', @@ -588,6 +598,7 @@ 'original_name': 'Tags', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tag_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_tag_count', @@ -639,6 +650,7 @@ 'original_name': 'Total characters', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'characters_count', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_characters_count', @@ -690,6 +702,7 @@ 'original_name': 'Total documents', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'documents_total', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_documents_total', @@ -741,6 +754,7 @@ 'original_name': 'Total storage', 'platform': 'paperless_ngx', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storage_total', 'unique_id': '0KLG00V55WEVTJ0CJHM0GADNGH_storage_total', diff --git a/tests/components/peblar/snapshots/test_binary_sensor.ambr b/tests/components/peblar/snapshots/test_binary_sensor.ambr index 9ad9c877ed2..ed39bbf171b 100644 --- a/tests/components/peblar/snapshots/test_binary_sensor.ambr +++ b/tests/components/peblar/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Active errors', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_error_codes', 'unique_id': '23-45-A4O-MOF_active_error_codes', @@ -75,6 +76,7 @@ 'original_name': 'Active warnings', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_warning_codes', 'unique_id': '23-45-A4O-MOF_active_warning_codes', diff --git a/tests/components/peblar/snapshots/test_button.ambr b/tests/components/peblar/snapshots/test_button.ambr index 6d31da0ae52..b46dc0b0eca 100644 --- a/tests/components/peblar/snapshots/test_button.ambr +++ b/tests/components/peblar/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Identify', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '23-45-A4O-MOF_identify', @@ -75,6 +76,7 @@ 'original_name': 'Restart', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '23-45-A4O-MOF_reboot', diff --git a/tests/components/peblar/snapshots/test_number.ambr b/tests/components/peblar/snapshots/test_number.ambr index d8e9c756c50..f7fd499d112 100644 --- a/tests/components/peblar/snapshots/test_number.ambr +++ b/tests/components/peblar/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Charge limit', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_current_limit', 'unique_id': '23-45-A4O-MOF_charge_current_limit', diff --git a/tests/components/peblar/snapshots/test_select.ambr b/tests/components/peblar/snapshots/test_select.ambr index 3a600653a84..95146997039 100644 --- a/tests/components/peblar/snapshots/test_select.ambr +++ b/tests/components/peblar/snapshots/test_select.ambr @@ -35,6 +35,7 @@ 'original_name': 'Smart charging', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_charging', 'unique_id': '23-45-A4O-MOF_smart_charging', diff --git a/tests/components/peblar/snapshots/test_sensor.ambr b/tests/components/peblar/snapshots/test_sensor.ambr index 5a1d1663ba2..34d109797e0 100644 --- a/tests/components/peblar/snapshots/test_sensor.ambr +++ b/tests/components/peblar/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Current', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '23-45-A4O-MOF_current_total', @@ -93,6 +94,7 @@ 'original_name': 'Current phase 1', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase_1', 'unique_id': '23-45-A4O-MOF_current_phase_1', @@ -151,6 +153,7 @@ 'original_name': 'Current phase 2', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase_2', 'unique_id': '23-45-A4O-MOF_current_phase_2', @@ -209,6 +212,7 @@ 'original_name': 'Current phase 3', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_phase_3', 'unique_id': '23-45-A4O-MOF_current_phase_3', @@ -267,6 +271,7 @@ 'original_name': 'Lifetime energy', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': '23-45-A4O-MOF_energy_total', @@ -337,6 +342,7 @@ 'original_name': 'Limit source', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_current_limit_source', 'unique_id': '23-45-A4O-MOF_charge_current_limit_source', @@ -406,6 +412,7 @@ 'original_name': 'Power', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '23-45-A4O-MOF_power_total', @@ -458,6 +465,7 @@ 'original_name': 'Power phase 1', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_phase_1', 'unique_id': '23-45-A4O-MOF_power_phase_1', @@ -510,6 +518,7 @@ 'original_name': 'Power phase 2', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_phase_2', 'unique_id': '23-45-A4O-MOF_power_phase_2', @@ -562,6 +571,7 @@ 'original_name': 'Power phase 3', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_phase_3', 'unique_id': '23-45-A4O-MOF_power_phase_3', @@ -620,6 +630,7 @@ 'original_name': 'Session energy', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_session', 'unique_id': '23-45-A4O-MOF_energy_session', @@ -680,6 +691,7 @@ 'original_name': 'State', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cp_state', 'unique_id': '23-45-A4O-MOF_cp_state', @@ -737,6 +749,7 @@ 'original_name': 'Uptime', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uptime', 'unique_id': '23-45-A4O-MOF_uptime', @@ -787,6 +800,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_phase_1', 'unique_id': '23-45-A4O-MOF_voltage_phase_1', @@ -839,6 +853,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_phase_2', 'unique_id': '23-45-A4O-MOF_voltage_phase_2', @@ -891,6 +906,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_phase_3', 'unique_id': '23-45-A4O-MOF_voltage_phase_3', diff --git a/tests/components/peblar/snapshots/test_switch.ambr b/tests/components/peblar/snapshots/test_switch.ambr index 46051974339..f3b9775e339 100644 --- a/tests/components/peblar/snapshots/test_switch.ambr +++ b/tests/components/peblar/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge', 'unique_id': '23-45-A4O-MOF_charge', @@ -74,6 +75,7 @@ 'original_name': 'Force single phase', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'force_single_phase', 'unique_id': '23-45-A4O-MOF_force_single_phase', diff --git a/tests/components/peblar/snapshots/test_update.ambr b/tests/components/peblar/snapshots/test_update.ambr index 0a6b2bf069f..48a92dcad49 100644 --- a/tests/components/peblar/snapshots/test_update.ambr +++ b/tests/components/peblar/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Customization', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'customization', 'unique_id': '23-45-A4O-MOF_customization', @@ -86,6 +87,7 @@ 'original_name': 'Firmware', 'platform': 'peblar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '23-45-A4O-MOF_firmware', diff --git a/tests/components/ping/snapshots/test_binary_sensor.ambr b/tests/components/ping/snapshots/test_binary_sensor.ambr index bb28432841f..c5a97fa5d22 100644 --- a/tests/components/ping/snapshots/test_binary_sensor.ambr +++ b/tests/components/ping/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'ping', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unit_of_measurement': None, diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr index 6b86c327863..cbba01ef272 100644 --- a/tests/components/ping/snapshots/test_sensor.ambr +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Round-trip time average', 'platform': 'ping', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'round_trip_time_avg', 'unit_of_measurement': , @@ -80,6 +81,7 @@ 'original_name': 'Round-trip time maximum', 'platform': 'ping', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'round_trip_time_max', 'unit_of_measurement': , @@ -137,6 +139,7 @@ 'original_name': 'Round-trip time minimum', 'platform': 'ping', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'round_trip_time_min', 'unit_of_measurement': , diff --git a/tests/components/plaato/snapshots/test_binary_sensor.ambr b/tests/components/plaato/snapshots/test_binary_sensor.ambr index 76c0a299c5e..2eb77505c11 100644 --- a/tests/components/plaato/snapshots/test_binary_sensor.ambr +++ b/tests/components/plaato/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Leaking', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.LEAK_DETECTION', @@ -78,6 +79,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Pouring', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.POURING', diff --git a/tests/components/plaato/snapshots/test_sensor.ambr b/tests/components/plaato/snapshots/test_sensor.ambr index 24ba62e28ca..8b7f2111365 100644 --- a/tests/components/plaato/snapshots/test_sensor.ambr +++ b/tests/components/plaato/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Alcohol By Volume', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.ABV', @@ -75,6 +76,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Batch Volume', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.BATCH_VOLUME', @@ -122,6 +124,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Bubbles', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.BUBBLES', @@ -170,6 +173,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Bubbles Per Minute', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.BPM', @@ -218,6 +222,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Co2 Volume', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.CO2_VOLUME', @@ -265,6 +270,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Original Gravity', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.OG', @@ -313,6 +319,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Specific Gravity', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.SG', @@ -361,6 +368,7 @@ 'original_name': 'Plaato Plaatodevicetype.Airlock Device_Name Temperature', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.TEMPERATURE', @@ -408,6 +416,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Beer Left', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.BEER_LEFT', @@ -458,6 +467,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Last Pour Amount', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.LAST_POUR', @@ -509,6 +519,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Percent Beer Left', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.PERCENT_BEER_LEFT', @@ -560,6 +571,7 @@ 'original_name': 'Plaato Plaatodevicetype.Keg Device_Name Temperature', 'platform': 'plaato', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'valid_token_Pins.TEMPERATURE', diff --git a/tests/components/poolsense/snapshots/test_binary_sensor.ambr b/tests/components/poolsense/snapshots/test_binary_sensor.ambr index b3d99b95308..f0e008d4f70 100644 --- a/tests/components/poolsense/snapshots/test_binary_sensor.ambr +++ b/tests/components/poolsense/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Chlorine status', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'chlorine_status', 'unique_id': 'test@test.com-Chlorine Status', @@ -76,6 +77,7 @@ 'original_name': 'pH status', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ph_status', 'unique_id': 'test@test.com-pH Status', diff --git a/tests/components/poolsense/snapshots/test_sensor.ambr b/tests/components/poolsense/snapshots/test_sensor.ambr index c0066ba9396..706e466d0cf 100644 --- a/tests/components/poolsense/snapshots/test_sensor.ambr +++ b/tests/components/poolsense/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test@test.com-Battery', @@ -77,6 +78,7 @@ 'original_name': 'Chlorine', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'chlorine', 'unique_id': 'test@test.com-Chlorine', @@ -126,6 +128,7 @@ 'original_name': 'Chlorine high', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'chlorine_high', 'unique_id': 'test@test.com-Chlorine High', @@ -175,6 +178,7 @@ 'original_name': 'Chlorine low', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'chlorine_low', 'unique_id': 'test@test.com-Chlorine Low', @@ -224,6 +228,7 @@ 'original_name': 'Last seen', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_seen', 'unique_id': 'test@test.com-Last Seen', @@ -273,6 +278,7 @@ 'original_name': 'pH', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test@test.com-pH', @@ -322,6 +328,7 @@ 'original_name': 'pH high', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ph_high', 'unique_id': 'test@test.com-pH High', @@ -370,6 +377,7 @@ 'original_name': 'pH low', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ph_low', 'unique_id': 'test@test.com-pH Low', @@ -418,6 +426,7 @@ 'original_name': 'Temperature', 'platform': 'poolsense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_temp', 'unique_id': 'test@test.com-Water Temp', diff --git a/tests/components/powerfox/snapshots/test_sensor.ambr b/tests/components/powerfox/snapshots/test_sensor.ambr index bae306ccabc..9be211ecd94 100644 --- a/tests/components/powerfox/snapshots/test_sensor.ambr +++ b/tests/components/powerfox/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Delta energy', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_delta_energy', 'unique_id': '9x9x1f12xx5x_heat_delta_energy', @@ -79,6 +80,7 @@ 'original_name': 'Delta volume', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_delta_volume', 'unique_id': '9x9x1f12xx5x_heat_delta_volume', @@ -130,6 +132,7 @@ 'original_name': 'Total energy', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_total_energy', 'unique_id': '9x9x1f12xx5x_heat_total_energy', @@ -182,6 +185,7 @@ 'original_name': 'Total volume', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_total_volume', 'unique_id': '9x9x1f12xx5x_heat_total_volume', @@ -234,6 +238,7 @@ 'original_name': 'Energy return', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_return', 'unique_id': '9x9x1f12xx3x_energy_return', @@ -286,6 +291,7 @@ 'original_name': 'Energy usage', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_usage', 'unique_id': '9x9x1f12xx3x_energy_usage', @@ -338,6 +344,7 @@ 'original_name': 'Energy usage high tariff', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_usage_high_tariff', 'unique_id': '9x9x1f12xx3x_energy_usage_high_tariff', @@ -390,6 +397,7 @@ 'original_name': 'Energy usage low tariff', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_usage_low_tariff', 'unique_id': '9x9x1f12xx3x_energy_usage_low_tariff', @@ -442,6 +450,7 @@ 'original_name': 'Power', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '9x9x1f12xx3x_power', @@ -494,6 +503,7 @@ 'original_name': 'Cold water', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cold_water', 'unique_id': '9x9x1f12xx4x_cold_water', @@ -546,6 +556,7 @@ 'original_name': 'Warm water', 'platform': 'powerfox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'warm_water', 'unique_id': '9x9x1f12xx4x_warm_water', diff --git a/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr index 9bd7abc830b..f9f6cbfc44f 100644 --- a/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr +++ b/tests/components/pterodactyl/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Status', 'platform': 'pterodactyl', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': '1-1-1-1-1_status', @@ -75,6 +76,7 @@ 'original_name': 'Status', 'platform': 'pterodactyl', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': '2-2-2-2-2_status', diff --git a/tests/components/pyload/snapshots/test_button.ambr b/tests/components/pyload/snapshots/test_button.ambr index 57a0358da42..4cc5bd42e6c 100644 --- a/tests/components/pyload/snapshots/test_button.ambr +++ b/tests/components/pyload/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Abort all running downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_abort_downloads', @@ -74,6 +75,7 @@ 'original_name': 'Delete finished files/packages', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_delete_finished', @@ -121,6 +123,7 @@ 'original_name': 'Restart all failed files', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_restart_failed', @@ -168,6 +171,7 @@ 'original_name': 'Restart pyload core', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_restart', diff --git a/tests/components/pyload/snapshots/test_sensor.ambr b/tests/components/pyload/snapshots/test_sensor.ambr index d9948f4273a..ce2b822a6aa 100644 --- a/tests/components/pyload/snapshots/test_sensor.ambr +++ b/tests/components/pyload/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Active downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', @@ -80,6 +81,7 @@ 'original_name': 'Downloads in queue', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', @@ -135,6 +137,7 @@ 'original_name': 'Free space', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_free_space', @@ -190,6 +193,7 @@ 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', @@ -241,6 +245,7 @@ 'original_name': 'Total downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', @@ -292,6 +297,7 @@ 'original_name': 'Active downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', @@ -343,6 +349,7 @@ 'original_name': 'Downloads in queue', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', @@ -398,6 +405,7 @@ 'original_name': 'Free space', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_free_space', @@ -453,6 +461,7 @@ 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', @@ -504,6 +513,7 @@ 'original_name': 'Total downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', @@ -555,6 +565,7 @@ 'original_name': 'Active downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', @@ -606,6 +617,7 @@ 'original_name': 'Downloads in queue', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', @@ -661,6 +673,7 @@ 'original_name': 'Free space', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_free_space', @@ -716,6 +729,7 @@ 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', @@ -767,6 +781,7 @@ 'original_name': 'Total downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', @@ -818,6 +833,7 @@ 'original_name': 'Active downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_active', @@ -869,6 +885,7 @@ 'original_name': 'Downloads in queue', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_queue', @@ -924,6 +941,7 @@ 'original_name': 'Free space', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_free_space', @@ -979,6 +997,7 @@ 'original_name': 'Speed', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_speed', @@ -1030,6 +1049,7 @@ 'original_name': 'Total downloads', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_total', diff --git a/tests/components/pyload/snapshots/test_switch.ambr b/tests/components/pyload/snapshots/test_switch.ambr index 479013b09e4..b1f566fc8c8 100644 --- a/tests/components/pyload/snapshots/test_switch.ambr +++ b/tests/components/pyload/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Auto-Reconnect', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_reconnect', @@ -75,6 +76,7 @@ 'original_name': 'Pause/Resume queue', 'platform': 'pyload', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'XXXXXXXXXXXXXX_download', diff --git a/tests/components/rainforest_raven/snapshots/test_sensor.ambr b/tests/components/rainforest_raven/snapshots/test_sensor.ambr index fc0d5862352..f95434e8592 100644 --- a/tests/components/rainforest_raven/snapshots/test_sensor.ambr +++ b/tests/components/rainforest_raven/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Energy price', 'platform': 'rainforest_raven', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_price', 'unique_id': '1234567890abcdef.PriceCluster.price', @@ -82,6 +83,7 @@ 'original_name': 'Power demand', 'platform': 'rainforest_raven', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_demand', 'unique_id': '1234567890abcdef.InstantaneousDemand.demand', @@ -134,6 +136,7 @@ 'original_name': 'Signal strength', 'platform': 'rainforest_raven', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'signal_strength', 'unique_id': 'abcdef0123456789.NetworkInfo.link_strength', @@ -186,6 +189,7 @@ 'original_name': 'Total energy delivered', 'platform': 'rainforest_raven', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_delivered', 'unique_id': '1234567890abcdef.CurrentSummationDelivered.summation_delivered', @@ -238,6 +242,7 @@ 'original_name': 'Total energy received', 'platform': 'rainforest_raven', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_received', 'unique_id': '1234567890abcdef.CurrentSummationDelivered.summation_received', diff --git a/tests/components/rainmachine/snapshots/test_binary_sensor.ambr b/tests/components/rainmachine/snapshots/test_binary_sensor.ambr index c4d6f2eeae1..1e7e15f2a49 100644 --- a/tests/components/rainmachine/snapshots/test_binary_sensor.ambr +++ b/tests/components/rainmachine/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Freeze restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freeze', 'unique_id': 'aa:bb:cc:dd:ee:ff_freeze', @@ -74,6 +75,7 @@ 'original_name': 'Hourly restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hourly', 'unique_id': 'aa:bb:cc:dd:ee:ff_hourly', @@ -121,6 +123,7 @@ 'original_name': 'Month restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'month', 'unique_id': 'aa:bb:cc:dd:ee:ff_month', @@ -168,6 +171,7 @@ 'original_name': 'Rain delay restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'raindelay', 'unique_id': 'aa:bb:cc:dd:ee:ff_raindelay', @@ -215,6 +219,7 @@ 'original_name': 'Rain sensor restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rainsensor', 'unique_id': 'aa:bb:cc:dd:ee:ff_rainsensor', @@ -262,6 +267,7 @@ 'original_name': 'Weekday restrictions', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekday', 'unique_id': 'aa:bb:cc:dd:ee:ff_weekday', diff --git a/tests/components/rainmachine/snapshots/test_button.ambr b/tests/components/rainmachine/snapshots/test_button.ambr index 68f83d9286a..8126c190a8d 100644 --- a/tests/components/rainmachine/snapshots/test_button.ambr +++ b/tests/components/rainmachine/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Restart', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_reboot', diff --git a/tests/components/rainmachine/snapshots/test_select.ambr b/tests/components/rainmachine/snapshots/test_select.ambr index d150f8c31b5..4b4ba86bb2e 100644 --- a/tests/components/rainmachine/snapshots/test_select.ambr +++ b/tests/components/rainmachine/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Freeze protection temperature', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freeze_protection_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff_freeze_protection_temperature', diff --git a/tests/components/rainmachine/snapshots/test_sensor.ambr b/tests/components/rainmachine/snapshots/test_sensor.ambr index 2475abecb51..4b9c98483ae 100644 --- a/tests/components/rainmachine/snapshots/test_sensor.ambr +++ b/tests/components/rainmachine/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Evening Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_run_completion_time_2', @@ -75,6 +76,7 @@ 'original_name': 'Flower Box Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_2', @@ -123,6 +125,7 @@ 'original_name': 'Landscaping Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_1', @@ -171,6 +174,7 @@ 'original_name': 'Morning Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_run_completion_time_1', @@ -219,6 +223,7 @@ 'original_name': 'Rain sensor rain start', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rain_sensor_rain_start', 'unique_id': 'aa:bb:cc:dd:ee:ff_rain_sensor_rain_start', @@ -268,6 +273,7 @@ 'original_name': 'TEST Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_3', @@ -316,6 +322,7 @@ 'original_name': 'Zone 10 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_10', @@ -364,6 +371,7 @@ 'original_name': 'Zone 11 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_11', @@ -412,6 +420,7 @@ 'original_name': 'Zone 12 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_12', @@ -460,6 +469,7 @@ 'original_name': 'Zone 4 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_4', @@ -508,6 +518,7 @@ 'original_name': 'Zone 5 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_5', @@ -556,6 +567,7 @@ 'original_name': 'Zone 6 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_6', @@ -604,6 +616,7 @@ 'original_name': 'Zone 7 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_7', @@ -652,6 +665,7 @@ 'original_name': 'Zone 8 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_8', @@ -700,6 +714,7 @@ 'original_name': 'Zone 9 Run Completion Time', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_run_completion_time_9', diff --git a/tests/components/rainmachine/snapshots/test_switch.ambr b/tests/components/rainmachine/snapshots/test_switch.ambr index d40913a7eb0..5ef256bc408 100644 --- a/tests/components/rainmachine/snapshots/test_switch.ambr +++ b/tests/components/rainmachine/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Evening', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_2', @@ -100,6 +101,7 @@ 'original_name': 'Evening enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_2_enabled', @@ -149,6 +151,7 @@ 'original_name': 'Extra water on hot days', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hot_days_extra_watering', 'unique_id': 'aa:bb:cc:dd:ee:ff_hot_days_extra_watering', @@ -197,6 +200,7 @@ 'original_name': 'Flower box', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_2', @@ -259,6 +263,7 @@ 'original_name': 'Flower box enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_2_enabled', @@ -308,6 +313,7 @@ 'original_name': 'Freeze protection', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freeze_protect_enabled', 'unique_id': 'aa:bb:cc:dd:ee:ff_freeze_protect_enabled', @@ -356,6 +362,7 @@ 'original_name': 'Landscaping', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_1', @@ -418,6 +425,7 @@ 'original_name': 'Landscaping enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_1_enabled', @@ -467,6 +475,7 @@ 'original_name': 'Morning', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_1', @@ -540,6 +549,7 @@ 'original_name': 'Morning enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_program_1_enabled', @@ -589,6 +599,7 @@ 'original_name': 'Test', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_3', @@ -651,6 +662,7 @@ 'original_name': 'Test enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_3_enabled', @@ -700,6 +712,7 @@ 'original_name': 'Zone 10', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_10', @@ -762,6 +775,7 @@ 'original_name': 'Zone 10 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_10_enabled', @@ -811,6 +825,7 @@ 'original_name': 'Zone 11', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_11', @@ -873,6 +888,7 @@ 'original_name': 'Zone 11 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_11_enabled', @@ -922,6 +938,7 @@ 'original_name': 'Zone 12', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_12', @@ -984,6 +1001,7 @@ 'original_name': 'Zone 12 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_12_enabled', @@ -1033,6 +1051,7 @@ 'original_name': 'Zone 4', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_4', @@ -1095,6 +1114,7 @@ 'original_name': 'Zone 4 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_4_enabled', @@ -1144,6 +1164,7 @@ 'original_name': 'Zone 5', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_5', @@ -1206,6 +1227,7 @@ 'original_name': 'Zone 5 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_5_enabled', @@ -1255,6 +1277,7 @@ 'original_name': 'Zone 6', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_6', @@ -1317,6 +1340,7 @@ 'original_name': 'Zone 6 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_6_enabled', @@ -1366,6 +1390,7 @@ 'original_name': 'Zone 7', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_7', @@ -1428,6 +1453,7 @@ 'original_name': 'Zone 7 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_7_enabled', @@ -1477,6 +1503,7 @@ 'original_name': 'Zone 8', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_8', @@ -1539,6 +1566,7 @@ 'original_name': 'Zone 8 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_8_enabled', @@ -1588,6 +1616,7 @@ 'original_name': 'Zone 9', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_9', @@ -1650,6 +1679,7 @@ 'original_name': 'Zone 9 enabled', 'platform': 'rainmachine', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff_zone_9_enabled', diff --git a/tests/components/rehlko/snapshots/test_binary_sensor.ambr b/tests/components/rehlko/snapshots/test_binary_sensor.ambr index 24284faa3cc..38b5b048d08 100644 --- a/tests/components/rehlko/snapshots/test_binary_sensor.ambr +++ b/tests/components/rehlko/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Auto run', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_run', 'unique_id': 'myemail@email.com_12345_switchState', @@ -74,6 +75,7 @@ 'original_name': 'Connectivity', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'myemail@email.com_12345_isConnected', @@ -122,6 +124,7 @@ 'original_name': 'Oil pressure', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oil_pressure', 'unique_id': 'myemail@email.com_12345_engineOilPressureOk', diff --git a/tests/components/rehlko/snapshots/test_sensor.ambr b/tests/components/rehlko/snapshots/test_sensor.ambr index 3f0334ec7b8..f63a9106de7 100644 --- a/tests/components/rehlko/snapshots/test_sensor.ambr +++ b/tests/components/rehlko/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery voltage', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'myemail@email.com_12345_batteryVoltageV', @@ -81,6 +82,7 @@ 'original_name': 'Controller temperature', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'controller_temperature', 'unique_id': 'myemail@email.com_12345_controllerTempF', @@ -131,6 +133,7 @@ 'original_name': 'Device IP address', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_ip_address', 'unique_id': 'myemail@email.com_12345_deviceIpAddress', @@ -180,6 +183,7 @@ 'original_name': 'Engine compartment temperature', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_compartment_temperature', 'unique_id': 'myemail@email.com_12345_engineCompartmentTempF', @@ -232,6 +236,7 @@ 'original_name': 'Engine coolant temperature', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_coolant_temperature', 'unique_id': 'myemail@email.com_12345_engineCoolantTempF', @@ -284,6 +289,7 @@ 'original_name': 'Engine frequency', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_frequency', 'unique_id': 'myemail@email.com_12345_engineFrequencyHz', @@ -339,6 +345,7 @@ 'original_name': 'Engine oil pressure', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_oil_pressure', 'unique_id': 'myemail@email.com_12345_engineOilPressurePsi', @@ -391,6 +398,7 @@ 'original_name': 'Engine speed', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_speed', 'unique_id': 'myemail@email.com_12345_engineSpeedRpm', @@ -440,6 +448,7 @@ 'original_name': 'Engine state', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'engine_state', 'unique_id': 'myemail@email.com_12345_engineState', @@ -489,6 +498,7 @@ 'original_name': 'Generator load', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_load', 'unique_id': 'myemail@email.com_12345_generatorLoadW', @@ -541,6 +551,7 @@ 'original_name': 'Generator load percentage', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_load_percent', 'unique_id': 'myemail@email.com_12345_generatorLoadPercent', @@ -590,6 +601,7 @@ 'original_name': 'Generator status', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_status', 'unique_id': 'myemail@email.com_12345_status', @@ -637,6 +649,7 @@ 'original_name': 'Last exercise', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_exercise', 'unique_id': 'myemail@email.com_12345_lastStartTimestamp', @@ -685,6 +698,7 @@ 'original_name': 'Last maintainance', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_maintainance', 'unique_id': 'myemail@email.com_12345_lastMaintenanceTimestamp', @@ -733,6 +747,7 @@ 'original_name': 'Last run', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_run', 'unique_id': 'myemail@email.com_12345_lastRanTimestamp', @@ -783,6 +798,7 @@ 'original_name': 'Lube oil temperature', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lube_oil_temperature', 'unique_id': 'myemail@email.com_12345_lubeOilTempF', @@ -833,6 +849,7 @@ 'original_name': 'Next exercise', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_exercise', 'unique_id': 'myemail@email.com_12345_nextStartTimestamp', @@ -881,6 +898,7 @@ 'original_name': 'Next maintainance', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'next_maintainance', 'unique_id': 'myemail@email.com_12345_nextMaintenanceTimestamp', @@ -929,6 +947,7 @@ 'original_name': 'Power source', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_source', 'unique_id': 'myemail@email.com_12345_powerSource', @@ -978,6 +997,7 @@ 'original_name': 'Runtime since last maintenance', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'runtime_since_last_maintenance', 'unique_id': 'myemail@email.com_12345_runtimeSinceLastMaintenanceHours', @@ -1028,6 +1048,7 @@ 'original_name': 'Server IP address', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'server_ip_address', 'unique_id': 'myemail@email.com_12345_serverIpAddress', @@ -1077,6 +1098,7 @@ 'original_name': 'Total operation', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_operation', 'unique_id': 'myemail@email.com_12345_totalOperationHours', @@ -1129,6 +1151,7 @@ 'original_name': 'Total runtime', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_runtime', 'unique_id': 'myemail@email.com_12345_totalRuntimeHours', @@ -1181,6 +1204,7 @@ 'original_name': 'Utility voltage', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'utility_voltage', 'unique_id': 'myemail@email.com_12345_utilityVoltageV', @@ -1233,6 +1257,7 @@ 'original_name': 'Voltage', 'platform': 'rehlko', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_voltage_avg', 'unique_id': 'myemail@email.com_12345_generatorVoltageAvgV', diff --git a/tests/components/renault/snapshots/test_binary_sensor.ambr b/tests/components/renault/snapshots/test_binary_sensor.ambr index e89873593e9..cee29a76dca 100644 --- a/tests/components/renault/snapshots/test_binary_sensor.ambr +++ b/tests/components/renault/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_charging', @@ -75,6 +76,7 @@ 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_status', 'unique_id': 'vf1zoe40vin_hvac_status', @@ -122,6 +124,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_plugged_in', @@ -170,6 +173,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_charging', @@ -218,6 +222,7 @@ 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_status', 'unique_id': 'vf1zoe40vin_hvac_status', @@ -265,6 +270,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_plugged_in', @@ -313,6 +319,7 @@ 'original_name': 'Driver door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'driver_door_status', 'unique_id': 'vf1capturfuelvin_driver_door_status', @@ -361,6 +368,7 @@ 'original_name': 'Hatch', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hatch_status', 'unique_id': 'vf1capturfuelvin_hatch_status', @@ -409,6 +417,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1capturfuelvin_lock_status', @@ -457,6 +466,7 @@ 'original_name': 'Passenger door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'passenger_door_status', 'unique_id': 'vf1capturfuelvin_passenger_door_status', @@ -505,6 +515,7 @@ 'original_name': 'Rear left door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_door_status', 'unique_id': 'vf1capturfuelvin_rear_left_door_status', @@ -553,6 +564,7 @@ 'original_name': 'Rear right door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_door_status', 'unique_id': 'vf1capturfuelvin_rear_right_door_status', @@ -601,6 +613,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1capturphevvin_charging', @@ -649,6 +662,7 @@ 'original_name': 'Driver door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'driver_door_status', 'unique_id': 'vf1capturphevvin_driver_door_status', @@ -697,6 +711,7 @@ 'original_name': 'Hatch', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hatch_status', 'unique_id': 'vf1capturphevvin_hatch_status', @@ -745,6 +760,7 @@ 'original_name': 'Lock', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1capturphevvin_lock_status', @@ -793,6 +809,7 @@ 'original_name': 'Passenger door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'passenger_door_status', 'unique_id': 'vf1capturphevvin_passenger_door_status', @@ -841,6 +858,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1capturphevvin_plugged_in', @@ -889,6 +907,7 @@ 'original_name': 'Rear left door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_left_door_status', 'unique_id': 'vf1capturphevvin_rear_left_door_status', @@ -937,6 +956,7 @@ 'original_name': 'Rear right door', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_right_door_status', 'unique_id': 'vf1capturphevvin_rear_right_door_status', @@ -985,6 +1005,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1twingoiiivin_charging', @@ -1033,6 +1054,7 @@ 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_status', 'unique_id': 'vf1twingoiiivin_hvac_status', @@ -1080,6 +1102,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1twingoiiivin_plugged_in', @@ -1128,6 +1151,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_charging', @@ -1176,6 +1200,7 @@ 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_status', 'unique_id': 'vf1zoe40vin_hvac_status', @@ -1223,6 +1248,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_plugged_in', @@ -1271,6 +1297,7 @@ 'original_name': 'Charging', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe50vin_charging', @@ -1319,6 +1346,7 @@ 'original_name': 'HVAC', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_status', 'unique_id': 'vf1zoe50vin_hvac_status', @@ -1366,6 +1394,7 @@ 'original_name': 'Plug', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe50vin_plugged_in', diff --git a/tests/components/renault/snapshots/test_button.ambr b/tests/components/renault/snapshots/test_button.ambr index 1c7d5f80af2..95e81aee4c5 100644 --- a/tests/components/renault/snapshots/test_button.ambr +++ b/tests/components/renault/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe40vin_start_air_conditioner', @@ -74,6 +75,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe40vin_start_charge', @@ -121,6 +123,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe40vin_stop_charge', @@ -168,6 +171,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe40vin_start_air_conditioner', @@ -215,6 +219,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe40vin_start_charge', @@ -262,6 +267,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe40vin_stop_charge', @@ -309,6 +315,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe40vin_start_air_conditioner', @@ -356,6 +363,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe40vin_start_charge', @@ -403,6 +411,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe40vin_stop_charge', @@ -450,6 +459,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe40vin_start_air_conditioner', @@ -497,6 +507,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe40vin_start_charge', @@ -544,6 +555,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe40vin_stop_charge', @@ -591,6 +603,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1capturfuelvin_start_air_conditioner', @@ -638,6 +651,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1capturphevvin_start_air_conditioner', @@ -685,6 +699,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1capturphevvin_start_charge', @@ -732,6 +747,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1capturphevvin_stop_charge', @@ -779,6 +795,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1twingoiiivin_start_air_conditioner', @@ -826,6 +843,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1twingoiiivin_start_charge', @@ -873,6 +891,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1twingoiiivin_stop_charge', @@ -920,6 +939,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe40vin_start_air_conditioner', @@ -967,6 +987,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe40vin_start_charge', @@ -1014,6 +1035,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe40vin_stop_charge', @@ -1061,6 +1083,7 @@ 'original_name': 'Start air conditioner', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_air_conditioner', 'unique_id': 'vf1zoe50vin_start_air_conditioner', @@ -1108,6 +1131,7 @@ 'original_name': 'Start charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'start_charge', 'unique_id': 'vf1zoe50vin_start_charge', @@ -1155,6 +1179,7 @@ 'original_name': 'Stop charge', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop_charge', 'unique_id': 'vf1zoe50vin_stop_charge', diff --git a/tests/components/renault/snapshots/test_device_tracker.ambr b/tests/components/renault/snapshots/test_device_tracker.ambr index 7a35f70b51c..15f95140a8f 100644 --- a/tests/components/renault/snapshots/test_device_tracker.ambr +++ b/tests/components/renault/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1zoe50vin_location', @@ -75,6 +76,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1zoe50vin_location', @@ -122,6 +124,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1capturfuelvin_location', @@ -173,6 +176,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1capturphevvin_location', @@ -224,6 +228,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1twingoiiivin_location', @@ -275,6 +280,7 @@ 'original_name': 'Location', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'vf1zoe50vin_location', diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 9df17d0a3ec..e0a1c779fc8 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1zoe40vin_charge_mode', @@ -94,6 +95,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1zoe40vin_charge_mode', @@ -154,6 +156,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1capturphevvin_charge_mode', @@ -214,6 +217,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1twingoiiivin_charge_mode', @@ -274,6 +278,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1zoe40vin_charge_mode', @@ -334,6 +339,7 @@ 'original_name': 'Charge mode', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_mode', 'unique_id': 'vf1zoe50vin_charge_mode', diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index b6c9569e0d3..d1c5a52d2b6 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_battery_level', @@ -81,6 +82,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1zoe40vin_battery_autonomy', @@ -133,6 +135,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1zoe40vin_battery_available_energy', @@ -185,6 +188,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1zoe40vin_battery_temperature', @@ -246,6 +250,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1zoe40vin_charge_state', @@ -306,6 +311,7 @@ 'original_name': 'Charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_power', 'unique_id': 'vf1zoe40vin_charging_power', @@ -358,6 +364,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1zoe40vin_charging_remaining_time', @@ -408,6 +415,7 @@ 'original_name': 'HVAC SoC threshold', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_soc_threshold', 'unique_id': 'vf1zoe40vin_hvac_soc_threshold', @@ -456,6 +464,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1zoe40vin_battery_last_activity', @@ -504,6 +513,7 @@ 'original_name': 'Last HVAC activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_last_activity', 'unique_id': 'vf1zoe40vin_hvac_last_activity', @@ -554,6 +564,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1zoe40vin_mileage', @@ -606,6 +617,7 @@ 'original_name': 'Outside temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'vf1zoe40vin_outside_temperature', @@ -664,6 +676,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1zoe40vin_plug_state', @@ -721,6 +734,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_battery_level', @@ -773,6 +787,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1zoe40vin_battery_autonomy', @@ -825,6 +840,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1zoe40vin_battery_available_energy', @@ -877,6 +893,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1zoe40vin_battery_temperature', @@ -938,6 +955,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1zoe40vin_charge_state', @@ -998,6 +1016,7 @@ 'original_name': 'Charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_power', 'unique_id': 'vf1zoe40vin_charging_power', @@ -1050,6 +1069,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1zoe40vin_charging_remaining_time', @@ -1100,6 +1120,7 @@ 'original_name': 'HVAC SoC threshold', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_soc_threshold', 'unique_id': 'vf1zoe40vin_hvac_soc_threshold', @@ -1148,6 +1169,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1zoe40vin_battery_last_activity', @@ -1196,6 +1218,7 @@ 'original_name': 'Last HVAC activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_last_activity', 'unique_id': 'vf1zoe40vin_hvac_last_activity', @@ -1246,6 +1269,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1zoe40vin_mileage', @@ -1298,6 +1322,7 @@ 'original_name': 'Outside temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'vf1zoe40vin_outside_temperature', @@ -1356,6 +1381,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1zoe40vin_plug_state', @@ -1413,6 +1439,7 @@ 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fuel_autonomy', 'unique_id': 'vf1capturfuelvin_fuel_autonomy', @@ -1465,6 +1492,7 @@ 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fuel_quantity', 'unique_id': 'vf1capturfuelvin_fuel_quantity', @@ -1515,6 +1543,7 @@ 'original_name': 'Last location activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location_last_activity', 'unique_id': 'vf1capturfuelvin_location_last_activity', @@ -1565,6 +1594,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1capturfuelvin_mileage', @@ -1615,6 +1645,7 @@ 'original_name': 'Remote engine start', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'res_state', 'unique_id': 'vf1capturfuelvin_res_state', @@ -1662,6 +1693,7 @@ 'original_name': 'Remote engine start code', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'res_state_code', 'unique_id': 'vf1capturfuelvin_res_state_code', @@ -1711,6 +1743,7 @@ 'original_name': 'Admissible charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'admissible_charging_power', 'unique_id': 'vf1capturphevvin_charging_power', @@ -1763,6 +1796,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1capturphevvin_battery_level', @@ -1815,6 +1849,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1capturphevvin_battery_autonomy', @@ -1867,6 +1902,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1capturphevvin_battery_available_energy', @@ -1919,6 +1955,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1capturphevvin_battery_temperature', @@ -1980,6 +2017,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1capturphevvin_charge_state', @@ -2040,6 +2078,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1capturphevvin_charging_remaining_time', @@ -2092,6 +2131,7 @@ 'original_name': 'Fuel autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fuel_autonomy', 'unique_id': 'vf1capturphevvin_fuel_autonomy', @@ -2144,6 +2184,7 @@ 'original_name': 'Fuel quantity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fuel_quantity', 'unique_id': 'vf1capturphevvin_fuel_quantity', @@ -2194,6 +2235,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1capturphevvin_battery_last_activity', @@ -2242,6 +2284,7 @@ 'original_name': 'Last location activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location_last_activity', 'unique_id': 'vf1capturphevvin_location_last_activity', @@ -2292,6 +2335,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1capturphevvin_mileage', @@ -2350,6 +2394,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1capturphevvin_plug_state', @@ -2405,6 +2450,7 @@ 'original_name': 'Remote engine start', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'res_state', 'unique_id': 'vf1capturphevvin_res_state', @@ -2452,6 +2498,7 @@ 'original_name': 'Remote engine start code', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'res_state_code', 'unique_id': 'vf1capturphevvin_res_state_code', @@ -2501,6 +2548,7 @@ 'original_name': 'Admissible charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'admissible_charging_power', 'unique_id': 'vf1twingoiiivin_charging_power', @@ -2553,6 +2601,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1twingoiiivin_battery_level', @@ -2605,6 +2654,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1twingoiiivin_battery_autonomy', @@ -2657,6 +2707,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1twingoiiivin_battery_available_energy', @@ -2709,6 +2760,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1twingoiiivin_battery_temperature', @@ -2770,6 +2822,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1twingoiiivin_charge_state', @@ -2830,6 +2883,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1twingoiiivin_charging_remaining_time', @@ -2880,6 +2934,7 @@ 'original_name': 'HVAC SoC threshold', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_soc_threshold', 'unique_id': 'vf1twingoiiivin_hvac_soc_threshold', @@ -2928,6 +2983,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1twingoiiivin_battery_last_activity', @@ -2976,6 +3032,7 @@ 'original_name': 'Last HVAC activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_last_activity', 'unique_id': 'vf1twingoiiivin_hvac_last_activity', @@ -3024,6 +3081,7 @@ 'original_name': 'Last location activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location_last_activity', 'unique_id': 'vf1twingoiiivin_location_last_activity', @@ -3074,6 +3132,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1twingoiiivin_mileage', @@ -3126,6 +3185,7 @@ 'original_name': 'Outside temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'vf1twingoiiivin_outside_temperature', @@ -3184,6 +3244,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1twingoiiivin_plug_state', @@ -3241,6 +3302,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe40vin_battery_level', @@ -3293,6 +3355,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1zoe40vin_battery_autonomy', @@ -3345,6 +3408,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1zoe40vin_battery_available_energy', @@ -3397,6 +3461,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1zoe40vin_battery_temperature', @@ -3458,6 +3523,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1zoe40vin_charge_state', @@ -3518,6 +3584,7 @@ 'original_name': 'Charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_power', 'unique_id': 'vf1zoe40vin_charging_power', @@ -3570,6 +3637,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1zoe40vin_charging_remaining_time', @@ -3620,6 +3688,7 @@ 'original_name': 'HVAC SoC threshold', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_soc_threshold', 'unique_id': 'vf1zoe40vin_hvac_soc_threshold', @@ -3668,6 +3737,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1zoe40vin_battery_last_activity', @@ -3716,6 +3786,7 @@ 'original_name': 'Last HVAC activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_last_activity', 'unique_id': 'vf1zoe40vin_hvac_last_activity', @@ -3766,6 +3837,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1zoe40vin_mileage', @@ -3818,6 +3890,7 @@ 'original_name': 'Outside temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'vf1zoe40vin_outside_temperature', @@ -3876,6 +3949,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1zoe40vin_plug_state', @@ -3933,6 +4007,7 @@ 'original_name': 'Admissible charging power', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'admissible_charging_power', 'unique_id': 'vf1zoe50vin_charging_power', @@ -3985,6 +4060,7 @@ 'original_name': 'Battery', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'vf1zoe50vin_battery_level', @@ -4037,6 +4113,7 @@ 'original_name': 'Battery autonomy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_autonomy', 'unique_id': 'vf1zoe50vin_battery_autonomy', @@ -4089,6 +4166,7 @@ 'original_name': 'Battery available energy', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_available_energy', 'unique_id': 'vf1zoe50vin_battery_available_energy', @@ -4141,6 +4219,7 @@ 'original_name': 'Battery temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_temperature', 'unique_id': 'vf1zoe50vin_battery_temperature', @@ -4202,6 +4281,7 @@ 'original_name': 'Charge state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state', 'unique_id': 'vf1zoe50vin_charge_state', @@ -4262,6 +4342,7 @@ 'original_name': 'Charging remaining time', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_remaining_time', 'unique_id': 'vf1zoe50vin_charging_remaining_time', @@ -4312,6 +4393,7 @@ 'original_name': 'HVAC SoC threshold', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_soc_threshold', 'unique_id': 'vf1zoe50vin_hvac_soc_threshold', @@ -4360,6 +4442,7 @@ 'original_name': 'Last battery activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_last_activity', 'unique_id': 'vf1zoe50vin_battery_last_activity', @@ -4408,6 +4491,7 @@ 'original_name': 'Last HVAC activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_last_activity', 'unique_id': 'vf1zoe50vin_hvac_last_activity', @@ -4456,6 +4540,7 @@ 'original_name': 'Last location activity', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location_last_activity', 'unique_id': 'vf1zoe50vin_location_last_activity', @@ -4506,6 +4591,7 @@ 'original_name': 'Mileage', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mileage', 'unique_id': 'vf1zoe50vin_mileage', @@ -4558,6 +4644,7 @@ 'original_name': 'Outside temperature', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'vf1zoe50vin_outside_temperature', @@ -4616,6 +4703,7 @@ 'original_name': 'Plug state', 'platform': 'renault', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plug_state', 'unique_id': 'vf1zoe50vin_plug_state', diff --git a/tests/components/ring/snapshots/test_binary_sensor.ambr b/tests/components/ring/snapshots/test_binary_sensor.ambr index 09dab9b0ecc..9fa57800ec9 100644 --- a/tests/components/ring/snapshots/test_binary_sensor.ambr +++ b/tests/components/ring/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_ding', 'supported_features': 0, 'translation_key': 'ding', 'unique_id': '987654-ding', @@ -76,6 +77,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_motion', 'supported_features': 0, 'translation_key': None, 'unique_id': '987654-motion', @@ -125,6 +127,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_motion', 'supported_features': 0, 'translation_key': None, 'unique_id': '765432-motion', @@ -174,6 +177,7 @@ 'original_name': 'Ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_ding', 'supported_features': 0, 'translation_key': 'ding', 'unique_id': '185036587-ding', @@ -223,6 +227,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_motion', 'supported_features': 0, 'translation_key': None, 'unique_id': '345678-motion', diff --git a/tests/components/ring/snapshots/test_button.ambr b/tests/components/ring/snapshots/test_button.ambr index 7da11d66194..fe9afb7964e 100644 --- a/tests/components/ring/snapshots/test_button.ambr +++ b/tests/components/ring/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Open door', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'open_door', 'unique_id': '185036587-open_door', diff --git a/tests/components/ring/snapshots/test_camera.ambr b/tests/components/ring/snapshots/test_camera.ambr index 0e5efd68753..bc0ecbdc794 100644 --- a/tests/components/ring/snapshots/test_camera.ambr +++ b/tests/components/ring/snapshots/test_camera.ambr @@ -27,6 +27,7 @@ 'original_name': 'Last recording', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_recording', 'unique_id': '987654-last_recording', @@ -81,6 +82,7 @@ 'original_name': 'Live view', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'live_view', 'unique_id': '987654-live_view', @@ -134,6 +136,7 @@ 'original_name': 'Last recording', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_recording', 'unique_id': '765432-last_recording', @@ -187,6 +190,7 @@ 'original_name': 'Live view', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'live_view', 'unique_id': '765432-live_view', @@ -240,6 +244,7 @@ 'original_name': 'Last recording', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_recording', 'unique_id': '345678-last_recording', @@ -294,6 +299,7 @@ 'original_name': 'Live view', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'live_view', 'unique_id': '345678-live_view', diff --git a/tests/components/ring/snapshots/test_event.ambr b/tests/components/ring/snapshots/test_event.ambr index 9c0fee906a0..f1d2d2fd09f 100644 --- a/tests/components/ring/snapshots/test_event.ambr +++ b/tests/components/ring/snapshots/test_event.ambr @@ -31,6 +31,7 @@ 'original_name': 'Ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ding', 'unique_id': '987654-ding', @@ -88,6 +89,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion', 'unique_id': '987654-motion', @@ -145,6 +147,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion', 'unique_id': '765432-motion', @@ -202,6 +205,7 @@ 'original_name': 'Ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ding', 'unique_id': '185036587-ding', @@ -259,6 +263,7 @@ 'original_name': 'Intercom unlock', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'intercom_unlock', 'unique_id': '185036587-intercom_unlock', @@ -316,6 +321,7 @@ 'original_name': 'Motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion', 'unique_id': '345678-motion', diff --git a/tests/components/ring/snapshots/test_light.ambr b/tests/components/ring/snapshots/test_light.ambr index 6c6effb93c1..8727adbb6e2 100644 --- a/tests/components/ring/snapshots/test_light.ambr +++ b/tests/components/ring/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'Light', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': '765432', @@ -88,6 +89,7 @@ 'original_name': 'Light', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': '345678', diff --git a/tests/components/ring/snapshots/test_number.ambr b/tests/components/ring/snapshots/test_number.ambr index abc63051f6a..b32a97f71d2 100644 --- a/tests/components/ring/snapshots/test_number.ambr +++ b/tests/components/ring/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '123456-volume', @@ -89,6 +90,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '987654-volume', @@ -146,6 +148,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '765432-volume', @@ -203,6 +206,7 @@ 'original_name': 'Doorbell volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'doorbell_volume', 'unique_id': '185036587-doorbell_volume', @@ -260,6 +264,7 @@ 'original_name': 'Mic volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mic_volume', 'unique_id': '185036587-mic_volume', @@ -317,6 +322,7 @@ 'original_name': 'Voice volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voice_volume', 'unique_id': '185036587-voice_volume', @@ -374,6 +380,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '345678-volume', diff --git a/tests/components/ring/snapshots/test_sensor.ambr b/tests/components/ring/snapshots/test_sensor.ambr index 615bd1df018..249a47548b8 100644 --- a/tests/components/ring/snapshots/test_sensor.ambr +++ b/tests/components/ring/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'downstairs_volume', 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '123456-volume', @@ -75,6 +76,7 @@ 'original_name': 'Wi-Fi signal category', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'downstairs_wifi_signal_category', 'supported_features': 0, 'translation_key': 'wifi_signal_category', 'unique_id': '123456-wifi_signal_category', @@ -123,6 +125,7 @@ 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'downstairs_wifi_signal_strength', 'supported_features': 0, 'translation_key': None, 'unique_id': '123456-wifi_signal_strength', @@ -175,6 +178,7 @@ 'original_name': 'Battery', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '765432-battery', @@ -228,6 +232,7 @@ 'original_name': 'Battery', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '987654-battery', @@ -279,6 +284,7 @@ 'original_name': 'Last activity', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_activity', 'unique_id': '987654-last_activity', @@ -328,6 +334,7 @@ 'original_name': 'Last ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_last_ding', 'supported_features': 0, 'translation_key': 'last_ding', 'unique_id': '987654-last_ding', @@ -377,6 +384,7 @@ 'original_name': 'Last motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_last_motion', 'supported_features': 0, 'translation_key': 'last_motion', 'unique_id': '987654-last_motion', @@ -426,6 +434,7 @@ 'original_name': 'Volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_volume', 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '765432-volume', @@ -474,6 +483,7 @@ 'original_name': 'Wi-Fi signal category', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_wifi_signal_category', 'supported_features': 0, 'translation_key': 'wifi_signal_category', 'unique_id': '987654-wifi_signal_category', @@ -522,6 +532,7 @@ 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_door_wifi_signal_strength', 'supported_features': 0, 'translation_key': None, 'unique_id': '987654-wifi_signal_strength', @@ -572,6 +583,7 @@ 'original_name': 'Last activity', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_activity', 'unique_id': '765432-last_activity', @@ -621,6 +633,7 @@ 'original_name': 'Last ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_last_ding', 'supported_features': 0, 'translation_key': 'last_ding', 'unique_id': '765432-last_ding', @@ -670,6 +683,7 @@ 'original_name': 'Last motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_last_motion', 'supported_features': 0, 'translation_key': 'last_motion', 'unique_id': '765432-last_motion', @@ -719,6 +733,7 @@ 'original_name': 'Wi-Fi signal category', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_wifi_signal_category', 'supported_features': 0, 'translation_key': 'wifi_signal_category', 'unique_id': '765432-wifi_signal_category', @@ -767,6 +782,7 @@ 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_wifi_signal_strength', 'supported_features': 0, 'translation_key': None, 'unique_id': '765432-wifi_signal_strength', @@ -819,6 +835,7 @@ 'original_name': 'Battery', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '185036587-battery', @@ -870,6 +887,7 @@ 'original_name': 'Doorbell volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_doorbell_volume', 'supported_features': 0, 'translation_key': 'doorbell_volume', 'unique_id': '185036587-doorbell_volume', @@ -918,6 +936,7 @@ 'original_name': 'Last activity', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_activity', 'unique_id': '185036587-last_activity', @@ -967,6 +986,7 @@ 'original_name': 'Mic volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_mic_volume', 'supported_features': 0, 'translation_key': 'mic_volume', 'unique_id': '185036587-mic_volume', @@ -1015,6 +1035,7 @@ 'original_name': 'Voice volume', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_voice_volume', 'supported_features': 0, 'translation_key': 'voice_volume', 'unique_id': '185036587-voice_volume', @@ -1063,6 +1084,7 @@ 'original_name': 'Wi-Fi signal category', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_wifi_signal_category', 'supported_features': 0, 'translation_key': 'wifi_signal_category', 'unique_id': '185036587-wifi_signal_category', @@ -1111,6 +1133,7 @@ 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'ingress_wifi_signal_strength', 'supported_features': 0, 'translation_key': None, 'unique_id': '185036587-wifi_signal_strength', @@ -1163,6 +1186,7 @@ 'original_name': 'Battery', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '345678-battery', @@ -1214,6 +1238,7 @@ 'original_name': 'Last activity', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_activity', 'unique_id': '345678-last_activity', @@ -1263,6 +1288,7 @@ 'original_name': 'Last ding', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_last_ding', 'supported_features': 0, 'translation_key': 'last_ding', 'unique_id': '345678-last_ding', @@ -1312,6 +1338,7 @@ 'original_name': 'Last motion', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_last_motion', 'supported_features': 0, 'translation_key': 'last_motion', 'unique_id': '345678-last_motion', @@ -1361,6 +1388,7 @@ 'original_name': 'Wi-Fi signal category', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_wifi_signal_category', 'supported_features': 0, 'translation_key': 'wifi_signal_category', 'unique_id': '345678-wifi_signal_category', @@ -1409,6 +1437,7 @@ 'original_name': 'Signal strength', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_wifi_signal_strength', 'supported_features': 0, 'translation_key': None, 'unique_id': '345678-wifi_signal_strength', diff --git a/tests/components/ring/snapshots/test_siren.ambr b/tests/components/ring/snapshots/test_siren.ambr index 8ef08815a1e..0c4ef24074a 100644 --- a/tests/components/ring/snapshots/test_siren.ambr +++ b/tests/components/ring/snapshots/test_siren.ambr @@ -32,6 +32,7 @@ 'original_name': 'Siren', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'siren', 'unique_id': '123456-siren', @@ -85,6 +86,7 @@ 'original_name': 'Siren', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'siren', 'unique_id': '765432', @@ -134,6 +136,7 @@ 'original_name': 'Siren', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'siren', 'unique_id': '345678', diff --git a/tests/components/ring/snapshots/test_switch.ambr b/tests/components/ring/snapshots/test_switch.ambr index 8c7c55d5169..69983644065 100644 --- a/tests/components/ring/snapshots/test_switch.ambr +++ b/tests/components/ring/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'In-home chime', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'in_home_chime', 'unique_id': '987654-in_home_chime', @@ -75,6 +76,7 @@ 'original_name': 'Motion detection', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_detection', 'unique_id': '987654-motion_detection', @@ -123,6 +125,7 @@ 'original_name': 'Motion detection', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_detection', 'unique_id': '765432-motion_detection', @@ -171,6 +174,7 @@ 'original_name': 'Siren', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'front_siren', 'supported_features': 0, 'translation_key': 'siren', 'unique_id': '765432-siren', @@ -219,6 +223,7 @@ 'original_name': 'Motion detection', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_detection', 'unique_id': '345678-motion_detection', @@ -267,6 +272,7 @@ 'original_name': 'Siren', 'platform': 'ring', 'previous_unique_id': None, + 'suggested_object_id': 'internal_siren', 'supported_features': 0, 'translation_key': 'siren', 'unique_id': '345678-siren', diff --git a/tests/components/rova/snapshots/test_sensor.ambr b/tests/components/rova/snapshots/test_sensor.ambr index 90cf29a1b89..7d3cb7c5962 100644 --- a/tests/components/rova/snapshots/test_sensor.ambr +++ b/tests/components/rova/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Bio', 'platform': 'rova', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bio', 'unique_id': '8381BE13_gft', @@ -75,6 +76,7 @@ 'original_name': 'Paper', 'platform': 'rova', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'paper', 'unique_id': '8381BE13_papier', @@ -123,6 +125,7 @@ 'original_name': 'Plastic', 'platform': 'rova', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'plastic', 'unique_id': '8381BE13_pmd', @@ -171,6 +174,7 @@ 'original_name': 'Residual', 'platform': 'rova', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'residual', 'unique_id': '8381BE13_restafval', diff --git a/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr b/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr index 1feaece1c3e..7da52a1acd7 100644 --- a/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr +++ b/tests/components/sabnzbd/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Warnings', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'warnings', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_warnings', diff --git a/tests/components/sabnzbd/snapshots/test_button.ambr b/tests/components/sabnzbd/snapshots/test_button.ambr index f09bb44e8e4..60970ef6abd 100644 --- a/tests/components/sabnzbd/snapshots/test_button.ambr +++ b/tests/components/sabnzbd/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Pause', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pause', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_pause', @@ -74,6 +75,7 @@ 'original_name': 'Resume', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'resume', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_resume', diff --git a/tests/components/sabnzbd/snapshots/test_number.ambr b/tests/components/sabnzbd/snapshots/test_number.ambr index 623002470b7..8fb7b0d79db 100644 --- a/tests/components/sabnzbd/snapshots/test_number.ambr +++ b/tests/components/sabnzbd/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Speedlimit', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'speedlimit', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_speedlimit', diff --git a/tests/components/sabnzbd/snapshots/test_sensor.ambr b/tests/components/sabnzbd/snapshots/test_sensor.ambr index 893d270a569..34341b63a4c 100644 --- a/tests/components/sabnzbd/snapshots/test_sensor.ambr +++ b/tests/components/sabnzbd/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Daily total', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_total', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_day_size', @@ -84,6 +85,7 @@ 'original_name': 'Free disk space', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'free_disk_space', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_diskspace1', @@ -136,6 +138,7 @@ 'original_name': 'Left to download', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'left', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_mbleft', @@ -191,6 +194,7 @@ 'original_name': 'Monthly total', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_total', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_month_size', @@ -246,6 +250,7 @@ 'original_name': 'Overall total', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overall_total', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_total_size', @@ -298,6 +303,7 @@ 'original_name': 'Queue', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'queue', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_mb', @@ -353,6 +359,7 @@ 'original_name': 'Queue count', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'queue_count', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_noofslots_total', @@ -409,6 +416,7 @@ 'original_name': 'Speed', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'speed', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_kbpersec', @@ -459,6 +467,7 @@ 'original_name': 'Status', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_status', @@ -508,6 +517,7 @@ 'original_name': 'Total disk space', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_disk_space', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_diskspacetotal1', @@ -563,6 +573,7 @@ 'original_name': 'Weekly total', 'platform': 'sabnzbd', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_total', 'unique_id': '01JD2YVVPBC62D620DGYNG2R8H_week_size', diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr index 6cf0254b66b..3e227879f01 100644 --- a/tests/components/sanix/snapshots/test_sensor.ambr +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1810088-battery', @@ -79,6 +80,7 @@ 'original_name': 'Device number', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_no', 'unique_id': '1810088-device_no', @@ -128,6 +130,7 @@ 'original_name': 'Distance', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1810088-distance', @@ -180,6 +183,7 @@ 'original_name': 'Filled', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fill_perc', 'unique_id': '1810088-fill_perc', @@ -229,6 +233,7 @@ 'original_name': 'Service date', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'service_date', 'unique_id': '1810088-service_date', @@ -277,6 +282,7 @@ 'original_name': 'SSID', 'platform': 'sanix', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ssid', 'unique_id': '1810088-ssid', diff --git a/tests/components/sense/snapshots/test_binary_sensor.ambr b/tests/components/sense/snapshots/test_binary_sensor.ambr index 7221a0bc518..aa803b40bd1 100644 --- a/tests/components/sense/snapshots/test_binary_sensor.ambr +++ b/tests/components/sense/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Power', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-abc123', @@ -77,6 +78,7 @@ 'original_name': 'Power', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-def456', diff --git a/tests/components/sense/snapshots/test_sensor.ambr b/tests/components/sense/snapshots/test_sensor.ambr index 0a68553cf04..1f96665cb22 100644 --- a/tests/components/sense/snapshots/test_sensor.ambr +++ b/tests/components/sense/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Bill energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bill_energy', 'unique_id': '12345-abc123-bill-energy', @@ -89,6 +90,7 @@ 'original_name': 'Daily energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_energy', 'unique_id': '12345-abc123-daily-energy', @@ -146,6 +148,7 @@ 'original_name': 'Monthly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_energy', 'unique_id': '12345-abc123-monthly-energy', @@ -200,6 +203,7 @@ 'original_name': 'Power', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-abc123-usage', @@ -257,6 +261,7 @@ 'original_name': 'Weekly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_energy', 'unique_id': '12345-abc123-weekly-energy', @@ -314,6 +319,7 @@ 'original_name': 'Yearly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yearly_energy', 'unique_id': '12345-abc123-yearly-energy', @@ -371,6 +377,7 @@ 'original_name': 'Bill energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bill_energy', 'unique_id': '12345-def456-bill-energy', @@ -428,6 +435,7 @@ 'original_name': 'Daily energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_energy', 'unique_id': '12345-def456-daily-energy', @@ -485,6 +493,7 @@ 'original_name': 'Monthly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_energy', 'unique_id': '12345-def456-monthly-energy', @@ -539,6 +548,7 @@ 'original_name': 'Power', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-def456-usage', @@ -596,6 +606,7 @@ 'original_name': 'Weekly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weekly_energy', 'unique_id': '12345-def456-weekly-energy', @@ -653,6 +664,7 @@ 'original_name': 'Yearly energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yearly_energy', 'unique_id': '12345-def456-yearly-energy', @@ -707,6 +719,7 @@ 'original_name': 'Bill Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-usage', @@ -761,6 +774,7 @@ 'original_name': 'Bill From Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-from_grid', @@ -815,6 +829,7 @@ 'original_name': 'Bill Net Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-net_production', @@ -867,6 +882,7 @@ 'original_name': 'Bill Net Production Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-production_pct', @@ -918,6 +934,7 @@ 'original_name': 'Bill Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-production', @@ -970,6 +987,7 @@ 'original_name': 'Bill Solar Powered Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-solar_powered', @@ -1021,6 +1039,7 @@ 'original_name': 'Bill To Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-bill-to_grid', @@ -1075,6 +1094,7 @@ 'original_name': 'Daily Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-usage', @@ -1129,6 +1149,7 @@ 'original_name': 'Daily From Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-from_grid', @@ -1183,6 +1204,7 @@ 'original_name': 'Daily Net Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-net_production', @@ -1235,6 +1257,7 @@ 'original_name': 'Daily Net Production Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-production_pct', @@ -1286,6 +1309,7 @@ 'original_name': 'Daily Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-production', @@ -1338,6 +1362,7 @@ 'original_name': 'Daily Solar Powered Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-solar_powered', @@ -1389,6 +1414,7 @@ 'original_name': 'Daily To Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-daily-to_grid', @@ -1443,6 +1469,7 @@ 'original_name': 'Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-active-usage', @@ -1496,6 +1523,7 @@ 'original_name': 'L1 Voltage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-L1', @@ -1549,6 +1577,7 @@ 'original_name': 'L2 Voltage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-L2', @@ -1602,6 +1631,7 @@ 'original_name': 'Monthly Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-usage', @@ -1656,6 +1686,7 @@ 'original_name': 'Monthly From Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-from_grid', @@ -1710,6 +1741,7 @@ 'original_name': 'Monthly Net Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-net_production', @@ -1762,6 +1794,7 @@ 'original_name': 'Monthly Net Production Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-production_pct', @@ -1813,6 +1846,7 @@ 'original_name': 'Monthly Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-production', @@ -1865,6 +1899,7 @@ 'original_name': 'Monthly Solar Powered Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-solar_powered', @@ -1916,6 +1951,7 @@ 'original_name': 'Monthly To Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-monthly-to_grid', @@ -1970,6 +2006,7 @@ 'original_name': 'Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-active-production', @@ -2023,6 +2060,7 @@ 'original_name': 'Weekly Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-usage', @@ -2077,6 +2115,7 @@ 'original_name': 'Weekly From Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-from_grid', @@ -2131,6 +2170,7 @@ 'original_name': 'Weekly Net Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-net_production', @@ -2183,6 +2223,7 @@ 'original_name': 'Weekly Net Production Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-production_pct', @@ -2234,6 +2275,7 @@ 'original_name': 'Weekly Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-production', @@ -2286,6 +2328,7 @@ 'original_name': 'Weekly Solar Powered Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-solar_powered', @@ -2337,6 +2380,7 @@ 'original_name': 'Weekly To Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-weekly-to_grid', @@ -2391,6 +2435,7 @@ 'original_name': 'Yearly Energy', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-usage', @@ -2445,6 +2490,7 @@ 'original_name': 'Yearly From Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-from_grid', @@ -2499,6 +2545,7 @@ 'original_name': 'Yearly Net Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-net_production', @@ -2551,6 +2598,7 @@ 'original_name': 'Yearly Net Production Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-production_pct', @@ -2602,6 +2650,7 @@ 'original_name': 'Yearly Production', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-production', @@ -2654,6 +2703,7 @@ 'original_name': 'Yearly Solar Powered Percentage', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-solar_powered', @@ -2705,6 +2755,7 @@ 'original_name': 'Yearly To Grid', 'platform': 'sense', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-yearly-to_grid', diff --git a/tests/components/sensibo/snapshots/test_binary_sensor.ambr b/tests/components/sensibo/snapshots/test_binary_sensor.ambr index 2e62c73acb4..fb12dce55ac 100644 --- a/tests/components/sensibo/snapshots/test_binary_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter clean required', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_clean', 'unique_id': 'BBZZBBZZ-filter_clean', @@ -75,6 +76,7 @@ 'original_name': 'Pure Boost linked with AC', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_ac_integration', 'unique_id': 'BBZZBBZZ-pure_ac_integration', @@ -123,6 +125,7 @@ 'original_name': 'Pure Boost linked with indoor air quality', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_measure_integration', 'unique_id': 'BBZZBBZZ-pure_measure_integration', @@ -171,6 +174,7 @@ 'original_name': 'Pure Boost linked with outdoor air quality', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_prime_integration', 'unique_id': 'BBZZBBZZ-pure_prime_integration', @@ -219,6 +223,7 @@ 'original_name': 'Pure Boost linked with presence', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_geo_integration', 'unique_id': 'BBZZBBZZ-pure_geo_integration', @@ -267,6 +272,7 @@ 'original_name': 'Filter clean required', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_clean', 'unique_id': 'ABC999111-filter_clean', @@ -315,6 +321,7 @@ 'original_name': 'Connectivity', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AABBCC-alive', @@ -363,6 +370,7 @@ 'original_name': 'Main sensor', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_main_sensor', 'unique_id': 'AABBCC-is_main_sensor', @@ -410,6 +418,7 @@ 'original_name': 'Motion', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AABBCC-motion', @@ -458,6 +467,7 @@ 'original_name': 'Room occupied', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'room_occupied', 'unique_id': 'ABC999111-room_occupied', @@ -506,6 +516,7 @@ 'original_name': 'Filter clean required', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_clean', 'unique_id': 'AAZZAAZZ-filter_clean', @@ -554,6 +565,7 @@ 'original_name': 'Pure Boost linked with AC', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_ac_integration', 'unique_id': 'AAZZAAZZ-pure_ac_integration', @@ -602,6 +614,7 @@ 'original_name': 'Pure Boost linked with indoor air quality', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_measure_integration', 'unique_id': 'AAZZAAZZ-pure_measure_integration', @@ -650,6 +663,7 @@ 'original_name': 'Pure Boost linked with outdoor air quality', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_prime_integration', 'unique_id': 'AAZZAAZZ-pure_prime_integration', @@ -698,6 +712,7 @@ 'original_name': 'Pure Boost linked with presence', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_geo_integration', 'unique_id': 'AAZZAAZZ-pure_geo_integration', diff --git a/tests/components/sensibo/snapshots/test_button.ambr b/tests/components/sensibo/snapshots/test_button.ambr index 6bfc4a5a44f..3632560b861 100644 --- a/tests/components/sensibo/snapshots/test_button.ambr +++ b/tests/components/sensibo/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reset filter', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter', 'unique_id': 'BBZZBBZZ-reset_filter', @@ -74,6 +75,7 @@ 'original_name': 'Reset filter', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter', 'unique_id': 'ABC999111-reset_filter', @@ -121,6 +123,7 @@ 'original_name': 'Reset filter', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filter', 'unique_id': 'AAZZAAZZ-reset_filter', diff --git a/tests/components/sensibo/snapshots/test_climate.ambr b/tests/components/sensibo/snapshots/test_climate.ambr index e3bd456ad23..fc6e6f64be8 100644 --- a/tests/components/sensibo/snapshots/test_climate.ambr +++ b/tests/components/sensibo/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_device', 'unique_id': 'BBZZBBZZ', @@ -116,6 +117,7 @@ 'original_name': None, 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_device', 'unique_id': 'ABC999111', @@ -208,6 +210,7 @@ 'original_name': None, 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_device', 'unique_id': 'AAZZAAZZ', diff --git a/tests/components/sensibo/snapshots/test_number.ambr b/tests/components/sensibo/snapshots/test_number.ambr index 458c7ca7183..e1556b3cdf8 100644 --- a/tests/components/sensibo/snapshots/test_number.ambr +++ b/tests/components/sensibo/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Humidity calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_humidity', 'unique_id': 'BBZZBBZZ-calibration_hum', @@ -90,6 +91,7 @@ 'original_name': 'Temperature calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_temperature', 'unique_id': 'BBZZBBZZ-calibration_temp', @@ -148,6 +150,7 @@ 'original_name': 'Humidity calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_humidity', 'unique_id': 'ABC999111-calibration_hum', @@ -206,6 +209,7 @@ 'original_name': 'Temperature calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_temperature', 'unique_id': 'ABC999111-calibration_temp', @@ -264,6 +268,7 @@ 'original_name': 'Humidity calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_humidity', 'unique_id': 'AAZZAAZZ-calibration_hum', @@ -322,6 +327,7 @@ 'original_name': 'Temperature calibration', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibration_temperature', 'unique_id': 'AAZZAAZZ-calibration_temp', diff --git a/tests/components/sensibo/snapshots/test_select.ambr b/tests/components/sensibo/snapshots/test_select.ambr index 05582a1ea16..2ac6eb445a5 100644 --- a/tests/components/sensibo/snapshots/test_select.ambr +++ b/tests/components/sensibo/snapshots/test_select.ambr @@ -32,6 +32,7 @@ 'original_name': 'Light', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'ABC999111-light', @@ -89,6 +90,7 @@ 'original_name': 'Light', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light', 'unique_id': 'AAZZAAZZ-light', diff --git a/tests/components/sensibo/snapshots/test_sensor.ambr b/tests/components/sensibo/snapshots/test_sensor.ambr index bfd5f2d3e9a..4d2c6b91ee2 100644 --- a/tests/components/sensibo/snapshots/test_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Filter last reset', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_last_reset', 'unique_id': 'BBZZBBZZ-filter_last_reset', @@ -81,6 +82,7 @@ 'original_name': 'Pure AQI', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pm25_pure', 'unique_id': 'BBZZBBZZ-pm25', @@ -134,6 +136,7 @@ 'original_name': 'Pure sensitivity', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensitivity', 'unique_id': 'BBZZBBZZ-pure_sensitivity', @@ -183,6 +186,7 @@ 'original_name': 'Climate React high temperature threshold', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_react_high', 'unique_id': 'ABC999111-climate_react_high', @@ -243,6 +247,7 @@ 'original_name': 'Climate React low temperature threshold', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_react_low', 'unique_id': 'ABC999111-climate_react_low', @@ -301,6 +306,7 @@ 'original_name': 'Climate React type', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_type', 'unique_id': 'ABC999111-climate_react_type', @@ -348,6 +354,7 @@ 'original_name': 'Filter last reset', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_last_reset', 'unique_id': 'ABC999111-filter_last_reset', @@ -398,6 +405,7 @@ 'original_name': 'Battery voltage', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'AABBCC-battery_voltage', @@ -450,6 +458,7 @@ 'original_name': 'Humidity', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AABBCC-humidity', @@ -502,6 +511,7 @@ 'original_name': 'RSSI', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rssi', 'unique_id': 'AABBCC-rssi', @@ -554,6 +564,7 @@ 'original_name': 'Temperature', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AABBCC-temperature', @@ -606,6 +617,7 @@ 'original_name': 'Temperature feels like', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feels_like', 'unique_id': 'ABC999111-feels_like', @@ -656,6 +668,7 @@ 'original_name': 'Timer end time', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'timer_time', 'unique_id': 'ABC999111-timer_time', @@ -706,6 +719,7 @@ 'original_name': 'Filter last reset', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_last_reset', 'unique_id': 'AAZZAAZZ-filter_last_reset', @@ -760,6 +774,7 @@ 'original_name': 'Pure AQI', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pm25_pure', 'unique_id': 'AAZZAAZZ-pm25', @@ -813,6 +828,7 @@ 'original_name': 'Pure sensitivity', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensitivity', 'unique_id': 'AAZZAAZZ-pure_sensitivity', diff --git a/tests/components/sensibo/snapshots/test_switch.ambr b/tests/components/sensibo/snapshots/test_switch.ambr index e0ea140eb37..f52f650ee7d 100644 --- a/tests/components/sensibo/snapshots/test_switch.ambr +++ b/tests/components/sensibo/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Pure Boost', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_boost_switch', 'unique_id': 'BBZZBBZZ-pure_boost_switch', @@ -75,6 +76,7 @@ 'original_name': 'Climate React', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_react_switch', 'unique_id': 'ABC999111-climate_react_switch', @@ -124,6 +126,7 @@ 'original_name': 'Timer', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'timer_on_switch', 'unique_id': 'ABC999111-timer_on_switch', @@ -174,6 +177,7 @@ 'original_name': 'Pure Boost', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pure_boost_switch', 'unique_id': 'AAZZAAZZ-pure_boost_switch', diff --git a/tests/components/sensibo/snapshots/test_update.ambr b/tests/components/sensibo/snapshots/test_update.ambr index c113d5615b1..b5e4b159264 100644 --- a/tests/components/sensibo/snapshots/test_update.ambr +++ b/tests/components/sensibo/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Firmware', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'BBZZBBZZ-fw_ver_available', @@ -87,6 +88,7 @@ 'original_name': 'Firmware', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ABC999111-fw_ver_available', @@ -147,6 +149,7 @@ 'original_name': 'Firmware', 'platform': 'sensibo', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AAZZAAZZ-fw_ver_available', diff --git a/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr b/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr index a78b012ac02..80256bfd2ec 100644 --- a/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr +++ b/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Altitude', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'altitude', 'unique_id': 'test-sensor-device-id-0_altitude', @@ -87,6 +88,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-0_atmospheric_pressure', @@ -139,6 +141,7 @@ 'original_name': 'Battery voltage', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'test-sensor-device-id-0_battery_voltage', @@ -191,6 +194,7 @@ 'original_name': 'Dew point', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dewpoint', 'unique_id': 'test-sensor-device-id-0_dewpoint', @@ -243,6 +247,7 @@ 'original_name': 'Humidity', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-0_humidity', @@ -295,6 +300,7 @@ 'original_name': 'Signal strength', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-0_signal_strength', @@ -347,6 +353,7 @@ 'original_name': 'Temperature', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-0_temperature', @@ -399,6 +406,7 @@ 'original_name': 'Vapor pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vapor_pressure', 'unique_id': 'test-sensor-device-id-0_vapor_pressure', @@ -454,6 +462,7 @@ 'original_name': 'Altitude', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'altitude', 'unique_id': 'test-sensor-device-id-1_altitude', @@ -509,6 +518,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-1_atmospheric_pressure', @@ -561,6 +571,7 @@ 'original_name': 'Battery voltage', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'test-sensor-device-id-1_battery_voltage', @@ -613,6 +624,7 @@ 'original_name': 'Dew point', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dewpoint', 'unique_id': 'test-sensor-device-id-1_dewpoint', @@ -665,6 +677,7 @@ 'original_name': 'Humidity', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-1_humidity', @@ -717,6 +730,7 @@ 'original_name': 'Signal strength', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-1_signal_strength', @@ -769,6 +783,7 @@ 'original_name': 'Temperature', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-1_temperature', @@ -821,6 +836,7 @@ 'original_name': 'Vapor pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vapor_pressure', 'unique_id': 'test-sensor-device-id-1_vapor_pressure', @@ -876,6 +892,7 @@ 'original_name': 'Altitude', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'altitude', 'unique_id': 'test-sensor-device-id-2_altitude', @@ -931,6 +948,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-2_atmospheric_pressure', @@ -983,6 +1001,7 @@ 'original_name': 'Battery voltage', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_voltage', 'unique_id': 'test-sensor-device-id-2_battery_voltage', @@ -1035,6 +1054,7 @@ 'original_name': 'Dew point', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dewpoint', 'unique_id': 'test-sensor-device-id-2_dewpoint', @@ -1087,6 +1107,7 @@ 'original_name': 'Humidity', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-2_humidity', @@ -1139,6 +1160,7 @@ 'original_name': 'Signal strength', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-2_signal_strength', @@ -1191,6 +1213,7 @@ 'original_name': 'Temperature', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'test-sensor-device-id-2_temperature', @@ -1243,6 +1266,7 @@ 'original_name': 'Vapor pressure', 'platform': 'sensorpush_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vapor_pressure', 'unique_id': 'test-sensor-device-id-2_vapor_pressure', diff --git a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr index 4718abc02b5..0ee34eebf3f 100644 --- a/tests/components/sfr_box/snapshots/test_binary_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_binary_sensor.ambr @@ -63,6 +63,7 @@ 'original_name': 'WAN status', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wan_status', 'unique_id': 'e4:5d:51:00:11:22_wan_status', @@ -95,6 +96,7 @@ 'original_name': 'DSL status', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_status', 'unique_id': 'e4:5d:51:00:11:22_dsl_status', @@ -194,6 +196,7 @@ 'original_name': 'WAN status', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wan_status', 'unique_id': 'e4:5d:51:00:11:22_wan_status', @@ -226,6 +229,7 @@ 'original_name': 'FTTH status', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ftth_status', 'unique_id': 'e4:5d:51:00:11:22_ftth_status', diff --git a/tests/components/sfr_box/snapshots/test_button.ambr b/tests/components/sfr_box/snapshots/test_button.ambr index 68a1e7f7227..39dd9e512ae 100644 --- a/tests/components/sfr_box/snapshots/test_button.ambr +++ b/tests/components/sfr_box/snapshots/test_button.ambr @@ -63,6 +63,7 @@ 'original_name': 'Restart', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_reboot', diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 3ad7395caad..4a179146457 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -70,6 +70,7 @@ 'original_name': 'Network infrastructure', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'net_infra', 'unique_id': 'e4:5d:51:00:11:22_system_net_infra', @@ -104,6 +105,7 @@ 'original_name': 'Voltage', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_alimvoltage', @@ -138,6 +140,7 @@ 'original_name': 'Temperature', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'e4:5d:51:00:11:22_system_temperature', @@ -178,6 +181,7 @@ 'original_name': 'WAN mode', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wan_mode', 'unique_id': 'e4:5d:51:00:11:22_wan_mode', @@ -210,6 +214,7 @@ 'original_name': 'DSL line mode', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_linemode', 'unique_id': 'e4:5d:51:00:11:22_dsl_linemode', @@ -242,6 +247,7 @@ 'original_name': 'DSL counter', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_counter', 'unique_id': 'e4:5d:51:00:11:22_dsl_counter', @@ -274,6 +280,7 @@ 'original_name': 'DSL CRC', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_crc', 'unique_id': 'e4:5d:51:00:11:22_dsl_crc', @@ -308,6 +315,7 @@ 'original_name': 'DSL noise down', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_noise_down', 'unique_id': 'e4:5d:51:00:11:22_dsl_noise_down', @@ -342,6 +350,7 @@ 'original_name': 'DSL noise up', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_noise_up', 'unique_id': 'e4:5d:51:00:11:22_dsl_noise_up', @@ -376,6 +385,7 @@ 'original_name': 'DSL attenuation down', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_attenuation_down', 'unique_id': 'e4:5d:51:00:11:22_dsl_attenuation_down', @@ -410,6 +420,7 @@ 'original_name': 'DSL attenuation up', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_attenuation_up', 'unique_id': 'e4:5d:51:00:11:22_dsl_attenuation_up', @@ -444,6 +455,7 @@ 'original_name': 'DSL rate down', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_rate_down', 'unique_id': 'e4:5d:51:00:11:22_dsl_rate_down', @@ -478,6 +490,7 @@ 'original_name': 'DSL rate up', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_rate_up', 'unique_id': 'e4:5d:51:00:11:22_dsl_rate_up', @@ -519,6 +532,7 @@ 'original_name': 'DSL line status', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_line_status', 'unique_id': 'e4:5d:51:00:11:22_dsl_line_status', @@ -564,6 +578,7 @@ 'original_name': 'DSL training', 'platform': 'sfr_box', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dsl_training', 'unique_id': 'e4:5d:51:00:11:22_dsl_training', diff --git a/tests/components/shelly/snapshots/test_binary_sensor.ambr b/tests/components/shelly/snapshots/test_binary_sensor.ambr index df8ed9cff4f..201f20c3de9 100644 --- a/tests/components/shelly/snapshots/test_binary_sensor.ambr +++ b/tests/components/shelly/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Calibration', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-blutrv:200-calibration', @@ -75,6 +76,7 @@ 'original_name': 'Kitchen flood', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-flood:0-flood', @@ -123,6 +125,7 @@ 'original_name': 'Kitchen mute', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-flood:0-mute', diff --git a/tests/components/shelly/snapshots/test_button.ambr b/tests/components/shelly/snapshots/test_button.ambr index 33410ec2bbf..09c2c5f3d8d 100644 --- a/tests/components/shelly/snapshots/test_button.ambr +++ b/tests/components/shelly/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Calibrate', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibrate', 'unique_id': 'f8:44:77:25:f0:dd_calibrate', @@ -74,6 +75,7 @@ 'original_name': 'Reboot', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC_reboot', diff --git a/tests/components/shelly/snapshots/test_climate.ambr b/tests/components/shelly/snapshots/test_climate.ambr index a434e1d8a9b..35746dd5c08 100644 --- a/tests/components/shelly/snapshots/test_climate.ambr +++ b/tests/components/shelly/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'f8:44:77:25:f0:dd-blutrv:200', @@ -104,6 +105,7 @@ 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABC-sensor_0', @@ -176,6 +178,7 @@ 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABC-thermostat:0', @@ -243,6 +246,7 @@ 'original_name': None, 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABC-thermostat:0', diff --git a/tests/components/shelly/snapshots/test_event.ambr b/tests/components/shelly/snapshots/test_event.ambr index ae719774aee..b87436ba4aa 100644 --- a/tests/components/shelly/snapshots/test_event.ambr +++ b/tests/components/shelly/snapshots/test_event.ambr @@ -32,6 +32,7 @@ 'original_name': 'test_script.js', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'script', 'unique_id': '123456789ABC-script:1', diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr index d715b342e79..138a0148ecb 100644 --- a/tests/components/shelly/snapshots/test_number.ambr +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'External temperature', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'external_temperature', 'unique_id': '123456789ABC-blutrv:200-external_temperature', @@ -89,6 +90,7 @@ 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve_position', 'unique_id': '123456789ABC-blutrv:200-valve_position', diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index 6fd0bd716b7..4b12dddae62 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-blutrv:200-blutrv_battery', @@ -81,6 +82,7 @@ 'original_name': 'Signal strength', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-blutrv:200-blutrv_rssi', @@ -133,6 +135,7 @@ 'original_name': 'Valve position', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'valve_position', 'unique_id': '123456789ABC-blutrv:200-valve_position', @@ -190,6 +193,7 @@ 'original_name': 'test switch_0 energy', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-switch:0-energy', @@ -248,6 +252,7 @@ 'original_name': 'test switch_0 returned energy', 'platform': 'shelly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABC-switch:0-ret_energy', diff --git a/tests/components/simplefin/snapshots/test_binary_sensor.ambr b/tests/components/simplefin/snapshots/test_binary_sensor.ambr index 3123100205e..6602e6e35a9 100644 --- a/tests/components/simplefin/snapshots/test_binary_sensor.ambr +++ b/tests/components/simplefin/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-4k5l6m7n-8o9p-1q2r-3s4t_possible_error', @@ -76,6 +77,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-1k2l3m4n-5o6p-7q8r-9s0t_possible_error', @@ -125,6 +127,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-5k6l7m8n-9o0p-1q2r-3s4t_possible_error', @@ -174,6 +177,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-7a8b9c0d-1e2f-3g4h-5i6j_possible_error', @@ -223,6 +227,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-6a7b8c9d-0e1f-2g3h-4i5j_possible_error', @@ -272,6 +277,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-3a4b5c6d-7e8f-9g0h-1i2j_possible_error', @@ -321,6 +327,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-2a3b4c5d-6e7f-8g9h-0i1j_possible_error', @@ -370,6 +377,7 @@ 'original_name': 'Possible error', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'possible_error', 'unique_id': 'account_ACT-1a2b3c4d-5e6f-7g8h-9i0j_possible_error', diff --git a/tests/components/simplefin/snapshots/test_sensor.ambr b/tests/components/simplefin/snapshots/test_sensor.ambr index dd305f7528f..7f3e8d342fb 100644 --- a/tests/components/simplefin/snapshots/test_sensor.ambr +++ b/tests/components/simplefin/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-4k5l6m7n-8o9p-1q2r-3s4t_balance', @@ -81,6 +82,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-4k5l6m7n-8o9p-1q2r-3s4t_age', @@ -132,6 +134,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-1k2l3m4n-5o6p-7q8r-9s0t_balance', @@ -184,6 +187,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-1k2l3m4n-5o6p-7q8r-9s0t_age', @@ -235,6 +239,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-5k6l7m8n-9o0p-1q2r-3s4t_balance', @@ -287,6 +292,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-5k6l7m8n-9o0p-1q2r-3s4t_age', @@ -338,6 +344,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-7a8b9c0d-1e2f-3g4h-5i6j_balance', @@ -390,6 +397,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-7a8b9c0d-1e2f-3g4h-5i6j_age', @@ -441,6 +449,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-6a7b8c9d-0e1f-2g3h-4i5j_balance', @@ -493,6 +502,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-6a7b8c9d-0e1f-2g3h-4i5j_age', @@ -544,6 +554,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-3a4b5c6d-7e8f-9g0h-1i2j_balance', @@ -596,6 +607,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-3a4b5c6d-7e8f-9g0h-1i2j_age', @@ -647,6 +659,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-2a3b4c5d-6e7f-8g9h-0i1j_balance', @@ -699,6 +712,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-2a3b4c5d-6e7f-8g9h-0i1j_age', @@ -750,6 +764,7 @@ 'original_name': 'Balance', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'balance', 'unique_id': 'account_ACT-1a2b3c4d-5e6f-7g8h-9i0j_balance', @@ -802,6 +817,7 @@ 'original_name': 'Data age', 'platform': 'simplefin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'age', 'unique_id': 'account_ACT-1a2b3c4d-5e6f-7g8h-9i0j_age', diff --git a/tests/components/slide_local/snapshots/test_button.ambr b/tests/components/slide_local/snapshots/test_button.ambr index 7b363f4d9ba..9ab1ff9623d 100644 --- a/tests/components/slide_local/snapshots/test_button.ambr +++ b/tests/components/slide_local/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Calibrate', 'platform': 'slide_local', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calibrate', 'unique_id': '1234567890ab-calibrate', diff --git a/tests/components/slide_local/snapshots/test_cover.ambr b/tests/components/slide_local/snapshots/test_cover.ambr index 172f5411a94..09d182a4bb6 100644 --- a/tests/components/slide_local/snapshots/test_cover.ambr +++ b/tests/components/slide_local/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'slide_local', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1234567890ab', diff --git a/tests/components/slide_local/snapshots/test_switch.ambr b/tests/components/slide_local/snapshots/test_switch.ambr index 9b1a7969539..ddfe7151f44 100644 --- a/tests/components/slide_local/snapshots/test_switch.ambr +++ b/tests/components/slide_local/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'TouchGo', 'platform': 'slide_local', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'touchgo', 'unique_id': '1234567890ab-touchgo', diff --git a/tests/components/sma/snapshots/test_sensor.ambr b/tests/components/sma/snapshots/test_sensor.ambr index 8911df46169..9d9d876c98e 100644 --- a/tests/components/sma/snapshots/test_sensor.ambr +++ b/tests/components/sma/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'SMA Device Name Battery Capacity A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499100_0', @@ -75,6 +76,7 @@ 'original_name': 'SMA Device Name Battery Capacity B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499100_1', @@ -123,6 +125,7 @@ 'original_name': 'SMA Device Name Battery Capacity C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499100_2', @@ -171,6 +174,7 @@ 'original_name': 'SMA Device Name Battery Capacity Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00696E00_0', @@ -221,6 +225,7 @@ 'original_name': 'SMA Device Name Battery Charge A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499500_0', @@ -273,6 +278,7 @@ 'original_name': 'SMA Device Name Battery Charge B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499500_1', @@ -325,6 +331,7 @@ 'original_name': 'SMA Device Name Battery Charge C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499500_2', @@ -377,6 +384,7 @@ 'original_name': 'SMA Device Name Battery Charge Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00496700_0', @@ -429,6 +437,7 @@ 'original_name': 'SMA Device Name Battery Charging Voltage A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6102_00493500_0', @@ -481,6 +490,7 @@ 'original_name': 'SMA Device Name Battery Charging Voltage B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6102_00493500_1', @@ -533,6 +543,7 @@ 'original_name': 'SMA Device Name Battery Charging Voltage C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6102_00493500_2', @@ -585,6 +596,7 @@ 'original_name': 'SMA Device Name Battery Current A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495D00_0', @@ -637,6 +649,7 @@ 'original_name': 'SMA Device Name Battery Current B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495D00_1', @@ -689,6 +702,7 @@ 'original_name': 'SMA Device Name Battery Current C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495D00_2', @@ -741,6 +755,7 @@ 'original_name': 'SMA Device Name Battery Discharge A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499600_0', @@ -793,6 +808,7 @@ 'original_name': 'SMA Device Name Battery Discharge B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499600_1', @@ -845,6 +861,7 @@ 'original_name': 'SMA Device Name Battery Discharge C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00499600_2', @@ -897,6 +914,7 @@ 'original_name': 'SMA Device Name Battery Discharge Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00496800_0', @@ -949,6 +967,7 @@ 'original_name': 'SMA Device Name Battery Power Charge A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499300_0', @@ -1001,6 +1020,7 @@ 'original_name': 'SMA Device Name Battery Power Charge B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499300_1', @@ -1053,6 +1073,7 @@ 'original_name': 'SMA Device Name Battery Power Charge C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499300_2', @@ -1105,6 +1126,7 @@ 'original_name': 'SMA Device Name Battery Power Charge Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00496900_0', @@ -1157,6 +1179,7 @@ 'original_name': 'SMA Device Name Battery Power Discharge A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499400_0', @@ -1209,6 +1232,7 @@ 'original_name': 'SMA Device Name Battery Power Discharge B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499400_1', @@ -1261,6 +1285,7 @@ 'original_name': 'SMA Device Name Battery Power Discharge C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00499400_2', @@ -1313,6 +1338,7 @@ 'original_name': 'SMA Device Name Battery Power Discharge Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00496A00_0', @@ -1365,6 +1391,7 @@ 'original_name': 'SMA Device Name Battery SOC A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00498F00_0', @@ -1417,6 +1444,7 @@ 'original_name': 'SMA Device Name Battery SOC B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00498F00_1', @@ -1469,6 +1497,7 @@ 'original_name': 'SMA Device Name Battery SOC C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00498F00_2', @@ -1521,6 +1550,7 @@ 'original_name': 'SMA Device Name Battery SOC Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00295A00_0', @@ -1571,6 +1601,7 @@ 'original_name': 'SMA Device Name Battery Status Operating Mode', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08495E00_0', @@ -1620,6 +1651,7 @@ 'original_name': 'SMA Device Name Battery Temp A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495B00_0', @@ -1672,6 +1704,7 @@ 'original_name': 'SMA Device Name Battery Temp B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495B00_1', @@ -1724,6 +1757,7 @@ 'original_name': 'SMA Device Name Battery Temp C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40495B00_2', @@ -1776,6 +1810,7 @@ 'original_name': 'SMA Device Name Battery Voltage A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00495C00_0', @@ -1828,6 +1863,7 @@ 'original_name': 'SMA Device Name Battery Voltage B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00495C00_1', @@ -1880,6 +1916,7 @@ 'original_name': 'SMA Device Name Battery Voltage C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00495C00_2', @@ -1932,6 +1969,7 @@ 'original_name': 'SMA Device Name Current L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40465300_0', @@ -1984,6 +2022,7 @@ 'original_name': 'SMA Device Name Current L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40465400_0', @@ -2036,6 +2075,7 @@ 'original_name': 'SMA Device Name Current L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40465500_0', @@ -2088,6 +2128,7 @@ 'original_name': 'SMA Device Name Current Total', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00664F00_0', @@ -2140,6 +2181,7 @@ 'original_name': 'SMA Device Name Daily Yield', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00262200_0', @@ -2192,6 +2234,7 @@ 'original_name': 'SMA Device Name Frequency', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00465700_0', @@ -2244,6 +2287,7 @@ 'original_name': 'SMA Device Name Grid Apparent Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666700_0', @@ -2296,6 +2340,7 @@ 'original_name': 'SMA Device Name Grid Apparent Power L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666800_0', @@ -2348,6 +2393,7 @@ 'original_name': 'SMA Device Name Grid Apparent Power L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666900_0', @@ -2400,6 +2446,7 @@ 'original_name': 'SMA Device Name Grid Apparent Power L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666A00_0', @@ -2450,6 +2497,7 @@ 'original_name': 'SMA Device Name Grid Connection Status', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_0846A700_0', @@ -2499,6 +2547,7 @@ 'original_name': 'SMA Device Name Grid Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40263F00_0', @@ -2551,6 +2600,7 @@ 'original_name': 'SMA Device Name Grid Power Factor', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00665900_0', @@ -2600,6 +2650,7 @@ 'original_name': 'SMA Device Name Grid Power Factor Excitation', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08465A00_0', @@ -2649,6 +2700,7 @@ 'original_name': 'SMA Device Name Grid Reactive Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40265F00_0', @@ -2701,6 +2753,7 @@ 'original_name': 'SMA Device Name Grid Reactive Power L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666000_0', @@ -2753,6 +2806,7 @@ 'original_name': 'SMA Device Name Grid Reactive Power L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666100_0', @@ -2805,6 +2859,7 @@ 'original_name': 'SMA Device Name Grid Reactive Power L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40666200_0', @@ -2855,6 +2910,7 @@ 'original_name': 'SMA Device Name Grid Relay Status', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08416400_0', @@ -2904,6 +2960,7 @@ 'original_name': 'SMA Device Name Insulation Residual Current', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6102_40254E00_0', @@ -2954,6 +3011,7 @@ 'original_name': 'SMA Device Name Inverter Condition', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08414C00_0', @@ -3003,6 +3061,7 @@ 'original_name': 'SMA Device Name Inverter Power Limit', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6800_00832A00_0', @@ -3053,6 +3112,7 @@ 'original_name': 'SMA Device Name Inverter System Init', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6800_08811F00_0', @@ -3102,6 +3162,7 @@ 'original_name': 'SMA Device Name Metering Active Power Draw L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046EB00_0', @@ -3154,6 +3215,7 @@ 'original_name': 'SMA Device Name Metering Active Power Draw L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046EC00_0', @@ -3206,6 +3268,7 @@ 'original_name': 'SMA Device Name Metering Active Power Draw L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046ED00_0', @@ -3258,6 +3321,7 @@ 'original_name': 'SMA Device Name Metering Active Power Feed L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046E800_0', @@ -3310,6 +3374,7 @@ 'original_name': 'SMA Device Name Metering Active Power Feed L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046E900_0', @@ -3362,6 +3427,7 @@ 'original_name': 'SMA Device Name Metering Active Power Feed L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046EA00_0', @@ -3414,6 +3480,7 @@ 'original_name': 'SMA Device Name Metering Current Consumption', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00543100_0', @@ -3466,6 +3533,7 @@ 'original_name': 'SMA Device Name Metering Current L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40466500_0', @@ -3518,6 +3586,7 @@ 'original_name': 'SMA Device Name Metering Current L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40466600_0', @@ -3570,6 +3639,7 @@ 'original_name': 'SMA Device Name Metering Current L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40466B00_0', @@ -3622,6 +3692,7 @@ 'original_name': 'SMA Device Name Metering Frequency', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00468100_0', @@ -3674,6 +3745,7 @@ 'original_name': 'SMA Device Name Metering Power Absorbed', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40463700_0', @@ -3726,6 +3798,7 @@ 'original_name': 'SMA Device Name Metering Power Supplied', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40463600_0', @@ -3778,6 +3851,7 @@ 'original_name': 'SMA Device Name Metering Total Absorbed', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00462500_0', @@ -3830,6 +3904,7 @@ 'original_name': 'SMA Device Name Metering Total Consumption', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00543A00_0', @@ -3882,6 +3957,7 @@ 'original_name': 'SMA Device Name Metering Total Yield', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00462400_0', @@ -3934,6 +4010,7 @@ 'original_name': 'SMA Device Name Metering Voltage L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046E500_0', @@ -3986,6 +4063,7 @@ 'original_name': 'SMA Device Name Metering Voltage L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046E600_0', @@ -4038,6 +4116,7 @@ 'original_name': 'SMA Device Name Metering Voltage L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046E700_0', @@ -4088,6 +4167,7 @@ 'original_name': 'SMA Device Name Operating Status', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08412B00_0', @@ -4135,6 +4215,7 @@ 'original_name': 'SMA Device Name Operating Status General', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08412800_0', @@ -4184,6 +4265,7 @@ 'original_name': 'SMA Device Name Optimizer Current', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40652900_0', @@ -4236,6 +4318,7 @@ 'original_name': 'SMA Device Name Optimizer Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40652A00_0', @@ -4288,6 +4371,7 @@ 'original_name': 'SMA Device Name Optimizer Temp', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40652B00_0', @@ -4340,6 +4424,7 @@ 'original_name': 'SMA Device Name Optimizer Voltage', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40652800_0', @@ -4392,6 +4477,7 @@ 'original_name': 'SMA Device Name Power L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40464000_0', @@ -4444,6 +4530,7 @@ 'original_name': 'SMA Device Name Power L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40464100_0', @@ -4496,6 +4583,7 @@ 'original_name': 'SMA Device Name Power L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_40464200_0', @@ -4548,6 +4636,7 @@ 'original_name': 'SMA Device Name PV Current A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40452100_0', @@ -4600,6 +4689,7 @@ 'original_name': 'SMA Device Name PV Current B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40452100_1', @@ -4652,6 +4742,7 @@ 'original_name': 'SMA Device Name PV Current C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40452100_2', @@ -4704,6 +4795,7 @@ 'original_name': 'SMA Device Name PV Gen Meter', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_0046C300_0', @@ -4756,6 +4848,7 @@ 'original_name': 'SMA Device Name PV Isolation Resistance', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6102_00254F00_0', @@ -4807,6 +4900,7 @@ 'original_name': 'SMA Device Name PV Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046C200_0', @@ -4859,6 +4953,7 @@ 'original_name': 'SMA Device Name PV Power A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40251E00_0', @@ -4911,6 +5006,7 @@ 'original_name': 'SMA Device Name PV Power B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40251E00_1', @@ -4963,6 +5059,7 @@ 'original_name': 'SMA Device Name PV Power C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40251E00_2', @@ -5015,6 +5112,7 @@ 'original_name': 'SMA Device Name PV Voltage A', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40451F00_0', @@ -5067,6 +5165,7 @@ 'original_name': 'SMA Device Name PV Voltage B', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40451F00_1', @@ -5119,6 +5218,7 @@ 'original_name': 'SMA Device Name PV Voltage C', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6380_40451F00_2', @@ -5171,6 +5271,7 @@ 'original_name': 'SMA Device Name Secure Power Supply Current', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046C700_0', @@ -5223,6 +5324,7 @@ 'original_name': 'SMA Device Name Secure Power Supply Power', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046C800_0', @@ -5275,6 +5377,7 @@ 'original_name': 'SMA Device Name Secure Power Supply Voltage', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_0046C600_0', @@ -5325,6 +5428,7 @@ 'original_name': 'SMA Device Name Status', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6180_08214800_0', @@ -5374,6 +5478,7 @@ 'original_name': 'SMA Device Name Total Yield', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6400_00260100_0', @@ -5426,6 +5531,7 @@ 'original_name': 'SMA Device Name Voltage L1', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00464800_0', @@ -5478,6 +5584,7 @@ 'original_name': 'SMA Device Name Voltage L2', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00464900_0', @@ -5530,6 +5637,7 @@ 'original_name': 'SMA Device Name Voltage L3', 'platform': 'sma', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789-6100_00464A00_0', diff --git a/tests/components/smarla/snapshots/test_switch.ambr b/tests/components/smarla/snapshots/test_switch.ambr index bd713c209c1..f73981b55ea 100644 --- a/tests/components/smarla/snapshots/test_switch.ambr +++ b/tests/components/smarla/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'smarla', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ABCD-swing_active', @@ -74,6 +75,7 @@ 'original_name': 'Smart Mode', 'platform': 'smarla', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smart_mode', 'unique_id': 'ABCD-smart_mode', diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 4f6d0d6d634..40784adcec6 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Motion', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '10e06a70-ee7d-4832-85e9-a0a06a7a05bd_main_motionSensor_motion_motion', @@ -75,6 +76,7 @@ 'original_name': 'Sound', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '10e06a70-ee7d-4832-85e9-a0a06a7a05bd_main_soundSensor_sound_sound', @@ -123,6 +125,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2d9a892b-1c93-45a5-84cb-0e81889498c6_main_contactSensor_contact_contact', @@ -171,6 +174,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_main_switch_switch_switch', @@ -219,6 +223,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_samsungce.kidsLock_lockState_lockState', @@ -266,6 +271,7 @@ 'original_name': 'Door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_samsungce.doorState_doorState_doorState', @@ -314,6 +320,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_switch_switch_switch', @@ -362,6 +369,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -409,6 +417,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_samsungce.kidsLock_lockState_lockState', @@ -456,6 +465,7 @@ 'original_name': 'Door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_samsungce.doorState_doorState_doorState', @@ -504,6 +514,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -551,6 +562,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_samsungce.kidsLock_lockState_lockState', @@ -598,6 +610,7 @@ 'original_name': 'Door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'door', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_samsungce.doorState_doorState_doorState', @@ -646,6 +659,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -693,6 +707,7 @@ 'original_name': 'Freezer door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_door', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_freezer_contactSensor_contact_contact', @@ -741,6 +756,7 @@ 'original_name': 'Fridge door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_door', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_contactSensor_contact_contact', @@ -789,6 +805,7 @@ 'original_name': 'CoolSelect+ door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cool_select_plus_door', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cvroom_contactSensor_contact_contact', @@ -837,6 +854,7 @@ 'original_name': 'Freezer door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_door', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_contactSensor_contact_contact', @@ -885,6 +903,7 @@ 'original_name': 'Fridge door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_door', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_contactSensor_contact_contact', @@ -933,6 +952,7 @@ 'original_name': 'Freezer door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_door', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_contactSensor_contact_contact', @@ -981,6 +1001,7 @@ 'original_name': 'Fridge door', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_door', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_contactSensor_contact_contact', @@ -1029,6 +1050,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_samsungce.kidsLock_lockState_lockState', @@ -1076,6 +1098,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_switch_switch_switch', @@ -1124,6 +1147,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -1171,6 +1195,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.kidsLock_lockState_lockState', @@ -1218,6 +1243,7 @@ 'original_name': 'Keep fresh mode active', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'keep_fresh_mode_active', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetKeepFreshMode_operatingState_operatingState', @@ -1265,6 +1291,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_switch_switch_switch', @@ -1313,6 +1340,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -1360,6 +1388,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_samsungce.kidsLock_lockState_lockState', @@ -1407,6 +1436,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_switch_switch_switch', @@ -1455,6 +1485,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -1502,6 +1533,7 @@ 'original_name': 'Wrinkle prevent active', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_wrinkle_prevent_active', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_custom.dryerWrinklePrevent_operatingState_operatingState', @@ -1549,6 +1581,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_samsungce.kidsLock_lockState_lockState', @@ -1596,6 +1629,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_switch_switch_switch', @@ -1644,6 +1678,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -1691,6 +1726,7 @@ 'original_name': 'Wrinkle prevent active', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_wrinkle_prevent_active', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_custom.dryerWrinklePrevent_operatingState_operatingState', @@ -1738,6 +1774,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_samsungce.kidsLock_lockState_lockState', @@ -1785,6 +1822,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_switch_switch_switch', @@ -1833,6 +1871,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -1880,6 +1919,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_samsungce.kidsLock_lockState_lockState', @@ -1927,6 +1967,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_switch_switch_switch', @@ -1975,6 +2016,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -2022,6 +2064,7 @@ 'original_name': 'Child lock', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_samsungce.kidsLock_lockState_lockState', @@ -2069,6 +2112,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_switch_switch_switch', @@ -2117,6 +2161,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -2164,6 +2209,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_switch_switch_switch', @@ -2212,6 +2258,7 @@ 'original_name': 'Remote control', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_control', 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_remoteControlStatus_remoteControlEnabled_remoteControlEnabled', @@ -2259,6 +2306,7 @@ 'original_name': 'Motion', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'd5dc3299-c266-41c7-bd08-f540aea54b89_main_motionSensor_motion_motion', @@ -2307,6 +2355,7 @@ 'original_name': 'Presence', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'd5dc3299-c266-41c7-bd08-f540aea54b89_main_presenceSensor_presence_presence', @@ -2355,6 +2404,7 @@ 'original_name': 'Presence', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '184c67cc-69e2-44b6-8f73-55c963068ad9_main_presenceSensor_presence_presence', @@ -2403,6 +2453,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_contactSensor_contact_contact', @@ -2451,6 +2502,7 @@ 'original_name': 'Acceleration', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'acceleration', 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_accelerationSensor_acceleration_acceleration', @@ -2499,6 +2551,7 @@ 'original_name': 'Moisture', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a2a6018b-2663-4727-9d1d-8f56953b5116_main_waterSensor_water_water', diff --git a/tests/components/smartthings/snapshots/test_button.ambr b/tests/components/smartthings/snapshots/test_button.ambr index 4a7c582f608..ad8e0ff276b 100644 --- a/tests/components/smartthings/snapshots/test_button.ambr +++ b/tests/components/smartthings/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Stop', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_stop', @@ -74,6 +75,7 @@ 'original_name': 'Stop', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenOperatingState_stop', @@ -121,6 +123,7 @@ 'original_name': 'Stop', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'stop', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenOperatingState_stop', @@ -168,6 +171,7 @@ 'original_name': 'Reset water filter', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_water_filter', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_custom.waterFilter_resetWaterFilter', @@ -215,6 +219,7 @@ 'original_name': 'Reset water filter', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_water_filter', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_resetWaterFilter', diff --git a/tests/components/smartthings/snapshots/test_climate.ambr b/tests/components/smartthings/snapshots/test_climate.ambr index a478605a3b1..6280bcf6770 100644 --- a/tests/components/smartthings/snapshots/test_climate.ambr +++ b/tests/components/smartthings/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'bf53a150-f8a4-45d1-aac4-86252475d551_main', @@ -98,6 +99,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '286ba274-4093-4bcb-849c-a1a3efe7b1e5_main', @@ -165,6 +167,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_INDOOR1', @@ -245,6 +248,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main', @@ -349,6 +353,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main', @@ -456,6 +461,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main', @@ -556,6 +562,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main', @@ -632,6 +639,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_INDOOR', @@ -699,6 +707,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_INDOOR', @@ -766,6 +775,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_INDOOR1', @@ -832,6 +842,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_INDOOR2', @@ -901,6 +912,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '028469cb-6e89-4f14-8d9a-bfbca5e0fbfc_main', @@ -973,6 +985,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1888b38f-6246-4f1e-911b-bfcfb66999db_main', @@ -1036,6 +1049,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a_main', @@ -1099,6 +1113,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '69a271f6-6537-4982-8cd9-979866872692_main', @@ -1173,6 +1188,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main', @@ -1251,6 +1267,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '2894dc93-0f11-49cc-8a81-3a684cebebf6_main', diff --git a/tests/components/smartthings/snapshots/test_cover.ambr b/tests/components/smartthings/snapshots/test_cover.ambr index 4b5cf705665..ff34a2a1fea 100644 --- a/tests/components/smartthings/snapshots/test_cover.ambr +++ b/tests/components/smartthings/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '571af102-15db-4030-b76b-245a691f74a5_main', @@ -77,6 +78,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '71afed1c-006d-4e48-b16e-e7f88f9fd638_main', diff --git a/tests/components/smartthings/snapshots/test_event.ambr b/tests/components/smartthings/snapshots/test_event.ambr index 79c57df5fd7..ef074b24ce5 100644 --- a/tests/components/smartthings/snapshots/test_event.ambr +++ b/tests/components/smartthings/snapshots/test_event.ambr @@ -33,6 +33,7 @@ 'original_name': 'button1', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button1_button', @@ -93,6 +94,7 @@ 'original_name': 'button2', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button2_button', @@ -153,6 +155,7 @@ 'original_name': 'button3', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button3_button', @@ -213,6 +216,7 @@ 'original_name': 'button4', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button4_button', @@ -273,6 +277,7 @@ 'original_name': 'button5', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button5_button', @@ -333,6 +338,7 @@ 'original_name': 'button6', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'button', 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_button6_button', diff --git a/tests/components/smartthings/snapshots/test_fan.ambr b/tests/components/smartthings/snapshots/test_fan.ambr index 1196118b3b5..10710c88617 100644 --- a/tests/components/smartthings/snapshots/test_fan.ambr +++ b/tests/components/smartthings/snapshots/test_fan.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'f1af21a2-d5a1-437c-b10a-b34a87394b71_main', @@ -95,6 +96,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '6d95a8b7-4ee3-429a-a13a-00ec9354170c_main', diff --git a/tests/components/smartthings/snapshots/test_light.ambr b/tests/components/smartthings/snapshots/test_light.ambr index 6826a555f6a..c54b40ffab9 100644 --- a/tests/components/smartthings/snapshots/test_light.ambr +++ b/tests/components/smartthings/snapshots/test_light.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '7c16163e-c94e-482f-95f6-139ae0cd9d5e_main', @@ -101,6 +102,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'd0268a69-abfb-4c92-a646-61cec2e510ad_main', @@ -158,6 +160,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'aaedaf28-2ae0-4c1d-b57e-87f6a420c298_main', @@ -219,6 +222,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '440063de-a200-40b5-8a6b-f3399eaa0370_main', @@ -300,6 +304,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'cb958955-b015-498c-9e62-fc0c51abd054_main', diff --git a/tests/components/smartthings/snapshots/test_lock.ambr b/tests/components/smartthings/snapshots/test_lock.ambr index 325ce0cc677..c2cdf9c6375 100644 --- a/tests/components/smartthings/snapshots/test_lock.ambr +++ b/tests/components/smartthings/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a9f587c5-5d8b-4273-8907-e7f609af5158_main', diff --git a/tests/components/smartthings/snapshots/test_media_player.ambr b/tests/components/smartthings/snapshots/test_media_player.ambr index 8eca654abe3..9b7bcba70fb 100644 --- a/tests/components/smartthings/snapshots/test_media_player.ambr +++ b/tests/components/smartthings/snapshots/test_media_player.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main', @@ -97,6 +98,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main', @@ -151,6 +153,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'c85fced9-c474-4a47-93c2-037cc7829536_main', @@ -205,6 +208,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main', @@ -260,6 +264,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6_main', @@ -316,6 +321,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main', diff --git a/tests/components/smartthings/snapshots/test_number.ambr b/tests/components/smartthings/snapshots/test_number.ambr index 37af2200899..e02b2ecc9b4 100644 --- a/tests/components/smartthings/snapshots/test_number.ambr +++ b/tests/components/smartthings/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Fan speed', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hood_fan_speed', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_hood_samsungce.hoodFanSpeed_hoodFanSpeed_hoodFanSpeed', @@ -88,6 +89,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -146,6 +148,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -204,6 +207,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -262,6 +266,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -320,6 +325,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -378,6 +384,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -436,6 +443,7 @@ 'original_name': 'Rinse cycles', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_rinse_cycles', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_custom.washerRinseCycles_washerRinseCycles_washerRinseCycles', @@ -493,6 +501,7 @@ 'original_name': 'Rinse cycles', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_rinse_cycles', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_custom.washerRinseCycles_washerRinseCycles_washerRinseCycles', @@ -550,6 +559,7 @@ 'original_name': 'Rinse cycles', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_rinse_cycles', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_custom.washerRinseCycles_washerRinseCycles_washerRinseCycles', diff --git a/tests/components/smartthings/snapshots/test_scene.ambr b/tests/components/smartthings/snapshots/test_scene.ambr index fd9abc9fcca..e7b2ac7b9f9 100644 --- a/tests/components/smartthings/snapshots/test_scene.ambr +++ b/tests/components/smartthings/snapshots/test_scene.ambr @@ -27,6 +27,7 @@ 'original_name': 'Away', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '743b0f37-89b8-476c-aedf-eea8ad8cd29d', @@ -77,6 +78,7 @@ 'original_name': 'Home', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f3341e8b-9b32-4509-af2e-4f7c952e98ba', diff --git a/tests/components/smartthings/snapshots/test_select.ambr b/tests/components/smartthings/snapshots/test_select.ambr index 0ef12a3fe90..7dd57e89c6a 100644 --- a/tests/components/smartthings/snapshots/test_select.ambr +++ b/tests/components/smartthings/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Lamp', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lamp', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_hood_samsungce.lamp_brightnessLevel_brightnessLevel', @@ -90,6 +91,7 @@ 'original_name': 'Lamp', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lamp', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_samsungce.lamp_brightnessLevel_brightnessLevel', @@ -146,6 +148,7 @@ 'original_name': 'Lamp', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lamp', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_samsungce.lamp_brightnessLevel_brightnessLevel', @@ -203,6 +206,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState', @@ -261,6 +265,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_dryerOperatingState_machineState_machineState', @@ -319,6 +324,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_machineState_machineState', @@ -377,6 +383,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_dryerOperatingState_machineState_machineState', @@ -435,6 +442,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_machineState_machineState', @@ -496,6 +504,7 @@ 'original_name': 'Soil level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'soil_level', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_custom.washerSoilLevel_washerSoilLevel_washerSoilLevel', @@ -560,6 +569,7 @@ 'original_name': 'Spin level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spin_level', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', @@ -621,6 +631,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_washerOperatingState_machineState_machineState', @@ -683,6 +694,7 @@ 'original_name': 'Spin level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spin_level', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', @@ -745,6 +757,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_washerOperatingState_machineState_machineState', @@ -804,6 +817,7 @@ 'original_name': 'Detergent dispense amount', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'detergent_amount', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_samsungce.autoDispenseDetergent_amount_amount', @@ -864,6 +878,7 @@ 'original_name': 'Flexible compartment dispense amount', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flexible_detergent_amount', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_samsungce.flexibleAutoDispenseDetergent_amount_amount', @@ -927,6 +942,7 @@ 'original_name': 'Spin level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spin_level', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_custom.washerSpinLevel_washerSpinLevel_washerSpinLevel', @@ -989,6 +1005,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operating_state', 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 8b3e91ee263..a0ea94901cb 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f0af21a2-d5a1-437c-b10a-b34a87394b71_main_energyMeter_energy_energy', @@ -81,6 +82,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f0af21a2-d5a1-437c-b10a-b34a87394b71_main_powerMeter_power_power', @@ -133,6 +135,7 @@ 'original_name': 'Voltage', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f0af21a2-d5a1-437c-b10a-b34a87394b71_main_voltageMeasurement_voltage_voltage', @@ -184,6 +187,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'bf53a150-f8a4-45d1-aac4-86252475d551_main_temperatureMeasurement_temperature_temperature', @@ -236,6 +240,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '68e786a6-7f61-4c3a-9e13-70b803cf782b_main_energyMeter_energy_energy', @@ -288,6 +293,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '68e786a6-7f61-4c3a-9e13-70b803cf782b_main_powerMeter_power_power', @@ -338,6 +344,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '286ba274-4093-4bcb-849c-a1a3efe7b1e5_main_battery_battery_battery', @@ -389,6 +396,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '286ba274-4093-4bcb-849c-a1a3efe7b1e5_main_temperatureMeasurement_temperature_temperature', @@ -446,6 +454,7 @@ 'original_name': 'Alarm', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm', 'unique_id': '10e06a70-ee7d-4832-85e9-a0a06a7a05bd_main_alarm_alarm_alarm', @@ -500,6 +509,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '10e06a70-ee7d-4832-85e9-a0a06a7a05bd_main_battery_battery_battery', @@ -551,6 +561,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'd0268a69-abfb-4c92-a646-61cec2e510ad_main_powerMeter_power_power', @@ -601,6 +612,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2d9a892b-1c93-45a5-84cb-0e81889498c6_main_battery_battery_battery', @@ -652,6 +664,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2d9a892b-1c93-45a5-84cb-0e81889498c6_main_temperatureMeasurement_temperature_temperature', @@ -704,6 +717,7 @@ 'original_name': 'Air quality', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_airQualitySensor_airQuality_airQuality', @@ -755,6 +769,7 @@ 'original_name': 'Carbon dioxide', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_carbonDioxideMeasurement_carbonDioxide_carbonDioxide', @@ -807,6 +822,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_relativeHumidityMeasurement_humidity_humidity', @@ -857,6 +873,7 @@ 'original_name': 'Odor sensor', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'odor_sensor', 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_odorSensor_odorLevel_odorLevel', @@ -906,6 +923,7 @@ 'original_name': 'PM1', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_veryFineDustSensor_veryFineDustLevel_veryFineDustLevel', @@ -958,6 +976,7 @@ 'original_name': 'PM10', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_dustLevel_dustLevel', @@ -1010,6 +1029,7 @@ 'original_name': 'PM2.5', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_dustSensor_fineDustLevel_fineDustLevel', @@ -1062,6 +1082,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a3a970ea-e09c-9c04-161b-94c934e21666_main_temperatureMeasurement_temperature_temperature', @@ -1117,6 +1138,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -1172,6 +1194,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -1227,6 +1250,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -1282,6 +1306,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_power_meter', @@ -1339,6 +1364,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -1394,6 +1420,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -1449,6 +1476,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -1504,6 +1532,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -1556,6 +1585,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_relativeHumidityMeasurement_humidity_humidity', @@ -1611,6 +1641,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_powerConsumptionReport_powerConsumption_power_meter', @@ -1668,6 +1699,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -1720,6 +1752,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_temperatureMeasurement_temperature_temperature', @@ -1770,6 +1803,7 @@ 'original_name': 'Volume', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'audio_volume', 'unique_id': '96a5ef74-5832-a84b-f1f7-ca799957065d_main_audioVolume_volume_volume', @@ -1823,6 +1857,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -1878,6 +1913,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -1933,6 +1969,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -1985,6 +2022,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_relativeHumidityMeasurement_humidity_humidity', @@ -2040,6 +2078,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_power_meter', @@ -2097,6 +2136,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -2149,6 +2189,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_temperatureMeasurement_temperature_temperature', @@ -2199,6 +2240,7 @@ 'original_name': 'Volume', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'audio_volume', 'unique_id': 'c76d6f38-1b7f-13dd-37b5-db18d5272783_main_audioVolume_volume_volume', @@ -2252,6 +2294,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -2307,6 +2350,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -2362,6 +2406,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -2414,6 +2459,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_relativeHumidityMeasurement_humidity_humidity', @@ -2469,6 +2515,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_powerConsumptionReport_powerConsumption_power_meter', @@ -2526,6 +2573,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -2578,6 +2626,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_temperatureMeasurement_temperature_temperature', @@ -2628,6 +2677,7 @@ 'original_name': 'Volume', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'audio_volume', 'unique_id': '4ece486b-89db-f06a-d54d-748b676b4d8e_main_audioVolume_volume_volume', @@ -2678,6 +2728,7 @@ 'original_name': 'Air quality', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_airQualitySensor_airQuality_airQuality', @@ -2729,6 +2780,7 @@ 'original_name': 'PM10', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_dustLevel_dustLevel', @@ -2781,6 +2833,7 @@ 'original_name': 'PM2.5', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_dustSensor_fineDustLevel_fineDustLevel', @@ -2833,6 +2886,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'F8042E25-0E53-0000-0000-000000000000_main_temperatureMeasurement_temperature_temperature', @@ -2889,6 +2943,7 @@ 'original_name': 'Burner 1 heating mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_mode', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_heatingMode_heatingMode', @@ -2942,6 +2997,7 @@ 'original_name': 'Burner 1 level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_level', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_manualLevel_manualLevel', @@ -2995,6 +3051,7 @@ 'original_name': 'Burner 2 heating mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_mode', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_heatingMode_heatingMode', @@ -3048,6 +3105,7 @@ 'original_name': 'Burner 2 level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_level', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_manualLevel_manualLevel', @@ -3101,6 +3159,7 @@ 'original_name': 'Burner 3 heating mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_mode', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_heatingMode_heatingMode', @@ -3154,6 +3213,7 @@ 'original_name': 'Burner 3 level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_level', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_manualLevel_manualLevel', @@ -3207,6 +3267,7 @@ 'original_name': 'Burner 4 heating mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_mode', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_heatingMode_heatingMode', @@ -3260,6 +3321,7 @@ 'original_name': 'Burner 4 level', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'manual_level', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_manualLevel_manualLevel', @@ -3313,6 +3375,7 @@ 'original_name': 'Operating state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooktop_operating_state', 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_main_custom.cooktopOperatingState_cooktopOperatingState_cooktopOperatingState', @@ -3366,6 +3429,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_completionTime_completionTime', @@ -3434,6 +3498,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_job_state', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_ovenJobState_ovenJobState', @@ -3507,6 +3572,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_machine_state', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_machineState_machineState', @@ -3588,6 +3654,7 @@ 'original_name': 'Oven mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_mode', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenMode_ovenMode_ovenMode', @@ -3663,6 +3730,7 @@ 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_setpoint', 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenSetpoint_ovenSetpoint_ovenSetpoint', @@ -3714,6 +3782,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2bad3237-4886-e699-1b90-4a51a3d55c8a_main_temperatureMeasurement_temperature_temperature', @@ -3764,6 +3833,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenOperatingState_completionTime_completionTime', @@ -3832,6 +3902,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_job_state', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenOperatingState_ovenJobState_ovenJobState', @@ -3905,6 +3976,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_machine_state', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenOperatingState_machineState_machineState', @@ -3986,6 +4058,7 @@ 'original_name': 'Oven mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_mode', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenMode_ovenMode_ovenMode', @@ -4061,6 +4134,7 @@ 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_setpoint', 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_ovenSetpoint_ovenSetpoint_ovenSetpoint', @@ -4112,6 +4186,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f_main_temperatureMeasurement_temperature_temperature', @@ -4162,6 +4237,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenOperatingState_completionTime_completionTime', @@ -4230,6 +4306,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_job_state', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenOperatingState_ovenJobState_ovenJobState', @@ -4303,6 +4380,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_machine_state', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenOperatingState_machineState_machineState', @@ -4361,6 +4439,7 @@ 'original_name': 'Operating state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooktop_operating_state', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_custom.cooktopOperatingState_cooktopOperatingState_cooktopOperatingState', @@ -4441,6 +4520,7 @@ 'original_name': 'Oven mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_mode', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenMode_ovenMode_ovenMode', @@ -4516,6 +4596,7 @@ 'original_name': 'Setpoint', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'oven_setpoint', 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_ovenSetpoint_ovenSetpoint_ovenSetpoint', @@ -4567,6 +4648,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2c3cbaa0-1899-5ddc-7b58-9d657bd48f18_main_temperatureMeasurement_temperature_temperature', @@ -4622,6 +4704,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -4677,6 +4760,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -4732,6 +4816,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -4784,6 +4869,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_freezer_temperatureMeasurement_temperature_temperature', @@ -4836,6 +4922,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_cooler_temperatureMeasurement_temperature_temperature', @@ -4891,6 +4978,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_powerConsumptionReport_powerConsumption_power_meter', @@ -4948,6 +5036,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -5003,6 +5092,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -5058,6 +5148,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -5113,6 +5204,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -5165,6 +5257,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_freezer_temperatureMeasurement_temperature_temperature', @@ -5217,6 +5310,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_cooler_temperatureMeasurement_temperature_temperature', @@ -5272,6 +5366,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_power_meter', @@ -5329,6 +5424,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -5384,6 +5480,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -5439,6 +5536,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -5494,6 +5592,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -5546,6 +5645,7 @@ 'original_name': 'Freezer temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'freezer_temperature', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_freezer_temperatureMeasurement_temperature_temperature', @@ -5598,6 +5698,7 @@ 'original_name': 'Fridge temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cooler_temperature', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_cooler_temperatureMeasurement_temperature_temperature', @@ -5653,6 +5754,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_power_meter', @@ -5710,6 +5812,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -5760,6 +5863,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main_battery_battery_battery', @@ -5818,6 +5922,7 @@ 'original_name': 'Cleaning mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'robot_cleaner_cleaning_mode', 'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main_robotCleanerCleaningMode_robotCleanerCleaningMode_robotCleanerCleaningMode', @@ -5887,6 +5992,7 @@ 'original_name': 'Movement', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'robot_cleaner_movement', 'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main_robotCleanerMovement_robotCleanerMovement_robotCleanerMovement', @@ -5954,6 +6060,7 @@ 'original_name': 'Turbo mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'robot_cleaner_turbo_mode', 'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main_robotCleanerTurboMode_robotCleanerTurboMode_robotCleanerTurboMode', @@ -6013,6 +6120,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -6068,6 +6176,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -6123,6 +6232,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -6178,6 +6288,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_powerConsumptionReport_powerConsumption_power_meter', @@ -6235,6 +6346,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -6290,6 +6402,7 @@ 'original_name': 'Valve position', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diverter_valve_position', 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main_samsungce.ehsDiverterValve_position_position', @@ -6347,6 +6460,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -6402,6 +6516,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -6457,6 +6572,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -6512,6 +6628,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_power_meter', @@ -6569,6 +6686,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -6624,6 +6742,7 @@ 'original_name': 'Valve position', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diverter_valve_position', 'unique_id': '6a7d5349-0a66-0277-058d-000001200101_main_samsungce.ehsDiverterValve_position_position', @@ -6681,6 +6800,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -6736,6 +6856,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -6791,6 +6912,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -6846,6 +6968,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_power_meter', @@ -6903,6 +7026,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -6958,6 +7082,7 @@ 'original_name': 'Valve position', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diverter_valve_position', 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main_samsungce.ehsDiverterValve_position_position', @@ -7010,6 +7135,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_completionTime_completionTime', @@ -7063,6 +7189,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -7118,6 +7245,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -7173,6 +7301,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -7236,6 +7365,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dishwasher_job_state', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_dishwasherJobState_dishwasherJobState', @@ -7302,6 +7432,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dishwasher_machine_state', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState', @@ -7360,6 +7491,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_powerConsumptionReport_powerConsumption_power_meter', @@ -7417,6 +7549,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': 'f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -7467,6 +7600,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_dryerOperatingState_completionTime_completionTime', @@ -7520,6 +7654,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -7575,6 +7710,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -7630,6 +7766,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -7698,6 +7835,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_job_state', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_dryerOperatingState_dryerJobState_dryerJobState', @@ -7769,6 +7907,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_machine_state', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_dryerOperatingState_machineState_machineState', @@ -7827,6 +7966,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_powerConsumptionReport_powerConsumption_power_meter', @@ -7884,6 +8024,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -7934,6 +8075,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_completionTime_completionTime', @@ -7987,6 +8129,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -8042,6 +8185,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -8097,6 +8241,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -8165,6 +8310,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_job_state', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_dryerJobState_dryerJobState', @@ -8236,6 +8382,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_machine_state', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_machineState_machineState', @@ -8294,6 +8441,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_powerConsumptionReport_powerConsumption_power_meter', @@ -8351,6 +8499,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -8401,6 +8550,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_dryerOperatingState_completionTime_completionTime', @@ -8454,6 +8604,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -8509,6 +8660,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -8564,6 +8716,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -8632,6 +8785,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_job_state', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_dryerOperatingState_dryerJobState_dryerJobState', @@ -8703,6 +8857,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_machine_state', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_dryerOperatingState_machineState_machineState', @@ -8761,6 +8916,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_powerConsumptionReport_powerConsumption_power_meter', @@ -8818,6 +8974,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -8868,6 +9025,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_completionTime_completionTime', @@ -8921,6 +9079,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -8976,6 +9135,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -9031,6 +9191,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -9100,6 +9261,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_job_state', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_washerJobState_washerJobState', @@ -9172,6 +9334,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_machine_state', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_machineState_machineState', @@ -9230,6 +9393,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_powerConsumptionReport_powerConsumption_power_meter', @@ -9287,6 +9451,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': 'f984b91d-f250-9d42-3436-33f09a422a47_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -9337,6 +9502,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_washerOperatingState_completionTime_completionTime', @@ -9390,6 +9556,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -9445,6 +9612,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -9500,6 +9668,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -9569,6 +9738,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_job_state', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_washerOperatingState_washerJobState_washerJobState', @@ -9641,6 +9811,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_machine_state', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_washerOperatingState_machineState_machineState', @@ -9699,6 +9870,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_powerConsumptionReport_powerConsumption_power_meter', @@ -9756,6 +9928,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -9806,6 +9979,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_washerOperatingState_completionTime_completionTime', @@ -9859,6 +10033,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -9914,6 +10089,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -9969,6 +10145,7 @@ 'original_name': 'Energy saved', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_saved', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_powerConsumptionReport_powerConsumption_energySaved_meter', @@ -10038,6 +10215,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_job_state', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_washerOperatingState_washerJobState_washerJobState', @@ -10110,6 +10288,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_machine_state', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_washerOperatingState_machineState_machineState', @@ -10168,6 +10347,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_powerConsumptionReport_powerConsumption_power_meter', @@ -10225,6 +10405,7 @@ 'original_name': 'Power energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_energy', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_powerConsumptionReport_powerConsumption_powerEnergy_meter', @@ -10277,6 +10458,7 @@ 'original_name': 'Water consumption', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_consumption', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_samsungce.waterConsumptionReport_waterConsumption_waterConsumption', @@ -10327,6 +10509,7 @@ 'original_name': 'Completion time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'completion_time', 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_completionTime_completionTime', @@ -10394,6 +10577,7 @@ 'original_name': 'Job state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_job_state', 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_washerJobState_washerJobState', @@ -10466,6 +10650,7 @@ 'original_name': 'Machine state', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_machine_state', 'unique_id': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee_main_washerOperatingState_machineState_machineState', @@ -10521,6 +10706,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'd5dc3299-c266-41c7-bd08-f540aea54b89_main_temperatureMeasurement_temperature_temperature', @@ -10573,6 +10759,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '028469cb-6e89-4f14-8d9a-bfbca5e0fbfc_main_relativeHumidityMeasurement_humidity_humidity', @@ -10625,6 +10812,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '028469cb-6e89-4f14-8d9a-bfbca5e0fbfc_main_temperatureMeasurement_temperature_temperature', @@ -10677,6 +10865,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1888b38f-6246-4f1e-911b-bfcfb66999db_main_relativeHumidityMeasurement_humidity_humidity', @@ -10729,6 +10918,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1888b38f-6246-4f1e-911b-bfcfb66999db_main_temperatureMeasurement_temperature_temperature', @@ -10783,6 +10973,7 @@ 'original_name': 'Gas', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterVolume_gasMeterVolume', @@ -10835,6 +11026,7 @@ 'original_name': 'Gas meter', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_meter', 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeter_gasMeter', @@ -10885,6 +11077,7 @@ 'original_name': 'Gas meter calorific', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_meter_calorific', 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterCalorific_gasMeterCalorific', @@ -10932,6 +11125,7 @@ 'original_name': 'Gas meter time', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_meter_time', 'unique_id': '3b57dca3-9a90-4f27-ba80-f947b1e60d58_main_gasMeter_gasMeterTime_gasMeterTime', @@ -10982,6 +11176,7 @@ 'original_name': 'Link quality', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'link_quality', 'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a_main_signalStrength_lqi_lqi', @@ -11032,6 +11227,7 @@ 'original_name': 'Signal strength', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a_main_signalStrength_rssi_rssi', @@ -11084,6 +11280,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a_main_temperatureMeasurement_temperature_temperature', @@ -11134,6 +11331,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '5e5b97f3-3094-44e6-abc0-f61283412d6a_main_battery_battery_battery', @@ -11185,6 +11383,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '69a271f6-6537-4982-8cd9-979866872692_main_energyMeter_energy_energy', @@ -11237,6 +11436,7 @@ 'original_name': 'Power', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '69a271f6-6537-4982-8cd9-979866872692_main_powerMeter_power_power', @@ -11289,6 +11489,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '69a271f6-6537-4982-8cd9-979866872692_main_temperatureMeasurement_temperature_temperature', @@ -11339,6 +11540,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '71afed1c-006d-4e48-b16e-e7f88f9fd638_main_battery_battery_battery', @@ -11393,6 +11595,7 @@ 'original_name': 'Atmospheric pressure', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_atmosphericPressureMeasurement_atmosphericPressure_atmosphericPressure', @@ -11443,6 +11646,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_battery_battery_battery', @@ -11494,6 +11698,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_relativeHumidityMeasurement_humidity_humidity', @@ -11546,6 +11751,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '692ea4e9-2022-4ed8-8a57-1b884a59cc38_main_temperatureMeasurement_temperature_temperature', @@ -11596,6 +11802,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_battery_battery_battery', @@ -11647,6 +11854,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_temperatureMeasurement_temperature_temperature', @@ -11697,6 +11905,7 @@ 'original_name': 'X coordinate', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'x_coordinate', 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_threeAxis_threeAxis_x_coordinate', @@ -11744,6 +11953,7 @@ 'original_name': 'Y coordinate', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'y_coordinate', 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_threeAxis_threeAxis_y_coordinate', @@ -11791,6 +12001,7 @@ 'original_name': 'Z coordinate', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'z_coordinate', 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main_threeAxis_threeAxis_z_coordinate', @@ -11840,6 +12051,7 @@ 'original_name': 'Humidity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main_relativeHumidityMeasurement_humidity_humidity', @@ -11892,6 +12104,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2409a73c-918a-4d1f-b4f5-c27468c71d70_main_temperatureMeasurement_temperature_temperature', @@ -11942,6 +12155,7 @@ 'original_name': 'Air conditioner mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_conditioner_mode', 'unique_id': 'bf4b1167-48a3-4af7-9186-0900a678ffa5_main_airConditionerMode_airConditionerMode_airConditionerMode', @@ -11989,6 +12203,7 @@ 'original_name': 'Cooling setpoint', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_cooling_setpoint', 'unique_id': 'bf4b1167-48a3-4af7-9186-0900a678ffa5_main_thermostatCoolingSetpoint_coolingSetpoint_coolingSetpoint', @@ -12043,6 +12258,7 @@ 'original_name': 'Energy', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1_main_powerConsumptionReport_powerConsumption_energy_meter', @@ -12098,6 +12314,7 @@ 'original_name': 'Energy difference', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_difference', 'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1_main_powerConsumptionReport_powerConsumption_deltaEnergy_meter', @@ -12150,6 +12367,7 @@ 'original_name': 'Brightness intensity', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness_intensity', 'unique_id': '5cc1c096-98b9-460c-8f1c-1045509ec605_main_relativeBrightness_brightnessIntensity_brightnessIntensity', @@ -12199,6 +12417,7 @@ 'original_name': 'TV channel', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tv_channel', 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_tvChannel_tvChannel_tvChannel', @@ -12246,6 +12465,7 @@ 'original_name': 'TV channel name', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tv_channel_name', 'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main_tvChannel_tvChannelName_tvChannelName', @@ -12293,6 +12513,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2894dc93-0f11-49cc-8a81-3a684cebebf6_main_battery_battery_battery', @@ -12344,6 +12565,7 @@ 'original_name': 'Temperature', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2894dc93-0f11-49cc-8a81-3a684cebebf6_main_temperatureMeasurement_temperature_temperature', @@ -12394,6 +12616,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a2a6018b-2663-4727-9d1d-8f56953b5116_main_battery_battery_battery', @@ -12443,6 +12666,7 @@ 'original_name': 'Battery', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a9f587c5-5d8b-4273-8907-e7f609af5158_main_battery_battery_battery', diff --git a/tests/components/smartthings/snapshots/test_switch.ambr b/tests/components/smartthings/snapshots/test_switch.ambr index 3b5aa4114ea..1323230e7ea 100644 --- a/tests/components/smartthings/snapshots/test_switch.ambr +++ b/tests/components/smartthings/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '10e06a70-ee7d-4832-85e9-a0a06a7a05bd_main_switch_switch_switch', @@ -74,6 +75,7 @@ 'original_name': 'Ice maker', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ice_maker', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_icemaker_switch_switch_switch', @@ -121,6 +123,7 @@ 'original_name': 'Power cool', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_cool', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_samsungce.powerCool_activated_activated', @@ -168,6 +171,7 @@ 'original_name': 'Power freeze', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_freeze', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_samsungce.powerFreeze_activated_activated', @@ -215,6 +219,7 @@ 'original_name': 'Sabbath mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sabbath_mode', 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_samsungce.sabbathMode_status_status', @@ -262,6 +267,7 @@ 'original_name': 'Ice maker', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ice_maker', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_icemaker_switch_switch_switch', @@ -309,6 +315,7 @@ 'original_name': 'Power cool', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_cool', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_samsungce.powerCool_activated_activated', @@ -356,6 +363,7 @@ 'original_name': 'Power freeze', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_freeze', 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_samsungce.powerFreeze_activated_activated', @@ -403,6 +411,7 @@ 'original_name': 'Power cool', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_cool', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_samsungce.powerCool_activated_activated', @@ -450,6 +459,7 @@ 'original_name': 'Power freeze', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_freeze', 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_samsungce.powerFreeze_activated_activated', @@ -497,6 +507,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main_switch_switch_switch', @@ -544,6 +555,7 @@ 'original_name': 'Auto cycle link', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_cycle_link', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetAutoCycleLink_steamClosetAutoCycleLink_steamClosetAutoCycleLink', @@ -591,6 +603,7 @@ 'original_name': 'Keep fresh mode', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'keep_fresh_mode', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetKeepFreshMode_status_status', @@ -638,6 +651,7 @@ 'original_name': 'Sanitize', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sanitize', 'unique_id': 'b93211bf-9d96-bd21-3b2f-964fcc87f5cc_main_samsungce.steamClosetSanitizeMode_status_status', @@ -685,6 +699,7 @@ 'original_name': 'Wrinkle prevent', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wrinkle_prevent', 'unique_id': '02f7256e-8353-5bdd-547f-bd5b1647e01b_main_custom.dryerWrinklePrevent_dryerWrinklePrevent_dryerWrinklePrevent', @@ -732,6 +747,7 @@ 'original_name': 'Wrinkle prevent', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wrinkle_prevent', 'unique_id': '3a6c4e05-811d-5041-e956-3d04c424cbcd_main_custom.dryerWrinklePrevent_dryerWrinklePrevent_dryerWrinklePrevent', @@ -779,6 +795,7 @@ 'original_name': 'Bubble Soak', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bubble_soak', 'unique_id': '63803fae-cbed-f356-a063-2cf148ae3ca7_main_samsungce.washerBubbleSoak_status_status', @@ -826,6 +843,7 @@ 'original_name': 'Bubble Soak', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bubble_soak', 'unique_id': 'b854ca5f-dc54-140d-6349-758b4d973c41_main_samsungce.washerBubbleSoak_status_status', @@ -873,6 +891,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '656569c2-7976-4232-a789-34b4d1176c3a_main_switch_switch_switch', @@ -920,6 +939,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'bf4b1167-48a3-4af7-9186-0900a678ffa5_main_switch_switch_switch', @@ -967,6 +987,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '550a1c72-65a0-4d55-b97b-75168e055398_main_switch_switch_switch', @@ -1014,6 +1035,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '6602696a-1e48-49e4-919f-69406f5b5da1_main_switch_switch_switch', @@ -1061,6 +1083,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '5cc1c096-98b9-460c-8f1c-1045509ec605_main_switch_switch_switch', diff --git a/tests/components/smartthings/snapshots/test_update.ambr b/tests/components/smartthings/snapshots/test_update.ambr index c27a0b9f5fc..3191411a429 100644 --- a/tests/components/smartthings/snapshots/test_update.ambr +++ b/tests/components/smartthings/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '286ba274-4093-4bcb-849c-a1a3efe7b1e5_main', @@ -87,6 +88,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'd0268a69-abfb-4c92-a646-61cec2e510ad_main', @@ -147,6 +149,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '2d9a892b-1c93-45a5-84cb-0e81889498c6_main', @@ -207,6 +210,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '71afed1c-006d-4e48-b16e-e7f88f9fd638_main', @@ -267,6 +271,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '7d246592-93db-4d72-a10d-5a51793ece8c_main', @@ -327,6 +332,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '550a1c72-65a0-4d55-b97b-75168e055398_main', @@ -387,6 +393,7 @@ 'original_name': 'Firmware', 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'a9f587c5-5d8b-4273-8907-e7f609af5158_main', diff --git a/tests/components/smartthings/snapshots/test_valve.ambr b/tests/components/smartthings/snapshots/test_valve.ambr index f82155c8499..1e291d5913c 100644 --- a/tests/components/smartthings/snapshots/test_valve.ambr +++ b/tests/components/smartthings/snapshots/test_valve.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '612ab3c2-3bb0-48f7-b2c0-15b169cb2fc3_main', diff --git a/tests/components/smartthings/snapshots/test_water_heater.ambr b/tests/components/smartthings/snapshots/test_water_heater.ambr index 759a95220de..3e5afed3b86 100644 --- a/tests/components/smartthings/snapshots/test_water_heater.ambr +++ b/tests/components/smartthings/snapshots/test_water_heater.ambr @@ -37,6 +37,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'water_heater', 'unique_id': '4165c51e-bf6b-c5b6-fd53-127d6248754b_main', @@ -109,6 +110,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'water_heater', 'unique_id': '1f98ebd0-ac48-d802-7f62-000001200100_main', @@ -181,6 +183,7 @@ 'original_name': None, 'platform': 'smartthings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'water_heater', 'unique_id': '3810e5ad-5351-d9f9-12ff-000001200000_main', diff --git a/tests/components/smarty/snapshots/test_binary_sensor.ambr b/tests/components/smarty/snapshots/test_binary_sensor.ambr index ad4b61f5070..935abfcfaaf 100644 --- a/tests/components/smarty/snapshots/test_binary_sensor.ambr +++ b/tests/components/smarty/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_alarm', @@ -75,6 +76,7 @@ 'original_name': 'Boost state', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boost_state', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_boost', @@ -122,6 +124,7 @@ 'original_name': 'Warning', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'warning', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_warning', diff --git a/tests/components/smarty/snapshots/test_button.ambr b/tests/components/smarty/snapshots/test_button.ambr index b5b86c80beb..380fb2317c4 100644 --- a/tests/components/smarty/snapshots/test_button.ambr +++ b/tests/components/smarty/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Reset filters timer', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reset_filters_timer', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_reset_filters_timer', diff --git a/tests/components/smarty/snapshots/test_fan.ambr b/tests/components/smarty/snapshots/test_fan.ambr index 2502bd6f09f..a4f4f8989bd 100644 --- a/tests/components/smarty/snapshots/test_fan.ambr +++ b/tests/components/smarty/snapshots/test_fan.ambr @@ -29,6 +29,7 @@ 'original_name': None, 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'fan', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H', diff --git a/tests/components/smarty/snapshots/test_sensor.ambr b/tests/components/smarty/snapshots/test_sensor.ambr index c32740fa38c..d62c47235be 100644 --- a/tests/components/smarty/snapshots/test_sensor.ambr +++ b/tests/components/smarty/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Extract air temperature', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'extract_air_temperature', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_extract_air_temperature', @@ -76,6 +77,7 @@ 'original_name': 'Extract fan speed', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'extract_fan_speed', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_extract_fan_speed', @@ -124,6 +126,7 @@ 'original_name': 'Filter days left', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_days_left', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_filter_days_left', @@ -172,6 +175,7 @@ 'original_name': 'Outdoor air temperature', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outdoor_air_temperature', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_outdoor_air_temperature', @@ -221,6 +225,7 @@ 'original_name': 'Supply air temperature', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_air_temperature', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_supply_air_temperature', @@ -270,6 +275,7 @@ 'original_name': 'Supply fan speed', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_fan_speed', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_supply_fan_speed', diff --git a/tests/components/smarty/snapshots/test_switch.ambr b/tests/components/smarty/snapshots/test_switch.ambr index 33c829adf31..b84cbf44be9 100644 --- a/tests/components/smarty/snapshots/test_switch.ambr +++ b/tests/components/smarty/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Boost', 'platform': 'smarty', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boost', 'unique_id': '01JAZ5DPW8C62D620DGYNG2R8H_boost', diff --git a/tests/components/smlight/snapshots/test_binary_sensor.ambr b/tests/components/smlight/snapshots/test_binary_sensor.ambr index edb2a914a5d..570bc554313 100644 --- a/tests/components/smlight/snapshots/test_binary_sensor.ambr +++ b/tests/components/smlight/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Ethernet', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ethernet', 'unique_id': 'aa:bb:cc:dd:ee:ff_ethernet', @@ -75,6 +76,7 @@ 'original_name': 'Internet', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'internet', 'unique_id': 'aa:bb:cc:dd:ee:ff_internet', @@ -123,6 +125,7 @@ 'original_name': 'VPN', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vpn', 'unique_id': 'aa:bb:cc:dd:ee:ff_vpn', @@ -171,6 +174,7 @@ 'original_name': 'Wi-Fi', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi', 'unique_id': 'aa:bb:cc:dd:ee:ff_wifi', diff --git a/tests/components/smlight/snapshots/test_sensor.ambr b/tests/components/smlight/snapshots/test_sensor.ambr index 542338e4dbf..63eb97aaf0b 100644 --- a/tests/components/smlight/snapshots/test_sensor.ambr +++ b/tests/components/smlight/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Connection mode', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_mode', 'unique_id': 'aa:bb:cc:dd:ee:ff_device_mode', @@ -91,6 +92,7 @@ 'original_name': 'Core chip temp', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'core_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff_core_temperature', @@ -141,6 +143,7 @@ 'original_name': 'Core uptime', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'core_uptime', 'unique_id': 'aa:bb:cc:dd:ee:ff_core_uptime', @@ -189,6 +192,7 @@ 'original_name': 'Filesystem usage', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fs_usage', 'unique_id': 'aa:bb:cc:dd:ee:ff_fs_usage', @@ -243,6 +247,7 @@ 'original_name': 'Firmware channel', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'firmware_channel', 'unique_id': 'aa:bb:cc:dd:ee:ff_firmware_channel', @@ -295,6 +300,7 @@ 'original_name': 'RAM usage', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ram_usage', 'unique_id': 'aa:bb:cc:dd:ee:ff_ram_usage', @@ -349,6 +355,7 @@ 'original_name': 'Zigbee chip temp', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'zigbee_temperature', 'unique_id': 'aa:bb:cc:dd:ee:ff_zigbee_temperature', @@ -405,6 +412,7 @@ 'original_name': 'Zigbee type', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'zigbee_type', 'unique_id': 'aa:bb:cc:dd:ee:ff_zigbee_type', @@ -458,6 +466,7 @@ 'original_name': 'Zigbee uptime', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'socket_uptime', 'unique_id': 'aa:bb:cc:dd:ee:ff_socket_uptime', diff --git a/tests/components/smlight/snapshots/test_switch.ambr b/tests/components/smlight/snapshots/test_switch.ambr index b748202a557..85084c73609 100644 --- a/tests/components/smlight/snapshots/test_switch.ambr +++ b/tests/components/smlight/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Auto Zigbee update', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_zigbee_update', 'unique_id': 'aa:bb:cc:dd:ee:ff-auto_zigbee_update', @@ -75,6 +76,7 @@ 'original_name': 'Disable LEDs', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disable_led', 'unique_id': 'aa:bb:cc:dd:ee:ff-disable_led', @@ -123,6 +125,7 @@ 'original_name': 'LED night mode', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'night_mode', 'unique_id': 'aa:bb:cc:dd:ee:ff-night_mode', @@ -171,6 +174,7 @@ 'original_name': 'VPN enabled', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vpn_enabled', 'unique_id': 'aa:bb:cc:dd:ee:ff-vpn_enabled', diff --git a/tests/components/smlight/snapshots/test_update.ambr b/tests/components/smlight/snapshots/test_update.ambr index dc6b8f46ca5..c1c04358ceb 100644 --- a/tests/components/smlight/snapshots/test_update.ambr +++ b/tests/components/smlight/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Core firmware', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'core_update', 'unique_id': 'aa:bb:cc:dd:ee:ff-core_update', @@ -87,6 +88,7 @@ 'original_name': 'Zigbee firmware', 'platform': 'smlight', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'zigbee_update', 'unique_id': 'aa:bb:cc:dd:ee:ff-zigbee_update', diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index c51f7627efc..ba9449f31f1 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Consumption year', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_1_consumption_year', @@ -87,6 +88,7 @@ 'original_name': 'Power', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power', 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_1_current_power', @@ -145,6 +147,7 @@ 'original_name': 'Consumption year', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_2_consumption_year', @@ -197,6 +200,7 @@ 'original_name': 'Power', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power', 'unique_id': 'ce5f5431554d101905d31797e1232da8_inverter_2_current_power', @@ -249,6 +253,7 @@ 'original_name': 'Alternator loss', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alternator_loss', 'unique_id': 'ce5f5431554d101905d31797e1232da8_alternator_loss', @@ -304,6 +309,7 @@ 'original_name': 'Capacity', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'capacity', 'unique_id': 'ce5f5431554d101905d31797e1232da8_capacity', @@ -356,6 +362,7 @@ 'original_name': 'Consumption AC', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_ac', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_ac', @@ -414,6 +421,7 @@ 'original_name': 'Consumption day', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_day', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_day', @@ -472,6 +480,7 @@ 'original_name': 'Consumption month', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_month', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_month', @@ -530,6 +539,7 @@ 'original_name': 'Consumption total', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_total', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_total', @@ -588,6 +598,7 @@ 'original_name': 'Consumption year', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_year', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_year', @@ -644,6 +655,7 @@ 'original_name': 'Consumption yesterday', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_yesterday', 'unique_id': 'ce5f5431554d101905d31797e1232da8_consumption_yesterday', @@ -698,6 +710,7 @@ 'original_name': 'Efficiency', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'efficiency', 'unique_id': 'ce5f5431554d101905d31797e1232da8_efficiency', @@ -750,6 +763,7 @@ 'original_name': 'Installed peak power', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_power', 'unique_id': 'ce5f5431554d101905d31797e1232da8_total_power', @@ -800,6 +814,7 @@ 'original_name': 'Last update', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_update', 'unique_id': 'ce5f5431554d101905d31797e1232da8_last_updated', @@ -850,6 +865,7 @@ 'original_name': 'Power AC', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_ac', 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_ac', @@ -902,6 +918,7 @@ 'original_name': 'Power available', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_available', 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_available', @@ -954,6 +971,7 @@ 'original_name': 'Power DC', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_dc', 'unique_id': 'ce5f5431554d101905d31797e1232da8_power_dc', @@ -1006,6 +1024,7 @@ 'original_name': 'Self-consumption year', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'self_consumption_year', 'unique_id': 'ce5f5431554d101905d31797e1232da8_self_consumption_year', @@ -1061,6 +1080,7 @@ 'original_name': 'Usage', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'usage', 'unique_id': 'ce5f5431554d101905d31797e1232da8_usage', @@ -1113,6 +1133,7 @@ 'original_name': 'Voltage AC', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_ac', 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_ac', @@ -1165,6 +1186,7 @@ 'original_name': 'Voltage DC', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_dc', 'unique_id': 'ce5f5431554d101905d31797e1232da8_voltage_dc', @@ -1223,6 +1245,7 @@ 'original_name': 'Yield day', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yield_day', 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_day', @@ -1281,6 +1304,7 @@ 'original_name': 'Yield month', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yield_month', 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_month', @@ -1339,6 +1363,7 @@ 'original_name': 'Yield total', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yield_total', 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_total', @@ -1394,6 +1419,7 @@ 'original_name': 'Yield year', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yield_year', 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_year', @@ -1450,6 +1476,7 @@ 'original_name': 'Yield yesterday', 'platform': 'solarlog', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yield_yesterday', 'unique_id': 'ce5f5431554d101905d31797e1232da8_yield_yesterday', diff --git a/tests/components/sonos/snapshots/test_media_player.ambr b/tests/components/sonos/snapshots/test_media_player.ambr index 7f4681d8915..66b322ea776 100644 --- a/tests/components/sonos/snapshots/test_media_player.ambr +++ b/tests/components/sonos/snapshots/test_media_player.ambr @@ -28,6 +28,7 @@ 'original_name': None, 'platform': 'sonos', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'RINCON_test', diff --git a/tests/components/spotify/snapshots/test_media_player.ambr b/tests/components/spotify/snapshots/test_media_player.ambr index 74dbcb50f92..c275446d999 100644 --- a/tests/components/spotify/snapshots/test_media_player.ambr +++ b/tests/components/spotify/snapshots/test_media_player.ambr @@ -31,6 +31,7 @@ 'original_name': None, 'platform': 'spotify', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'spotify', 'unique_id': '1112264111', @@ -101,6 +102,7 @@ 'original_name': None, 'platform': 'spotify', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'spotify', 'unique_id': '1112264111', diff --git a/tests/components/squeezebox/snapshots/test_media_player.ambr b/tests/components/squeezebox/snapshots/test_media_player.ambr index 5e2e59f447e..4bb00dea5c6 100644 --- a/tests/components/squeezebox/snapshots/test_media_player.ambr +++ b/tests/components/squeezebox/snapshots/test_media_player.ambr @@ -65,6 +65,7 @@ 'original_name': None, 'platform': 'squeezebox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'aa:bb:cc:dd:ee:ff', diff --git a/tests/components/stookwijzer/snapshots/test_sensor.ambr b/tests/components/stookwijzer/snapshots/test_sensor.ambr index ff1f6a12b8a..e0e3de207d0 100644 --- a/tests/components/stookwijzer/snapshots/test_sensor.ambr +++ b/tests/components/stookwijzer/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Advice code', 'platform': 'stookwijzer', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'advice', 'unique_id': '12345_advice', @@ -89,6 +90,7 @@ 'original_name': 'Air quality index', 'platform': 'stookwijzer', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345_air_quality_index', @@ -147,6 +149,7 @@ 'original_name': 'Wind speed', 'platform': 'stookwijzer', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345_windspeed', diff --git a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr index d13a19bc656..38cbef26f6a 100644 --- a/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Away mode', 'platform': 'streamlabswater', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'away_mode', 'unique_id': '945e7c52-854a-41e1-8524-50c6993277e1-away_mode', diff --git a/tests/components/streamlabswater/snapshots/test_sensor.ambr b/tests/components/streamlabswater/snapshots/test_sensor.ambr index c1248f2c0a0..404e636bd3e 100644 --- a/tests/components/streamlabswater/snapshots/test_sensor.ambr +++ b/tests/components/streamlabswater/snapshots/test_sensor.ambr @@ -30,6 +30,7 @@ 'original_name': 'Daily usage', 'platform': 'streamlabswater', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_usage', 'unique_id': '945e7c52-854a-41e1-8524-50c6993277e1-daily_usage', @@ -82,6 +83,7 @@ 'original_name': 'Monthly usage', 'platform': 'streamlabswater', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'monthly_usage', 'unique_id': '945e7c52-854a-41e1-8524-50c6993277e1-monthly_usage', @@ -134,6 +136,7 @@ 'original_name': 'Yearly usage', 'platform': 'streamlabswater', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'yearly_usage', 'unique_id': '945e7c52-854a-41e1-8524-50c6993277e1-yearly_usage', diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index 0ce631bf1b3..ffb442694e4 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Water price', 'platform': 'suez_water', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_price', 'unique_id': '123456_water_price', @@ -77,6 +78,7 @@ 'original_name': 'Water usage yesterday', 'platform': 'suez_water', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_usage_yesterday', 'unique_id': '123456_water_usage_yesterday', diff --git a/tests/components/swiss_public_transport/snapshots/test_sensor.ambr b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr index 5ba65b2bd70..fb16aeae338 100644 --- a/tests/components/swiss_public_transport/snapshots/test_sensor.ambr +++ b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Delay', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'delay', 'unique_id': 'Zürich Bern_delay', @@ -77,6 +78,7 @@ 'original_name': 'Departure', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure0', 'unique_id': 'Zürich Bern_departure', @@ -126,6 +128,7 @@ 'original_name': 'Departure +1', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure1', 'unique_id': 'Zürich Bern_departure1', @@ -175,6 +178,7 @@ 'original_name': 'Departure +2', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'departure2', 'unique_id': 'Zürich Bern_departure2', @@ -224,6 +228,7 @@ 'original_name': 'Line', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'line', 'unique_id': 'Zürich Bern_line', @@ -272,6 +277,7 @@ 'original_name': 'Platform', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'platform', 'unique_id': 'Zürich Bern_platform', @@ -320,6 +326,7 @@ 'original_name': 'Transfers', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'transfers', 'unique_id': 'Zürich Bern_transfers', @@ -371,6 +378,7 @@ 'original_name': 'Trip duration', 'platform': 'swiss_public_transport', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'trip_duration', 'unique_id': 'Zürich Bern_duration', diff --git a/tests/components/switchbot_cloud/snapshots/test_sensor.ambr b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr index 2446add959b..e6bf75c4b25 100644 --- a/tests/components/switchbot_cloud/snapshots/test_sensor.ambr +++ b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_battery', @@ -81,6 +82,7 @@ 'original_name': 'Humidity', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_humidity', @@ -133,6 +135,7 @@ 'original_name': 'Temperature', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_temperature', @@ -185,6 +188,7 @@ 'original_name': 'Battery', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_battery', @@ -237,6 +241,7 @@ 'original_name': 'Humidity', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_humidity', @@ -289,6 +294,7 @@ 'original_name': 'Temperature', 'platform': 'switchbot_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'meter-id-1_temperature', diff --git a/tests/components/syncthru/snapshots/test_binary_sensor.ambr b/tests/components/syncthru/snapshots/test_binary_sensor.ambr index 4f8809fd984..41be0698ad9 100644 --- a/tests/components/syncthru/snapshots/test_binary_sensor.ambr +++ b/tests/components/syncthru/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Connectivity', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '08HRB8GJ3F019DD_online', @@ -75,6 +76,7 @@ 'original_name': 'Problem', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '08HRB8GJ3F019DD_problem', diff --git a/tests/components/syncthru/snapshots/test_sensor.ambr b/tests/components/syncthru/snapshots/test_sensor.ambr index b7edc046879..5d86fc41cc0 100644 --- a/tests/components/syncthru/snapshots/test_sensor.ambr +++ b/tests/components/syncthru/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '08HRB8GJ3F019DD_main', @@ -76,6 +77,7 @@ 'original_name': 'Active alerts', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_alerts', 'unique_id': '08HRB8GJ3F019DD_active_alerts', @@ -125,6 +127,7 @@ 'original_name': 'Black toner level', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'toner_black', 'unique_id': '08HRB8GJ3F019DD_toner_black', @@ -178,6 +181,7 @@ 'original_name': 'Cyan toner level', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'toner_cyan', 'unique_id': '08HRB8GJ3F019DD_toner_cyan', @@ -231,6 +235,7 @@ 'original_name': 'Input tray 1', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tray', 'unique_id': '08HRB8GJ3F019DD_tray_tray_1', @@ -287,6 +292,7 @@ 'original_name': 'Magenta toner level', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'toner_magenta', 'unique_id': '08HRB8GJ3F019DD_toner_magenta', @@ -340,6 +346,7 @@ 'original_name': 'Output tray 1', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'output_tray', 'unique_id': '08HRB8GJ3F019DD_output_tray_1', @@ -391,6 +398,7 @@ 'original_name': 'Yellow toner level', 'platform': 'syncthru', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'toner_yellow', 'unique_id': '08HRB8GJ3F019DD_toner_yellow', diff --git a/tests/components/tailwind/snapshots/test_binary_sensor.ambr b/tests/components/tailwind/snapshots/test_binary_sensor.ambr index d04f2e726b5..5d166018160 100644 --- a/tests/components/tailwind/snapshots/test_binary_sensor.ambr +++ b/tests/components/tailwind/snapshots/test_binary_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': 'Operational problem', 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_problem', 'unique_id': '_3c_e9_e_6d_21_84_-door1-locked_out', @@ -122,6 +123,7 @@ 'original_name': 'Operational problem', 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'operational_problem', 'unique_id': '_3c_e9_e_6d_21_84_-door2-locked_out', diff --git a/tests/components/tailwind/snapshots/test_button.ambr b/tests/components/tailwind/snapshots/test_button.ambr index 7d3d10aa609..0e4bb4e4e41 100644 --- a/tests/components/tailwind/snapshots/test_button.ambr +++ b/tests/components/tailwind/snapshots/test_button.ambr @@ -41,6 +41,7 @@ 'original_name': 'Identify', 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '_3c_e9_e_6d_21_84_-identify', diff --git a/tests/components/tailwind/snapshots/test_cover.ambr b/tests/components/tailwind/snapshots/test_cover.ambr index 1a26a6c98a7..a1a98b028e3 100644 --- a/tests/components/tailwind/snapshots/test_cover.ambr +++ b/tests/components/tailwind/snapshots/test_cover.ambr @@ -42,6 +42,7 @@ 'original_name': None, 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '_3c_e9_e_6d_21_84_-door1', @@ -124,6 +125,7 @@ 'original_name': None, 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '_3c_e9_e_6d_21_84_-door2', diff --git a/tests/components/tailwind/snapshots/test_number.ambr b/tests/components/tailwind/snapshots/test_number.ambr index 7b906ef1976..ffa2c5df7fd 100644 --- a/tests/components/tailwind/snapshots/test_number.ambr +++ b/tests/components/tailwind/snapshots/test_number.ambr @@ -50,6 +50,7 @@ 'original_name': 'Status LED brightness', 'platform': 'tailwind', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brightness', 'unique_id': '_3c_e9_e_6d_21_84_-brightness', diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index 8a5a78cd366..af83e6b3872 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -45,6 +45,7 @@ 'original_name': 'DHT11 Temperature', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_DHT11_Temperature', @@ -125,6 +126,7 @@ 'original_name': 'TX23 Speed Act', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_TX23_Speed_Act', @@ -172,6 +174,7 @@ 'original_name': 'TX23 Dir Card', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_TX23_Dir_Card', @@ -278,6 +281,7 @@ 'original_name': 'ENERGY TotalTariff 0', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_0', @@ -426,6 +430,7 @@ 'original_name': 'ENERGY TotalTariff 1', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_TotalTariff_1', @@ -478,6 +483,7 @@ 'original_name': 'ENERGY ExportTariff 0', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_0', @@ -530,6 +536,7 @@ 'original_name': 'ENERGY ExportTariff 1', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_ExportTariff_1', @@ -614,6 +621,7 @@ 'original_name': 'DS18B20 Temperature', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Temperature', @@ -661,6 +669,7 @@ 'original_name': 'DS18B20 Id', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_DS18B20_Id', @@ -771,6 +780,7 @@ 'original_name': 'ENERGY Total', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total', @@ -855,6 +865,7 @@ 'original_name': 'ENERGY Total 0', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_0', @@ -907,6 +918,7 @@ 'original_name': 'ENERGY Total 1', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_1', @@ -1023,6 +1035,7 @@ 'original_name': 'ENERGY Total Phase1', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase1', @@ -1075,6 +1088,7 @@ 'original_name': 'ENERGY Total Phase2', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ENERGY_Total_Phase2', @@ -1191,6 +1205,7 @@ 'original_name': 'ANALOG Temperature1', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature1', @@ -1275,6 +1290,7 @@ 'original_name': 'ANALOG Temperature2', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Temperature2', @@ -1327,6 +1343,7 @@ 'original_name': 'ANALOG Illuminance3', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_Illuminance3', @@ -1443,6 +1460,7 @@ 'original_name': 'ANALOG CTEnergy1 Energy', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Energy', @@ -1591,6 +1609,7 @@ 'original_name': 'ANALOG CTEnergy1 Power', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Power', @@ -1643,6 +1662,7 @@ 'original_name': 'ANALOG CTEnergy1 Voltage', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Voltage', @@ -1695,6 +1715,7 @@ 'original_name': 'ANALOG CTEnergy1 Current', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_ANALOG_CTEnergy1_Current', @@ -1774,6 +1795,7 @@ 'original_name': 'SENSOR1 Unknown', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_SENSOR1_Unknown', @@ -1903,6 +1925,7 @@ 'original_name': 'SENSOR2 Unknown', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_SENSOR2_Unknown', @@ -1953,6 +1976,7 @@ 'original_name': 'SENSOR3 Unknown', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_SENSOR3_Unknown', @@ -2003,6 +2027,7 @@ 'original_name': 'SENSOR4 Unknown', 'platform': 'tasmota', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00000049A3BC_sensor_sensor_SENSOR4_Unknown', diff --git a/tests/components/technove/snapshots/test_binary_sensor.ambr b/tests/components/technove/snapshots/test_binary_sensor.ambr index 5d9bcd2175a..7ab19670da4 100644 --- a/tests/components/technove/snapshots/test_binary_sensor.ambr +++ b/tests/components/technove/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery protected', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_battery_protected', 'unique_id': 'AA:AA:AA:AA:AA:BB_is_battery_protected', @@ -74,6 +75,7 @@ 'original_name': 'Conflict with power sharing mode', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'conflict_in_sharing_config', 'unique_id': 'AA:AA:AA:AA:AA:BB_conflict_in_sharing_config', @@ -121,6 +123,7 @@ 'original_name': 'Power sharing mode', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'in_sharing_mode', 'unique_id': 'AA:AA:AA:AA:AA:BB_in_sharing_mode', @@ -168,6 +171,7 @@ 'original_name': 'Static IP', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_static_ip', 'unique_id': 'AA:AA:AA:AA:AA:BB_is_static_ip', @@ -215,6 +219,7 @@ 'original_name': 'Update', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:BB_update_available', diff --git a/tests/components/technove/snapshots/test_number.ambr b/tests/components/technove/snapshots/test_number.ambr index eea4b0cb64c..1be2d26ad44 100644 --- a/tests/components/technove/snapshots/test_number.ambr +++ b/tests/components/technove/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Maximum current', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_current', 'unique_id': 'AA:AA:AA:AA:AA:BB_max_current', diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index aaec5667e55..f79c70f3364 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Current', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:BB_current', @@ -81,6 +82,7 @@ 'original_name': 'Input voltage', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_in', 'unique_id': 'AA:AA:AA:AA:AA:BB_voltage_in', @@ -133,6 +135,7 @@ 'original_name': 'Last session energy usage', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_session', 'unique_id': 'AA:AA:AA:AA:AA:BB_energy_session', @@ -185,6 +188,7 @@ 'original_name': 'Max station current', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'max_station_current', 'unique_id': 'AA:AA:AA:AA:AA:BB_max_station_current', @@ -237,6 +241,7 @@ 'original_name': 'Output voltage', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_out', 'unique_id': 'AA:AA:AA:AA:AA:BB_voltage_out', @@ -289,6 +294,7 @@ 'original_name': 'Signal strength', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'AA:AA:AA:AA:AA:BB_rssi', @@ -347,6 +353,7 @@ 'original_name': 'Status', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'AA:AA:AA:AA:AA:BB_status', @@ -404,6 +411,7 @@ 'original_name': 'Total energy usage', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_total', 'unique_id': 'AA:AA:AA:AA:AA:BB_energy_total', @@ -454,6 +462,7 @@ 'original_name': 'Wi-Fi network name', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ssid', 'unique_id': 'AA:AA:AA:AA:AA:BB_ssid', diff --git a/tests/components/technove/snapshots/test_switch.ambr b/tests/components/technove/snapshots/test_switch.ambr index 0e93143ffed..f8e86db58b5 100644 --- a/tests/components/technove/snapshots/test_switch.ambr +++ b/tests/components/technove/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Auto-charge', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_charge', 'unique_id': 'AA:AA:AA:AA:AA:BB_auto_charge', @@ -74,6 +75,7 @@ 'original_name': 'Charging enabled', 'platform': 'technove', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'session_active', 'unique_id': 'AA:AA:AA:AA:AA:BB_session_active', diff --git a/tests/components/tedee/snapshots/test_binary_sensor.ambr b/tests/components/tedee/snapshots/test_binary_sensor.ambr index c2210a7ca5d..05d0e34037e 100644 --- a/tests/components/tedee/snapshots/test_binary_sensor.ambr +++ b/tests/components/tedee/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charging', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-charging', @@ -75,6 +76,7 @@ 'original_name': 'Lock uncalibrated', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uncalibrated', 'unique_id': '12345-uncalibrated', @@ -123,6 +125,7 @@ 'original_name': 'Pullspring enabled', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pullspring_enabled', 'unique_id': '12345-pullspring_enabled', @@ -170,6 +173,7 @@ 'original_name': 'Semi locked', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'semi_locked', 'unique_id': '12345-semi_locked', @@ -217,6 +221,7 @@ 'original_name': 'Charging', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '98765-charging', @@ -265,6 +270,7 @@ 'original_name': 'Lock uncalibrated', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'uncalibrated', 'unique_id': '98765-uncalibrated', @@ -313,6 +319,7 @@ 'original_name': 'Pullspring enabled', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pullspring_enabled', 'unique_id': '98765-pullspring_enabled', @@ -360,6 +367,7 @@ 'original_name': 'Semi locked', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'semi_locked', 'unique_id': '98765-semi_locked', diff --git a/tests/components/tedee/snapshots/test_lock.ambr b/tests/components/tedee/snapshots/test_lock.ambr index 432c3ebd19f..a568a7dcd82 100644 --- a/tests/components/tedee/snapshots/test_lock.ambr +++ b/tests/components/tedee/snapshots/test_lock.ambr @@ -41,6 +41,7 @@ 'original_name': None, 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '98765-lock', @@ -108,6 +109,7 @@ 'original_name': None, 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12345-lock', @@ -156,6 +158,7 @@ 'original_name': None, 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '98765-lock', diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr index 22679c4153a..7416b51f9f5 100644 --- a/tests/components/tedee/snapshots/test_sensor.ambr +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '12345-battery_sensor', @@ -81,6 +82,7 @@ 'original_name': 'Pullspring duration', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pullspring_duration', 'unique_id': '12345-pullspring_duration', @@ -133,6 +135,7 @@ 'original_name': 'Battery', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '98765-battery_sensor', @@ -185,6 +188,7 @@ 'original_name': 'Pullspring duration', 'platform': 'tedee', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pullspring_duration', 'unique_id': '98765-pullspring_duration', diff --git a/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr index 4e34f586280..96de02d77d6 100644 --- a/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Backup capable', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_capable', 'unique_id': '123456-backup_capable', @@ -74,6 +75,7 @@ 'original_name': 'Grid services active', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_active', 'unique_id': '123456-grid_services_active', @@ -121,6 +123,7 @@ 'original_name': 'Grid services enabled', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_grid_services_enabled', 'unique_id': '123456-components_grid_services_enabled', @@ -168,6 +171,7 @@ 'original_name': 'Storm watch active', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storm_mode_active', 'unique_id': '123456-storm_mode_active', @@ -215,6 +219,7 @@ 'original_name': 'Battery heater', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_heater_on', 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_heater_on', @@ -263,6 +268,7 @@ 'original_name': 'Cabin overheat protection actively cooling', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_cabin_overheat_protection_actively_cooling', 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection_actively_cooling', @@ -311,6 +317,7 @@ 'original_name': 'Charge cable', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_conn_charge_cable', 'unique_id': 'LRWXF7EK4KC700000-charge_state_conn_charge_cable', @@ -359,6 +366,7 @@ 'original_name': 'Charger has multiple phases', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_phases', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_phases', @@ -406,6 +414,7 @@ 'original_name': 'Dashcam', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dashcam_state', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_dashcam_state', @@ -454,6 +463,7 @@ 'original_name': 'Front driver door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_df', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_df', @@ -502,6 +512,7 @@ 'original_name': 'Front driver window', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fd_window', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_fd_window', @@ -550,6 +561,7 @@ 'original_name': 'Front passenger door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pf', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_pf', @@ -598,6 +610,7 @@ 'original_name': 'Front passenger window', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fp_window', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_fp_window', @@ -646,6 +659,7 @@ 'original_name': 'Preconditioning', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_is_preconditioning', 'unique_id': 'LRWXF7EK4KC700000-climate_state_is_preconditioning', @@ -693,6 +707,7 @@ 'original_name': 'Preconditioning enabled', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_preconditioning_enabled', 'unique_id': 'LRWXF7EK4KC700000-charge_state_preconditioning_enabled', @@ -740,6 +755,7 @@ 'original_name': 'Rear driver door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_dr', @@ -788,6 +804,7 @@ 'original_name': 'Rear driver window', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rd_window', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rd_window', @@ -836,6 +853,7 @@ 'original_name': 'Rear passenger door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_pr', @@ -884,6 +902,7 @@ 'original_name': 'Rear passenger window', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rp_window', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rp_window', @@ -932,6 +951,7 @@ 'original_name': 'Scheduled charging pending', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_scheduled_charging_pending', 'unique_id': 'LRWXF7EK4KC700000-charge_state_scheduled_charging_pending', @@ -979,6 +999,7 @@ 'original_name': 'Status', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': 'LRWXF7EK4KC700000-state', @@ -1027,6 +1048,7 @@ 'original_name': 'Tire pressure warning front left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fl', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_fl', @@ -1075,6 +1097,7 @@ 'original_name': 'Tire pressure warning front right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_fr', @@ -1123,6 +1146,7 @@ 'original_name': 'Tire pressure warning rear left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rl', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_rl', @@ -1171,6 +1195,7 @@ 'original_name': 'Tire pressure warning rear right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_soft_warning_rr', @@ -1219,6 +1244,7 @@ 'original_name': 'Trip charging', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_trip_charging', 'unique_id': 'LRWXF7EK4KC700000-charge_state_trip_charging', @@ -1266,6 +1292,7 @@ 'original_name': 'User present', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_is_user_present', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_is_user_present', diff --git a/tests/components/tesla_fleet/snapshots/test_button.ambr b/tests/components/tesla_fleet/snapshots/test_button.ambr index 145b10112b3..bb0e120a96f 100644 --- a/tests/components/tesla_fleet/snapshots/test_button.ambr +++ b/tests/components/tesla_fleet/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Flash lights', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flash_lights', 'unique_id': 'LRWXF7EK4KC700000-flash_lights', @@ -74,6 +75,7 @@ 'original_name': 'Homelink', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'homelink', 'unique_id': 'LRWXF7EK4KC700000-homelink', @@ -121,6 +123,7 @@ 'original_name': 'Honk horn', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'honk', 'unique_id': 'LRWXF7EK4KC700000-honk', @@ -168,6 +171,7 @@ 'original_name': 'Keyless driving', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'enable_keyless_driving', 'unique_id': 'LRWXF7EK4KC700000-enable_keyless_driving', @@ -215,6 +219,7 @@ 'original_name': 'Play fart', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boombox', 'unique_id': 'LRWXF7EK4KC700000-boombox', @@ -262,6 +267,7 @@ 'original_name': 'Wake', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wake', 'unique_id': 'LRWXF7EK4KC700000-wake', diff --git a/tests/components/tesla_fleet/snapshots/test_climate.ambr b/tests/components/tesla_fleet/snapshots/test_climate.ambr index f3b36730c3f..0f1a2beb113 100644 --- a/tests/components/tesla_fleet/snapshots/test_climate.ambr +++ b/tests/components/tesla_fleet/snapshots/test_climate.ambr @@ -36,6 +36,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', @@ -107,6 +108,7 @@ 'original_name': 'Climate', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'LRWXF7EK4KC700000-driver_temp', @@ -179,6 +181,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', @@ -249,6 +252,7 @@ 'original_name': 'Climate', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'LRWXF7EK4KC700000-driver_temp', @@ -321,6 +325,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRWXF7EK4KC700000-climate_state_cabin_overheat_protection', @@ -391,6 +396,7 @@ 'original_name': 'Climate', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'LRWXF7EK4KC700000-driver_temp', diff --git a/tests/components/tesla_fleet/snapshots/test_cover.ambr b/tests/components/tesla_fleet/snapshots/test_cover.ambr index ed6969262f1..a721e899a26 100644 --- a/tests/components/tesla_fleet/snapshots/test_cover.ambr +++ b/tests/components/tesla_fleet/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge port door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_port_door_open', @@ -76,6 +77,7 @@ 'original_name': 'Frunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_ft', @@ -125,6 +127,7 @@ 'original_name': 'Sunroof', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_sun_roof_state', @@ -174,6 +177,7 @@ 'original_name': 'Trunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rt', @@ -223,6 +227,7 @@ 'original_name': 'Windows', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'windows', 'unique_id': 'LRWXF7EK4KC700000-windows', @@ -272,6 +277,7 @@ 'original_name': 'Charge port door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_port_door_open', @@ -321,6 +327,7 @@ 'original_name': 'Frunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_ft', @@ -370,6 +377,7 @@ 'original_name': 'Sunroof', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_sun_roof_state', @@ -419,6 +427,7 @@ 'original_name': 'Trunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rt', @@ -468,6 +477,7 @@ 'original_name': 'Windows', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'windows', 'unique_id': 'LRWXF7EK4KC700000-windows', @@ -517,6 +527,7 @@ 'original_name': 'Charge port door', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_port_door_open', @@ -566,6 +577,7 @@ 'original_name': 'Frunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_ft', @@ -615,6 +627,7 @@ 'original_name': 'Sunroof', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_sun_roof_state', @@ -664,6 +677,7 @@ 'original_name': 'Trunk', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_rt', @@ -713,6 +727,7 @@ 'original_name': 'Windows', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'LRWXF7EK4KC700000-windows', diff --git a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr index dc142c4ffeb..879c50b15bb 100644 --- a/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr +++ b/tests/components/tesla_fleet/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Location', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'LRWXF7EK4KC700000-location', @@ -78,6 +79,7 @@ 'original_name': 'Route', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'route', 'unique_id': 'LRWXF7EK4KC700000-route', diff --git a/tests/components/tesla_fleet/snapshots/test_lock.ambr b/tests/components/tesla_fleet/snapshots/test_lock.ambr index e98ad09caad..4c7c85fd2e5 100644 --- a/tests/components/tesla_fleet/snapshots/test_lock.ambr +++ b/tests/components/tesla_fleet/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge cable lock', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_latch', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_port_latch', @@ -75,6 +76,7 @@ 'original_name': 'Lock', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_locked', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_locked', diff --git a/tests/components/tesla_fleet/snapshots/test_media_player.ambr b/tests/components/tesla_fleet/snapshots/test_media_player.ambr index 77c46faedd7..ccd39ff33ac 100644 --- a/tests/components/tesla_fleet/snapshots/test_media_player.ambr +++ b/tests/components/tesla_fleet/snapshots/test_media_player.ambr @@ -28,6 +28,7 @@ 'original_name': 'Media player', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'media', 'unique_id': 'LRWXF7EK4KC700000-media', @@ -107,6 +108,7 @@ 'original_name': 'Media player', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'media', 'unique_id': 'LRWXF7EK4KC700000-media', diff --git a/tests/components/tesla_fleet/snapshots/test_number.ambr b/tests/components/tesla_fleet/snapshots/test_number.ambr index a3fccf3a45a..926c2f23ce8 100644 --- a/tests/components/tesla_fleet/snapshots/test_number.ambr +++ b/tests/components/tesla_fleet/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Backup reserve', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_reserve_percent', 'unique_id': '123456-backup_reserve_percent', @@ -91,6 +92,7 @@ 'original_name': 'Off-grid reserve', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_grid_vehicle_charging_reserve_percent', 'unique_id': '123456-off_grid_vehicle_charging_reserve_percent', @@ -150,6 +152,7 @@ 'original_name': 'Charge current', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_current_request', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_current_request', @@ -208,6 +211,7 @@ 'original_name': 'Charge limit', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_limit_soc', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_limit_soc', diff --git a/tests/components/tesla_fleet/snapshots/test_select.ambr b/tests/components/tesla_fleet/snapshots/test_select.ambr index 171b52decf1..7e698a088be 100644 --- a/tests/components/tesla_fleet/snapshots/test_select.ambr +++ b/tests/components/tesla_fleet/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Allow export', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_customer_preferred_export_rule', 'unique_id': '123456-components_customer_preferred_export_rule', @@ -91,6 +92,7 @@ 'original_name': 'Operation mode', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'default_real_mode', 'unique_id': '123456-default_real_mode', @@ -150,6 +152,7 @@ 'original_name': 'Seat heater front left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_left', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_left', @@ -210,6 +213,7 @@ 'original_name': 'Seat heater front right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_right', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_right', @@ -270,6 +274,7 @@ 'original_name': 'Seat heater rear center', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_center', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_rear_center', @@ -330,6 +335,7 @@ 'original_name': 'Seat heater rear left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_left', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_rear_left', @@ -390,6 +396,7 @@ 'original_name': 'Seat heater rear right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_right', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_rear_right', @@ -450,6 +457,7 @@ 'original_name': 'Seat heater third row left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_third_row_left', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_third_row_left', @@ -510,6 +518,7 @@ 'original_name': 'Seat heater third row right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_third_row_right', 'unique_id': 'LRWXF7EK4KC700000-climate_state_seat_heater_third_row_right', @@ -569,6 +578,7 @@ 'original_name': 'Steering wheel heater', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_steering_wheel_heat_level', 'unique_id': 'LRWXF7EK4KC700000-climate_state_steering_wheel_heat_level', diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index f7349c9e2d8..5aeb6f59d0d 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Battery charged', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_battery_charge', 'unique_id': '123456-total_battery_charge', @@ -109,6 +110,7 @@ 'original_name': 'Battery discharged', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_battery_discharge', 'unique_id': '123456-total_battery_discharge', @@ -183,6 +185,7 @@ 'original_name': 'Battery exported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_exported', 'unique_id': '123456-battery_energy_exported', @@ -257,6 +260,7 @@ 'original_name': 'Battery imported from generator', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_generator', 'unique_id': '123456-battery_energy_imported_from_generator', @@ -331,6 +335,7 @@ 'original_name': 'Battery imported from grid', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_grid', 'unique_id': '123456-battery_energy_imported_from_grid', @@ -405,6 +410,7 @@ 'original_name': 'Battery imported from solar', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_solar', 'unique_id': '123456-battery_energy_imported_from_solar', @@ -479,6 +485,7 @@ 'original_name': 'Battery power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_power', 'unique_id': '123456-battery_power', @@ -553,6 +560,7 @@ 'original_name': 'Consumer imported from battery', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_battery', 'unique_id': '123456-consumer_energy_imported_from_battery', @@ -627,6 +635,7 @@ 'original_name': 'Consumer imported from generator', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_generator', 'unique_id': '123456-consumer_energy_imported_from_generator', @@ -701,6 +710,7 @@ 'original_name': 'Consumer imported from grid', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_grid', 'unique_id': '123456-consumer_energy_imported_from_grid', @@ -775,6 +785,7 @@ 'original_name': 'Consumer imported from solar', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_solar', 'unique_id': '123456-consumer_energy_imported_from_solar', @@ -849,6 +860,7 @@ 'original_name': 'Energy left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_left', 'unique_id': '123456-energy_left', @@ -923,6 +935,7 @@ 'original_name': 'Generator exported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_energy_exported', 'unique_id': '123456-generator_energy_exported', @@ -997,6 +1010,7 @@ 'original_name': 'Generator power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_power', 'unique_id': '123456-generator_power', @@ -1071,6 +1085,7 @@ 'original_name': 'Grid exported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_grid_energy_exported', 'unique_id': '123456-total_grid_energy_exported', @@ -1145,6 +1160,7 @@ 'original_name': 'Grid exported from battery', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_battery', 'unique_id': '123456-grid_energy_exported_from_battery', @@ -1219,6 +1235,7 @@ 'original_name': 'Grid exported from generator', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_generator', 'unique_id': '123456-grid_energy_exported_from_generator', @@ -1293,6 +1310,7 @@ 'original_name': 'Grid exported from solar', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_solar', 'unique_id': '123456-grid_energy_exported_from_solar', @@ -1367,6 +1385,7 @@ 'original_name': 'Grid imported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_imported', 'unique_id': '123456-grid_energy_imported', @@ -1441,6 +1460,7 @@ 'original_name': 'Grid power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_power', 'unique_id': '123456-grid_power', @@ -1515,6 +1535,7 @@ 'original_name': 'Grid services exported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_energy_exported', 'unique_id': '123456-grid_services_energy_exported', @@ -1589,6 +1610,7 @@ 'original_name': 'Grid services imported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_energy_imported', 'unique_id': '123456-grid_services_energy_imported', @@ -1663,6 +1685,7 @@ 'original_name': 'Grid services power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_power', 'unique_id': '123456-grid_services_power', @@ -1737,6 +1760,7 @@ 'original_name': 'Grid Status', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'island_status', 'unique_id': '123456-island_status', @@ -1821,6 +1845,7 @@ 'original_name': 'Home usage', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_home_usage', 'unique_id': '123456-total_home_usage', @@ -1895,6 +1920,7 @@ 'original_name': 'Load power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_power', 'unique_id': '123456-load_power', @@ -1966,6 +1992,7 @@ 'original_name': 'Percentage charged', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'percentage_charged', 'unique_id': '123456-percentage_charged', @@ -2040,6 +2067,7 @@ 'original_name': 'Solar exported', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_energy_exported', 'unique_id': '123456-solar_energy_exported', @@ -2114,6 +2142,7 @@ 'original_name': 'Solar generated', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_solar_generation', 'unique_id': '123456-total_solar_generation', @@ -2188,6 +2217,7 @@ 'original_name': 'Solar power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_power', 'unique_id': '123456-solar_power', @@ -2262,6 +2292,7 @@ 'original_name': 'Total pack energy', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_pack_energy', 'unique_id': '123456-total_pack_energy', @@ -2328,6 +2359,7 @@ 'original_name': 'version', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'version', 'unique_id': '123456-version', @@ -2388,6 +2420,7 @@ 'original_name': 'VPP backup reserve', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vpp_backup_reserve_percent', 'unique_id': '123456-vpp_backup_reserve_percent', @@ -2454,6 +2487,7 @@ 'original_name': 'Battery level', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_level', 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_level', @@ -2528,6 +2562,7 @@ 'original_name': 'Battery range', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_range', 'unique_id': 'LRWXF7EK4KC700000-charge_state_battery_range', @@ -2594,6 +2629,7 @@ 'original_name': 'Charge cable', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_conn_charge_cable', 'unique_id': 'LRWXF7EK4KC700000-charge_state_conn_charge_cable', @@ -2659,6 +2695,7 @@ 'original_name': 'Charge energy added', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_energy_added', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_energy_added', @@ -2730,6 +2767,7 @@ 'original_name': 'Charge rate', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_rate', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charge_rate', @@ -2798,6 +2836,7 @@ 'original_name': 'Charger current', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_actual_current', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_actual_current', @@ -2866,6 +2905,7 @@ 'original_name': 'Charger power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_power', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_power', @@ -2934,6 +2974,7 @@ 'original_name': 'Charger voltage', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_voltage', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charger_voltage', @@ -3009,6 +3050,7 @@ 'original_name': 'Charging', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRWXF7EK4KC700000-charge_state_charging_state', @@ -3092,6 +3134,7 @@ 'original_name': 'Distance to arrival', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_miles_to_arrival', 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_miles_to_arrival', @@ -3163,6 +3206,7 @@ 'original_name': 'Driver temperature setting', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_driver_temp_setting', 'unique_id': 'LRWXF7EK4KC700000-climate_state_driver_temp_setting', @@ -3237,6 +3281,7 @@ 'original_name': 'Estimate battery range', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_est_battery_range', 'unique_id': 'LRWXF7EK4KC700000-charge_state_est_battery_range', @@ -3303,6 +3348,7 @@ 'original_name': 'Fast charger type', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_fast_charger_type', 'unique_id': 'LRWXF7EK4KC700000-charge_state_fast_charger_type', @@ -3371,6 +3417,7 @@ 'original_name': 'Ideal battery range', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_ideal_battery_range', 'unique_id': 'LRWXF7EK4KC700000-charge_state_ideal_battery_range', @@ -3442,6 +3489,7 @@ 'original_name': 'Inside temperature', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_inside_temp', 'unique_id': 'LRWXF7EK4KC700000-climate_state_inside_temp', @@ -3516,6 +3564,7 @@ 'original_name': 'Odometer', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_odometer', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_odometer', @@ -3587,6 +3636,7 @@ 'original_name': 'Outside temperature', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_outside_temp', 'unique_id': 'LRWXF7EK4KC700000-climate_state_outside_temp', @@ -3658,6 +3708,7 @@ 'original_name': 'Passenger temperature setting', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_passenger_temp_setting', 'unique_id': 'LRWXF7EK4KC700000-climate_state_passenger_temp_setting', @@ -3726,6 +3777,7 @@ 'original_name': 'Power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_power', 'unique_id': 'LRWXF7EK4KC700000-drive_state_power', @@ -3799,6 +3851,7 @@ 'original_name': 'Shift state', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_shift_state', 'unique_id': 'LRWXF7EK4KC700000-drive_state_shift_state', @@ -3878,6 +3931,7 @@ 'original_name': 'Speed', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_speed', 'unique_id': 'LRWXF7EK4KC700000-drive_state_speed', @@ -3946,6 +4000,7 @@ 'original_name': 'State of charge at arrival', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_energy_at_arrival', 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_energy_at_arrival', @@ -4012,6 +4067,7 @@ 'original_name': 'Time to arrival', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_minutes_to_arrival', 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_minutes_to_arrival', @@ -4074,6 +4130,7 @@ 'original_name': 'Time to full charge', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_minutes_to_full_charge', 'unique_id': 'LRWXF7EK4KC700000-charge_state_minutes_to_full_charge', @@ -4144,6 +4201,7 @@ 'original_name': 'Tire pressure front left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fl', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_fl', @@ -4218,6 +4276,7 @@ 'original_name': 'Tire pressure front right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_fr', @@ -4292,6 +4351,7 @@ 'original_name': 'Tire pressure rear left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rl', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_rl', @@ -4366,6 +4426,7 @@ 'original_name': 'Tire pressure rear right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rr', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_tpms_pressure_rr', @@ -4434,6 +4495,7 @@ 'original_name': 'Traffic delay', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_traffic_minutes_delay', 'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_traffic_minutes_delay', @@ -4502,6 +4564,7 @@ 'original_name': 'Usable battery level', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_usable_battery_level', 'unique_id': 'LRWXF7EK4KC700000-charge_state_usable_battery_level', @@ -4568,6 +4631,7 @@ 'original_name': 'Fault state code', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_fault_state', 'unique_id': '123456-abd-123-wall_connector_fault_state', @@ -4628,6 +4692,7 @@ 'original_name': 'Fault state code', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_fault_state', 'unique_id': '123456-bcd-234-wall_connector_fault_state', @@ -4696,6 +4761,7 @@ 'original_name': 'Power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-abd-123-wall_connector_power', @@ -4770,6 +4836,7 @@ 'original_name': 'Power', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-bcd-234-wall_connector_power', @@ -4836,6 +4903,7 @@ 'original_name': 'State code', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-abd-123-wall_connector_state', @@ -4896,6 +4964,7 @@ 'original_name': 'State code', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-bcd-234-wall_connector_state', @@ -4956,6 +5025,7 @@ 'original_name': 'Vehicle', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-abd-123-vin', @@ -5016,6 +5086,7 @@ 'original_name': 'Vehicle', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-bcd-234-vin', diff --git a/tests/components/tesla_fleet/snapshots/test_switch.ambr b/tests/components/tesla_fleet/snapshots/test_switch.ambr index 2ea3bcc5ee5..b9efff6f23b 100644 --- a/tests/components/tesla_fleet/snapshots/test_switch.ambr +++ b/tests/components/tesla_fleet/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Allow charging from grid', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_disallow_charge_from_grid_with_solar_installed', 'unique_id': '123456-components_disallow_charge_from_grid_with_solar_installed', @@ -75,6 +76,7 @@ 'original_name': 'Storm watch', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'user_settings_storm_mode_enabled', 'unique_id': '123456-user_settings_storm_mode_enabled', @@ -123,6 +125,7 @@ 'original_name': 'Auto seat climate left', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_left', 'unique_id': 'LRWXF7EK4KC700000-climate_state_auto_seat_climate_left', @@ -171,6 +174,7 @@ 'original_name': 'Auto seat climate right', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_right', 'unique_id': 'LRWXF7EK4KC700000-climate_state_auto_seat_climate_right', @@ -219,6 +223,7 @@ 'original_name': 'Auto steering wheel heater', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_steering_wheel_heat', 'unique_id': 'LRWXF7EK4KC700000-climate_state_auto_steering_wheel_heat', @@ -267,6 +272,7 @@ 'original_name': 'Charge', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRWXF7EK4KC700000-charge_state_user_charge_enable_request', @@ -315,6 +321,7 @@ 'original_name': 'Defrost', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_defrost_mode', 'unique_id': 'LRWXF7EK4KC700000-climate_state_defrost_mode', @@ -363,6 +370,7 @@ 'original_name': 'Sentry mode', 'platform': 'tesla_fleet', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_sentry_mode', 'unique_id': 'LRWXF7EK4KC700000-vehicle_state_sentry_mode', diff --git a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr index 0af85a6846d..8bcd837d06f 100644 --- a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Backup capable', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_capable', 'unique_id': '123456-backup_capable', @@ -74,6 +75,7 @@ 'original_name': 'Grid services active', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_active', 'unique_id': '123456-grid_services_active', @@ -121,6 +123,7 @@ 'original_name': 'Grid services enabled', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_grid_services_enabled', 'unique_id': '123456-components_grid_services_enabled', @@ -168,6 +171,7 @@ 'original_name': 'Grid status', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_status', 'unique_id': '123456-grid_status', @@ -216,6 +220,7 @@ 'original_name': 'Storm watch active', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storm_mode_active', 'unique_id': '123456-storm_mode_active', @@ -263,6 +268,7 @@ 'original_name': 'Automatic blind spot camera', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'automatic_blind_spot_camera', 'unique_id': 'LRW3F7EK4NC700000-automatic_blind_spot_camera', @@ -310,6 +316,7 @@ 'original_name': 'Automatic emergency braking off', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'automatic_emergency_braking_off', 'unique_id': 'LRW3F7EK4NC700000-automatic_emergency_braking_off', @@ -357,6 +364,7 @@ 'original_name': 'Battery heater', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_heater_on', 'unique_id': 'LRW3F7EK4NC700000-charge_state_battery_heater_on', @@ -405,6 +413,7 @@ 'original_name': 'Blind spot collision warning chime', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'blind_spot_collision_warning_chime', 'unique_id': 'LRW3F7EK4NC700000-blind_spot_collision_warning_chime', @@ -452,6 +461,7 @@ 'original_name': 'BMS full charge', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bms_full_charge_complete', 'unique_id': 'LRW3F7EK4NC700000-bms_full_charge_complete', @@ -499,6 +509,7 @@ 'original_name': 'Brake pedal', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'brake_pedal', 'unique_id': 'LRW3F7EK4NC700000-brake_pedal', @@ -546,6 +557,7 @@ 'original_name': 'Cabin overheat protection active', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_cabin_overheat_protection_actively_cooling', 'unique_id': 'LRW3F7EK4NC700000-climate_state_cabin_overheat_protection_actively_cooling', @@ -594,6 +606,7 @@ 'original_name': 'Cellular', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cellular', 'unique_id': 'LRW3F7EK4NC700000-cellular', @@ -642,6 +655,7 @@ 'original_name': 'Charge cable', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_conn_charge_cable', 'unique_id': 'LRW3F7EK4NC700000-charge_state_conn_charge_cable', @@ -690,6 +704,7 @@ 'original_name': 'Charge enable request', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_enable_request', 'unique_id': 'LRW3F7EK4NC700000-charge_enable_request', @@ -737,6 +752,7 @@ 'original_name': 'Charge port cold weather mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_port_cold_weather_mode', 'unique_id': 'LRW3F7EK4NC700000-charge_port_cold_weather_mode', @@ -784,6 +800,7 @@ 'original_name': 'Charger has multiple phases', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_phases', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charger_phases', @@ -831,6 +848,7 @@ 'original_name': 'Dashcam', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dashcam_state', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_dashcam_state', @@ -879,6 +897,7 @@ 'original_name': 'DC to DC converter', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dc_dc_enable', 'unique_id': 'LRW3F7EK4NC700000-dc_dc_enable', @@ -926,6 +945,7 @@ 'original_name': 'Defrost for preconditioning', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'defrost_for_preconditioning', 'unique_id': 'LRW3F7EK4NC700000-defrost_for_preconditioning', @@ -973,6 +993,7 @@ 'original_name': 'Drive rail', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_rail', 'unique_id': 'LRW3F7EK4NC700000-drive_rail', @@ -1020,6 +1041,7 @@ 'original_name': 'Driver seat belt', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'driver_seat_belt', 'unique_id': 'LRW3F7EK4NC700000-driver_seat_belt', @@ -1067,6 +1089,7 @@ 'original_name': 'Driver seat occupied', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'driver_seat_occupied', 'unique_id': 'LRW3F7EK4NC700000-driver_seat_occupied', @@ -1114,6 +1137,7 @@ 'original_name': 'Emergency lane departure avoidance', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'emergency_lane_departure_avoidance', 'unique_id': 'LRW3F7EK4NC700000-emergency_lane_departure_avoidance', @@ -1161,6 +1185,7 @@ 'original_name': 'European vehicle', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'europe_vehicle', 'unique_id': 'LRW3F7EK4NC700000-europe_vehicle', @@ -1208,6 +1233,7 @@ 'original_name': 'Fast charger present', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fast_charger_present', 'unique_id': 'LRW3F7EK4NC700000-fast_charger_present', @@ -1255,6 +1281,7 @@ 'original_name': 'Front driver door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_df', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_df', @@ -1303,6 +1330,7 @@ 'original_name': 'Front driver window', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fd_window', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_fd_window', @@ -1351,6 +1379,7 @@ 'original_name': 'Front passenger door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pf', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_pf', @@ -1399,6 +1428,7 @@ 'original_name': 'Front passenger window', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fp_window', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_fp_window', @@ -1447,6 +1477,7 @@ 'original_name': 'GPS state', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gps_state', 'unique_id': 'LRW3F7EK4NC700000-gps_state', @@ -1495,6 +1526,7 @@ 'original_name': 'Guest mode enabled', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'guest_mode_enabled', 'unique_id': 'LRW3F7EK4NC700000-guest_mode_enabled', @@ -1542,6 +1574,7 @@ 'original_name': 'Hazard lights', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lights_hazards_active', 'unique_id': 'LRW3F7EK4NC700000-lights_hazards_active', @@ -1589,6 +1622,7 @@ 'original_name': 'High beams', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lights_high_beams', 'unique_id': 'LRW3F7EK4NC700000-lights_high_beams', @@ -1636,6 +1670,7 @@ 'original_name': 'High voltage interlock loop fault', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvil', 'unique_id': 'LRW3F7EK4NC700000-hvil', @@ -1684,6 +1719,7 @@ 'original_name': 'Homelink nearby', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'homelink_nearby', 'unique_id': 'LRW3F7EK4NC700000-homelink_nearby', @@ -1731,6 +1767,7 @@ 'original_name': 'HVAC auto mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hvac_auto_mode', 'unique_id': 'LRW3F7EK4NC700000-hvac_auto_mode', @@ -1778,6 +1815,7 @@ 'original_name': 'Located at favorite', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'located_at_favorite', 'unique_id': 'LRW3F7EK4NC700000-located_at_favorite', @@ -1825,6 +1863,7 @@ 'original_name': 'Located at home', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'located_at_home', 'unique_id': 'LRW3F7EK4NC700000-located_at_home', @@ -1872,6 +1911,7 @@ 'original_name': 'Located at work', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'located_at_work', 'unique_id': 'LRW3F7EK4NC700000-located_at_work', @@ -1919,6 +1959,7 @@ 'original_name': 'Offroad lightbar', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'offroad_lightbar_present', 'unique_id': 'LRW3F7EK4NC700000-offroad_lightbar_present', @@ -1966,6 +2007,7 @@ 'original_name': 'Passenger seat belt', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'passenger_seat_belt', 'unique_id': 'LRW3F7EK4NC700000-passenger_seat_belt', @@ -2013,6 +2055,7 @@ 'original_name': 'PIN to Drive enabled', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pin_to_drive_enabled', 'unique_id': 'LRW3F7EK4NC700000-pin_to_drive_enabled', @@ -2060,6 +2103,7 @@ 'original_name': 'Preconditioning', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_is_preconditioning', 'unique_id': 'LRW3F7EK4NC700000-climate_state_is_preconditioning', @@ -2107,6 +2151,7 @@ 'original_name': 'Preconditioning enabled', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_preconditioning_enabled', 'unique_id': 'LRW3F7EK4NC700000-charge_state_preconditioning_enabled', @@ -2154,6 +2199,7 @@ 'original_name': 'Rear display HVAC', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rear_display_hvac_enabled', 'unique_id': 'LRW3F7EK4NC700000-rear_display_hvac_enabled', @@ -2201,6 +2247,7 @@ 'original_name': 'Rear driver door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_dr', @@ -2249,6 +2296,7 @@ 'original_name': 'Rear driver window', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rd_window', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_rd_window', @@ -2297,6 +2345,7 @@ 'original_name': 'Rear passenger door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_pr', @@ -2345,6 +2394,7 @@ 'original_name': 'Rear passenger window', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rp_window', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_rp_window', @@ -2393,6 +2443,7 @@ 'original_name': 'Remote start', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'remote_start_enabled', 'unique_id': 'LRW3F7EK4NC700000-remote_start_enabled', @@ -2440,6 +2491,7 @@ 'original_name': 'Right hand drive', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'right_hand_drive', 'unique_id': 'LRW3F7EK4NC700000-right_hand_drive', @@ -2487,6 +2539,7 @@ 'original_name': 'Scheduled charging pending', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_scheduled_charging_pending', 'unique_id': 'LRW3F7EK4NC700000-charge_state_scheduled_charging_pending', @@ -2534,6 +2587,7 @@ 'original_name': 'Seat vent enabled', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'seat_vent_enabled', 'unique_id': 'LRW3F7EK4NC700000-seat_vent_enabled', @@ -2581,6 +2635,7 @@ 'original_name': 'Service mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'service_mode', 'unique_id': 'LRW3F7EK4NC700000-service_mode', @@ -2628,6 +2683,7 @@ 'original_name': 'Speed limited', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'speed_limit_mode', 'unique_id': 'LRW3F7EK4NC700000-speed_limit_mode', @@ -2675,6 +2731,7 @@ 'original_name': 'Status', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': 'LRW3F7EK4NC700000-state', @@ -2723,6 +2780,7 @@ 'original_name': 'Supercharger session trip planner', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supercharger_session_trip_planner', 'unique_id': 'LRW3F7EK4NC700000-supercharger_session_trip_planner', @@ -2770,6 +2828,7 @@ 'original_name': 'Tire pressure warning front left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fl', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_soft_warning_fl', @@ -2818,6 +2877,7 @@ 'original_name': 'Tire pressure warning front right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_soft_warning_fr', @@ -2866,6 +2926,7 @@ 'original_name': 'Tire pressure warning rear left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rl', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_soft_warning_rl', @@ -2914,6 +2975,7 @@ 'original_name': 'Tire pressure warning rear right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_soft_warning_rr', @@ -2962,6 +3024,7 @@ 'original_name': 'Trip charging', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_trip_charging', 'unique_id': 'LRW3F7EK4NC700000-charge_state_trip_charging', @@ -3009,6 +3072,7 @@ 'original_name': 'User present', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_is_user_present', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_is_user_present', @@ -3057,6 +3121,7 @@ 'original_name': 'Wi-Fi', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi', 'unique_id': 'LRW3F7EK4NC700000-wifi', @@ -3105,6 +3170,7 @@ 'original_name': 'Wiper heat', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wiper_heat_enabled', 'unique_id': 'LRW3F7EK4NC700000-wiper_heat_enabled', diff --git a/tests/components/teslemetry/snapshots/test_button.ambr b/tests/components/teslemetry/snapshots/test_button.ambr index e4e20215020..714d4ed1f6d 100644 --- a/tests/components/teslemetry/snapshots/test_button.ambr +++ b/tests/components/teslemetry/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Flash lights', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flash_lights', 'unique_id': 'LRW3F7EK4NC700000-flash_lights', @@ -74,6 +75,7 @@ 'original_name': 'Homelink', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'homelink', 'unique_id': 'LRW3F7EK4NC700000-homelink', @@ -121,6 +123,7 @@ 'original_name': 'Honk horn', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'honk', 'unique_id': 'LRW3F7EK4NC700000-honk', @@ -168,6 +171,7 @@ 'original_name': 'Keyless driving', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'enable_keyless_driving', 'unique_id': 'LRW3F7EK4NC700000-enable_keyless_driving', @@ -215,6 +219,7 @@ 'original_name': 'Play fart', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boombox', 'unique_id': 'LRW3F7EK4NC700000-boombox', @@ -262,6 +267,7 @@ 'original_name': 'Wake', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wake', 'unique_id': 'LRW3F7EK4NC700000-wake', diff --git a/tests/components/teslemetry/snapshots/test_climate.ambr b/tests/components/teslemetry/snapshots/test_climate.ambr index e0e68f23c79..1aa68b59ee3 100644 --- a/tests/components/teslemetry/snapshots/test_climate.ambr +++ b/tests/components/teslemetry/snapshots/test_climate.ambr @@ -36,6 +36,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRW3F7EK4NC700000-climate_state_cabin_overheat_protection', @@ -111,6 +112,7 @@ 'original_name': 'Climate', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'LRW3F7EK4NC700000-driver_temp', @@ -188,6 +190,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRW3F7EK4NC700000-climate_state_cabin_overheat_protection', @@ -262,6 +265,7 @@ 'original_name': 'Climate', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': , 'unique_id': 'LRW3F7EK4NC700000-driver_temp', @@ -339,6 +343,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'LRW3F7EK4NC700000-climate_state_cabin_overheat_protection', @@ -380,6 +385,7 @@ 'original_name': 'Climate', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': , 'unique_id': 'LRW3F7EK4NC700000-driver_temp', diff --git a/tests/components/teslemetry/snapshots/test_cover.ambr b/tests/components/teslemetry/snapshots/test_cover.ambr index 438738ff2b9..cec35e79fc7 100644 --- a/tests/components/teslemetry/snapshots/test_cover.ambr +++ b/tests/components/teslemetry/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge port door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_door_open', @@ -76,6 +77,7 @@ 'original_name': 'Frunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_ft', @@ -125,6 +127,7 @@ 'original_name': 'Sunroof', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_sun_roof_state', @@ -174,6 +177,7 @@ 'original_name': 'Trunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_rt', @@ -223,6 +227,7 @@ 'original_name': 'Windows', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'windows', 'unique_id': 'LRW3F7EK4NC700000-windows', @@ -272,6 +277,7 @@ 'original_name': 'Charge port door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_door_open', @@ -321,6 +327,7 @@ 'original_name': 'Frunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_ft', @@ -370,6 +377,7 @@ 'original_name': 'Trunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_rt', @@ -419,6 +427,7 @@ 'original_name': 'Windows', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'windows', 'unique_id': 'LRW3F7EK4NC700000-windows', @@ -468,6 +477,7 @@ 'original_name': 'Charge port door', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_door_open', @@ -517,6 +527,7 @@ 'original_name': 'Frunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_ft', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_ft', @@ -566,6 +577,7 @@ 'original_name': 'Sunroof', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_sun_roof_state', @@ -615,6 +627,7 @@ 'original_name': 'Trunk', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rt', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_rt', @@ -664,6 +677,7 @@ 'original_name': 'Windows', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'windows', 'unique_id': 'LRW3F7EK4NC700000-windows', diff --git a/tests/components/teslemetry/snapshots/test_device_tracker.ambr b/tests/components/teslemetry/snapshots/test_device_tracker.ambr index b9e381ee42d..c71f818479a 100644 --- a/tests/components/teslemetry/snapshots/test_device_tracker.ambr +++ b/tests/components/teslemetry/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Location', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'LRW3F7EK4NC700000-location', @@ -78,6 +79,7 @@ 'original_name': 'Route', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'route', 'unique_id': 'LRW3F7EK4NC700000-route', diff --git a/tests/components/teslemetry/snapshots/test_lock.ambr b/tests/components/teslemetry/snapshots/test_lock.ambr index d6b29f0d7d4..e84c00e46de 100644 --- a/tests/components/teslemetry/snapshots/test_lock.ambr +++ b/tests/components/teslemetry/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge cable lock', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_latch', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_latch', @@ -75,6 +76,7 @@ 'original_name': 'Lock', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_locked', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_locked', @@ -123,6 +125,7 @@ 'original_name': 'Charge cable lock', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_latch', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_port_latch', @@ -171,6 +174,7 @@ 'original_name': 'Lock', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_locked', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_locked', diff --git a/tests/components/teslemetry/snapshots/test_media_player.ambr b/tests/components/teslemetry/snapshots/test_media_player.ambr index 7f721b95289..75f482700cc 100644 --- a/tests/components/teslemetry/snapshots/test_media_player.ambr +++ b/tests/components/teslemetry/snapshots/test_media_player.ambr @@ -28,6 +28,7 @@ 'original_name': 'Media player', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'media', 'unique_id': 'LRW3F7EK4NC700000-media', @@ -108,6 +109,7 @@ 'original_name': 'Media player', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'media', 'unique_id': 'LRW3F7EK4NC700000-media', diff --git a/tests/components/teslemetry/snapshots/test_number.ambr b/tests/components/teslemetry/snapshots/test_number.ambr index 2c6705074f3..70d7bfd33a9 100644 --- a/tests/components/teslemetry/snapshots/test_number.ambr +++ b/tests/components/teslemetry/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Backup reserve', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_reserve_percent', 'unique_id': '123456-backup_reserve_percent', @@ -91,6 +92,7 @@ 'original_name': 'Off-grid reserve', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_grid_vehicle_charging_reserve_percent', 'unique_id': '123456-off_grid_vehicle_charging_reserve_percent', @@ -150,6 +152,7 @@ 'original_name': 'Charge current', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_current_request', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_current_request', @@ -208,6 +211,7 @@ 'original_name': 'Charge limit', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_limit_soc', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_limit_soc', diff --git a/tests/components/teslemetry/snapshots/test_select.ambr b/tests/components/teslemetry/snapshots/test_select.ambr index 755a1a82c41..08b70a22569 100644 --- a/tests/components/teslemetry/snapshots/test_select.ambr +++ b/tests/components/teslemetry/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Allow export', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_customer_preferred_export_rule', 'unique_id': '123456-components_customer_preferred_export_rule', @@ -91,6 +92,7 @@ 'original_name': 'Operation mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'default_real_mode', 'unique_id': '123456-default_real_mode', @@ -150,6 +152,7 @@ 'original_name': 'Seat heater front left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_left', 'unique_id': 'LRW3F7EK4NC700000-climate_state_seat_heater_left', @@ -210,6 +213,7 @@ 'original_name': 'Seat heater front right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_right', 'unique_id': 'LRW3F7EK4NC700000-climate_state_seat_heater_right', @@ -270,6 +274,7 @@ 'original_name': 'Seat heater rear center', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_center', 'unique_id': 'LRW3F7EK4NC700000-climate_state_seat_heater_rear_center', @@ -330,6 +335,7 @@ 'original_name': 'Seat heater rear left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_left', 'unique_id': 'LRW3F7EK4NC700000-climate_state_seat_heater_rear_left', @@ -390,6 +396,7 @@ 'original_name': 'Seat heater rear right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_right', 'unique_id': 'LRW3F7EK4NC700000-climate_state_seat_heater_rear_right', @@ -449,6 +456,7 @@ 'original_name': 'Steering wheel heater', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_steering_wheel_heat_level', 'unique_id': 'LRW3F7EK4NC700000-climate_state_steering_wheel_heat_level', diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 13d87dbe88b..5c3a40ea979 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Battery charged', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_battery_charge', 'unique_id': '123456-total_battery_charge', @@ -109,6 +110,7 @@ 'original_name': 'Battery discharged', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_battery_discharge', 'unique_id': '123456-total_battery_discharge', @@ -183,6 +185,7 @@ 'original_name': 'Battery exported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_exported', 'unique_id': '123456-battery_energy_exported', @@ -257,6 +260,7 @@ 'original_name': 'Battery imported from generator', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_generator', 'unique_id': '123456-battery_energy_imported_from_generator', @@ -331,6 +335,7 @@ 'original_name': 'Battery imported from grid', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_grid', 'unique_id': '123456-battery_energy_imported_from_grid', @@ -405,6 +410,7 @@ 'original_name': 'Battery imported from solar', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_energy_imported_from_solar', 'unique_id': '123456-battery_energy_imported_from_solar', @@ -479,6 +485,7 @@ 'original_name': 'Battery power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_power', 'unique_id': '123456-battery_power', @@ -553,6 +560,7 @@ 'original_name': 'Consumer imported from battery', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_battery', 'unique_id': '123456-consumer_energy_imported_from_battery', @@ -627,6 +635,7 @@ 'original_name': 'Consumer imported from generator', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_generator', 'unique_id': '123456-consumer_energy_imported_from_generator', @@ -701,6 +710,7 @@ 'original_name': 'Consumer imported from grid', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_grid', 'unique_id': '123456-consumer_energy_imported_from_grid', @@ -775,6 +785,7 @@ 'original_name': 'Consumer imported from solar', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumer_energy_imported_from_solar', 'unique_id': '123456-consumer_energy_imported_from_solar', @@ -849,6 +860,7 @@ 'original_name': 'Energy left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_left', 'unique_id': '123456-energy_left', @@ -923,6 +935,7 @@ 'original_name': 'Generator exported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_energy_exported', 'unique_id': '123456-generator_energy_exported', @@ -997,6 +1010,7 @@ 'original_name': 'Generator power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_power', 'unique_id': '123456-generator_power', @@ -1071,6 +1085,7 @@ 'original_name': 'Grid exported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_grid_energy_exported', 'unique_id': '123456-total_grid_energy_exported', @@ -1145,6 +1160,7 @@ 'original_name': 'Grid exported from battery', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_battery', 'unique_id': '123456-grid_energy_exported_from_battery', @@ -1219,6 +1235,7 @@ 'original_name': 'Grid exported from generator', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_generator', 'unique_id': '123456-grid_energy_exported_from_generator', @@ -1293,6 +1310,7 @@ 'original_name': 'Grid exported from solar', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_exported_from_solar', 'unique_id': '123456-grid_energy_exported_from_solar', @@ -1367,6 +1385,7 @@ 'original_name': 'Grid imported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_energy_imported', 'unique_id': '123456-grid_energy_imported', @@ -1441,6 +1460,7 @@ 'original_name': 'Grid power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_power', 'unique_id': '123456-grid_power', @@ -1515,6 +1535,7 @@ 'original_name': 'Grid services exported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_energy_exported', 'unique_id': '123456-grid_services_energy_exported', @@ -1589,6 +1610,7 @@ 'original_name': 'Grid services imported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_energy_imported', 'unique_id': '123456-grid_services_energy_imported', @@ -1663,6 +1685,7 @@ 'original_name': 'Grid services power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_power', 'unique_id': '123456-grid_services_power', @@ -1737,6 +1760,7 @@ 'original_name': 'Home usage', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_home_usage', 'unique_id': '123456-total_home_usage', @@ -1811,6 +1835,7 @@ 'original_name': 'Island status', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'island_status', 'unique_id': '123456-island_status', @@ -1895,6 +1920,7 @@ 'original_name': 'Load power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_power', 'unique_id': '123456-load_power', @@ -1966,6 +1992,7 @@ 'original_name': 'Percentage charged', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'percentage_charged', 'unique_id': '123456-percentage_charged', @@ -2040,6 +2067,7 @@ 'original_name': 'Solar exported', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_energy_exported', 'unique_id': '123456-solar_energy_exported', @@ -2114,6 +2142,7 @@ 'original_name': 'Solar generated', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_solar_generation', 'unique_id': '123456-total_solar_generation', @@ -2188,6 +2217,7 @@ 'original_name': 'Solar power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_power', 'unique_id': '123456-solar_power', @@ -2262,6 +2292,7 @@ 'original_name': 'Total pack energy', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_pack_energy', 'unique_id': '123456-total_pack_energy', @@ -2328,6 +2359,7 @@ 'original_name': 'Version', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'version', 'unique_id': '123456-version', @@ -2388,6 +2420,7 @@ 'original_name': 'VPP backup reserve', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vpp_backup_reserve_percent', 'unique_id': '123456-vpp_backup_reserve_percent', @@ -2457,6 +2490,7 @@ 'original_name': 'Teslemetry credits', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'credit_balance', 'unique_id': 'abc-123_credit_balance', @@ -2526,6 +2560,7 @@ 'original_name': 'Battery level', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_level', 'unique_id': 'LRW3F7EK4NC700000-charge_state_battery_level', @@ -2600,6 +2635,7 @@ 'original_name': 'Battery range', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_range', 'unique_id': 'LRW3F7EK4NC700000-charge_state_battery_range', @@ -2666,6 +2702,7 @@ 'original_name': 'Charge cable', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_conn_charge_cable', 'unique_id': 'LRW3F7EK4NC700000-charge_state_conn_charge_cable', @@ -2731,6 +2768,7 @@ 'original_name': 'Charge energy added', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_energy_added', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_energy_added', @@ -2802,6 +2840,7 @@ 'original_name': 'Charge rate', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_rate', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charge_rate', @@ -2870,6 +2909,7 @@ 'original_name': 'Charger current', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_actual_current', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charger_actual_current', @@ -2938,6 +2978,7 @@ 'original_name': 'Charger power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_power', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charger_power', @@ -3006,6 +3047,7 @@ 'original_name': 'Charger voltage', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_voltage', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charger_voltage', @@ -3081,6 +3123,7 @@ 'original_name': 'Charging', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRW3F7EK4NC700000-charge_state_charging_state', @@ -3164,6 +3207,7 @@ 'original_name': 'Distance to arrival', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_miles_to_arrival', 'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_miles_to_arrival', @@ -3235,6 +3279,7 @@ 'original_name': 'Driver temperature setting', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_driver_temp_setting', 'unique_id': 'LRW3F7EK4NC700000-climate_state_driver_temp_setting', @@ -3309,6 +3354,7 @@ 'original_name': 'Estimate battery range', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_est_battery_range', 'unique_id': 'LRW3F7EK4NC700000-charge_state_est_battery_range', @@ -3375,6 +3421,7 @@ 'original_name': 'Fast charger type', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_fast_charger_type', 'unique_id': 'LRW3F7EK4NC700000-charge_state_fast_charger_type', @@ -3443,6 +3490,7 @@ 'original_name': 'Ideal battery range', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_ideal_battery_range', 'unique_id': 'LRW3F7EK4NC700000-charge_state_ideal_battery_range', @@ -3514,6 +3562,7 @@ 'original_name': 'Inside temperature', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_inside_temp', 'unique_id': 'LRW3F7EK4NC700000-climate_state_inside_temp', @@ -3588,6 +3637,7 @@ 'original_name': 'Odometer', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_odometer', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_odometer', @@ -3659,6 +3709,7 @@ 'original_name': 'Outside temperature', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_outside_temp', 'unique_id': 'LRW3F7EK4NC700000-climate_state_outside_temp', @@ -3730,6 +3781,7 @@ 'original_name': 'Passenger temperature setting', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_passenger_temp_setting', 'unique_id': 'LRW3F7EK4NC700000-climate_state_passenger_temp_setting', @@ -3798,6 +3850,7 @@ 'original_name': 'Power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_power', 'unique_id': 'LRW3F7EK4NC700000-drive_state_power', @@ -3871,6 +3924,7 @@ 'original_name': 'Shift state', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_shift_state', 'unique_id': 'LRW3F7EK4NC700000-drive_state_shift_state', @@ -3950,6 +4004,7 @@ 'original_name': 'Speed', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_speed', 'unique_id': 'LRW3F7EK4NC700000-drive_state_speed', @@ -4018,6 +4073,7 @@ 'original_name': 'State of charge at arrival', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_energy_at_arrival', 'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_energy_at_arrival', @@ -4084,6 +4140,7 @@ 'original_name': 'Time to arrival', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_minutes_to_arrival', 'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_minutes_to_arrival', @@ -4146,6 +4203,7 @@ 'original_name': 'Time to full charge', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_minutes_to_full_charge', 'unique_id': 'LRW3F7EK4NC700000-charge_state_minutes_to_full_charge', @@ -4216,6 +4274,7 @@ 'original_name': 'Tire pressure front left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fl', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_pressure_fl', @@ -4290,6 +4349,7 @@ 'original_name': 'Tire pressure front right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_pressure_fr', @@ -4364,6 +4424,7 @@ 'original_name': 'Tire pressure rear left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rl', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_pressure_rl', @@ -4438,6 +4499,7 @@ 'original_name': 'Tire pressure rear right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rr', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_tpms_pressure_rr', @@ -4506,6 +4568,7 @@ 'original_name': 'Traffic delay', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_traffic_minutes_delay', 'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_traffic_minutes_delay', @@ -4577,6 +4640,7 @@ 'original_name': 'Usable battery level', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_usable_battery_level', 'unique_id': 'LRW3F7EK4NC700000-charge_state_usable_battery_level', @@ -4643,6 +4707,7 @@ 'original_name': 'Fault state code', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_fault_state', 'unique_id': '123456-abd-123-wall_connector_fault_state', @@ -4703,6 +4768,7 @@ 'original_name': 'Fault state code', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_fault_state', 'unique_id': '123456-bcd-234-wall_connector_fault_state', @@ -4771,6 +4837,7 @@ 'original_name': 'Power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-abd-123-wall_connector_power', @@ -4845,6 +4912,7 @@ 'original_name': 'Power', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-bcd-234-wall_connector_power', @@ -4911,6 +4979,7 @@ 'original_name': 'State code', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-abd-123-wall_connector_state', @@ -4971,6 +5040,7 @@ 'original_name': 'State code', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-bcd-234-wall_connector_state', @@ -5031,6 +5101,7 @@ 'original_name': 'Vehicle', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-abd-123-vin', @@ -5091,6 +5162,7 @@ 'original_name': 'Vehicle', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-bcd-234-vin', diff --git a/tests/components/teslemetry/snapshots/test_switch.ambr b/tests/components/teslemetry/snapshots/test_switch.ambr index ffbfc06026e..bbcadd25a48 100644 --- a/tests/components/teslemetry/snapshots/test_switch.ambr +++ b/tests/components/teslemetry/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Allow charging from grid', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_disallow_charge_from_grid_with_solar_installed', 'unique_id': '123456-components_disallow_charge_from_grid_with_solar_installed', @@ -75,6 +76,7 @@ 'original_name': 'Storm watch', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'user_settings_storm_mode_enabled', 'unique_id': '123456-user_settings_storm_mode_enabled', @@ -123,6 +125,7 @@ 'original_name': 'Auto seat climate left', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_left', 'unique_id': 'LRW3F7EK4NC700000-climate_state_auto_seat_climate_left', @@ -171,6 +174,7 @@ 'original_name': 'Auto seat climate right', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_right', 'unique_id': 'LRW3F7EK4NC700000-climate_state_auto_seat_climate_right', @@ -219,6 +223,7 @@ 'original_name': 'Auto steering wheel heater', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_steering_wheel_heat', 'unique_id': 'LRW3F7EK4NC700000-climate_state_auto_steering_wheel_heat', @@ -267,6 +272,7 @@ 'original_name': 'Charge', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'LRW3F7EK4NC700000-charge_state_user_charge_enable_request', @@ -315,6 +321,7 @@ 'original_name': 'Defrost', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_defrost_mode', 'unique_id': 'LRW3F7EK4NC700000-climate_state_defrost_mode', @@ -363,6 +370,7 @@ 'original_name': 'Sentry mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_sentry_mode', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_sentry_mode', @@ -411,6 +419,7 @@ 'original_name': 'Valet mode', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_valet_mode', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_valet_mode', diff --git a/tests/components/teslemetry/snapshots/test_update.ambr b/tests/components/teslemetry/snapshots/test_update.ambr index 391d81c086e..6f939c667b2 100644 --- a/tests/components/teslemetry/snapshots/test_update.ambr +++ b/tests/components/teslemetry/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Update', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_software_update_status', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_software_update_status', @@ -86,6 +87,7 @@ 'original_name': 'Update', 'platform': 'teslemetry', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_software_update_status', 'unique_id': 'LRW3F7EK4NC700000-vehicle_state_software_update_status', diff --git a/tests/components/tessie/snapshots/test_binary_sensor.ambr b/tests/components/tessie/snapshots/test_binary_sensor.ambr index 2fe97b88811..e1875626f76 100644 --- a/tests/components/tessie/snapshots/test_binary_sensor.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Backup capable', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_capable', 'unique_id': '123456-backup_capable', @@ -74,6 +75,7 @@ 'original_name': 'Grid services active', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_active', 'unique_id': '123456-grid_services_active', @@ -121,6 +123,7 @@ 'original_name': 'Grid services enabled', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_grid_services_enabled', 'unique_id': '123456-components_grid_services_enabled', @@ -168,6 +171,7 @@ 'original_name': 'Storm watch active', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'storm_mode_active', 'unique_id': '123456-storm_mode_active', @@ -215,6 +219,7 @@ 'original_name': 'Auto seat climate left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_left', 'unique_id': 'VINVINVIN-climate_state_auto_seat_climate_left', @@ -262,6 +267,7 @@ 'original_name': 'Auto seat climate right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_seat_climate_right', 'unique_id': 'VINVINVIN-climate_state_auto_seat_climate_right', @@ -309,6 +315,7 @@ 'original_name': 'Auto steering wheel heater', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_auto_steering_wheel_heat', 'unique_id': 'VINVINVIN-climate_state_auto_steering_wheel_heat', @@ -356,6 +363,7 @@ 'original_name': 'Battery heater', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_battery_heater', 'unique_id': 'VINVINVIN-climate_state_battery_heater', @@ -404,6 +412,7 @@ 'original_name': 'Cabin overheat protection', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_cabin_overheat_protection', 'unique_id': 'VINVINVIN-climate_state_cabin_overheat_protection', @@ -452,6 +461,7 @@ 'original_name': 'Cabin overheat protection actively cooling', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_cabin_overheat_protection_actively_cooling', 'unique_id': 'VINVINVIN-climate_state_cabin_overheat_protection_actively_cooling', @@ -500,6 +510,7 @@ 'original_name': 'Charge cable', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_conn_charge_cable', 'unique_id': 'VINVINVIN-charge_state_conn_charge_cable', @@ -548,6 +559,7 @@ 'original_name': 'Charging', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'VINVINVIN-charge_state_charging_state', @@ -596,6 +608,7 @@ 'original_name': 'Dashcam', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dashcam_state', 'unique_id': 'VINVINVIN-vehicle_state_dashcam_state', @@ -644,6 +657,7 @@ 'original_name': 'Front driver door', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_df', 'unique_id': 'VINVINVIN-vehicle_state_df', @@ -692,6 +706,7 @@ 'original_name': 'Front driver window', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fd_window', 'unique_id': 'VINVINVIN-vehicle_state_fd_window', @@ -740,6 +755,7 @@ 'original_name': 'Front passenger door', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pf', 'unique_id': 'VINVINVIN-vehicle_state_pf', @@ -788,6 +804,7 @@ 'original_name': 'Front passenger window', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_fp_window', 'unique_id': 'VINVINVIN-vehicle_state_fp_window', @@ -836,6 +853,7 @@ 'original_name': 'Preconditioning enabled', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_preconditioning_enabled', 'unique_id': 'VINVINVIN-charge_state_preconditioning_enabled', @@ -883,6 +901,7 @@ 'original_name': 'Rear driver door', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_dr', 'unique_id': 'VINVINVIN-vehicle_state_dr', @@ -931,6 +950,7 @@ 'original_name': 'Rear driver window', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rd_window', 'unique_id': 'VINVINVIN-vehicle_state_rd_window', @@ -979,6 +999,7 @@ 'original_name': 'Rear passenger door', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_pr', 'unique_id': 'VINVINVIN-vehicle_state_pr', @@ -1027,6 +1048,7 @@ 'original_name': 'Rear passenger window', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_rp_window', 'unique_id': 'VINVINVIN-vehicle_state_rp_window', @@ -1075,6 +1097,7 @@ 'original_name': 'Scheduled charging pending', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_scheduled_charging_pending', 'unique_id': 'VINVINVIN-charge_state_scheduled_charging_pending', @@ -1122,6 +1145,7 @@ 'original_name': 'Status', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': 'VINVINVIN-state', @@ -1170,6 +1194,7 @@ 'original_name': 'Tire pressure warning front left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_soft_warning_fl', @@ -1218,6 +1243,7 @@ 'original_name': 'Tire pressure warning front right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_fr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_soft_warning_fr', @@ -1266,6 +1292,7 @@ 'original_name': 'Tire pressure warning rear left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_soft_warning_rl', @@ -1314,6 +1341,7 @@ 'original_name': 'Tire pressure warning rear right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_soft_warning_rr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_soft_warning_rr', @@ -1362,6 +1390,7 @@ 'original_name': 'Trip charging', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_trip_charging', 'unique_id': 'VINVINVIN-charge_state_trip_charging', @@ -1409,6 +1438,7 @@ 'original_name': 'User present', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_is_user_present', 'unique_id': 'VINVINVIN-vehicle_state_is_user_present', diff --git a/tests/components/tessie/snapshots/test_button.ambr b/tests/components/tessie/snapshots/test_button.ambr index 96ece94a1c9..fda5fe9a59f 100644 --- a/tests/components/tessie/snapshots/test_button.ambr +++ b/tests/components/tessie/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Flash lights', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'flash_lights', 'unique_id': 'VINVINVIN-flash_lights', @@ -74,6 +75,7 @@ 'original_name': 'Homelink', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'trigger_homelink', 'unique_id': 'VINVINVIN-trigger_homelink', @@ -121,6 +123,7 @@ 'original_name': 'Honk horn', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'honk', 'unique_id': 'VINVINVIN-honk', @@ -168,6 +171,7 @@ 'original_name': 'Keyless driving', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'enable_keyless_driving', 'unique_id': 'VINVINVIN-enable_keyless_driving', @@ -215,6 +219,7 @@ 'original_name': 'Play fart', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boombox', 'unique_id': 'VINVINVIN-boombox', @@ -262,6 +267,7 @@ 'original_name': 'Wake', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wake', 'unique_id': 'VINVINVIN-wake', diff --git a/tests/components/tessie/snapshots/test_climate.ambr b/tests/components/tessie/snapshots/test_climate.ambr index 415988e783e..50756cef338 100644 --- a/tests/components/tessie/snapshots/test_climate.ambr +++ b/tests/components/tessie/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': 'Climate', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'primary', 'unique_id': 'VINVINVIN-primary', diff --git a/tests/components/tessie/snapshots/test_cover.ambr b/tests/components/tessie/snapshots/test_cover.ambr index fdf2a967048..bcb2a13dbef 100644 --- a/tests/components/tessie/snapshots/test_cover.ambr +++ b/tests/components/tessie/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge port door', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'charge_state_charge_port_door_open', 'unique_id': 'VINVINVIN-charge_state_charge_port_door_open', @@ -76,6 +77,7 @@ 'original_name': 'Frunk', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_ft', 'unique_id': 'VINVINVIN-vehicle_state_ft', @@ -125,6 +127,7 @@ 'original_name': 'Sunroof', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_sun_roof_state', 'unique_id': 'VINVINVIN-vehicle_state_sun_roof_state', @@ -174,6 +177,7 @@ 'original_name': 'Trunk', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vehicle_state_rt', 'unique_id': 'VINVINVIN-vehicle_state_rt', @@ -223,6 +227,7 @@ 'original_name': 'Vent windows', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'windows', 'unique_id': 'VINVINVIN-windows', diff --git a/tests/components/tessie/snapshots/test_device_tracker.ambr b/tests/components/tessie/snapshots/test_device_tracker.ambr index 92502340aa2..5887d1abd2b 100644 --- a/tests/components/tessie/snapshots/test_device_tracker.ambr +++ b/tests/components/tessie/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Location', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'location', 'unique_id': 'VINVINVIN-location', @@ -80,6 +81,7 @@ 'original_name': 'Route', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'route', 'unique_id': 'VINVINVIN-route', diff --git a/tests/components/tessie/snapshots/test_lock.ambr b/tests/components/tessie/snapshots/test_lock.ambr index f819281d79b..57cbcd4434f 100644 --- a/tests/components/tessie/snapshots/test_lock.ambr +++ b/tests/components/tessie/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': 'Charge cable lock', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_port_latch', 'unique_id': 'VINVINVIN-charge_state_charge_port_latch', @@ -75,6 +76,7 @@ 'original_name': 'Lock', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_locked', 'unique_id': 'VINVINVIN-vehicle_state_locked', diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index 911598004a6..ff0f6c794a7 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -28,6 +28,7 @@ 'original_name': 'Media player', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'media', 'unique_id': 'VINVINVIN-media', diff --git a/tests/components/tessie/snapshots/test_number.ambr b/tests/components/tessie/snapshots/test_number.ambr index e865058c4a2..dd81c439e0c 100644 --- a/tests/components/tessie/snapshots/test_number.ambr +++ b/tests/components/tessie/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Backup reserve', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'backup_reserve_percent', 'unique_id': '123456-backup_reserve_percent', @@ -91,6 +92,7 @@ 'original_name': 'Off-grid reserve', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'off_grid_vehicle_charging_reserve_percent', 'unique_id': '123456-off_grid_vehicle_charging_reserve_percent', @@ -150,6 +152,7 @@ 'original_name': 'Charge current', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_current_request', 'unique_id': 'VINVINVIN-charge_state_charge_current_request', @@ -208,6 +211,7 @@ 'original_name': 'Charge limit', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_limit_soc', 'unique_id': 'VINVINVIN-charge_state_charge_limit_soc', @@ -266,6 +270,7 @@ 'original_name': 'Speed limit', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_speed_limit_mode_current_limit_mph', 'unique_id': 'VINVINVIN-vehicle_state_speed_limit_mode_current_limit_mph', diff --git a/tests/components/tessie/snapshots/test_select.ambr b/tests/components/tessie/snapshots/test_select.ambr index f118633aded..6a08b7b2b91 100644 --- a/tests/components/tessie/snapshots/test_select.ambr +++ b/tests/components/tessie/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Allow export', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_customer_preferred_export_rule', 'unique_id': '123456-components_customer_preferred_export_rule', @@ -91,6 +92,7 @@ 'original_name': 'Operation mode', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'default_real_mode', 'unique_id': '123456-default_real_mode', @@ -150,6 +152,7 @@ 'original_name': 'Seat cooler left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_fan_front_left', 'unique_id': 'VINVINVIN-climate_state_seat_fan_front_left', @@ -210,6 +213,7 @@ 'original_name': 'Seat cooler right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_fan_front_right', 'unique_id': 'VINVINVIN-climate_state_seat_fan_front_right', @@ -270,6 +274,7 @@ 'original_name': 'Seat heater left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_left', 'unique_id': 'VINVINVIN-climate_state_seat_heater_left', @@ -330,6 +335,7 @@ 'original_name': 'Seat heater rear center', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_center', 'unique_id': 'VINVINVIN-climate_state_seat_heater_rear_center', @@ -390,6 +396,7 @@ 'original_name': 'Seat heater rear left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_left', 'unique_id': 'VINVINVIN-climate_state_seat_heater_rear_left', @@ -450,6 +457,7 @@ 'original_name': 'Seat heater rear right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_rear_right', 'unique_id': 'VINVINVIN-climate_state_seat_heater_rear_right', @@ -510,6 +518,7 @@ 'original_name': 'Seat heater right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_seat_heater_right', 'unique_id': 'VINVINVIN-climate_state_seat_heater_right', diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index b40cf204bca..cad22558519 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -35,6 +35,7 @@ 'original_name': 'Battery power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_power', 'unique_id': '123456-battery_power', @@ -93,6 +94,7 @@ 'original_name': 'Energy left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_left', 'unique_id': '123456-energy_left', @@ -151,6 +153,7 @@ 'original_name': 'Generator power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'generator_power', 'unique_id': '123456-generator_power', @@ -209,6 +212,7 @@ 'original_name': 'Grid power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_power', 'unique_id': '123456-grid_power', @@ -267,6 +271,7 @@ 'original_name': 'Grid services power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'grid_services_power', 'unique_id': '123456-grid_services_power', @@ -325,6 +330,7 @@ 'original_name': 'Load power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_power', 'unique_id': '123456-load_power', @@ -380,6 +386,7 @@ 'original_name': 'Percentage charged', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'percentage_charged', 'unique_id': '123456-percentage_charged', @@ -438,6 +445,7 @@ 'original_name': 'Solar power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'solar_power', 'unique_id': '123456-solar_power', @@ -496,6 +504,7 @@ 'original_name': 'Total pack energy', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_pack_energy', 'unique_id': '123456-total_pack_energy', @@ -546,6 +555,7 @@ 'original_name': 'VPP backup reserve', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vpp_backup_reserve_percent', 'unique_id': '123456-vpp_backup_reserve_percent', @@ -597,6 +607,7 @@ 'original_name': 'Battery level', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_usable_battery_level', 'unique_id': 'VINVINVIN-charge_state_usable_battery_level', @@ -655,6 +666,7 @@ 'original_name': 'Battery range', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_battery_range', 'unique_id': 'VINVINVIN-charge_state_battery_range', @@ -713,6 +725,7 @@ 'original_name': 'Battery range estimate', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_est_battery_range', 'unique_id': 'VINVINVIN-charge_state_est_battery_range', @@ -771,6 +784,7 @@ 'original_name': 'Battery range ideal', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_ideal_battery_range', 'unique_id': 'VINVINVIN-charge_state_ideal_battery_range', @@ -826,6 +840,7 @@ 'original_name': 'Charge energy added', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_energy_added', 'unique_id': 'VINVINVIN-charge_state_charge_energy_added', @@ -881,6 +896,7 @@ 'original_name': 'Charge rate', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charge_rate', 'unique_id': 'VINVINVIN-charge_state_charge_rate', @@ -933,6 +949,7 @@ 'original_name': 'Charger current', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_actual_current', 'unique_id': 'VINVINVIN-charge_state_charger_actual_current', @@ -985,6 +1002,7 @@ 'original_name': 'Charger power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_power', 'unique_id': 'VINVINVIN-charge_state_charger_power', @@ -1037,6 +1055,7 @@ 'original_name': 'Charger voltage', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charger_voltage', 'unique_id': 'VINVINVIN-charge_state_charger_voltage', @@ -1096,6 +1115,7 @@ 'original_name': 'Charging', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'VINVINVIN-charge_state_charging_state', @@ -1152,6 +1172,7 @@ 'original_name': 'Destination', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_destination', 'unique_id': 'VINVINVIN-drive_state_active_route_destination', @@ -1204,6 +1225,7 @@ 'original_name': 'Distance to arrival', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_miles_to_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_miles_to_arrival', @@ -1259,6 +1281,7 @@ 'original_name': 'Driver temperature setting', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_driver_temp_setting', 'unique_id': 'VINVINVIN-climate_state_driver_temp_setting', @@ -1314,6 +1337,7 @@ 'original_name': 'Inside temperature', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_inside_temp', 'unique_id': 'VINVINVIN-climate_state_inside_temp', @@ -1372,6 +1396,7 @@ 'original_name': 'Odometer', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_odometer', 'unique_id': 'VINVINVIN-vehicle_state_odometer', @@ -1427,6 +1452,7 @@ 'original_name': 'Outside temperature', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_outside_temp', 'unique_id': 'VINVINVIN-climate_state_outside_temp', @@ -1482,6 +1508,7 @@ 'original_name': 'Passenger temperature setting', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_passenger_temp_setting', 'unique_id': 'VINVINVIN-climate_state_passenger_temp_setting', @@ -1534,6 +1561,7 @@ 'original_name': 'Power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_power', 'unique_id': 'VINVINVIN-drive_state_power', @@ -1591,6 +1619,7 @@ 'original_name': 'Shift state', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_shift_state', 'unique_id': 'VINVINVIN-drive_state_shift_state', @@ -1650,6 +1679,7 @@ 'original_name': 'Speed', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_speed', 'unique_id': 'VINVINVIN-drive_state_speed', @@ -1702,6 +1732,7 @@ 'original_name': 'State of charge at arrival', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_energy_at_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_energy_at_arrival', @@ -1752,6 +1783,7 @@ 'original_name': 'Time to arrival', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_minutes_to_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_minutes_to_arrival', @@ -1800,6 +1832,7 @@ 'original_name': 'Time to full charge', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_minutes_to_full_charge', 'unique_id': 'VINVINVIN-charge_state_minutes_to_full_charge', @@ -1856,6 +1889,7 @@ 'original_name': 'Tire pressure front left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fl', @@ -1914,6 +1948,7 @@ 'original_name': 'Tire pressure front right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fr', @@ -1972,6 +2007,7 @@ 'original_name': 'Tire pressure rear left', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rl', @@ -2030,6 +2066,7 @@ 'original_name': 'Tire pressure rear right', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rr', @@ -2082,6 +2119,7 @@ 'original_name': 'Traffic delay', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'drive_state_active_route_traffic_minutes_delay', 'unique_id': 'VINVINVIN-drive_state_active_route_traffic_minutes_delay', @@ -2140,6 +2178,7 @@ 'original_name': 'Power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-abd-123-wall_connector_power', @@ -2198,6 +2237,7 @@ 'original_name': 'Power', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_power', 'unique_id': '123456-bcd-234-wall_connector_power', @@ -2261,6 +2301,7 @@ 'original_name': 'State', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-abd-123-wall_connector_state', @@ -2334,6 +2375,7 @@ 'original_name': 'State', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wall_connector_state', 'unique_id': '123456-bcd-234-wall_connector_state', @@ -2394,6 +2436,7 @@ 'original_name': 'Vehicle', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-abd-123-vin', @@ -2441,6 +2484,7 @@ 'original_name': 'Vehicle', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vin', 'unique_id': '123456-bcd-234-vin', diff --git a/tests/components/tessie/snapshots/test_switch.ambr b/tests/components/tessie/snapshots/test_switch.ambr index 371ef822122..e0a59cd967b 100644 --- a/tests/components/tessie/snapshots/test_switch.ambr +++ b/tests/components/tessie/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Allow charging from grid', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'components_disallow_charge_from_grid_with_solar_installed', 'unique_id': '123456-components_disallow_charge_from_grid_with_solar_installed', @@ -74,6 +75,7 @@ 'original_name': 'Storm watch', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'user_settings_storm_mode_enabled', 'unique_id': '123456-user_settings_storm_mode_enabled', @@ -121,6 +123,7 @@ 'original_name': 'Charge', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_state_charging_state', 'unique_id': 'VINVINVIN-charge_state_charge_enable_request', @@ -169,6 +172,7 @@ 'original_name': 'Defrost mode', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_defrost_mode', 'unique_id': 'VINVINVIN-climate_state_defrost_mode', @@ -217,6 +221,7 @@ 'original_name': 'Sentry mode', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_sentry_mode', 'unique_id': 'VINVINVIN-vehicle_state_sentry_mode', @@ -265,6 +270,7 @@ 'original_name': 'Steering wheel heater', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'climate_state_steering_wheel_heater', 'unique_id': 'VINVINVIN-climate_state_steering_wheel_heater', @@ -313,6 +319,7 @@ 'original_name': 'Valet mode', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vehicle_state_valet_mode', 'unique_id': 'VINVINVIN-vehicle_state_valet_mode', diff --git a/tests/components/tessie/snapshots/test_update.ambr b/tests/components/tessie/snapshots/test_update.ambr index e4c25e2230f..8780f64bb09 100644 --- a/tests/components/tessie/snapshots/test_update.ambr +++ b/tests/components/tessie/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': 'Update', 'platform': 'tessie', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'update', 'unique_id': 'VINVINVIN-update', diff --git a/tests/components/tile/snapshots/test_binary_sensor.ambr b/tests/components/tile/snapshots/test_binary_sensor.ambr index 6de356ebf51..1a8cbdbff36 100644 --- a/tests/components/tile/snapshots/test_binary_sensor.ambr +++ b/tests/components/tile/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Lost', 'platform': 'tile', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lost', 'unique_id': 'user@host.com_19264d2dffdbca32_lost', diff --git a/tests/components/tile/snapshots/test_device_tracker.ambr b/tests/components/tile/snapshots/test_device_tracker.ambr index 3f94f679f10..069d66a42e6 100644 --- a/tests/components/tile/snapshots/test_device_tracker.ambr +++ b/tests/components/tile/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'tile', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tile', 'unique_id': 'user@host.com_19264d2dffdbca32', diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr index ac32b50762f..174ab96e8dc 100644 --- a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456', @@ -78,6 +79,7 @@ 'original_name': 'Partition 2', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'partition', 'unique_id': '123456_2', diff --git a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr index ac79455a0d5..75aaddf8572 100644 --- a/tests/components/totalconnect/snapshots/test_binary_sensor.ambr +++ b/tests/components/totalconnect/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_2_zone', @@ -78,6 +79,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_2_low_battery', @@ -129,6 +131,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_2_tamper', @@ -180,6 +183,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_3_zone', @@ -231,6 +235,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_3_low_battery', @@ -282,6 +287,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_3_tamper', @@ -333,6 +339,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_5_zone', @@ -384,6 +391,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_4_zone', @@ -435,6 +443,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_4_low_battery', @@ -486,6 +495,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_4_tamper', @@ -537,6 +547,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_1_zone', @@ -588,6 +599,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_1_low_battery', @@ -639,6 +651,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_1_tamper', @@ -690,6 +703,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_7_zone', @@ -741,6 +755,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_7_low_battery', @@ -792,6 +807,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_7_tamper', @@ -843,6 +859,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_low_battery', @@ -892,6 +909,7 @@ 'original_name': 'Carbon monoxide', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_carbon_monoxide', @@ -941,6 +959,7 @@ 'original_name': 'Police emergency', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'police', 'unique_id': '123456_police', @@ -989,6 +1008,7 @@ 'original_name': 'Power', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_power', @@ -1038,6 +1058,7 @@ 'original_name': 'Smoke', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_smoke', @@ -1087,6 +1108,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_tamper', @@ -1136,6 +1158,7 @@ 'original_name': None, 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_6_zone', @@ -1187,6 +1210,7 @@ 'original_name': 'Battery', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_6_low_battery', @@ -1238,6 +1262,7 @@ 'original_name': 'Tamper', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456_6_tamper', diff --git a/tests/components/totalconnect/snapshots/test_button.ambr b/tests/components/totalconnect/snapshots/test_button.ambr index 96d38567236..4367b035cc8 100644 --- a/tests/components/totalconnect/snapshots/test_button.ambr +++ b/tests/components/totalconnect/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Bypass', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', 'unique_id': '123456_2_bypass', @@ -74,6 +75,7 @@ 'original_name': 'Bypass', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', 'unique_id': '123456_3_bypass', @@ -121,6 +123,7 @@ 'original_name': 'Bypass', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', 'unique_id': '123456_4_bypass', @@ -168,6 +171,7 @@ 'original_name': 'Bypass', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass', 'unique_id': '123456_1_bypass', @@ -215,6 +219,7 @@ 'original_name': 'Bypass all', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bypass_all', 'unique_id': '123456_bypass_all', @@ -262,6 +267,7 @@ 'original_name': 'Clear bypass', 'platform': 'totalconnect', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clear_bypass', 'unique_id': '123456_clear_bypass', diff --git a/tests/components/tplink/snapshots/test_binary_sensor.ambr b/tests/components/tplink/snapshots/test_binary_sensor.ambr index 17aa2c248e5..c8251bccd4f 100644 --- a/tests/components/tplink/snapshots/test_binary_sensor.ambr +++ b/tests/components/tplink/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_low', 'unique_id': '123456789ABCDEFGH_battery_low', @@ -61,6 +62,7 @@ 'original_name': 'Cloud connection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cloud_connection', 'unique_id': '123456789ABCDEFGH_cloud_connection', @@ -109,6 +111,7 @@ 'original_name': 'Door', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'is_open', 'unique_id': '123456789ABCDEFGH_is_open', @@ -157,6 +160,7 @@ 'original_name': 'Humidity warning', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity_warning', 'unique_id': '123456789ABCDEFGH_humidity_warning', @@ -191,6 +195,7 @@ 'original_name': 'Moisture', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_alert', 'unique_id': '123456789ABCDEFGH_water_alert', @@ -239,6 +244,7 @@ 'original_name': 'Motion', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_detected', 'unique_id': '123456789ABCDEFGH_motion_detected', @@ -287,6 +293,7 @@ 'original_name': 'Overheated', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overheated', 'unique_id': '123456789ABCDEFGH_overheated', @@ -335,6 +342,7 @@ 'original_name': 'Overloaded', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'overloaded', 'unique_id': '123456789ABCDEFGH_overloaded', @@ -383,6 +391,7 @@ 'original_name': 'Temperature warning', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_warning', 'unique_id': '123456789ABCDEFGH_temperature_warning', diff --git a/tests/components/tplink/snapshots/test_button.ambr b/tests/components/tplink/snapshots/test_button.ambr index bb4e9f85d58..84cc8f73bf3 100644 --- a/tests/components/tplink/snapshots/test_button.ambr +++ b/tests/components/tplink/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Pair new device', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pair', 'unique_id': '123456789ABCDEFGH_pair', @@ -74,6 +75,7 @@ 'original_name': 'Pan left', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pan_left', 'unique_id': '123456789ABCDEFGH_pan_left', @@ -121,6 +123,7 @@ 'original_name': 'Pan right', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pan_right', 'unique_id': '123456789ABCDEFGH_pan_right', @@ -168,6 +171,7 @@ 'original_name': 'Reset charging contacts consumable', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_contacts_reset', 'unique_id': '123456789ABCDEFGH_charging_contacts_reset', @@ -202,6 +206,7 @@ 'original_name': 'Reset filter consumable', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_reset', 'unique_id': '123456789ABCDEFGH_filter_reset', @@ -236,6 +241,7 @@ 'original_name': 'Reset main brush consumable', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'main_brush_reset', 'unique_id': '123456789ABCDEFGH_main_brush_reset', @@ -270,6 +276,7 @@ 'original_name': 'Reset sensor consumable', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensor_reset', 'unique_id': '123456789ABCDEFGH_sensor_reset', @@ -304,6 +311,7 @@ 'original_name': 'Reset side brush consumable', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'side_brush_reset', 'unique_id': '123456789ABCDEFGH_side_brush_reset', @@ -338,6 +346,7 @@ 'original_name': 'Restart', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reboot', 'unique_id': '123456789ABCDEFGH_reboot', @@ -372,6 +381,7 @@ 'original_name': 'Stop alarm', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': 'my_device_stop_alarm', 'supported_features': 0, 'translation_key': 'stop_alarm', 'unique_id': '123456789ABCDEFGH_stop_alarm', @@ -419,6 +429,7 @@ 'original_name': 'Test alarm', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': 'my_device_test_alarm', 'supported_features': 0, 'translation_key': 'test_alarm', 'unique_id': '123456789ABCDEFGH_test_alarm', @@ -466,6 +477,7 @@ 'original_name': 'Tilt down', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tilt_down', 'unique_id': '123456789ABCDEFGH_tilt_down', @@ -513,6 +525,7 @@ 'original_name': 'Tilt up', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tilt_up', 'unique_id': '123456789ABCDEFGH_tilt_up', @@ -560,6 +573,7 @@ 'original_name': 'Unpair device', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'unpair', 'unique_id': '123456789ABCDEFGH_unpair', diff --git a/tests/components/tplink/snapshots/test_camera.ambr b/tests/components/tplink/snapshots/test_camera.ambr index 67749b30d1a..f50c5d70362 100644 --- a/tests/components/tplink/snapshots/test_camera.ambr +++ b/tests/components/tplink/snapshots/test_camera.ambr @@ -27,6 +27,7 @@ 'original_name': 'Live view', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'live_view', 'unique_id': '123456789ABCDEFGH-live_view', diff --git a/tests/components/tplink/snapshots/test_climate.ambr b/tests/components/tplink/snapshots/test_climate.ambr index 02492de92b9..df63291175a 100644 --- a/tests/components/tplink/snapshots/test_climate.ambr +++ b/tests/components/tplink/snapshots/test_climate.ambr @@ -34,6 +34,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH_climate', diff --git a/tests/components/tplink/snapshots/test_fan.ambr b/tests/components/tplink/snapshots/test_fan.ambr index 9c395dc2f21..ad0321accef 100644 --- a/tests/components/tplink/snapshots/test_fan.ambr +++ b/tests/components/tplink/snapshots/test_fan.ambr @@ -29,6 +29,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH', @@ -83,6 +84,7 @@ 'original_name': 'my_fan_0', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH00', @@ -137,6 +139,7 @@ 'original_name': 'my_fan_1', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH01', diff --git a/tests/components/tplink/snapshots/test_number.ambr b/tests/components/tplink/snapshots/test_number.ambr index 0415039a0ce..5ff1d9c5458 100644 --- a/tests/components/tplink/snapshots/test_number.ambr +++ b/tests/components/tplink/snapshots/test_number.ambr @@ -69,6 +69,7 @@ 'original_name': 'Clean count', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clean_count', 'unique_id': '123456789ABCDEFGH_clean_count', @@ -125,6 +126,7 @@ 'original_name': 'Pan degrees', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pan_step', 'unique_id': '123456789ABCDEFGH_pan_step', @@ -181,6 +183,7 @@ 'original_name': 'Power protection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_protection_threshold', 'unique_id': '123456789ABCDEFGH_power_protection_threshold', @@ -237,6 +240,7 @@ 'original_name': 'Smooth off', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smooth_transition_off', 'unique_id': '123456789ABCDEFGH_smooth_transition_off', @@ -293,6 +297,7 @@ 'original_name': 'Smooth on', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smooth_transition_on', 'unique_id': '123456789ABCDEFGH_smooth_transition_on', @@ -349,6 +354,7 @@ 'original_name': 'Temperature offset', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature_offset', 'unique_id': '123456789ABCDEFGH_temperature_offset', @@ -405,6 +411,7 @@ 'original_name': 'Tilt degrees', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tilt_step', 'unique_id': '123456789ABCDEFGH_tilt_step', @@ -461,6 +468,7 @@ 'original_name': 'Turn off in', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_off_minutes', 'unique_id': '123456789ABCDEFGH_auto_off_minutes', diff --git a/tests/components/tplink/snapshots/test_select.ambr b/tests/components/tplink/snapshots/test_select.ambr index e5191937ee9..9fc5181c45d 100644 --- a/tests/components/tplink/snapshots/test_select.ambr +++ b/tests/components/tplink/snapshots/test_select.ambr @@ -86,6 +86,7 @@ 'original_name': 'Alarm sound', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_sound', 'unique_id': '123456789ABCDEFGH_alarm_sound', @@ -160,6 +161,7 @@ 'original_name': 'Alarm volume', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_volume', 'unique_id': '123456789ABCDEFGH_alarm_volume', @@ -218,6 +220,7 @@ 'original_name': 'Light preset', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_preset', 'unique_id': '123456789ABCDEFGH_light_preset', diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 73fcdc8565d..47fc5a2bd35 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -64,6 +64,7 @@ 'original_name': 'Alarm source', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm_source', 'unique_id': '123456789ABCDEFGH_alarm_source', @@ -98,6 +99,7 @@ 'original_name': 'Auto-off at', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_off_at', 'unique_id': '123456789ABCDEFGH_auto_off_at', @@ -148,6 +150,7 @@ 'original_name': 'Battery', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_level', 'unique_id': '123456789ABCDEFGH_battery_level', @@ -201,6 +204,7 @@ 'original_name': 'Charging contacts remaining', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_contacts_remaining', 'unique_id': '123456789ABCDEFGH_charging_contacts_remaining', @@ -238,6 +242,7 @@ 'original_name': 'Charging contacts used', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charging_contacts_used', 'unique_id': '123456789ABCDEFGH_charging_contacts_used', @@ -277,6 +282,7 @@ 'original_name': 'Cleaning area', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clean_area', 'unique_id': '123456789ABCDEFGH_clean_area', @@ -329,6 +335,7 @@ 'original_name': 'Cleaning progress', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clean_progress', 'unique_id': '123456789ABCDEFGH_clean_progress', @@ -366,6 +373,7 @@ 'original_name': 'Cleaning time', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'clean_time', 'unique_id': '123456789ABCDEFGH_clean_time', @@ -420,6 +428,7 @@ 'original_name': 'Current', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current', 'unique_id': '123456789ABCDEFGH_current_a', @@ -475,6 +484,7 @@ 'original_name': 'Current consumption', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_consumption', 'unique_id': '123456789ABCDEFGH_current_power_w', @@ -525,6 +535,7 @@ 'original_name': 'Device time', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_time', 'unique_id': '123456789ABCDEFGH_device_time', @@ -574,6 +585,7 @@ 'original_name': 'Error', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vacuum_error', 'unique_id': '123456789ABCDEFGH_vacuum_error', @@ -639,6 +651,7 @@ 'original_name': 'Filter remaining', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_remaining', 'unique_id': '123456789ABCDEFGH_filter_remaining', @@ -676,6 +689,7 @@ 'original_name': 'Filter used', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_used', 'unique_id': '123456789ABCDEFGH_filter_used', @@ -712,6 +726,7 @@ 'original_name': 'Humidity', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'humidity', 'unique_id': '123456789ABCDEFGH_humidity', @@ -762,6 +777,7 @@ 'original_name': 'Last clean start', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_clean_timestamp', 'unique_id': '123456789ABCDEFGH_last_clean_timestamp', @@ -799,6 +815,7 @@ 'original_name': 'Last cleaned area', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_clean_area', 'unique_id': '123456789ABCDEFGH_last_clean_area', @@ -838,6 +855,7 @@ 'original_name': 'Last cleaned time', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_clean_time', 'unique_id': '123456789ABCDEFGH_last_clean_time', @@ -872,6 +890,7 @@ 'original_name': 'Last water leak alert', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_alert_timestamp', 'unique_id': '123456789ABCDEFGH_water_alert_timestamp', @@ -923,6 +942,7 @@ 'original_name': 'Main brush remaining', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'main_brush_remaining', 'unique_id': '123456789ABCDEFGH_main_brush_remaining', @@ -960,6 +980,7 @@ 'original_name': 'Main brush used', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'main_brush_used', 'unique_id': '123456789ABCDEFGH_main_brush_used', @@ -994,6 +1015,7 @@ 'original_name': 'On since', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'on_since', 'unique_id': '123456789ABCDEFGH_on_since', @@ -1028,6 +1050,7 @@ 'original_name': 'Report interval', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'report_interval', 'unique_id': '123456789ABCDEFGH_report_interval', @@ -1065,6 +1088,7 @@ 'original_name': 'Sensor remaining', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensor_remaining', 'unique_id': '123456789ABCDEFGH_sensor_remaining', @@ -1102,6 +1126,7 @@ 'original_name': 'Sensor used', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sensor_used', 'unique_id': '123456789ABCDEFGH_sensor_used', @@ -1139,6 +1164,7 @@ 'original_name': 'Side brush remaining', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'side_brush_remaining', 'unique_id': '123456789ABCDEFGH_side_brush_remaining', @@ -1176,6 +1202,7 @@ 'original_name': 'Side brush used', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'side_brush_used', 'unique_id': '123456789ABCDEFGH_side_brush_used', @@ -1212,6 +1239,7 @@ 'original_name': 'Signal level', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'signal_level', 'unique_id': '123456789ABCDEFGH_signal_level', @@ -1262,6 +1290,7 @@ 'original_name': 'Signal strength', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rssi', 'unique_id': '123456789ABCDEFGH_rssi', @@ -1296,6 +1325,7 @@ 'original_name': 'SSID', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ssid', 'unique_id': '123456789ABCDEFGH_ssid', @@ -1332,6 +1362,7 @@ 'original_name': 'Temperature', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'temperature', 'unique_id': '123456789ABCDEFGH_temperature', @@ -1371,6 +1402,7 @@ 'original_name': "This month's consumption", 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_this_month', 'unique_id': '123456789ABCDEFGH_consumption_this_month', @@ -1426,6 +1458,7 @@ 'original_name': "Today's consumption", 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_today', 'unique_id': '123456789ABCDEFGH_today_energy_kwh', @@ -1481,6 +1514,7 @@ 'original_name': 'Total cleaning area', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_clean_area', 'unique_id': '123456789ABCDEFGH_total_clean_area', @@ -1517,6 +1551,7 @@ 'original_name': 'Total cleaning count', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_clean_count', 'unique_id': '123456789ABCDEFGH_total_clean_count', @@ -1556,6 +1591,7 @@ 'original_name': 'Total cleaning time', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_clean_time', 'unique_id': '123456789ABCDEFGH_total_clean_time', @@ -1595,6 +1631,7 @@ 'original_name': 'Total consumption', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'consumption_total', 'unique_id': '123456789ABCDEFGH_total_energy_kwh', @@ -1650,6 +1687,7 @@ 'original_name': 'Voltage', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage', 'unique_id': '123456789ABCDEFGH_voltage', diff --git a/tests/components/tplink/snapshots/test_siren.ambr b/tests/components/tplink/snapshots/test_siren.ambr index 7365e449707..761df4fcf21 100644 --- a/tests/components/tplink/snapshots/test_siren.ambr +++ b/tests/components/tplink/snapshots/test_siren.ambr @@ -69,6 +69,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '123456789ABCDEFGH', diff --git a/tests/components/tplink/snapshots/test_switch.ambr b/tests/components/tplink/snapshots/test_switch.ambr index fd398434a07..4b04587db05 100644 --- a/tests/components/tplink/snapshots/test_switch.ambr +++ b/tests/components/tplink/snapshots/test_switch.ambr @@ -64,6 +64,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '123456789ABCDEFGH', @@ -111,6 +112,7 @@ 'original_name': 'Auto-off enabled', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_off_enabled', 'unique_id': '123456789ABCDEFGH_auto_off_enabled', @@ -158,6 +160,7 @@ 'original_name': 'Auto-update enabled', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_update_enabled', 'unique_id': '123456789ABCDEFGH_auto_update_enabled', @@ -205,6 +208,7 @@ 'original_name': 'Baby cry detection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'baby_cry_detection', 'unique_id': '123456789ABCDEFGH_baby_cry_detection', @@ -252,6 +256,7 @@ 'original_name': 'Carpet boost', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'carpet_boost', 'unique_id': '123456789ABCDEFGH_carpet_boost', @@ -299,6 +304,7 @@ 'original_name': 'Child lock', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'child_lock', 'unique_id': '123456789ABCDEFGH_child_lock', @@ -346,6 +352,7 @@ 'original_name': 'Fan sleep mode', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fan_sleep_mode', 'unique_id': '123456789ABCDEFGH_fan_sleep_mode', @@ -393,6 +400,7 @@ 'original_name': 'LED', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'led', 'unique_id': '123456789ABCDEFGH_led', @@ -440,6 +448,7 @@ 'original_name': 'Motion detection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'motion_detection', 'unique_id': '123456789ABCDEFGH_motion_detection', @@ -487,6 +496,7 @@ 'original_name': 'Motion sensor', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pir_enabled', 'unique_id': '123456789ABCDEFGH_pir_enabled', @@ -534,6 +544,7 @@ 'original_name': 'Person detection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'person_detection', 'unique_id': '123456789ABCDEFGH_person_detection', @@ -581,6 +592,7 @@ 'original_name': 'Smooth transitions', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'smooth_transitions', 'unique_id': '123456789ABCDEFGH_smooth_transitions', @@ -628,6 +640,7 @@ 'original_name': 'Tamper detection', 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tamper_detection', 'unique_id': '123456789ABCDEFGH_tamper_detection', diff --git a/tests/components/tplink/snapshots/test_vacuum.ambr b/tests/components/tplink/snapshots/test_vacuum.ambr index e010c9545d1..68d14270b55 100644 --- a/tests/components/tplink/snapshots/test_vacuum.ambr +++ b/tests/components/tplink/snapshots/test_vacuum.ambr @@ -69,6 +69,7 @@ 'original_name': None, 'platform': 'tplink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vacuum', 'unique_id': '123456789ABCDEFGH-vacuum', diff --git a/tests/components/tplink_omada/snapshots/test_sensor.ambr b/tests/components/tplink_omada/snapshots/test_sensor.ambr index 62167fc9d40..dde4c4b8e7a 100644 --- a/tests/components/tplink_omada/snapshots/test_sensor.ambr +++ b/tests/components/tplink_omada/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'CPU usage', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cpu_usage', 'unique_id': '54-AF-97-00-00-01_cpu_usage', @@ -88,6 +89,7 @@ 'original_name': 'Device status', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_status', 'unique_id': '54-AF-97-00-00-01_device_status', @@ -147,6 +149,7 @@ 'original_name': 'Memory usage', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mem_usage', 'unique_id': '54-AF-97-00-00-01_mem_usage', @@ -198,6 +201,7 @@ 'original_name': 'CPU usage', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cpu_usage', 'unique_id': 'AA-BB-CC-DD-EE-FF_cpu_usage', @@ -257,6 +261,7 @@ 'original_name': 'Device status', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_status', 'unique_id': 'AA-BB-CC-DD-EE-FF_device_status', @@ -316,6 +321,7 @@ 'original_name': 'Memory usage', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mem_usage', 'unique_id': 'AA-BB-CC-DD-EE-FF_mem_usage', diff --git a/tests/components/tplink_omada/snapshots/test_switch.ambr b/tests/components/tplink_omada/snapshots/test_switch.ambr index eae97f2aae1..513173248f0 100644 --- a/tests/components/tplink_omada/snapshots/test_switch.ambr +++ b/tests/components/tplink_omada/snapshots/test_switch.ambr @@ -92,6 +92,7 @@ 'original_name': 'Port 1 PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000001_poe', @@ -139,6 +140,7 @@ 'original_name': 'Port 2 (Renamed Port) PoE', 'platform': 'tplink_omada', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'poe_control', 'unique_id': '54-AF-97-00-00-01_000000000000000000000002_poe', diff --git a/tests/components/tractive/snapshots/test_binary_sensor.ambr b/tests/components/tractive/snapshots/test_binary_sensor.ambr index c7252da7a3b..150318cc753 100644 --- a/tests/components/tractive/snapshots/test_binary_sensor.ambr +++ b/tests/components/tractive/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Tracker battery charging', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_battery_charging', 'unique_id': 'pet_id_123_battery_charging', @@ -75,6 +76,7 @@ 'original_name': 'Tracker power saving', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_power_saving', 'unique_id': 'pet_id_123_power_saving', diff --git a/tests/components/tractive/snapshots/test_device_tracker.ambr b/tests/components/tractive/snapshots/test_device_tracker.ambr index ef511299e68..ca8a4b6d48b 100644 --- a/tests/components/tractive/snapshots/test_device_tracker.ambr +++ b/tests/components/tractive/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Tracker', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker', 'unique_id': 'pet_id_123', diff --git a/tests/components/tractive/snapshots/test_sensor.ambr b/tests/components/tractive/snapshots/test_sensor.ambr index 4551492e36e..af4222486b1 100644 --- a/tests/components/tractive/snapshots/test_sensor.ambr +++ b/tests/components/tractive/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Activity', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity', 'unique_id': 'pet_id_123_activity_label', @@ -88,6 +89,7 @@ 'original_name': 'Activity time', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_time', 'unique_id': 'pet_id_123_minutes_active', @@ -139,6 +141,7 @@ 'original_name': 'Calories burned', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calories', 'unique_id': 'pet_id_123_calories', @@ -188,6 +191,7 @@ 'original_name': 'Daily goal', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'daily_goal', 'unique_id': 'pet_id_123_daily_goal', @@ -238,6 +242,7 @@ 'original_name': 'Day sleep', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'minutes_day_sleep', 'unique_id': 'pet_id_123_minutes_day_sleep', @@ -289,6 +294,7 @@ 'original_name': 'Night sleep', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'minutes_night_sleep', 'unique_id': 'pet_id_123_minutes_night_sleep', @@ -340,6 +346,7 @@ 'original_name': 'Rest time', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rest_time', 'unique_id': 'pet_id_123_minutes_rest', @@ -395,6 +402,7 @@ 'original_name': 'Sleep', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sleep', 'unique_id': 'pet_id_123_sleep_label', @@ -448,6 +456,7 @@ 'original_name': 'Tracker battery', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_battery_level', 'unique_id': 'pet_id_123_battery_level', @@ -505,6 +514,7 @@ 'original_name': 'Tracker state', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_state', 'unique_id': 'pet_id_123_tracker_state', diff --git a/tests/components/tractive/snapshots/test_switch.ambr b/tests/components/tractive/snapshots/test_switch.ambr index d443611ef92..f83436e9a60 100644 --- a/tests/components/tractive/snapshots/test_switch.ambr +++ b/tests/components/tractive/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Live tracking', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'live_tracking', 'unique_id': 'pet_id_123_live_tracking', @@ -74,6 +75,7 @@ 'original_name': 'Tracker buzzer', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_buzzer', 'unique_id': 'pet_id_123_buzzer', @@ -121,6 +123,7 @@ 'original_name': 'Tracker LED', 'platform': 'tractive', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tracker_led', 'unique_id': 'pet_id_123_led', diff --git a/tests/components/twentemilieu/snapshots/test_calendar.ambr b/tests/components/twentemilieu/snapshots/test_calendar.ambr index 0576fcd6a70..915c0f5080e 100644 --- a/tests/components/twentemilieu/snapshots/test_calendar.ambr +++ b/tests/components/twentemilieu/snapshots/test_calendar.ambr @@ -72,6 +72,7 @@ 'original_name': None, 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'calendar', 'unique_id': '12345', diff --git a/tests/components/twentemilieu/snapshots/test_sensor.ambr b/tests/components/twentemilieu/snapshots/test_sensor.ambr index b40ac0ba9e6..9e8bb6f7381 100644 --- a/tests/components/twentemilieu/snapshots/test_sensor.ambr +++ b/tests/components/twentemilieu/snapshots/test_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': 'Christmas tree pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'christmas_tree_pickup', 'unique_id': 'twentemilieu_12345_tree', @@ -122,6 +123,7 @@ 'original_name': 'Non-recyclable waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'non_recyclable_waste_pickup', 'unique_id': 'twentemilieu_12345_Non-recyclable', @@ -203,6 +205,7 @@ 'original_name': 'Organic waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'organic_waste_pickup', 'unique_id': 'twentemilieu_12345_Organic', @@ -284,6 +287,7 @@ 'original_name': 'Packages waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'packages_waste_pickup', 'unique_id': 'twentemilieu_12345_Plastic', @@ -365,6 +369,7 @@ 'original_name': 'Paper waste pickup', 'platform': 'twentemilieu', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'paper_waste_pickup', 'unique_id': 'twentemilieu_12345_Paper', diff --git a/tests/components/twinkly/snapshots/test_light.ambr b/tests/components/twinkly/snapshots/test_light.ambr index 77a97a0cdd9..5b5137d2b73 100644 --- a/tests/components/twinkly/snapshots/test_light.ambr +++ b/tests/components/twinkly/snapshots/test_light.ambr @@ -35,6 +35,7 @@ 'original_name': None, 'platform': 'twinkly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'light', 'unique_id': '00:2d:13:3b:aa:bb', diff --git a/tests/components/twinkly/snapshots/test_select.ambr b/tests/components/twinkly/snapshots/test_select.ambr index 6700aecd1f2..58d796ea2e4 100644 --- a/tests/components/twinkly/snapshots/test_select.ambr +++ b/tests/components/twinkly/snapshots/test_select.ambr @@ -37,6 +37,7 @@ 'original_name': 'Mode', 'platform': 'twinkly', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mode', 'unique_id': '00:2d:13:3b:aa:bb_mode', diff --git a/tests/components/unifi/snapshots/test_button.ambr b/tests/components/unifi/snapshots/test_button.ambr index 369b0823063..b0fbe9cdbb8 100644 --- a/tests/components/unifi/snapshots/test_button.ambr +++ b/tests/components/unifi/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Regenerate Password', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wlan_regenerate_password', 'unique_id': 'regenerate_password-012345678910111213141516', @@ -75,6 +76,7 @@ 'original_name': 'Port 1 Power Cycle', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'power_cycle-00:00:00:00:01:01_1', @@ -123,6 +125,7 @@ 'original_name': 'Restart', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'device_restart-00:00:00:00:01:01', diff --git a/tests/components/unifi/snapshots/test_device_tracker.ambr b/tests/components/unifi/snapshots/test_device_tracker.ambr index 5d3407e4e8e..2a8af0dd765 100644 --- a/tests/components/unifi/snapshots/test_device_tracker.ambr +++ b/tests/components/unifi/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'Switch 1', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '00:00:00:00:01:01', @@ -77,6 +78,7 @@ 'original_name': 'wd_client_1', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'site_id-00:00:00:00:00:02', @@ -127,6 +129,7 @@ 'original_name': 'ws_client_1', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'site_id-00:00:00:00:00:01', diff --git a/tests/components/unifi/snapshots/test_image.ambr b/tests/components/unifi/snapshots/test_image.ambr index 05cca2c305b..d27e9ade3aa 100644 --- a/tests/components/unifi/snapshots/test_image.ambr +++ b/tests/components/unifi/snapshots/test_image.ambr @@ -27,6 +27,7 @@ 'original_name': 'QR Code', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wlan_qr_code', 'unique_id': 'qr_code-012345678910111213141516', diff --git a/tests/components/unifi/snapshots/test_sensor.ambr b/tests/components/unifi/snapshots/test_sensor.ambr index 4d109f630c5..9f0c5f39a9d 100644 --- a/tests/components/unifi/snapshots/test_sensor.ambr +++ b/tests/components/unifi/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Clients', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_clients', 'unique_id': 'device_clients-20:00:00:00:01:01', @@ -92,6 +93,7 @@ 'original_name': 'State', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_state', 'unique_id': 'device_state-20:00:00:00:01:01', @@ -154,6 +156,7 @@ 'original_name': 'Temperature', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'device_temperature-20:00:00:00:01:01', @@ -203,6 +206,7 @@ 'original_name': 'Uptime', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'device_uptime-20:00:00:00:01:01', @@ -256,6 +260,7 @@ 'original_name': 'AC Power Budget', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ac_power_budget-01:02:03:04:05:ff', @@ -311,6 +316,7 @@ 'original_name': 'AC Power Consumption', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'ac_power_conumption-01:02:03:04:05:ff', @@ -363,6 +369,7 @@ 'original_name': 'Clients', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_clients', 'unique_id': 'device_clients-01:02:03:04:05:ff', @@ -413,6 +420,7 @@ 'original_name': 'CPU utilization', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_cpu_utilization', 'unique_id': 'cpu_utilization-01:02:03:04:05:ff', @@ -464,6 +472,7 @@ 'original_name': 'Memory utilization', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_memory_utilization', 'unique_id': 'memory_utilization-01:02:03:04:05:ff', @@ -515,6 +524,7 @@ 'original_name': 'Outlet 2 Outlet Power', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet_power-01:02:03:04:05:ff_2', @@ -580,6 +590,7 @@ 'original_name': 'State', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_state', 'unique_id': 'device_state-01:02:03:04:05:ff', @@ -642,6 +653,7 @@ 'original_name': 'Uptime', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'device_uptime-01:02:03:04:05:ff', @@ -692,6 +704,7 @@ 'original_name': 'Clients', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_clients', 'unique_id': 'device_clients-10:00:00:00:01:01', @@ -742,6 +755,7 @@ 'original_name': 'Cloudflare WAN2 latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'cloudflare_wan2_latency-10:00:00:00:01:01', @@ -794,6 +808,7 @@ 'original_name': 'Cloudflare WAN latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'cloudflare_wan_latency-10:00:00:00:01:01', @@ -846,6 +861,7 @@ 'original_name': 'Google WAN2 latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'google_wan2_latency-10:00:00:00:01:01', @@ -898,6 +914,7 @@ 'original_name': 'Google WAN latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'google_wan_latency-10:00:00:00:01:01', @@ -950,6 +967,7 @@ 'original_name': 'Microsoft WAN2 latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'microsoft_wan2_latency-10:00:00:00:01:01', @@ -1002,6 +1020,7 @@ 'original_name': 'Microsoft WAN latency', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'microsoft_wan_latency-10:00:00:00:01:01', @@ -1054,6 +1073,7 @@ 'original_name': 'Port 1 PoE Power', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'poe_power-10:00:00:00:01:01_1', @@ -1109,6 +1129,7 @@ 'original_name': 'Port 1 RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_rx', 'unique_id': 'port_rx-10:00:00:00:01:01_1', @@ -1164,6 +1185,7 @@ 'original_name': 'Port 1 TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_tx', 'unique_id': 'port_tx-10:00:00:00:01:01_1', @@ -1216,6 +1238,7 @@ 'original_name': 'Port 2 PoE Power', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'poe_power-10:00:00:00:01:01_2', @@ -1271,6 +1294,7 @@ 'original_name': 'Port 2 RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_rx', 'unique_id': 'port_rx-10:00:00:00:01:01_2', @@ -1326,6 +1350,7 @@ 'original_name': 'Port 2 TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_tx', 'unique_id': 'port_tx-10:00:00:00:01:01_2', @@ -1381,6 +1406,7 @@ 'original_name': 'Port 3 RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_rx', 'unique_id': 'port_rx-10:00:00:00:01:01_3', @@ -1436,6 +1462,7 @@ 'original_name': 'Port 3 TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_tx', 'unique_id': 'port_tx-10:00:00:00:01:01_3', @@ -1488,6 +1515,7 @@ 'original_name': 'Port 4 PoE Power', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'poe_power-10:00:00:00:01:01_4', @@ -1543,6 +1571,7 @@ 'original_name': 'Port 4 RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_rx', 'unique_id': 'port_rx-10:00:00:00:01:01_4', @@ -1598,6 +1627,7 @@ 'original_name': 'Port 4 TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_bandwidth_tx', 'unique_id': 'port_tx-10:00:00:00:01:01_4', @@ -1663,6 +1693,7 @@ 'original_name': 'State', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_state', 'unique_id': 'device_state-10:00:00:00:01:01', @@ -1725,6 +1756,7 @@ 'original_name': 'Uptime', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'device_uptime-10:00:00:00:01:01', @@ -1775,6 +1807,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wlan_clients', 'unique_id': 'wlan_clients-012345678910111213141516', @@ -1825,6 +1858,7 @@ 'original_name': 'RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'client_bandwidth_rx', 'unique_id': 'rx-00:00:00:00:00:01', @@ -1877,6 +1911,7 @@ 'original_name': 'TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'client_bandwidth_tx', 'unique_id': 'tx-00:00:00:00:00:01', @@ -1927,6 +1962,7 @@ 'original_name': 'Uptime', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'uptime-00:00:00:00:00:01', @@ -1977,6 +2013,7 @@ 'original_name': 'RX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'client_bandwidth_rx', 'unique_id': 'rx-00:00:00:00:00:02', @@ -2029,6 +2066,7 @@ 'original_name': 'TX', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'client_bandwidth_tx', 'unique_id': 'tx-00:00:00:00:00:02', @@ -2079,6 +2117,7 @@ 'original_name': 'Uptime', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'uptime-00:00:00:00:00:02', diff --git a/tests/components/unifi/snapshots/test_switch.ambr b/tests/components/unifi/snapshots/test_switch.ambr index c07a4799b5a..017fe237025 100644 --- a/tests/components/unifi/snapshots/test_switch.ambr +++ b/tests/components/unifi/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'block_client', 'unique_id': 'block-00:00:00:00:01:01', @@ -75,6 +76,7 @@ 'original_name': 'Block Media Streaming', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dpi_restriction', 'unique_id': '5f976f4ae3c58f018ec7dff6', @@ -122,6 +124,7 @@ 'original_name': 'Outlet 2', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet-01:02:03:04:05:ff_2', @@ -170,6 +173,7 @@ 'original_name': 'USB Outlet 1', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet-01:02:03:04:05:ff_1', @@ -218,6 +222,7 @@ 'original_name': 'Port 1 PoE', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'poe_port_control', 'unique_id': 'poe-10:00:00:00:01:01_1', @@ -266,6 +271,7 @@ 'original_name': 'Port 2 PoE', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'poe_port_control', 'unique_id': 'poe-10:00:00:00:01:01_2', @@ -314,6 +320,7 @@ 'original_name': 'Port 4 PoE', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'poe_port_control', 'unique_id': 'poe-10:00:00:00:01:01_4', @@ -362,6 +369,7 @@ 'original_name': 'Outlet 1', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet-fc:ec:da:76:4f:5f_1', @@ -410,6 +418,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wlan_control', 'unique_id': 'wlan-012345678910111213141516', @@ -458,6 +467,7 @@ 'original_name': 'plex', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'port_forward_control', 'unique_id': 'port_forward-5a32aa4ee4b0412345678911', @@ -506,6 +516,7 @@ 'original_name': 'Test Traffic Rule', 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'traffic_rule_control', 'unique_id': 'traffic_rule-6452cd9b859d5b11aa002ea1', diff --git a/tests/components/unifi/snapshots/test_update.ambr b/tests/components/unifi/snapshots/test_update.ambr index ef3803ac53d..caa23768857 100644 --- a/tests/components/unifi/snapshots/test_update.ambr +++ b/tests/components/unifi/snapshots/test_update.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'device_update-00:00:00:00:01:01', @@ -87,6 +88,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'device_update-00:00:00:00:01:02', @@ -147,6 +149,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'device_update-00:00:00:00:01:01', @@ -207,6 +210,7 @@ 'original_name': None, 'platform': 'unifi', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'device_update-00:00:00:00:01:02', diff --git a/tests/components/uptime/snapshots/test_sensor.ambr b/tests/components/uptime/snapshots/test_sensor.ambr index d6d896dbcec..5c9ed6d4683 100644 --- a/tests/components/uptime/snapshots/test_sensor.ambr +++ b/tests/components/uptime/snapshots/test_sensor.ambr @@ -41,6 +41,7 @@ 'original_name': None, 'platform': 'uptime', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unit_of_measurement': None, diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 46054b21324..32b4e1b6bb4 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Battery power', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery_power', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_battery_power', @@ -81,6 +82,7 @@ 'original_name': 'Charge energy', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_energy', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_charge_energy', @@ -133,6 +135,7 @@ 'original_name': 'Charge power', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_power', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_charge_power', @@ -185,6 +188,7 @@ 'original_name': 'Charge time', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'charge_time', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_charge_time', @@ -237,6 +241,7 @@ 'original_name': 'House power', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'house_power', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_house_power', @@ -289,6 +294,7 @@ 'original_name': 'Installation voltage', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'voltage_installation', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_voltage_installation', @@ -339,6 +345,7 @@ 'original_name': 'IP address', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ip_address', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_ip_address', @@ -424,6 +431,7 @@ 'original_name': 'Meter error', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'meter_error', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_meter_error', @@ -511,6 +519,7 @@ 'original_name': 'Photovoltaic power', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fv_power', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_fv_power', @@ -563,6 +572,7 @@ 'original_name': 'Signal status', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'signal_status', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_signal_status', @@ -611,6 +621,7 @@ 'original_name': 'SSID', 'platform': 'v2c', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ssid', 'unique_id': 'da58ee91f38c2406c2a36d0a1a7f8569_ssid', diff --git a/tests/components/velbus/snapshots/test_binary_sensor.ambr b/tests/components/velbus/snapshots/test_binary_sensor.ambr index 70db53257a1..6ba8ad096c0 100644 --- a/tests/components/velbus/snapshots/test_binary_sensor.ambr +++ b/tests/components/velbus/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'ButtonOn', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-1', diff --git a/tests/components/velbus/snapshots/test_button.ambr b/tests/components/velbus/snapshots/test_button.ambr index 856ebdb1e21..7b06cbfb548 100644 --- a/tests/components/velbus/snapshots/test_button.ambr +++ b/tests/components/velbus/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'ButtonOn', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-1', diff --git a/tests/components/velbus/snapshots/test_climate.ambr b/tests/components/velbus/snapshots/test_climate.ambr index 1d1f49d14d9..027f06c3858 100644 --- a/tests/components/velbus/snapshots/test_climate.ambr +++ b/tests/components/velbus/snapshots/test_climate.ambr @@ -40,6 +40,7 @@ 'original_name': 'Temperature', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'asdfghjk-3', diff --git a/tests/components/velbus/snapshots/test_cover.ambr b/tests/components/velbus/snapshots/test_cover.ambr index 0be18034bc0..53b6c921e23 100644 --- a/tests/components/velbus/snapshots/test_cover.ambr +++ b/tests/components/velbus/snapshots/test_cover.ambr @@ -27,6 +27,7 @@ 'original_name': 'CoverName', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1234-9', @@ -76,6 +77,7 @@ 'original_name': 'CoverNameNoPos', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '12345-11', diff --git a/tests/components/velbus/snapshots/test_light.ambr b/tests/components/velbus/snapshots/test_light.ambr index 6dd2ca4939d..44240415797 100644 --- a/tests/components/velbus/snapshots/test_light.ambr +++ b/tests/components/velbus/snapshots/test_light.ambr @@ -31,6 +31,7 @@ 'original_name': 'LED ButtonOn', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-1', @@ -87,6 +88,7 @@ 'original_name': 'Dimmer', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6g7-10', diff --git a/tests/components/velbus/snapshots/test_select.ambr b/tests/components/velbus/snapshots/test_select.ambr index 94bb109fc71..1137563698d 100644 --- a/tests/components/velbus/snapshots/test_select.ambr +++ b/tests/components/velbus/snapshots/test_select.ambr @@ -34,6 +34,7 @@ 'original_name': 'select', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'qwerty1234567-33-program_select', diff --git a/tests/components/velbus/snapshots/test_sensor.ambr b/tests/components/velbus/snapshots/test_sensor.ambr index 6f562f399af..8aebb226060 100644 --- a/tests/components/velbus/snapshots/test_sensor.ambr +++ b/tests/components/velbus/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'ButtonCounter', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-2', @@ -81,6 +82,7 @@ 'original_name': 'ButtonCounter-counter', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-2-counter', @@ -134,6 +136,7 @@ 'original_name': 'LightSensor', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-4', @@ -185,6 +188,7 @@ 'original_name': 'SensorNumber', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a1b2c3d4e5f6-3', @@ -236,6 +240,7 @@ 'original_name': 'Temperature', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'asdfghjk-3', diff --git a/tests/components/velbus/snapshots/test_switch.ambr b/tests/components/velbus/snapshots/test_switch.ambr index 60458b196a8..7eb886cdd7b 100644 --- a/tests/components/velbus/snapshots/test_switch.ambr +++ b/tests/components/velbus/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'RelayName', 'platform': 'velbus', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'qwerty123-55', diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 412bd8a1b2e..fe330b82ca7 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -68,6 +68,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vesync', 'unique_id': 'air-purifier', @@ -167,6 +168,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vesync', 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55', @@ -267,6 +269,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vesync', 'unique_id': '400s-purifier', @@ -368,6 +371,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vesync', 'unique_id': '600s-purifier', @@ -666,6 +670,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'vesync', 'unique_id': 'smarttowerfan', diff --git a/tests/components/vesync/snapshots/test_light.ambr b/tests/components/vesync/snapshots/test_light.ambr index bed711b1040..20bf56ef9c4 100644 --- a/tests/components/vesync/snapshots/test_light.ambr +++ b/tests/components/vesync/snapshots/test_light.ambr @@ -223,6 +223,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'dimmable-bulb', @@ -315,6 +316,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'dimmable-switch', @@ -569,6 +571,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'tunable-bulb', diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index ecae8fa7674..4ab9a38548a 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -65,6 +65,7 @@ 'original_name': 'Filter lifetime', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_life', 'unique_id': 'air-purifier-filter-life', @@ -97,6 +98,7 @@ 'original_name': 'Air quality', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': 'air-purifier-air-quality', @@ -198,6 +200,7 @@ 'original_name': 'Filter lifetime', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_life', 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55-filter-life', @@ -286,6 +289,7 @@ 'original_name': 'Filter lifetime', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_life', 'unique_id': '400s-purifier-filter-life', @@ -318,6 +322,7 @@ 'original_name': 'Air quality', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '400s-purifier-air-quality', @@ -352,6 +357,7 @@ 'original_name': 'PM2.5', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '400s-purifier-pm25', @@ -469,6 +475,7 @@ 'original_name': 'Filter lifetime', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'filter_life', 'unique_id': '600s-purifier-filter-life', @@ -501,6 +508,7 @@ 'original_name': 'Air quality', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_quality', 'unique_id': '600s-purifier-air-quality', @@ -535,6 +543,7 @@ 'original_name': 'PM2.5', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '600s-purifier-pm25', @@ -730,6 +739,7 @@ 'original_name': 'Humidity', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '200s-humidifier4321-humidity', @@ -819,6 +829,7 @@ 'original_name': 'Humidity', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '600s-humidifier-humidity', @@ -908,6 +919,7 @@ 'original_name': 'Current power', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_power', 'unique_id': 'outlet-power', @@ -942,6 +954,7 @@ 'original_name': 'Energy use today', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_today', 'unique_id': 'outlet-energy', @@ -976,6 +989,7 @@ 'original_name': 'Energy use weekly', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_week', 'unique_id': 'outlet-energy-weekly', @@ -1010,6 +1024,7 @@ 'original_name': 'Energy use monthly', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_month', 'unique_id': 'outlet-energy-monthly', @@ -1044,6 +1059,7 @@ 'original_name': 'Energy use yearly', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_year', 'unique_id': 'outlet-energy-yearly', @@ -1078,6 +1094,7 @@ 'original_name': 'Current voltage', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'current_voltage', 'unique_id': 'outlet-voltage', diff --git a/tests/components/vesync/snapshots/test_switch.ambr b/tests/components/vesync/snapshots/test_switch.ambr index f25aaf3d51b..edd2eee8b1f 100644 --- a/tests/components/vesync/snapshots/test_switch.ambr +++ b/tests/components/vesync/snapshots/test_switch.ambr @@ -63,6 +63,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': 'air-purifier-display', @@ -147,6 +148,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': 'asd_sdfKIHG7IJHGwJGJ7GJ_ag5h3G55-display', @@ -231,6 +233,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': '400s-purifier-display', @@ -315,6 +318,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': '600s-purifier-display', @@ -477,6 +481,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': '200s-humidifier4321-display', @@ -561,6 +566,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': '600s-humidifier-display', @@ -645,6 +651,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'outlet-device_status', @@ -730,6 +737,7 @@ 'original_name': 'Display', 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'display', 'unique_id': 'smarttowerfan-display', @@ -853,6 +861,7 @@ 'original_name': None, 'platform': 'vesync', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'switch-device_status', diff --git a/tests/components/vicare/snapshots/test_binary_sensor.ambr b/tests/components/vicare/snapshots/test_binary_sensor.ambr index 93e407ea505..7a6e09c55a5 100644 --- a/tests/components/vicare/snapshots/test_binary_sensor.ambr +++ b/tests/components/vicare/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Burner', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'burner', 'unique_id': 'gateway0_deviceSerialVitodens300W-burner_active-0', @@ -75,6 +76,7 @@ 'original_name': 'Circulation pump', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'circulation_pump', 'unique_id': 'gateway0_deviceSerialVitodens300W-circulationpump_active-0', @@ -123,6 +125,7 @@ 'original_name': 'Circulation pump', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'circulation_pump', 'unique_id': 'gateway0_deviceSerialVitodens300W-circulationpump_active-1', @@ -171,6 +174,7 @@ 'original_name': 'DHW charging', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'domestic_hot_water_charging', 'unique_id': 'gateway0_deviceSerialVitodens300W-charging_active', @@ -219,6 +223,7 @@ 'original_name': 'DHW circulation pump', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'domestic_hot_water_circulation_pump', 'unique_id': 'gateway0_deviceSerialVitodens300W-dhw_circulationpump_active', @@ -267,6 +272,7 @@ 'original_name': 'DHW pump', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'domestic_hot_water_pump', 'unique_id': 'gateway0_deviceSerialVitodens300W-dhw_pump_active', @@ -315,6 +321,7 @@ 'original_name': 'Frost protection', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frost_protection', 'unique_id': 'gateway0_deviceSerialVitodens300W-frost_protection_active-0', @@ -362,6 +369,7 @@ 'original_name': 'Frost protection', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'frost_protection', 'unique_id': 'gateway0_deviceSerialVitodens300W-frost_protection_active-1', @@ -409,6 +417,7 @@ 'original_name': 'One-time charge', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'one_time_charge', 'unique_id': 'gateway0_deviceSerialVitodens300W-one_time_charge', diff --git a/tests/components/vicare/snapshots/test_button.ambr b/tests/components/vicare/snapshots/test_button.ambr index 17dfc29e96e..445af364520 100644 --- a/tests/components/vicare/snapshots/test_button.ambr +++ b/tests/components/vicare/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Activate one-time charge', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activate_onetimecharge', 'unique_id': 'gateway0_deviceSerialVitodens300W-activate_onetimecharge', diff --git a/tests/components/vicare/snapshots/test_climate.ambr b/tests/components/vicare/snapshots/test_climate.ambr index e1709acea42..4ae868ab4b4 100644 --- a/tests/components/vicare/snapshots/test_climate.ambr +++ b/tests/components/vicare/snapshots/test_climate.ambr @@ -39,6 +39,7 @@ 'original_name': 'Heating', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'heating', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating-0', @@ -123,6 +124,7 @@ 'original_name': 'Heating', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'heating', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating-1', diff --git a/tests/components/vicare/snapshots/test_fan.ambr b/tests/components/vicare/snapshots/test_fan.ambr index 2a44fb87b65..e6f494c0fd1 100644 --- a/tests/components/vicare/snapshots/test_fan.ambr +++ b/tests/components/vicare/snapshots/test_fan.ambr @@ -34,6 +34,7 @@ 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ventilation', 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation', @@ -103,6 +104,7 @@ 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ventilation', 'unique_id': 'gateway1_deviceId1-ventilation', @@ -171,6 +173,7 @@ 'original_name': 'Ventilation', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ventilation', 'unique_id': 'gateway2_################-ventilation', diff --git a/tests/components/vicare/snapshots/test_number.ambr b/tests/components/vicare/snapshots/test_number.ambr index b26d2d33590..729d1403ad8 100644 --- a/tests/components/vicare/snapshots/test_number.ambr +++ b/tests/components/vicare/snapshots/test_number.ambr @@ -32,6 +32,7 @@ 'original_name': 'Comfort temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'comfort_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-comfort_temperature-0', @@ -90,6 +91,7 @@ 'original_name': 'Comfort temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'comfort_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-comfort_temperature-1', @@ -148,6 +150,7 @@ 'original_name': 'DHW temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dhw_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-dhw_temperature', @@ -206,6 +209,7 @@ 'original_name': 'Heating curve shift', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_curve_shift', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating curve shift-0', @@ -264,6 +268,7 @@ 'original_name': 'Heating curve shift', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_curve_shift', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating curve shift-1', @@ -322,6 +327,7 @@ 'original_name': 'Heating curve slope', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_curve_slope', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating curve slope-0', @@ -378,6 +384,7 @@ 'original_name': 'Heating curve slope', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_curve_slope', 'unique_id': 'gateway0_deviceSerialVitodens300W-heating curve slope-1', @@ -434,6 +441,7 @@ 'original_name': 'Normal temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'normal_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-normal_temperature-0', @@ -492,6 +500,7 @@ 'original_name': 'Normal temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'normal_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-normal_temperature-1', @@ -550,6 +559,7 @@ 'original_name': 'Reduced temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reduced_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-reduced_temperature-0', @@ -608,6 +618,7 @@ 'original_name': 'Reduced temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reduced_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-reduced_temperature-1', diff --git a/tests/components/vicare/snapshots/test_sensor.ambr b/tests/components/vicare/snapshots/test_sensor.ambr index a0d4bf374c8..561eee3f612 100644 --- a/tests/components/vicare/snapshots/test_sensor.ambr +++ b/tests/components/vicare/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Boiler temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'boiler_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-boiler_temperature', @@ -81,6 +82,7 @@ 'original_name': 'Burner hours', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'burner_hours', 'unique_id': 'gateway0_deviceSerialVitodens300W-burner_hours-0', @@ -132,6 +134,7 @@ 'original_name': 'Burner modulation', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'burner_modulation', 'unique_id': 'gateway0_deviceSerialVitodens300W-burner_modulation-0', @@ -183,6 +186,7 @@ 'original_name': 'Burner starts', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'burner_starts', 'unique_id': 'gateway0_deviceSerialVitodens300W-burner_starts-0', @@ -233,6 +237,7 @@ 'original_name': 'DHW gas consumption this month', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_gas_consumption_heating_this_month', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_gas_consumption_heating_this_month', @@ -283,6 +288,7 @@ 'original_name': 'DHW gas consumption this week', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_gas_consumption_heating_this_week', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_gas_consumption_heating_this_week', @@ -333,6 +339,7 @@ 'original_name': 'DHW gas consumption this year', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_gas_consumption_heating_this_year', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_gas_consumption_heating_this_year', @@ -383,6 +390,7 @@ 'original_name': 'DHW gas consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_gas_consumption_today', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_gas_consumption_today', @@ -433,6 +441,7 @@ 'original_name': 'DHW max temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_max_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_max_temperature', @@ -485,6 +494,7 @@ 'original_name': 'DHW min temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_min_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-hotwater_min_temperature', @@ -537,6 +547,7 @@ 'original_name': 'Electricity consumption this week', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_consumption_this_week', 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this week', @@ -589,6 +600,7 @@ 'original_name': 'Electricity consumption this year', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_consumption_this_year', 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this year', @@ -641,6 +653,7 @@ 'original_name': 'Electricity consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_consumption_today', 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption today', @@ -693,6 +706,7 @@ 'original_name': 'Energy', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power consumption this month', 'unique_id': 'gateway0_deviceSerialVitodens300W-power consumption this month', @@ -745,6 +759,7 @@ 'original_name': 'Heating gas consumption this month', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_consumption_heating_this_month', 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_month', @@ -795,6 +810,7 @@ 'original_name': 'Heating gas consumption this week', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_consumption_heating_this_week', 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_week', @@ -845,6 +861,7 @@ 'original_name': 'Heating gas consumption this year', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_consumption_heating_this_year', 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_this_year', @@ -895,6 +912,7 @@ 'original_name': 'Heating gas consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'gas_consumption_heating_today', 'unique_id': 'gateway0_deviceSerialVitodens300W-gas_consumption_heating_today', @@ -945,6 +963,7 @@ 'original_name': 'Outside temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-outside_temperature', @@ -997,6 +1016,7 @@ 'original_name': 'Supply temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-supply_temperature-0', @@ -1049,6 +1069,7 @@ 'original_name': 'Supply temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_temperature', 'unique_id': 'gateway0_deviceSerialVitodens300W-supply_temperature-1', @@ -1101,6 +1122,7 @@ 'original_name': 'Buffer main temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'buffer_main_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-buffer main temperature', @@ -1153,6 +1175,7 @@ 'original_name': 'Compressor hours', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'compressor_hours', 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_hours-0', @@ -1202,6 +1225,7 @@ 'original_name': 'Compressor phase', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'compressor_phase', 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_phase-0', @@ -1251,6 +1275,7 @@ 'original_name': 'Compressor starts', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'compressor_starts', 'unique_id': 'gateway0_deviceSerialVitocal250A-compressor_starts-0', @@ -1301,6 +1326,7 @@ 'original_name': 'DHW electricity consumption last seven days', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_summary_dhw_consumption_heating_lastsevendays', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_dhw_consumption_heating_lastsevendays', @@ -1353,6 +1379,7 @@ 'original_name': 'DHW electricity consumption this month', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_dhw_summary_consumption_heating_currentmonth', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentmonth', @@ -1405,6 +1432,7 @@ 'original_name': 'DHW electricity consumption this year', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_dhw_summary_consumption_heating_currentyear', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentyear', @@ -1457,6 +1485,7 @@ 'original_name': 'DHW electricity consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_dhw_summary_consumption_heating_currentday', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_dhw_summary_consumption_heating_currentday', @@ -1509,6 +1538,7 @@ 'original_name': 'DHW max temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_max_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-hotwater_max_temperature', @@ -1561,6 +1591,7 @@ 'original_name': 'DHW min temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hotwater_min_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-hotwater_min_temperature', @@ -1613,6 +1644,7 @@ 'original_name': 'DHW storage temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dhw_storage_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-dhw_storage_temperature', @@ -1665,6 +1697,7 @@ 'original_name': 'Electricity consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_consumption_today', 'unique_id': 'gateway0_deviceSerialVitocal250A-power consumption today', @@ -1717,6 +1750,7 @@ 'original_name': 'Heating electricity consumption last seven days', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_summary_consumption_heating_lastsevendays', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_lastsevendays', @@ -1769,6 +1803,7 @@ 'original_name': 'Heating electricity consumption this month', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_summary_consumption_heating_currentmonth', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentmonth', @@ -1821,6 +1856,7 @@ 'original_name': 'Heating electricity consumption this year', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_summary_consumption_heating_currentyear', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentyear', @@ -1873,6 +1909,7 @@ 'original_name': 'Heating electricity consumption today', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_summary_consumption_heating_currentday', 'unique_id': 'gateway0_deviceSerialVitocal250A-energy_summary_consumption_heating_currentday', @@ -1925,6 +1962,7 @@ 'original_name': 'Heating rod hours', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_rod_hours', 'unique_id': 'gateway0_deviceSerialVitocal250A-heating_rod_hours', @@ -1976,6 +2014,7 @@ 'original_name': 'Heating rod starts', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heating_rod_starts', 'unique_id': 'gateway0_deviceSerialVitocal250A-heating_rod_starts', @@ -2026,6 +2065,7 @@ 'original_name': 'Outside temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-outside_temperature', @@ -2078,6 +2118,7 @@ 'original_name': 'Primary circuit supply temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'primary_circuit_supply_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-primary_circuit_supply_temperature', @@ -2130,6 +2171,7 @@ 'original_name': 'Return temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'return_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-return_temperature', @@ -2182,6 +2224,7 @@ 'original_name': 'Seasonal performance factor', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spf_total', 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_total', @@ -2232,6 +2275,7 @@ 'original_name': 'Seasonal performance factor - domestic hot water', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spf_dhw', 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_dhw', @@ -2282,6 +2326,7 @@ 'original_name': 'Seasonal performance factor - heating', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spf_heating', 'unique_id': 'gateway0_deviceSerialVitocal250A-spf_heating', @@ -2332,6 +2377,7 @@ 'original_name': 'Secondary circuit supply temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'secondary_circuit_supply_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-secondary_circuit_supply_temperature', @@ -2384,6 +2430,7 @@ 'original_name': 'Supply pressure', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_pressure', 'unique_id': 'gateway0_deviceSerialVitocal250A-supply_pressure', @@ -2435,6 +2482,7 @@ 'original_name': 'Supply temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'supply_temperature', 'unique_id': 'gateway0_deviceSerialVitocal250A-supply_temperature-1', @@ -2487,6 +2535,7 @@ 'original_name': 'Volumetric flow', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volumetric_flow', 'unique_id': 'gateway0_deviceSerialVitocal250A-volumetric_flow', @@ -2544,6 +2593,7 @@ 'original_name': 'Ventilation level', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ventilation_level', 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation_level', @@ -2608,6 +2658,7 @@ 'original_name': 'Ventilation reason', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ventilation_reason', 'unique_id': 'gateway0_deviceSerialViAir300F-ventilation_reason', @@ -2666,6 +2717,7 @@ 'original_name': 'Battery', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'gateway0_zigbee_d87a3bfffe5d844a-battery_level', @@ -2718,6 +2770,7 @@ 'original_name': 'Humidity', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'gateway0_zigbee_d87a3bfffe5d844a-room_humidity', @@ -2770,6 +2823,7 @@ 'original_name': 'Temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'gateway0_zigbee_d87a3bfffe5d844a-room_temperature', @@ -2822,6 +2876,7 @@ 'original_name': 'Humidity', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'gateway1_zigbee_5cc7c1fffea33a3b-room_humidity', @@ -2874,6 +2929,7 @@ 'original_name': 'Temperature', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'gateway1_zigbee_5cc7c1fffea33a3b-room_temperature', diff --git a/tests/components/vicare/snapshots/test_water_heater.ambr b/tests/components/vicare/snapshots/test_water_heater.ambr index 7b7ab91e086..87d98561a86 100644 --- a/tests/components/vicare/snapshots/test_water_heater.ambr +++ b/tests/components/vicare/snapshots/test_water_heater.ambr @@ -30,6 +30,7 @@ 'original_name': 'Domestic hot water', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'domestic_hot_water', 'unique_id': 'gateway0_deviceSerialVitodens300W-0', @@ -87,6 +88,7 @@ 'original_name': 'Domestic hot water', 'platform': 'vicare', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': 'domestic_hot_water', 'unique_id': 'gateway0_deviceSerialVitodens300W-1', diff --git a/tests/components/vodafone_station/snapshots/test_button.ambr b/tests/components/vodafone_station/snapshots/test_button.ambr index 736f590241a..f644da96c09 100644 --- a/tests/components/vodafone_station/snapshots/test_button.ambr +++ b/tests/components/vodafone_station/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Restart', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'm123456789_reboot', diff --git a/tests/components/vodafone_station/snapshots/test_device_tracker.ambr b/tests/components/vodafone_station/snapshots/test_device_tracker.ambr index 7f98aad1405..f4f88c17aa6 100644 --- a/tests/components/vodafone_station/snapshots/test_device_tracker.ambr +++ b/tests/components/vodafone_station/snapshots/test_device_tracker.ambr @@ -27,6 +27,7 @@ 'original_name': 'LanDevice1', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_tracker', 'unique_id': 'yy:yy:yy:yy:yy:yy', @@ -78,6 +79,7 @@ 'original_name': 'WifiDevice0', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'device_tracker', 'unique_id': 'xx:xx:xx:xx:xx:xx', diff --git a/tests/components/vodafone_station/snapshots/test_sensor.ambr b/tests/components/vodafone_station/snapshots/test_sensor.ambr index 169ee92a24b..d046f1f1f0e 100644 --- a/tests/components/vodafone_station/snapshots/test_sensor.ambr +++ b/tests/components/vodafone_station/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Active connection', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_connection', 'unique_id': 'm123456789_inter_ip_address', @@ -86,6 +87,7 @@ 'original_name': 'CPU usage', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sys_cpu_usage', 'unique_id': 'm123456789_sys_cpu_usage', @@ -134,6 +136,7 @@ 'original_name': 'Memory usage', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sys_memory_usage', 'unique_id': 'm123456789_sys_memory_usage', @@ -182,6 +185,7 @@ 'original_name': 'Reboot cause', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sys_reboot_cause', 'unique_id': 'm123456789_sys_reboot_cause', @@ -229,6 +233,7 @@ 'original_name': 'Uptime', 'platform': 'vodafone_station', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sys_uptime', 'unique_id': 'm123456789_sys_uptime', diff --git a/tests/components/watergate/snapshots/test_event.ambr b/tests/components/watergate/snapshots/test_event.ambr index 97f453697ca..a7a019cc83b 100644 --- a/tests/components/watergate/snapshots/test_event.ambr +++ b/tests/components/watergate/snapshots/test_event.ambr @@ -31,6 +31,7 @@ 'original_name': 'Duration auto shut-off', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_shut_off_duration', 'unique_id': 'a63182948ce2896a.auto_shut_off_duration', @@ -86,6 +87,7 @@ 'original_name': 'Volume auto shut-off', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'auto_shut_off_volume', 'unique_id': 'a63182948ce2896a.auto_shut_off_volume', diff --git a/tests/components/watergate/snapshots/test_sensor.ambr b/tests/components/watergate/snapshots/test_sensor.ambr index b4b6c4ee0a4..a399d36cc5f 100644 --- a/tests/components/watergate/snapshots/test_sensor.ambr +++ b/tests/components/watergate/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'MQTT up since', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mqtt_up_since', 'unique_id': 'a63182948ce2896a.mqtt_up_since', @@ -81,6 +82,7 @@ 'original_name': 'Power supply mode', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_supply_mode', 'unique_id': 'a63182948ce2896a.power_supply_mode', @@ -136,6 +138,7 @@ 'original_name': 'Signal strength', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a63182948ce2896a.rssi', @@ -186,6 +189,7 @@ 'original_name': 'Up since', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'up_since', 'unique_id': 'a63182948ce2896a.up_since', @@ -236,6 +240,7 @@ 'original_name': 'Volume flow rate', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'a63182948ce2896a.water_flow_rate', @@ -288,6 +293,7 @@ 'original_name': 'Water meter duration', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_meter_duration', 'unique_id': 'a63182948ce2896a.water_meter_duration', @@ -340,6 +346,7 @@ 'original_name': 'Water meter volume', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_meter_volume', 'unique_id': 'a63182948ce2896a.water_meter_volume', @@ -392,6 +399,7 @@ 'original_name': 'Water pressure', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_pressure', 'unique_id': 'a63182948ce2896a.water_pressure', @@ -444,6 +452,7 @@ 'original_name': 'Water temperature', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_temperature', 'unique_id': 'a63182948ce2896a.water_temperature', @@ -494,6 +503,7 @@ 'original_name': 'Wi-Fi up since', 'platform': 'watergate', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wifi_up_since', 'unique_id': 'a63182948ce2896a.wifi_up_since', diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index c06229302c5..5f8d0037bfb 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -32,6 +32,7 @@ 'original_name': 'Air density', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_density', 'unique_id': '24432_air_density', @@ -87,6 +88,7 @@ 'original_name': 'Dew point', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dew_point', 'unique_id': '24432_dew_point', @@ -143,6 +145,7 @@ 'original_name': 'Feels like', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'feels_like', 'unique_id': '24432_feels_like', @@ -199,6 +202,7 @@ 'original_name': 'Heat index', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_index', 'unique_id': '24432_heat_index', @@ -252,6 +256,7 @@ 'original_name': 'Lightning count', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lightning_strike_count', 'unique_id': '24432_lightning_strike_count', @@ -303,6 +308,7 @@ 'original_name': 'Lightning count last 1 hr', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lightning_strike_count_last_1hr', 'unique_id': '24432_lightning_strike_count_last_1hr', @@ -354,6 +360,7 @@ 'original_name': 'Lightning count last 3 hr', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lightning_strike_count_last_3hr', 'unique_id': '24432_lightning_strike_count_last_3hr', @@ -405,6 +412,7 @@ 'original_name': 'Lightning last distance', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lightning_strike_last_distance', 'unique_id': '24432_lightning_strike_last_distance', @@ -456,6 +464,7 @@ 'original_name': 'Lightning last strike', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'lightning_strike_last_epoch', 'unique_id': '24432_lightning_strike_last_epoch', @@ -513,6 +522,7 @@ 'original_name': 'Pressure barometric', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'barometric_pressure', 'unique_id': '24432_barometric_pressure', @@ -572,6 +582,7 @@ 'original_name': 'Pressure sea level', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sea_level_pressure', 'unique_id': '24432_sea_level_pressure', @@ -628,6 +639,7 @@ 'original_name': 'Temperature', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'air_temperature', 'unique_id': '24432_air_temperature', @@ -684,6 +696,7 @@ 'original_name': 'Wet bulb globe temperature', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wet_bulb_globe_temperature', 'unique_id': '24432_wet_bulb_globe_temperature', @@ -740,6 +753,7 @@ 'original_name': 'Wet bulb temperature', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wet_bulb_temperature', 'unique_id': '24432_wet_bulb_temperature', @@ -796,6 +810,7 @@ 'original_name': 'Wind chill', 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wind_chill', 'unique_id': '24432_wind_chill', diff --git a/tests/components/weatherflow_cloud/snapshots/test_weather.ambr b/tests/components/weatherflow_cloud/snapshots/test_weather.ambr index 0b0d66c34a7..867f7874ed3 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_weather.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_weather.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'weatherflow_cloud', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'weatherflow_forecast_24432', diff --git a/tests/components/webmin/snapshots/test_sensor.ambr b/tests/components/webmin/snapshots/test_sensor.ambr index 1af5fe46b5c..6352c2bcf61 100644 --- a/tests/components/webmin/snapshots/test_sensor.ambr +++ b/tests/components/webmin/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Disk free inodes /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_ifree', 'unique_id': '12:34:56:78:9a:bc_/_ifree', @@ -79,6 +80,7 @@ 'original_name': 'Disk free inodes /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_ifree', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_ifree', @@ -129,6 +131,7 @@ 'original_name': 'Disk free inodes /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_ifree', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_ifree', @@ -185,6 +188,7 @@ 'original_name': 'Disk free space /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_free', 'unique_id': '12:34:56:78:9a:bc_/_free', @@ -243,6 +247,7 @@ 'original_name': 'Disk free space /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_free', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_free', @@ -301,6 +306,7 @@ 'original_name': 'Disk free space /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_free', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_free', @@ -353,6 +359,7 @@ 'original_name': 'Disk inode usage /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused_percent', 'unique_id': '12:34:56:78:9a:bc_/_iused_percent', @@ -404,6 +411,7 @@ 'original_name': 'Disk inode usage /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused_percent', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_iused_percent', @@ -455,6 +463,7 @@ 'original_name': 'Disk inode usage /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused_percent', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_iused_percent', @@ -506,6 +515,7 @@ 'original_name': 'Disk total inodes /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_itotal', 'unique_id': '12:34:56:78:9a:bc_/_itotal', @@ -556,6 +566,7 @@ 'original_name': 'Disk total inodes /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_itotal', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_itotal', @@ -606,6 +617,7 @@ 'original_name': 'Disk total inodes /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_itotal', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_itotal', @@ -662,6 +674,7 @@ 'original_name': 'Disk total space /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_total', 'unique_id': '12:34:56:78:9a:bc_/_total', @@ -720,6 +733,7 @@ 'original_name': 'Disk total space /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_total', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_total', @@ -778,6 +792,7 @@ 'original_name': 'Disk total space /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_total', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_total', @@ -830,6 +845,7 @@ 'original_name': 'Disk usage /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used_percent', 'unique_id': '12:34:56:78:9a:bc_/_used_percent', @@ -881,6 +897,7 @@ 'original_name': 'Disk usage /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used_percent', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_used_percent', @@ -932,6 +949,7 @@ 'original_name': 'Disk usage /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used_percent', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_used_percent', @@ -983,6 +1001,7 @@ 'original_name': 'Disk used inodes /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused', 'unique_id': '12:34:56:78:9a:bc_/_iused', @@ -1033,6 +1052,7 @@ 'original_name': 'Disk used inodes /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_iused', @@ -1083,6 +1103,7 @@ 'original_name': 'Disk used inodes /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_iused', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_iused', @@ -1139,6 +1160,7 @@ 'original_name': 'Disk used space /', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used', 'unique_id': '12:34:56:78:9a:bc_/_used', @@ -1197,6 +1219,7 @@ 'original_name': 'Disk used space /media/disk1', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used', 'unique_id': '12:34:56:78:9a:bc_/media/disk1_used', @@ -1255,6 +1278,7 @@ 'original_name': 'Disk used space /media/disk2', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_fs_used', 'unique_id': '12:34:56:78:9a:bc_/media/disk2_used', @@ -1313,6 +1337,7 @@ 'original_name': 'Disks free space', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_free', 'unique_id': '12:34:56:78:9a:bc_disk_free', @@ -1371,6 +1396,7 @@ 'original_name': 'Disks total space', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_total', 'unique_id': '12:34:56:78:9a:bc_disk_total', @@ -1429,6 +1455,7 @@ 'original_name': 'Disks used space', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'disk_used', 'unique_id': '12:34:56:78:9a:bc_disk_used', @@ -1481,6 +1508,7 @@ 'original_name': 'Load (15 min)', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_15m', 'unique_id': '12:34:56:78:9a:bc_load_15m', @@ -1531,6 +1559,7 @@ 'original_name': 'Load (1 min)', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_1m', 'unique_id': '12:34:56:78:9a:bc_load_1m', @@ -1581,6 +1610,7 @@ 'original_name': 'Load (5 min)', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'load_5m', 'unique_id': '12:34:56:78:9a:bc_load_5m', @@ -1637,6 +1667,7 @@ 'original_name': 'Memory free', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mem_free', 'unique_id': '12:34:56:78:9a:bc_mem_free', @@ -1695,6 +1726,7 @@ 'original_name': 'Memory total', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'mem_total', 'unique_id': '12:34:56:78:9a:bc_mem_total', @@ -1753,6 +1785,7 @@ 'original_name': 'Swap free', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'swap_free', 'unique_id': '12:34:56:78:9a:bc_swap_free', @@ -1811,6 +1844,7 @@ 'original_name': 'Swap total', 'platform': 'webmin', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'swap_total', 'unique_id': '12:34:56:78:9a:bc_swap_total', diff --git a/tests/components/weheat/snapshots/test_binary_sensor.ambr b/tests/components/weheat/snapshots/test_binary_sensor.ambr index bdcd727fbcc..8f6f635d79e 100644 --- a/tests/components/weheat/snapshots/test_binary_sensor.ambr +++ b/tests/components/weheat/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Indoor unit auxiliary water pump', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_unit_auxiliary_pump_state', 'unique_id': '0000-1111-2222-3333_indoor_unit_auxiliary_pump_state', @@ -75,6 +76,7 @@ 'original_name': 'Indoor unit electric heater', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_unit_electric_heater_state', 'unique_id': '0000-1111-2222-3333_indoor_unit_electric_heater_state', @@ -123,6 +125,7 @@ 'original_name': 'Indoor unit gas boiler heating allowed', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_unit_gas_boiler_state', 'unique_id': '0000-1111-2222-3333_indoor_unit_gas_boiler_state', @@ -170,6 +173,7 @@ 'original_name': 'Indoor unit water pump', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'indoor_unit_water_pump_state', 'unique_id': '0000-1111-2222-3333_indoor_unit_water_pump_state', diff --git a/tests/components/weheat/snapshots/test_sensor.ambr b/tests/components/weheat/snapshots/test_sensor.ambr index b968d925675..91614d0a608 100644 --- a/tests/components/weheat/snapshots/test_sensor.ambr +++ b/tests/components/weheat/snapshots/test_sensor.ambr @@ -39,6 +39,7 @@ 'original_name': None, 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heat_pump_state', 'unique_id': '0000-1111-2222-3333_heat_pump_state', @@ -103,6 +104,7 @@ 'original_name': 'Central heating inlet temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ch_inlet_temperature', 'unique_id': '0000-1111-2222-3333_ch_inlet_temperature', @@ -158,6 +160,7 @@ 'original_name': 'Central heating pump flow', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'central_heating_flow_volume', 'unique_id': '0000-1111-2222-3333_central_heating_flow_volume', @@ -210,6 +213,7 @@ 'original_name': 'Compressor speed', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'compressor_rpm', 'unique_id': '0000-1111-2222-3333_compressor_rpm', @@ -261,6 +265,7 @@ 'original_name': 'Compressor usage', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'compressor_percentage', 'unique_id': '0000-1111-2222-3333_compressor_percentage', @@ -315,6 +320,7 @@ 'original_name': 'COP', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'cop', 'unique_id': '0000-1111-2222-3333_cop', @@ -368,6 +374,7 @@ 'original_name': 'Current room temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_room_temperature', 'unique_id': '0000-1111-2222-3333_thermostat_room_temperature', @@ -423,6 +430,7 @@ 'original_name': 'DHW bottom temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dhw_bottom_temperature', 'unique_id': '0000-1111-2222-3333_dhw_bottom_temperature', @@ -478,6 +486,7 @@ 'original_name': 'DHW pump flow', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dhw_flow_volume', 'unique_id': '0000-1111-2222-3333_dhw_flow_volume', @@ -533,6 +542,7 @@ 'original_name': 'DHW top temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dhw_top_temperature', 'unique_id': '0000-1111-2222-3333_dhw_top_temperature', @@ -585,6 +595,7 @@ 'original_name': 'Electricity used', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electricity_used', 'unique_id': '0000-1111-2222-3333_electricity_used', @@ -640,6 +651,7 @@ 'original_name': 'Input power', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_input', 'unique_id': '0000-1111-2222-3333_power_input', @@ -695,6 +707,7 @@ 'original_name': 'Output power', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_output', 'unique_id': '0000-1111-2222-3333_power_output', @@ -750,6 +763,7 @@ 'original_name': 'Outside temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'outside_temperature', 'unique_id': '0000-1111-2222-3333_outside_temperature', @@ -805,6 +819,7 @@ 'original_name': 'Room temperature setpoint', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_room_temperature_setpoint', 'unique_id': '0000-1111-2222-3333_thermostat_room_temperature_setpoint', @@ -857,6 +872,7 @@ 'original_name': 'Total energy output', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_output', 'unique_id': '0000-1111-2222-3333_energy_output', @@ -912,6 +928,7 @@ 'original_name': 'Water inlet temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_inlet_temperature', 'unique_id': '0000-1111-2222-3333_water_inlet_temperature', @@ -967,6 +984,7 @@ 'original_name': 'Water outlet temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'water_outlet_temperature', 'unique_id': '0000-1111-2222-3333_water_outlet_temperature', @@ -1022,6 +1040,7 @@ 'original_name': 'Water target temperature', 'platform': 'weheat', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'thermostat_water_setpoint', 'unique_id': '0000-1111-2222-3333_thermostat_water_setpoint', diff --git a/tests/components/whirlpool/snapshots/test_binary_sensor.ambr b/tests/components/whirlpool/snapshots/test_binary_sensor.ambr index 1a902f806cf..1a0445a4803 100644 --- a/tests/components/whirlpool/snapshots/test_binary_sensor.ambr +++ b/tests/components/whirlpool/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Door', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'said_dryer-door', @@ -75,6 +76,7 @@ 'original_name': 'Door', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'said_washer-door', diff --git a/tests/components/whirlpool/snapshots/test_climate.ambr b/tests/components/whirlpool/snapshots/test_climate.ambr index 2957a609fa2..58b894d07cb 100644 --- a/tests/components/whirlpool/snapshots/test_climate.ambr +++ b/tests/components/whirlpool/snapshots/test_climate.ambr @@ -48,6 +48,7 @@ 'original_name': None, 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'said1', @@ -142,6 +143,7 @@ 'original_name': None, 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': 'said2', diff --git a/tests/components/whirlpool/snapshots/test_sensor.ambr b/tests/components/whirlpool/snapshots/test_sensor.ambr index 6a0465ba8b9..843e71b62ea 100644 --- a/tests/components/whirlpool/snapshots/test_sensor.ambr +++ b/tests/components/whirlpool/snapshots/test_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'End time', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'end_time', 'unique_id': 'said_dryer-timeremaining', @@ -105,6 +106,7 @@ 'original_name': 'State', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'dryer_state', 'unique_id': 'said_dryer-state', @@ -189,6 +191,7 @@ 'original_name': 'Detergent level', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'whirlpool_tank', 'unique_id': 'said_washer-DispenseLevel', @@ -244,6 +247,7 @@ 'original_name': 'End time', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'end_time', 'unique_id': 'said_washer-timeremaining', @@ -322,6 +326,7 @@ 'original_name': 'State', 'platform': 'whirlpool', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'washer_state', 'unique_id': 'said_washer-state', diff --git a/tests/components/whois/snapshots/test_sensor.ambr b/tests/components/whois/snapshots/test_sensor.ambr index 61499ba0f9d..67f6baf45bb 100644 --- a/tests/components/whois/snapshots/test_sensor.ambr +++ b/tests/components/whois/snapshots/test_sensor.ambr @@ -40,6 +40,7 @@ 'original_name': 'Admin', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'admin', 'unique_id': 'home-assistant.io_admin', @@ -121,6 +122,7 @@ 'original_name': 'Created', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'creation_date', 'unique_id': 'home-assistant.io_creation_date', @@ -206,6 +208,7 @@ 'original_name': 'Days until expiration', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'days_until_expiration', 'unique_id': 'home-assistant.io_days_until_expiration', @@ -287,6 +290,7 @@ 'original_name': 'Expires', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'expiration_date', 'unique_id': 'home-assistant.io_expiration_date', @@ -368,6 +372,7 @@ 'original_name': 'Last updated', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_updated', 'unique_id': 'home-assistant.io_last_updated', @@ -448,6 +453,7 @@ 'original_name': 'Owner', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'owner', 'unique_id': 'home-assistant.io_owner', @@ -528,6 +534,7 @@ 'original_name': 'Registrant', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'registrant', 'unique_id': 'home-assistant.io_registrant', @@ -608,6 +615,7 @@ 'original_name': 'Registrar', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'registrar', 'unique_id': 'home-assistant.io_registrar', @@ -688,6 +696,7 @@ 'original_name': 'Reseller', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reseller', 'unique_id': 'home-assistant.io_reseller', @@ -820,6 +829,7 @@ 'original_name': 'Status', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'status', 'unique_id': 'home-assistant.io_status', @@ -901,6 +911,7 @@ 'original_name': 'Last updated', 'platform': 'whois', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_updated', 'unique_id': 'home-assistant.io_last_updated', diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index f735c506f65..f53bd645728 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -33,6 +33,7 @@ 'original_name': 'Battery', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': 'f998be4b9ccc9e136fd8cd8e8e344c31ec3b271d_battery', @@ -91,6 +92,7 @@ 'original_name': 'Active calories burnt today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_active_calories_burnt_today', 'unique_id': 'withings_12345_activity_active_calories_burnt_today', @@ -146,6 +148,7 @@ 'original_name': 'Active time today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_active_duration_today', 'unique_id': 'withings_12345_activity_active_duration_today', @@ -199,6 +202,7 @@ 'original_name': 'Average heart rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'average_heart_rate', 'unique_id': 'withings_12345_sleep_heart_rate_average_bpm', @@ -250,6 +254,7 @@ 'original_name': 'Average respiratory rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'average_respiratory_rate', 'unique_id': 'withings_12345_sleep_respiratory_average_bpm', @@ -301,6 +306,7 @@ 'original_name': 'Body temperature', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'body_temperature', 'unique_id': 'withings_12345_body_temperature_c', @@ -356,6 +362,7 @@ 'original_name': 'Bone mass', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'bone_mass', 'unique_id': 'withings_12345_bone_mass_kg', @@ -408,6 +415,7 @@ 'original_name': 'Breathing disturbances intensity', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'breathing_disturbances_intensity', 'unique_id': 'withings_12345_sleep_breathing_disturbances_intensity', @@ -459,6 +467,7 @@ 'original_name': 'Calories burnt last workout', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_active_calories_burnt', 'unique_id': 'withings_12345_workout_active_calories_burnt', @@ -512,6 +521,7 @@ 'original_name': 'Deep sleep', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'deep_sleep', 'unique_id': 'withings_12345_sleep_deep_duration_seconds', @@ -564,6 +574,7 @@ 'original_name': 'Diastolic blood pressure', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'diastolic_blood_pressure', 'unique_id': 'withings_12345_diastolic_blood_pressure_mmhg', @@ -616,6 +627,7 @@ 'original_name': 'Distance travelled last workout', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_distance', 'unique_id': 'withings_12345_workout_distance', @@ -670,6 +682,7 @@ 'original_name': 'Distance travelled today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_distance_today', 'unique_id': 'withings_12345_activity_distance_today', @@ -721,6 +734,7 @@ 'original_name': 'Electrodermal activity feet', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electrodermal_activity_feet', 'unique_id': 'withings_12345_electrodermal_activity_feet', @@ -769,6 +783,7 @@ 'original_name': 'Electrodermal activity left foot', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electrodermal_activity_left_foot', 'unique_id': 'withings_12345_electrodermal_activity_left_foot', @@ -817,6 +832,7 @@ 'original_name': 'Electrodermal activity right foot', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'electrodermal_activity_right_foot', 'unique_id': 'withings_12345_electrodermal_activity_right_foot', @@ -865,6 +881,7 @@ 'original_name': 'Elevation change last workout', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_elevation', 'unique_id': 'withings_12345_workout_floors_climbed', @@ -916,6 +933,7 @@ 'original_name': 'Elevation change today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_elevation_today', 'unique_id': 'withings_12345_activity_floors_climbed_today', @@ -969,6 +987,7 @@ 'original_name': 'Extracellular water', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'extracellular_water', 'unique_id': 'withings_12345_extracellular_water', @@ -1024,6 +1043,7 @@ 'original_name': 'Fat free mass', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass', 'unique_id': 'withings_12345_fat_free_mass_kg', @@ -1079,6 +1099,7 @@ 'original_name': 'Fat free mass in left arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass_for_segments_left_arm', 'unique_id': 'withings_12345_fat_free_mass_for_segments_left_arm', @@ -1134,6 +1155,7 @@ 'original_name': 'Fat free mass in left leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass_for_segments_left_leg', 'unique_id': 'withings_12345_fat_free_mass_for_segments_left_leg', @@ -1189,6 +1211,7 @@ 'original_name': 'Fat free mass in right arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass_for_segments_right_arm', 'unique_id': 'withings_12345_fat_free_mass_for_segments_right_arm', @@ -1244,6 +1267,7 @@ 'original_name': 'Fat free mass in right leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass_for_segments_right_leg', 'unique_id': 'withings_12345_fat_free_mass_for_segments_right_leg', @@ -1299,6 +1323,7 @@ 'original_name': 'Fat free mass in torso', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_free_mass_for_segments_torso', 'unique_id': 'withings_12345_fat_free_mass_for_segments_torso', @@ -1354,6 +1379,7 @@ 'original_name': 'Fat mass', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass', 'unique_id': 'withings_12345_fat_mass_kg', @@ -1409,6 +1435,7 @@ 'original_name': 'Fat mass in left arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass_for_segments_left_arm', 'unique_id': 'withings_12345_fat_mass_for_segments_left_arm', @@ -1464,6 +1491,7 @@ 'original_name': 'Fat mass in left leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass_for_segments_left_leg', 'unique_id': 'withings_12345_fat_mass_for_segments_left_leg', @@ -1519,6 +1547,7 @@ 'original_name': 'Fat mass in right arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass_for_segments_right_arm', 'unique_id': 'withings_12345_fat_mass_for_segments_right_arm', @@ -1574,6 +1603,7 @@ 'original_name': 'Fat mass in right leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass_for_segments_right_leg', 'unique_id': 'withings_12345_fat_mass_for_segments_right_leg', @@ -1629,6 +1659,7 @@ 'original_name': 'Fat mass in torso', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_mass_for_segments_torso', 'unique_id': 'withings_12345_fat_mass_for_segments_torso', @@ -1684,6 +1715,7 @@ 'original_name': 'Fat ratio', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'fat_ratio', 'unique_id': 'withings_12345_fat_ratio_pct', @@ -1735,6 +1767,7 @@ 'original_name': 'Heart pulse', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'heart_pulse', 'unique_id': 'withings_12345_heart_pulse_bpm', @@ -1789,6 +1822,7 @@ 'original_name': 'Height', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'height', 'unique_id': 'withings_12345_height_m', @@ -1841,6 +1875,7 @@ 'original_name': 'Hydration', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'hydration', 'unique_id': 'withings_12345_hydration', @@ -1896,6 +1931,7 @@ 'original_name': 'Intense activity today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_intense_duration_today', 'unique_id': 'withings_12345_activity_intense_duration_today', @@ -1949,6 +1985,7 @@ 'original_name': 'Intracellular water', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'intracellular_water', 'unique_id': 'withings_12345_intracellular_water', @@ -2002,6 +2039,7 @@ 'original_name': 'Last workout duration', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_duration', 'unique_id': 'withings_12345_workout_duration', @@ -2051,6 +2089,7 @@ 'original_name': 'Last workout intensity', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_intensity', 'unique_id': 'withings_12345_workout_intensity', @@ -2150,6 +2189,7 @@ 'original_name': 'Last workout type', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_type', 'unique_id': 'withings_12345_workout_type', @@ -2254,6 +2294,7 @@ 'original_name': 'Light sleep', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'light_sleep', 'unique_id': 'withings_12345_sleep_light_duration_seconds', @@ -2306,6 +2347,7 @@ 'original_name': 'Maximum heart rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'maximum_heart_rate', 'unique_id': 'withings_12345_sleep_heart_rate_max_bpm', @@ -2357,6 +2399,7 @@ 'original_name': 'Maximum respiratory rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'maximum_respiratory_rate', 'unique_id': 'withings_12345_sleep_respiratory_max_bpm', @@ -2408,6 +2451,7 @@ 'original_name': 'Minimum heart rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'minimum_heart_rate', 'unique_id': 'withings_12345_sleep_heart_rate_min_bpm', @@ -2459,6 +2503,7 @@ 'original_name': 'Minimum respiratory rate', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'minimum_respiratory_rate', 'unique_id': 'withings_12345_sleep_respiratory_min_bpm', @@ -2513,6 +2558,7 @@ 'original_name': 'Moderate activity today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_moderate_duration_today', 'unique_id': 'withings_12345_activity_moderate_duration_today', @@ -2569,6 +2615,7 @@ 'original_name': 'Muscle mass', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass', 'unique_id': 'withings_12345_muscle_mass_kg', @@ -2624,6 +2671,7 @@ 'original_name': 'Muscle mass in left arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass_for_segments_left_arm', 'unique_id': 'withings_12345_muscle_mass_for_segments_left_arm', @@ -2679,6 +2727,7 @@ 'original_name': 'Muscle mass in left leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass_for_segments_left_leg', 'unique_id': 'withings_12345_muscle_mass_for_segments_left_leg', @@ -2734,6 +2783,7 @@ 'original_name': 'Muscle mass in right arm', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass_for_segments_right_arm', 'unique_id': 'withings_12345_muscle_mass_for_segments_right_arm', @@ -2789,6 +2839,7 @@ 'original_name': 'Muscle mass in right leg', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass_for_segments_right_leg', 'unique_id': 'withings_12345_muscle_mass_for_segments_right_leg', @@ -2844,6 +2895,7 @@ 'original_name': 'Muscle mass in torso', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'muscle_mass_for_segments_torso', 'unique_id': 'withings_12345_muscle_mass_for_segments_torso', @@ -2897,6 +2949,7 @@ 'original_name': 'Pause during last workout', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'workout_pause_duration', 'unique_id': 'withings_12345_workout_pause_duration', @@ -2948,6 +3001,7 @@ 'original_name': 'Pulse wave velocity', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pulse_wave_velocity', 'unique_id': 'withings_12345_pulse_wave_velocity', @@ -3003,6 +3057,7 @@ 'original_name': 'REM sleep', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'rem_sleep', 'unique_id': 'withings_12345_sleep_rem_duration_seconds', @@ -3055,6 +3110,7 @@ 'original_name': 'Skin temperature', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'skin_temperature', 'unique_id': 'withings_12345_skin_temperature_c', @@ -3110,6 +3166,7 @@ 'original_name': 'Sleep goal', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sleep_goal', 'unique_id': 'withings_12345_sleep_goal', @@ -3162,6 +3219,7 @@ 'original_name': 'Sleep score', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sleep_score', 'unique_id': 'withings_12345_sleep_score', @@ -3216,6 +3274,7 @@ 'original_name': 'Snoring', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'snoring', 'unique_id': 'withings_12345_sleep_snoring', @@ -3268,6 +3327,7 @@ 'original_name': 'Snoring episode count', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'snoring_episode_count', 'unique_id': 'withings_12345_sleep_snoring_eposode_count', @@ -3321,6 +3381,7 @@ 'original_name': 'Soft activity today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_soft_duration_today', 'unique_id': 'withings_12345_activity_soft_duration_today', @@ -3374,6 +3435,7 @@ 'original_name': 'SpO2', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'spo2', 'unique_id': 'withings_12345_spo2_pct', @@ -3425,6 +3487,7 @@ 'original_name': 'Step goal', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'step_goal', 'unique_id': 'withings_12345_step_goal', @@ -3476,6 +3539,7 @@ 'original_name': 'Steps today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_steps_today', 'unique_id': 'withings_12345_activity_steps_today', @@ -3528,6 +3592,7 @@ 'original_name': 'Systolic blood pressure', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'systolic_blood_pressure', 'unique_id': 'withings_12345_systolic_blood_pressure_mmhg', @@ -3579,6 +3644,7 @@ 'original_name': 'Temperature', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'withings_12345_temperature_c', @@ -3634,6 +3700,7 @@ 'original_name': 'Time to sleep', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_to_sleep', 'unique_id': 'withings_12345_sleep_tosleep_duration_seconds', @@ -3689,6 +3756,7 @@ 'original_name': 'Time to wakeup', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'time_to_wakeup', 'unique_id': 'withings_12345_sleep_towakeup_duration_seconds', @@ -3744,6 +3812,7 @@ 'original_name': 'Total calories burnt today', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'activity_total_calories_burnt_today', 'unique_id': 'withings_12345_activity_total_calories_burnt_today', @@ -3794,6 +3863,7 @@ 'original_name': 'Vascular age', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vascular_age', 'unique_id': 'withings_12345_vascular_age', @@ -3841,6 +3911,7 @@ 'original_name': 'Visceral fat index', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'visceral_fat_index', 'unique_id': 'withings_12345_visceral_fat', @@ -3890,6 +3961,7 @@ 'original_name': 'VO2 max', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'vo2_max', 'unique_id': 'withings_12345_vo2_max', @@ -3941,6 +4013,7 @@ 'original_name': 'Wakeup count', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wakeup_count', 'unique_id': 'withings_12345_sleep_wakeup_count', @@ -3995,6 +4068,7 @@ 'original_name': 'Wakeup time', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'wakeup_time', 'unique_id': 'withings_12345_sleep_wakeup_duration_seconds', @@ -4050,6 +4124,7 @@ 'original_name': 'Weight', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'withings_12345_weight_kg', @@ -4102,6 +4177,7 @@ 'original_name': 'Weight goal', 'platform': 'withings', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'weight_goal', 'unique_id': 'withings_12345_weight_goal', diff --git a/tests/components/wled/snapshots/test_button.ambr b/tests/components/wled/snapshots/test_button.ambr index a22c1a3fb85..d8a29ed7c48 100644 --- a/tests/components/wled/snapshots/test_button.ambr +++ b/tests/components/wled/snapshots/test_button.ambr @@ -41,6 +41,7 @@ 'original_name': 'Restart', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'aabbccddeeff_restart', diff --git a/tests/components/wled/snapshots/test_number.ambr b/tests/components/wled/snapshots/test_number.ambr index a99831d1440..877c8baa93e 100644 --- a/tests/components/wled/snapshots/test_number.ambr +++ b/tests/components/wled/snapshots/test_number.ambr @@ -49,6 +49,7 @@ 'original_name': 'Segment 1 intensity', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'segment_intensity', 'unique_id': 'aabbccddeeff_intensity_1', @@ -142,6 +143,7 @@ 'original_name': 'Segment 1 speed', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'segment_speed', 'unique_id': 'aabbccddeeff_speed_1', diff --git a/tests/components/wled/snapshots/test_select.ambr b/tests/components/wled/snapshots/test_select.ambr index d3f8fbcc21d..6cfbe1de5d4 100644 --- a/tests/components/wled/snapshots/test_select.ambr +++ b/tests/components/wled/snapshots/test_select.ambr @@ -51,6 +51,7 @@ 'original_name': 'Live override', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'live_override', 'unique_id': 'aabbccddeeff_live_override', @@ -282,6 +283,7 @@ 'original_name': 'Segment 1 color palette', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'segment_color_palette', 'unique_id': 'aabbccddeeff_palette_1', @@ -375,6 +377,7 @@ 'original_name': 'Playlist', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'playlist', 'unique_id': 'aabbccddeeff_playlist', @@ -468,6 +471,7 @@ 'original_name': 'Preset', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'preset', 'unique_id': 'aabbccddeeff_preset', diff --git a/tests/components/wled/snapshots/test_switch.ambr b/tests/components/wled/snapshots/test_switch.ambr index 99358153fe1..c32bc314cc0 100644 --- a/tests/components/wled/snapshots/test_switch.ambr +++ b/tests/components/wled/snapshots/test_switch.ambr @@ -42,6 +42,7 @@ 'original_name': 'Nightlight', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'nightlight', 'unique_id': 'aabbccddeeff_nightlight', @@ -126,6 +127,7 @@ 'original_name': 'Reverse', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'reverse', 'unique_id': 'aabbccddeeff_reverse_0', @@ -211,6 +213,7 @@ 'original_name': 'Sync receive', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sync_receive', 'unique_id': 'aabbccddeeff_sync_receive', @@ -296,6 +299,7 @@ 'original_name': 'Sync send', 'platform': 'wled', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'sync_send', 'unique_id': 'aabbccddeeff_sync_send', diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index c1ff80c9630..a7289e669fc 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -60,6 +60,7 @@ 'original_name': 'Energy Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:6005200000', @@ -112,6 +113,7 @@ 'original_name': 'Flow Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:11005200000', @@ -164,6 +166,7 @@ 'original_name': 'Frequency Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:9005200000', @@ -216,6 +219,7 @@ 'original_name': 'Hours Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:7005200000', @@ -268,6 +272,7 @@ 'original_name': 'List Item Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'state', 'unique_id': '1234:8005200000', @@ -318,6 +323,7 @@ 'original_name': 'Percentage Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:2005200000', @@ -369,6 +375,7 @@ 'original_name': 'Power Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:5005200000', @@ -421,6 +428,7 @@ 'original_name': 'Pressure Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:4005200000', @@ -475,6 +483,7 @@ 'original_name': 'RPM Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:10005200000', @@ -527,6 +536,7 @@ 'original_name': 'Simple Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:1005200000', @@ -577,6 +587,7 @@ 'original_name': 'Temperature Parameter', 'platform': 'wolflink', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1234:3005200000', diff --git a/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr b/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr index daa232ab141..2b732056991 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_alarm_control_panel.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': , 'translation_key': None, 'unique_id': '1', diff --git a/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr b/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr index 39b3ef09196..9724125b989 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_binary_sensor.ambr @@ -27,6 +27,7 @@ 'original_name': 'Battery', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF4-battery', @@ -75,6 +76,7 @@ 'original_name': 'Door', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF4', @@ -123,6 +125,7 @@ 'original_name': 'Battery', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF5-battery', @@ -171,6 +174,7 @@ 'original_name': 'Door', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF5', @@ -219,6 +223,7 @@ 'original_name': 'Battery', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF6-battery', @@ -267,6 +272,7 @@ 'original_name': 'Door', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': 'RF6', @@ -315,6 +321,7 @@ 'original_name': 'Battery', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'battery', 'unique_id': '1-battery', @@ -363,6 +370,7 @@ 'original_name': 'Jam', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'jam', 'unique_id': '1-jam', @@ -411,6 +419,7 @@ 'original_name': 'Power loss', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'power_loss', 'unique_id': '1-acfail', @@ -459,6 +468,7 @@ 'original_name': 'Tamper', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'tamper', 'unique_id': '1-tamper', diff --git a/tests/components/yale_smart_alarm/snapshots/test_button.ambr b/tests/components/yale_smart_alarm/snapshots/test_button.ambr index 7d52d1d7206..65c36cbddad 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_button.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_button.ambr @@ -27,6 +27,7 @@ 'original_name': 'Panic button', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'panic', 'unique_id': 'yale_smart_alarm-panic', diff --git a/tests/components/yale_smart_alarm/snapshots/test_lock.ambr b/tests/components/yale_smart_alarm/snapshots/test_lock.ambr index e7c97b9001b..ebed9ac4316 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_lock.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_lock.ambr @@ -27,6 +27,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '1111', @@ -76,6 +77,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '2222', @@ -125,6 +127,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '3333', @@ -174,6 +177,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '7777', @@ -223,6 +227,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '8888', @@ -272,6 +277,7 @@ 'original_name': None, 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': None, 'unique_id': '9999', diff --git a/tests/components/yale_smart_alarm/snapshots/test_select.ambr b/tests/components/yale_smart_alarm/snapshots/test_select.ambr index 2899e716ea1..04ec15b6ccb 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_select.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_select.ambr @@ -33,6 +33,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '1111-volume', @@ -91,6 +92,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '2222-volume', @@ -149,6 +151,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '3333-volume', @@ -207,6 +210,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '7777-volume', @@ -265,6 +269,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '8888-volume', @@ -323,6 +328,7 @@ 'original_name': 'Volume', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'volume', 'unique_id': '9999-volume', diff --git a/tests/components/yale_smart_alarm/snapshots/test_switch.ambr b/tests/components/yale_smart_alarm/snapshots/test_switch.ambr index 17c44bf6ebf..451523fd51d 100644 --- a/tests/components/yale_smart_alarm/snapshots/test_switch.ambr +++ b/tests/components/yale_smart_alarm/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '1111-autolock', @@ -74,6 +75,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '2222-autolock', @@ -121,6 +123,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '3333-autolock', @@ -168,6 +171,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '7777-autolock', @@ -215,6 +219,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '8888-autolock', @@ -262,6 +267,7 @@ 'original_name': 'Autolock', 'platform': 'yale_smart_alarm', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'autolock', 'unique_id': '9999-autolock', diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr index 8cb28776d74..a4008bab8de 100644 --- a/tests/components/youless/snapshots/test_sensor.ambr +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Energy export tariff 1', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'youless_localhost_delivery_low', @@ -81,6 +82,7 @@ 'original_name': 'Energy export tariff 2', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_export_tariff_kwh', 'unique_id': 'youless_localhost_delivery_high', @@ -133,6 +135,7 @@ 'original_name': 'Total gas usage', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_gas_m3', 'unique_id': 'youless_localhost_gas', @@ -185,6 +188,7 @@ 'original_name': 'Average peak', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'average_peak', 'unique_id': 'youless_localhost_average_peak', @@ -237,6 +241,7 @@ 'original_name': 'Current phase 1', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_1_current', @@ -289,6 +294,7 @@ 'original_name': 'Current phase 2', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_2_current', @@ -341,6 +347,7 @@ 'original_name': 'Current phase 3', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_current_phase_a', 'unique_id': 'youless_localhost_phase_3_current', @@ -393,6 +400,7 @@ 'original_name': 'Current power usage', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_w', 'unique_id': 'youless_localhost_usage', @@ -445,6 +453,7 @@ 'original_name': 'Energy import tariff 1', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'youless_localhost_power_low', @@ -497,6 +506,7 @@ 'original_name': 'Energy import tariff 2', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_tariff_kwh', 'unique_id': 'youless_localhost_power_high', @@ -549,6 +559,7 @@ 'original_name': 'Month peak', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'month_peak', 'unique_id': 'youless_localhost_month_peak', @@ -601,6 +612,7 @@ 'original_name': 'Power phase 1', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'youless_localhost_phase_1_power', @@ -653,6 +665,7 @@ 'original_name': 'Power phase 2', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'youless_localhost_phase_2_power', @@ -705,6 +718,7 @@ 'original_name': 'Power phase 3', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_power_phase_w', 'unique_id': 'youless_localhost_phase_3_power', @@ -760,6 +774,7 @@ 'original_name': 'Tariff', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_tariff', 'unique_id': 'youless_localhost_tariff', @@ -814,6 +829,7 @@ 'original_name': 'Total energy import', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_energy_import_kwh', 'unique_id': 'youless_localhost_power_total', @@ -866,6 +882,7 @@ 'original_name': 'Voltage phase 1', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'youless_localhost_phase_1_voltage', @@ -918,6 +935,7 @@ 'original_name': 'Voltage phase 2', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'youless_localhost_phase_2_voltage', @@ -970,6 +988,7 @@ 'original_name': 'Voltage phase 3', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_voltage_phase_v', 'unique_id': 'youless_localhost_phase_3_voltage', @@ -1022,6 +1041,7 @@ 'original_name': 'Current usage', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'active_s0_w', 'unique_id': 'youless_localhost_extra_usage', @@ -1074,6 +1094,7 @@ 'original_name': 'Total energy', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_s0_kwh', 'unique_id': 'youless_localhost_extra_total', @@ -1126,6 +1147,7 @@ 'original_name': 'Total water usage', 'platform': 'youless', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'total_water', 'unique_id': 'youless_localhost_water', diff --git a/tests/components/zeversolar/snapshots/test_sensor.ambr b/tests/components/zeversolar/snapshots/test_sensor.ambr index f948eec79df..393b46d3709 100644 --- a/tests/components/zeversolar/snapshots/test_sensor.ambr +++ b/tests/components/zeversolar/snapshots/test_sensor.ambr @@ -29,6 +29,7 @@ 'original_name': 'Energy today', 'platform': 'zeversolar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'energy_today', 'unique_id': '123456778_energy_today', @@ -81,6 +82,7 @@ 'original_name': 'Power', 'platform': 'zeversolar', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'pac', 'unique_id': '123456778_pac', diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 77ac85ed4ed..08510364eba 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1550,6 +1550,7 @@ async def test_entity_info_added_to_entity_registry( original_icon="nice:icon", original_name="best name", options=None, + suggested_object_id=None, supported_features=5, translation_key="my_translation_key", unit_of_measurement=PERCENTAGE, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 671c2ddeb29..cef52810fa0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -144,6 +144,7 @@ def test_get_or_create_updates_data( original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", + suggested_object_id=None, supported_features=5, translation_key="initial-translation_key", unit_of_measurement="initial-unit_of_measurement", @@ -202,6 +203,7 @@ def test_get_or_create_updates_data( original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", + suggested_object_id=None, supported_features=10, translation_key="updated-translation_key", unit_of_measurement="updated-unit_of_measurement", @@ -254,6 +256,7 @@ def test_get_or_create_updates_data( original_device_class=None, original_icon=None, original_name=None, + suggested_object_id=None, supported_features=0, # supported_features is stored as an int translation_key=None, unit_of_measurement=None, @@ -514,6 +517,7 @@ async def test_load_bad_data( { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": None, "categories": {}, "config_entry_id": None, @@ -537,6 +541,7 @@ async def test_load_bad_data( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": 123, # Should trigger warning @@ -545,6 +550,7 @@ async def test_load_bad_data( { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": None, "categories": {}, "config_entry_id": None, @@ -568,6 +574,7 @@ async def test_load_bad_data( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": ["not", "valid"], # Should not load @@ -922,6 +929,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": "very_unique", @@ -1101,6 +1109,7 @@ async def test_migration_1_11( "original_name": None, "platform": "super_platform", "previous_unique_id": None, + "suggested_object_id": None, "supported_features": 0, "translation_key": None, "unique_id": "very_unique", @@ -2577,6 +2586,7 @@ async def test_restore_entity( original_device_class="device_class_2", original_icon="original_icon_2", original_name="original_name_2", + suggested_object_id="suggested_2", supported_features=2, translation_key="translation_key_2", unit_of_measurement="unit_2", From 5ea6811d013048ed0603d655ef34c0fd2b9141da Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 26 May 2025 19:31:25 +0200 Subject: [PATCH 0934/1175] Add translation for ZHA light effect (#145630) * Add translations for ZHA light effects * Add icons for ZHA light effects * Fix capitalization of "Color loop" Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/zha/icons.json | 11 +++++++++++ homeassistant/components/zha/strings.json | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/icons.json b/homeassistant/components/zha/icons.json index d43e213aa4a..e487f2ee24f 100644 --- a/homeassistant/components/zha/icons.json +++ b/homeassistant/components/zha/icons.json @@ -5,6 +5,17 @@ "default": "mdi:hand-wave" } }, + "light": { + "light": { + "state_attributes": { + "effect": { + "state": { + "colorloop": "mdi:looks" + } + } + } + } + }, "number": { "timer_duration": { "default": "mdi:timer" diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 33158dacf70..95bf339f7d9 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -675,7 +675,14 @@ }, "light": { "light": { - "name": "[%key:component::light::title%]" + "name": "[%key:component::light::title%]", + "state_attributes": { + "effect": { + "state": { + "colorloop": "Color loop" + } + } + } }, "light_group": { "name": "Light group" From eec766641668a4ceae09bf3b259239d56cc90a4e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 26 May 2025 19:35:07 +0200 Subject: [PATCH 0935/1175] Update squeezebox test snapshots (#145632) --- tests/components/squeezebox/snapshots/test_switch.ambr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/squeezebox/snapshots/test_switch.ambr b/tests/components/squeezebox/snapshots/test_switch.ambr index b084e3a583d..275fc26baa7 100644 --- a/tests/components/squeezebox/snapshots/test_switch.ambr +++ b/tests/components/squeezebox/snapshots/test_switch.ambr @@ -27,6 +27,7 @@ 'original_name': 'Alarm (1)', 'platform': 'squeezebox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarm', 'unique_id': 'aa:bb:cc:dd:ee:ff_alarm_1', @@ -75,6 +76,7 @@ 'original_name': 'Alarms enabled', 'platform': 'squeezebox', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'alarms_enabled', 'unique_id': 'aa:bb:cc:dd:ee:ff_alarms_enabled', From b15989f2bfa621c148a96696e89820e71ca6c2dd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 19:39:11 +0200 Subject: [PATCH 0936/1175] Make tests less dependent on issue registry size (#145631) * Make tests less dependent on issue registry size * Make tests less dependent on issue registry size --- tests/components/camera/test_init.py | 6 +-- tests/components/esphome/test_repairs.py | 6 +-- tests/components/group/test_sensor.py | 6 ++- .../components/homeassistant/test_repairs.py | 44 ++++++------------- tests/components/lcn/test_binary_sensor.py | 2 - .../smartthings/test_binary_sensor.py | 4 -- tests/components/smartthings/test_sensor.py | 4 -- 7 files changed, 23 insertions(+), 49 deletions(-) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 2348ca58673..7c56d142920 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -237,6 +237,7 @@ async def test_snapshot_service( expected_filename: str, expected_issues: list, snapshot: SnapshotAssertion, + issue_registry: ir.IssueRegistry, ) -> None: """Test snapshot service.""" mopen = mock_open() @@ -265,8 +266,6 @@ async def test_snapshot_service( assert len(mock_write.mock_calls) == 1 assert mock_write.mock_calls[0][1][0] == b"Test" - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 + len(expected_issues) for expected_issue in expected_issues: issue = issue_registry.async_get_issue(DOMAIN, expected_issue) assert issue is not None @@ -638,6 +637,7 @@ async def test_record_service( expected_filename: str, expected_issues: list, snapshot: SnapshotAssertion, + issue_registry: ir.IssueRegistry, ) -> None: """Test record service.""" with ( @@ -666,8 +666,6 @@ async def test_record_service( ANY, expected_filename, duration=30, lookback=0 ) - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 + len(expected_issues) for expected_issue in expected_issues: issue = issue_registry.async_get_issue(DOMAIN, expected_issue) assert issue is not None diff --git a/tests/components/esphome/test_repairs.py b/tests/components/esphome/test_repairs.py index 268b30f8b52..692a7dd9cc9 100644 --- a/tests/components/esphome/test_repairs.py +++ b/tests/components/esphome/test_repairs.py @@ -70,8 +70,7 @@ async def test_device_conflict_manual( issues = await get_repairs(hass, hass_ws_client) assert issues - assert len(issues) == 1 - assert any(True for issue in issues if issue["issue_id"] == issue_id) + assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None await async_process_repairs_platforms(hass) client = await hass_client() @@ -182,8 +181,7 @@ async def test_device_conflict_migration( issues = await get_repairs(hass, hass_ws_client) assert issues - assert len(issues) == 1 - assert any(True for issue in issues if issue["issue_id"] == issue_id) + assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None await async_process_repairs_platforms(hass) client = await hass_client() diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index 187991141e7..de48c711587 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -481,7 +481,11 @@ async def test_sensor_with_uoms_but_no_device_class( assert state.attributes.get("unit_of_measurement") == "W" assert state.state == str(float(sum(VALUES))) - assert not issue_registry.issues + assert not [ + issue + for issue in issue_registry.issues.values() + if issue.domain == GROUP_DOMAIN + ] hass.states.async_set( entity_ids[0], diff --git a/tests/components/homeassistant/test_repairs.py b/tests/components/homeassistant/test_repairs.py index f84b29d8d2d..d9329744694 100644 --- a/tests/components/homeassistant/test_repairs.py +++ b/tests/components/homeassistant/test_repairs.py @@ -2,6 +2,7 @@ from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN 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 @@ -10,13 +11,13 @@ from tests.components.repairs import ( process_repair_fix_flow, start_repair_fix_flow, ) -from tests.typing import ClientSessionGenerator, WebSocketGenerator +from tests.typing import ClientSessionGenerator async def test_integration_not_found_confirm_step( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, + issue_registry: ir.IssueRegistry, ) -> None: """Test the integration_not_found issue confirm step.""" assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) @@ -33,17 +34,11 @@ async def test_integration_not_found_confirm_step( issue_id = "integration_not_found.test1" await async_process_repairs_platforms(hass) - ws_client = await hass_ws_client(hass) http_client = await hass_client() - # Assert the issue is present - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - assert msg["success"] - assert len(msg["result"]["issues"]) == 1 - issue = msg["result"]["issues"][0] - assert issue["issue_id"] == issue_id - assert issue["translation_placeholders"] == {"domain": "test1"} + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) + assert issue is not None + assert issue.translation_placeholders == {"domain": "test1"} data = await start_repair_fix_flow(http_client, HOMEASSISTANT_DOMAIN, issue_id) @@ -68,16 +63,13 @@ async def test_integration_not_found_confirm_step( assert hass.config_entries.async_get_entry(entry2.entry_id) is None # Assert the issue is resolved - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - assert msg["success"] - assert len(msg["result"]["issues"]) == 0 + assert not issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) async def test_integration_not_found_ignore_step( hass: HomeAssistant, hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, + issue_registry: ir.IssueRegistry, ) -> None: """Test the integration_not_found issue ignore step.""" assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) @@ -92,17 +84,11 @@ async def test_integration_not_found_ignore_step( issue_id = "integration_not_found.test1" await async_process_repairs_platforms(hass) - ws_client = await hass_ws_client(hass) http_client = await hass_client() - # Assert the issue is present - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - assert msg["success"] - assert len(msg["result"]["issues"]) == 1 - issue = msg["result"]["issues"][0] - assert issue["issue_id"] == issue_id - assert issue["translation_placeholders"] == {"domain": "test1"} + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) + assert issue is not None + assert issue.translation_placeholders == {"domain": "test1"} data = await start_repair_fix_flow(http_client, HOMEASSISTANT_DOMAIN, issue_id) @@ -128,8 +114,6 @@ async def test_integration_not_found_ignore_step( assert hass.config_entries.async_get_entry(entry1.entry_id) # Assert the issue is resolved - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - assert msg["success"] - assert len(msg["result"]["issues"]) == 1 - assert msg["result"]["issues"][0].get("dismissed_version") is not None + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) + assert issue is not None + assert issue.dismissed_version is not None diff --git a/tests/components/lcn/test_binary_sensor.py b/tests/components/lcn/test_binary_sensor.py index 7e828dbc588..b9362dcd242 100644 --- a/tests/components/lcn/test_binary_sensor.py +++ b/tests/components/lcn/test_binary_sensor.py @@ -190,5 +190,3 @@ async def test_create_issue( assert issue_registry.async_get_issue( DOMAIN, f"deprecated_binary_sensor_{entity_id}" ) - - assert len(issue_registry.issues) == 1 diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 45643f80d2c..ab9531bbef6 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -190,7 +190,6 @@ async def test_create_issue_with_items( assert automations_with_entity(hass, entity_id)[0] == "automation.test" assert scripts_with_entity(hass, entity_id)[0] == "script.test" - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_binary_{issue_string}_scripts" @@ -210,7 +209,6 @@ async def test_create_issue_with_items( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 @pytest.mark.parametrize( @@ -258,7 +256,6 @@ async def test_create_issue( assert hass.states.get(entity_id).state == STATE_OFF - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_binary_{issue_string}" @@ -277,4 +274,3 @@ async def test_create_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index bfb203c1485..a004dec214a 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -205,7 +205,6 @@ async def test_create_issue_with_items( assert automations_with_entity(hass, entity_id)[0] == "automation.test" assert scripts_with_entity(hass, entity_id)[0] == "script.test" - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_{issue_string}_scripts" @@ -226,7 +225,6 @@ async def test_create_issue_with_items( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 @pytest.mark.parametrize( @@ -333,7 +331,6 @@ async def test_create_issue( assert hass.states.get(entity_id).state == expected_state - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_{issue_string}" @@ -353,7 +350,6 @@ async def test_create_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 @pytest.mark.parametrize("device_fixture", ["da_ac_rac_000001"]) From b626204f634af671d8ac04c3a481f28e31ec7739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 26 May 2025 18:40:29 +0100 Subject: [PATCH 0937/1175] Add default device class display precision for Sensor (#145013) * Add default device class display precision for Sensor * Renaming, docstrings, cleanup * Simplify units list * Fix tests * Fix missing precision when suggested is specified * Update snapshots * Fix when unit of measurement is not valid * Fix tests * Fix deprecated unit usage * Fix goalzero tests The sensor native_value method was accessing the data dict and trowing, since the mock did not have any data for the sensors. Since now the precision is always specified (it was missing for those sensors), the throw was hitting async_update_entity_options in _update_suggested_precision. Previously, async_update_entity_options was not called since it had no precision. * Fix metoffice * Fix smartthings * Add default sensor data for Tesla Wall Connector tests * Update snapshots * Revert spaces * Update smartthings snapshots * Add missing sensor mock for tesla wall connector * Address review comments * Add doc comment * Add cap to doc comment * Update comment * Update snapshots * Update comment --- homeassistant/components/sensor/__init__.py | 131 +++-- homeassistant/components/sensor/const.py | 47 ++ tests/common.py | 25 + tests/components/abode/test_sensor.py | 4 +- .../acaia/snapshots/test_sensor.ambr | 3 + .../accuweather/snapshots/test_sensor.ambr | 180 +++++++ tests/components/accuweather/test_sensor.py | 4 +- .../airgradient/snapshots/test_sensor.ambr | 21 + .../airzone/snapshots/test_sensor.ambr | 31 +- .../apcupsd/snapshots/test_sensor.ambr | 39 ++ .../apsystems/snapshots/test_sensor.ambr | 27 + .../aquacell/snapshots/test_sensor.ambr | 6 + .../arve/snapshots/test_sensor.ambr | 3 + .../autarco/snapshots/test_sensor.ambr | 45 ++ .../bluemaestro/snapshots/test_sensor.ambr | 6 + .../bsblan/snapshots/test_sensor.ambr | 6 + .../deconz/snapshots/test_sensor.ambr | 15 + .../snapshots/test_sensor.ambr | 9 + .../ecovacs/snapshots/test_sensor.ambr | 44 +- .../eheimdigital/snapshots/test_sensor.ambr | 3 + tests/components/eheimdigital/test_sensor.py | 8 +- .../emoncms/snapshots/test_sensor.ambr | 3 + .../snapshots/test_diagnostics.ambr | 12 + .../enphase_envoy/snapshots/test_sensor.ambr | 92 +++- .../filesize/snapshots/test_sensor.ambr | 6 + .../flexit_bacnet/snapshots/test_sensor.ambr | 15 + .../fritz/snapshots/test_sensor.ambr | 24 + .../fritzbox/snapshots/test_sensor.ambr | 27 + .../fronius/snapshots/test_sensor.ambr | 372 ++++++++++++++ .../fujitsu_fglair/snapshots/test_sensor.ambr | 6 + .../fyta/snapshots/test_sensor.ambr | 12 + .../glances/snapshots/test_sensor.ambr | 75 ++- tests/components/hddtemp/test_sensor.py | 2 +- .../homee/snapshots/test_sensor.ambr | 51 ++ .../snapshots/test_init.ambr | 123 +++++ .../homewizard/snapshots/test_sensor.ambr | 396 +++++++++++++++ tests/components/honeywell/test_sensor.py | 10 +- .../snapshots/test_sensor.ambr | 28 +- .../husqvarna_automower/test_sensor.py | 2 +- .../hydrawise/snapshots/test_sensor.ambr | 9 + .../imeon_inverter/snapshots/test_sensor.ambr | 111 ++++ .../incomfort/snapshots/test_sensor.ambr | 9 + .../intellifire/snapshots/test_sensor.ambr | 6 + .../iron_os/snapshots/test_sensor.ambr | 21 + .../components/lcn/snapshots/test_sensor.ambr | 6 + .../lektrico/snapshots/test_sensor.ambr | 26 +- .../letpot/snapshots/test_sensor.ambr | 3 + .../lg_thinq/snapshots/test_sensor.ambr | 6 + .../madvr/snapshots/test_sensor.ambr | 12 + .../matter/snapshots/test_sensor.ambr | 61 ++- .../meteo_france/snapshots/test_sensor.ambr | 15 + tests/components/metoffice/const.py | 4 +- tests/components/metoffice/test_sensor.py | 18 +- .../miele/snapshots/test_sensor.ambr | 36 ++ tests/components/mobile_app/test_sensor.py | 22 +- tests/components/mqtt/test_sensor.py | 6 +- .../myuplink/snapshots/test_sensor.ambr | 180 +++++++ .../netatmo/snapshots/test_sensor.ambr | 72 +++ tests/components/nexia/test_sensor.py | 6 +- .../nextcloud/snapshots/test_sensor.ambr | 3 + tests/components/nws/const.py | 17 +- tests/components/nws/test_sensor.py | 4 +- .../nyt_games/snapshots/test_sensor.ambr | 12 + tests/components/nzbget/test_sensor.py | 6 +- .../ohme/snapshots/test_sensor.ambr | 9 + .../omnilogic/snapshots/test_sensor.ambr | 10 +- .../ondilo_ico/snapshots/test_sensor.ambr | 6 + .../onewire/snapshots/test_sensor.ambr | 114 +++++ .../openweathermap/snapshots/test_sensor.ambr | 52 +- .../palazzetti/snapshots/test_sensor.ambr | 24 + .../paperless_ngx/snapshots/test_sensor.ambr | 6 + .../peblar/snapshots/test_sensor.ambr | 21 + .../ping/snapshots/test_sensor.ambr | 9 + .../plaato/snapshots/test_sensor.ambr | 3 + .../poolsense/snapshots/test_sensor.ambr | 3 + .../powerfox/snapshots/test_sensor.ambr | 30 ++ .../snapshots/test_sensor.ambr | 9 + .../rehlko/snapshots/test_sensor.ambr | 39 ++ .../renault/snapshots/test_sensor.ambr | 138 +++++ .../sabnzbd/snapshots/test_sensor.ambr | 12 + .../sanix/snapshots/test_sensor.ambr | 3 + .../sense/snapshots/test_sensor.ambr | 93 ++++ .../sensibo/snapshots/test_sensor.ambr | 15 + tests/components/sensor/test_init.py | 476 +++++++++++------- .../snapshots/test_sensor.ambr | 66 ++- .../sfr_box/snapshots/test_sensor.ambr | 6 + .../components/sma/snapshots/test_sensor.ambr | 267 ++++++++++ .../smartthings/snapshots/test_sensor.ambr | 150 +++++- .../smarty/snapshots/test_sensor.ambr | 9 + .../smlight/snapshots/test_sensor.ambr | 6 + .../solarlog/snapshots/test_sensor.ambr | 38 +- tests/components/steamist/test_sensor.py | 4 +- tests/components/subaru/api_responses.py | 12 +- tests/components/subaru/test_sensor.py | 6 +- .../suez_water/snapshots/test_sensor.ambr | 3 + .../snapshots/test_sensor.ambr | 8 +- .../swiss_public_transport/test_sensor.py | 6 +- .../snapshots/test_sensor.ambr | 6 + .../tasmota/snapshots/test_sensor.ambr | 51 ++ .../technove/snapshots/test_sensor.ambr | 18 + .../tedee/snapshots/test_sensor.ambr | 6 + .../tesla_fleet/snapshots/test_sensor.ambr | 36 +- .../tesla_wall_connector/test_init.py | 11 +- .../tesla_wall_connector/test_sensor.py | 2 +- .../teslemetry/snapshots/test_sensor.ambr | 32 +- .../tessie/snapshots/test_sensor.ambr | 28 +- tests/components/tilt_ble/test_sensor.py | 7 +- tests/components/tomorrowio/test_sensor.py | 13 +- .../tplink/snapshots/test_sensor.ambr | 10 +- .../unifi/snapshots/test_sensor.ambr | 85 +++- tests/components/unifi/test_sensor.py | 10 +- .../components/v2c/snapshots/test_sensor.ambr | 21 + .../velbus/snapshots/test_sensor.ambr | 9 + tests/components/vera/test_sensor.py | 7 +- .../vesync/snapshots/test_sensor.ambr | 18 + .../vicare/snapshots/test_sensor.ambr | 90 ++++ .../watergate/snapshots/test_sensor.ambr | 15 + .../snapshots/test_sensor.ambr | 3 + .../weheat/snapshots/test_sensor.ambr | 6 + .../withings/snapshots/test_sensor.ambr | 92 +++- .../wolflink/snapshots/test_sensor.ambr | 18 + .../youless/snapshots/test_sensor.ambr | 63 +++ .../zeversolar/snapshots/test_sensor.ambr | 6 + 123 files changed, 4523 insertions(+), 377 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e06ee85cd03..9948860fd5f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -38,6 +38,7 @@ from .const import ( # noqa: F401 ATTR_OPTIONS, ATTR_STATE_CLASS, CONF_STATE_CLASS, + DEFAULT_PRECISION_LIMIT, DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_UNITS, DEVICE_CLASSES, @@ -48,6 +49,7 @@ from .const import ( # noqa: F401 STATE_CLASSES, STATE_CLASSES_SCHEMA, UNIT_CONVERTERS, + UNITS_PRECISION, SensorDeviceClass, SensorStateClass, ) @@ -137,6 +139,29 @@ def _numeric_state_expected( return device_class is not None +def _calculate_precision_from_ratio( + device_class: SensorDeviceClass, from_unit: str, to_unit: str, base_precision: int +) -> int | None: + """Calculate the precision for a unit conversion. + + Adjusts the base precision based on the ratio between the source and target units + for the given sensor device class. Returns the new precision or None if conversion + is not possible. + """ + if device_class not in UNIT_CONVERTERS: + return None + converter = UNIT_CONVERTERS[device_class] + + if from_unit not in converter.VALID_UNITS or to_unit not in converter.VALID_UNITS: + return None + + # Scale the precision when converting to a larger or smaller unit + # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = log10(converter.get_unit_ratio(from_unit, to_unit)) + ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) + return max(0, base_precision + ratio_log) + + CACHED_PROPERTIES_WITH_ATTR_ = { "device_class", "last_reset", @@ -663,30 +688,10 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): converter := UNIT_CONVERTERS.get(device_class) ): # Unit conversion needed - converted_numerical_value = converter.converter_factory( - native_unit_of_measurement, - unit_of_measurement, + value = converter.converter_factory( + native_unit_of_measurement, unit_of_measurement )(float(numerical_value)) - # If unit conversion is happening, and there's no rounding for display, - # do a best effort rounding here. - if ( - suggested_precision is None - and self._sensor_option_display_precision is None - ): - # Deduce the precision by finding the decimal point, if any - value_s = str(value) - # Scale the precision when converting to a larger unit - # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh - precision = ( - len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 - ) + converter.get_unit_floored_log_ratio( - native_unit_of_measurement, unit_of_measurement - ) - value = f"{converted_numerical_value:z.{precision}f}" - else: - value = converted_numerical_value - # Validate unit of measurement used for sensors with a device class if ( not self._invalid_unit_of_measurement_reported @@ -739,34 +744,78 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return cast(int, precision) return None - def _update_suggested_precision(self) -> None: - """Update suggested display precision stored in registry.""" - assert self.registry_entry + def _get_adjusted_display_precision(self) -> int | None: + """Return the display precision for the sensor. - device_class = self.device_class + When the integration has specified a suggested display precision, it will be used. + If a unit conversion is needed, the display precision will be adjusted based on + the ratio from the native unit to the current one. + + When the integration does not specify a suggested display precision, a default + device class precision will be used from UNITS_PRECISION, and the final precision + will be adjusted based on the ratio from the default unit to the current one. It + will also be capped so that the extra precision (from the base unit) does not + exceed DEFAULT_PRECISION_LIMIT. + """ display_precision = self.suggested_display_precision + device_class = self.device_class + if device_class is None: + return display_precision + default_unit_of_measurement = ( self.suggested_unit_of_measurement or self.native_unit_of_measurement ) + if default_unit_of_measurement is None: + return display_precision + unit_of_measurement = self.unit_of_measurement + if unit_of_measurement is None: + return display_precision - if ( - display_precision is not None - and default_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERTERS - ): - converter = UNIT_CONVERTERS[device_class] - - # Scale the precision when converting to a larger or smaller unit - # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh - ratio_log = log10( - converter.get_unit_ratio( - default_unit_of_measurement, unit_of_measurement + if display_precision is not None: + if default_unit_of_measurement != unit_of_measurement: + return ( + _calculate_precision_from_ratio( + device_class, + default_unit_of_measurement, + unit_of_measurement, + display_precision, + ) + or display_precision ) - ) - ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) - display_precision = max(0, display_precision + ratio_log) + return display_precision + # Get the base unit and precision for the device class so we can use it to infer + # the display precision for the current unit + if device_class not in UNITS_PRECISION: + return None + device_class_base_unit, device_class_base_precision = UNITS_PRECISION[ + device_class + ] + + precision = ( + _calculate_precision_from_ratio( + device_class, + device_class_base_unit, + unit_of_measurement, + device_class_base_precision, + ) + if device_class_base_unit != unit_of_measurement + else device_class_base_precision + ) + if precision is None: + return None + + # Since we are inferring the precision from the device class, cap it to avoid + # having too many decimals + return min(precision, device_class_base_precision + DEFAULT_PRECISION_LIMIT) + + def _update_suggested_precision(self) -> None: + """Update suggested display precision stored in registry.""" + + display_precision = self._get_adjusted_display_precision() + + assert self.registry_entry sensor_options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {}) if "suggested_display_precision" not in sensor_options: if display_precision is None: diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index f26edcd6c35..994c29b6bbf 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -643,6 +643,53 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), } +# Maximum precision (decimals) deviation from default device class precision. +DEFAULT_PRECISION_LIMIT = 2 + +# Map one unit for each device class to its default precision. +# The biggest unit with the lowest precision should be used. For example, if W should +# have 0 decimals, that one should be used and not mW, even though mW also should have +# 0 decimals. Otherwise the smaller units will have more decimals than expected. +UNITS_PRECISION = { + SensorDeviceClass.APPARENT_POWER: (UnitOfApparentPower.VOLT_AMPERE, 0), + SensorDeviceClass.AREA: (UnitOfArea.SQUARE_CENTIMETERS, 0), + SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.PA, 0), + SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: ( + UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, + 0, + ), + SensorDeviceClass.CONDUCTIVITY: (UnitOfConductivity.MICROSIEMENS_PER_CM, 1), + SensorDeviceClass.CURRENT: (UnitOfElectricCurrent.MILLIAMPERE, 0), + SensorDeviceClass.DATA_RATE: (UnitOfDataRate.KILOBITS_PER_SECOND, 0), + SensorDeviceClass.DATA_SIZE: (UnitOfInformation.KILOBITS, 0), + SensorDeviceClass.DISTANCE: (UnitOfLength.CENTIMETERS, 0), + SensorDeviceClass.DURATION: (UnitOfTime.MILLISECONDS, 0), + SensorDeviceClass.ENERGY: (UnitOfEnergy.WATT_HOUR, 0), + SensorDeviceClass.ENERGY_DISTANCE: (UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, 0), + SensorDeviceClass.ENERGY_STORAGE: (UnitOfEnergy.WATT_HOUR, 0), + SensorDeviceClass.FREQUENCY: (UnitOfFrequency.HERTZ, 0), + SensorDeviceClass.GAS: (UnitOfVolume.MILLILITERS, 0), + SensorDeviceClass.IRRADIANCE: (UnitOfIrradiance.WATTS_PER_SQUARE_METER, 0), + SensorDeviceClass.POWER: (UnitOfPower.WATT, 0), + SensorDeviceClass.PRECIPITATION: (UnitOfPrecipitationDepth.CENTIMETERS, 0), + SensorDeviceClass.PRECIPITATION_INTENSITY: ( + UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + 0, + ), + SensorDeviceClass.PRESSURE: (UnitOfPressure.PA, 0), + SensorDeviceClass.REACTIVE_POWER: (UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 0), + SensorDeviceClass.SOUND_PRESSURE: (UnitOfSoundPressure.DECIBEL, 0), + SensorDeviceClass.SPEED: (UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), + SensorDeviceClass.TEMPERATURE: (UnitOfTemperature.KELVIN, 1), + SensorDeviceClass.VOLTAGE: (UnitOfElectricPotential.VOLT, 0), + SensorDeviceClass.VOLUME: (UnitOfVolume.MILLILITERS, 0), + SensorDeviceClass.VOLUME_FLOW_RATE: (UnitOfVolumeFlowRate.LITERS_PER_SECOND, 0), + SensorDeviceClass.VOLUME_STORAGE: (UnitOfVolume.MILLILITERS, 0), + SensorDeviceClass.WATER: (UnitOfVolume.MILLILITERS, 0), + SensorDeviceClass.WEIGHT: (UnitOfMass.GRAMS, 0), + SensorDeviceClass.WIND_SPEED: (UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), +} + DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, diff --git a/tests/common.py b/tests/common.py index 8d51a1e99a1..869291c9463 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1959,3 +1959,28 @@ def get_schema_suggested_value(schema: vol.Schema, key: str) -> Any | None: return None return schema_key.description["suggested_value"] return None + + +def get_sensor_display_state( + hass: HomeAssistant, entity_registry: er.EntityRegistry, entity_id: str +) -> str: + """Return the state rounded for presentation.""" + state = hass.states.get(entity_id) + assert state + value = state.state + + entity_entry = entity_registry.async_get(entity_id) + if entity_entry is None: + return value + + if ( + precision := entity_entry.options.get("sensor", {}).get( + "suggested_display_precision" + ) + ) is None: + return value + + with suppress(TypeError, ValueError): + numerical_value = float(value) + value = f"{numerical_value:z.{precision}f}" + return value diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index e92748bb162..e92957b1657 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -1,5 +1,7 @@ """Tests for the Abode sensor device.""" +import pytest + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.const import ( @@ -45,5 +47,5 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get("sensor.environment_sensor_temperature") # Abodepy device JSON reports 19.5, but Home Assistant shows 19.4 - assert state.state == "19.4" + assert float(state.state) == pytest.approx(19.44444) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS diff --git a/tests/components/acaia/snapshots/test_sensor.ambr b/tests/components/acaia/snapshots/test_sensor.ambr index 6b2585c8ba1..811485a64ee 100644 --- a/tests/components/acaia/snapshots/test_sensor.ambr +++ b/tests/components/acaia/snapshots/test_sensor.ambr @@ -132,6 +132,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/accuweather/snapshots/test_sensor.ambr b/tests/components/accuweather/snapshots/test_sensor.ambr index 6e47f3b0c06..67337d4d0e4 100644 --- a/tests/components/accuweather/snapshots/test_sensor.ambr +++ b/tests/components/accuweather/snapshots/test_sensor.ambr @@ -348,6 +348,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1502,6 +1505,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2370,6 +2376,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2798,6 +2807,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2850,6 +2862,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2901,6 +2916,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2952,6 +2970,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3003,6 +3024,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3054,6 +3078,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3105,6 +3132,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3156,6 +3186,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3207,6 +3240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3258,6 +3294,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3309,6 +3348,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3362,6 +3404,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3414,6 +3459,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3465,6 +3513,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3516,6 +3567,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3567,6 +3621,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3618,6 +3675,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3669,6 +3729,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3720,6 +3783,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3771,6 +3837,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3822,6 +3891,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3873,6 +3945,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3924,6 +3999,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3975,6 +4053,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4026,6 +4107,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4077,6 +4161,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4128,6 +4215,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4179,6 +4269,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4230,6 +4323,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4281,6 +4377,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4332,6 +4431,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4383,6 +4485,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4436,6 +4541,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5554,6 +5662,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5608,6 +5719,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5662,6 +5776,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5714,6 +5831,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5766,6 +5886,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5818,6 +5941,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5870,6 +5996,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5922,6 +6051,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5974,6 +6106,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6026,6 +6161,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6078,6 +6216,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6130,6 +6271,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6182,6 +6326,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6236,6 +6383,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6288,6 +6438,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6340,6 +6493,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6392,6 +6548,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6444,6 +6603,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6496,6 +6658,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6548,6 +6713,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6600,6 +6768,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6652,6 +6823,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6704,6 +6878,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6756,6 +6933,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 87737c2f40c..855c9f3e4d5 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -163,12 +163,12 @@ async def test_sensor_imperial_units( state = hass.states.get("sensor.home_wind_speed") assert state - assert state.state == "9.0" + assert float(state.state) == pytest.approx(9.00988) assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfSpeed.MILES_PER_HOUR state = hass.states.get("sensor.home_realfeel_temperature") assert state - assert state.state == "77.2" + assert state.state == "77.18" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.FAHRENHEIT ) diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index a0daaef2bdc..575c596404b 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -74,6 +74,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -502,6 +505,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -975,6 +981,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1077,6 +1086,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1127,6 +1139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1228,6 +1243,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1486,6 +1504,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/airzone/snapshots/test_sensor.ambr b/tests/components/airzone/snapshots/test_sensor.ambr index 2982f76efe7..491b6c6313b 100644 --- a/tests/components/airzone/snapshots/test_sensor.ambr +++ b/tests/components/airzone/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +132,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +241,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -446,6 +455,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -472,7 +484,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '21.20', + 'state': '21.2', }) # --- # name: test_airzone_create_sensors[sensor.dkn_plus_temperature-entry] @@ -499,6 +511,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -525,7 +540,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '21.7', + 'state': '21.6666666666667', }) # --- # name: test_airzone_create_sensors[sensor.dorm_1_battery-entry] @@ -710,6 +725,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -921,6 +939,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1132,6 +1153,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1238,6 +1262,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/apcupsd/snapshots/test_sensor.ambr b/tests/components/apcupsd/snapshots/test_sensor.ambr index 814a3c63a81..9c0b2de4fdc 100644 --- a/tests/components/apcupsd/snapshots/test_sensor.ambr +++ b/tests/components/apcupsd/snapshots/test_sensor.ambr @@ -123,6 +123,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -321,6 +324,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -614,6 +620,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -667,6 +676,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1010,6 +1022,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1060,6 +1075,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1110,6 +1128,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1162,6 +1183,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1649,6 +1673,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1702,6 +1729,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1755,6 +1785,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1905,6 +1938,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1955,6 +1991,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/apsystems/snapshots/test_sensor.ambr b/tests/components/apsystems/snapshots/test_sensor.ambr index 42021d88001..f163c4db840 100644 --- a/tests/components/apsystems/snapshots/test_sensor.ambr +++ b/tests/components/apsystems/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -394,6 +415,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -447,6 +471,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr index f032f8937de..ec89cb34bca 100644 --- a/tests/components/aquacell/snapshots/test_sensor.ambr +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -123,6 +123,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -225,6 +228,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/arve/snapshots/test_sensor.ambr b/tests/components/arve/snapshots/test_sensor.ambr index a0f23adf339..eb51aa8c1f2 100644 --- a/tests/components/arve/snapshots/test_sensor.ambr +++ b/tests/components/arve/snapshots/test_sensor.ambr @@ -208,6 +208,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/autarco/snapshots/test_sensor.ambr b/tests/components/autarco/snapshots/test_sensor.ambr index 23af1b9c990..73a07d71656 100644 --- a/tests/components/autarco/snapshots/test_sensor.ambr +++ b/tests/components/autarco/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -447,6 +468,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -500,6 +524,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -553,6 +580,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -606,6 +636,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -659,6 +692,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -712,6 +748,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -765,6 +804,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -818,6 +860,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/bluemaestro/snapshots/test_sensor.ambr b/tests/components/bluemaestro/snapshots/test_sensor.ambr index 0848baf1571..055ceb2731f 100644 --- a/tests/components/bluemaestro/snapshots/test_sensor.ambr +++ b/tests/components/bluemaestro/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +238,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/bsblan/snapshots/test_sensor.ambr b/tests/components/bsblan/snapshots/test_sensor.ambr index f87c9a8e9be..eb80858eb5d 100644 --- a/tests/components/bsblan/snapshots/test_sensor.ambr +++ b/tests/components/bsblan/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/deconz/snapshots/test_sensor.ambr b/tests/components/deconz/snapshots/test_sensor.ambr index 6e683241b6b..04f93738b18 100644 --- a/tests/components/deconz/snapshots/test_sensor.ambr +++ b/tests/components/deconz/snapshots/test_sensor.ambr @@ -181,6 +181,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -869,6 +872,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -925,6 +931,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1302,6 +1311,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2206,6 +2218,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/devolo_home_control/snapshots/test_sensor.ambr b/tests/components/devolo_home_control/snapshots/test_sensor.ambr index a93ce7d6ceb..77f18621364 100644 --- a/tests/components/devolo_home_control/snapshots/test_sensor.ambr +++ b/tests/components/devolo_home_control/snapshots/test_sensor.ambr @@ -144,6 +144,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -197,6 +200,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -250,6 +256,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/ecovacs/snapshots/test_sensor.ambr b/tests/components/ecovacs/snapshots/test_sensor.ambr index 4c242103d14..fcd043e10fa 100644 --- a/tests/components/ecovacs/snapshots/test_sensor.ambr +++ b/tests/components/ecovacs/snapshots/test_sensor.ambr @@ -175,6 +175,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -203,7 +206,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0010', + 'state': '0.001', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_battery:entity-registry] @@ -327,6 +330,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -528,6 +534,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -581,6 +590,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -610,7 +622,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40.000', + 'state': '40.0', }) # --- # name: test_sensors[5xu9h3][sensor.goat_g1_total_cleanings:entity-registry] @@ -782,6 +794,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -885,6 +900,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1291,6 +1309,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1344,6 +1365,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1373,7 +1397,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40.000', + 'state': '40.0', }) # --- # name: test_sensors[qhe2o2][sensor.dusty_total_cleanings:entity-registry] @@ -1594,6 +1618,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1697,6 +1724,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1996,6 +2026,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2049,6 +2082,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2078,7 +2114,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40.000', + 'state': '40.0', }) # --- # name: test_sensors[yna5x1][sensor.ozmo_950_total_cleanings:entity-registry] diff --git a/tests/components/eheimdigital/snapshots/test_sensor.ambr b/tests/components/eheimdigital/snapshots/test_sensor.ambr index 7d86d92eaf8..7f12e9fbf9b 100644 --- a/tests/components/eheimdigital/snapshots/test_sensor.ambr +++ b/tests/components/eheimdigital/snapshots/test_sensor.ambr @@ -130,6 +130,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), diff --git a/tests/components/eheimdigital/test_sensor.py b/tests/components/eheimdigital/test_sensor.py index 42df22368a9..a2c0fae5b16 100644 --- a/tests/components/eheimdigital/test_sensor.py +++ b/tests/components/eheimdigital/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers import entity_registry as er from .conftest import init_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, get_sensor_display_state, snapshot_platform @pytest.mark.usefixtures("classic_vario_mock") @@ -69,7 +69,7 @@ async def test_setup_classic_vario( "classic_vario_data", "serviceHour", 100, - str(round(100 / 24, 1)), + str(round(100 / 24, 2)), ), ], ), @@ -77,6 +77,7 @@ async def test_setup_classic_vario( ) async def test_state_update( hass: HomeAssistant, + entity_registry: er.EntityRegistry, eheimdigital_hub_mock: MagicMock, mock_config_entry: MockConfigEntry, device_name: str, @@ -96,5 +97,4 @@ async def test_state_update( for item in entity_list: getattr(device, item[1])[item[2]] = item[3] await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]() - assert (state := hass.states.get(item[0])) - assert state.state == str(item[4]) + assert get_sensor_display_state(hass, entity_registry, item[0]) == str(item[4]) diff --git a/tests/components/emoncms/snapshots/test_sensor.ambr b/tests/components/emoncms/snapshots/test_sensor.ambr index 7dc6f0674e4..1ad7a6c3aa5 100644 --- a/tests/components/emoncms/snapshots/test_sensor.ambr +++ b/tests/components/emoncms/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr index f02f594a2ec..7eb57488d66 100644 --- a/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr +++ b/tests/components/enphase_envoy/snapshots/test_diagnostics.ambr @@ -336,6 +336,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': 'power', 'original_icon': None, @@ -791,6 +794,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': 'power', 'original_icon': None, @@ -1288,6 +1294,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': 'power', 'original_icon': None, @@ -1557,6 +1566,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': 'power', 'original_icon': None, diff --git a/tests/components/enphase_envoy/snapshots/test_sensor.ambr b/tests/components/enphase_envoy/snapshots/test_sensor.ambr index 82f5aad2e25..d548b2a0f93 100644 --- a/tests/components/enphase_envoy/snapshots/test_sensor.ambr +++ b/tests/components/enphase_envoy/snapshots/test_sensor.ambr @@ -256,6 +256,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1793,6 +1796,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2005,6 +2011,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2055,6 +2064,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2204,6 +2216,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2254,6 +2269,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2304,6 +2322,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2354,6 +2375,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2454,6 +2478,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2504,6 +2531,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2663,6 +2693,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6170,6 +6203,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6744,6 +6780,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6844,6 +6883,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6993,6 +7035,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7043,6 +7088,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -7093,6 +7141,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7252,6 +7303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10759,6 +10813,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11333,6 +11390,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11433,6 +11493,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11582,6 +11645,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11632,6 +11698,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11731,6 +11800,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11756,7 +11828,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '26', + 'state': '26.1111111111111', }) # --- # name: test_sensor[envoy_metered_batt_relay][sensor.envoy_1234_available_battery_energy-entry] @@ -11781,6 +11853,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -12117,6 +12192,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18783,6 +18861,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -19829,6 +19910,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -25671,6 +25755,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -26461,6 +26548,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/filesize/snapshots/test_sensor.ambr b/tests/components/filesize/snapshots/test_sensor.ambr index d78be02f5a7..10eaa915616 100644 --- a/tests/components/filesize/snapshots/test_sensor.ambr +++ b/tests/components/filesize/snapshots/test_sensor.ambr @@ -121,6 +121,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -174,6 +177,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr index 3567a976a6c..c3c3b8f185d 100644 --- a/tests/components/flexit_bacnet/snapshots/test_sensor.ambr +++ b/tests/components/flexit_bacnet/snapshots/test_sensor.ambr @@ -233,6 +233,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -283,6 +286,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -493,6 +499,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -599,6 +608,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -753,6 +765,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/fritz/snapshots/test_sensor.ambr b/tests/components/fritz/snapshots/test_sensor.ambr index d2bf4884db3..4efae5951e8 100644 --- a/tests/components/fritz/snapshots/test_sensor.ambr +++ b/tests/components/fritz/snapshots/test_sensor.ambr @@ -72,6 +72,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -221,6 +224,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -274,6 +280,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -472,6 +481,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -620,6 +632,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -670,6 +685,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -720,6 +738,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -772,6 +793,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/fritzbox/snapshots/test_sensor.ambr b/tests/components/fritzbox/snapshots/test_sensor.ambr index a3522202661..bcf27e25fee 100644 --- a/tests/components/fritzbox/snapshots/test_sensor.ambr +++ b/tests/components/fritzbox/snapshots/test_sensor.ambr @@ -127,6 +127,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -225,6 +228,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -372,6 +378,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -530,6 +539,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -583,6 +595,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -636,6 +651,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -689,6 +707,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -742,6 +763,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -795,6 +819,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/fronius/snapshots/test_sensor.ambr b/tests/components/fronius/snapshots/test_sensor.ambr index d26ee76d909..14ca17d81c1 100644 --- a/tests/components/fronius/snapshots/test_sensor.ambr +++ b/tests/components/fronius/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -688,6 +709,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -907,6 +931,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -960,6 +987,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1013,6 +1043,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1066,6 +1099,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1119,6 +1155,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1172,6 +1211,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1225,6 +1267,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1278,6 +1323,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1331,6 +1379,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1808,6 +1859,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1861,6 +1915,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1914,6 +1971,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1967,6 +2027,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2020,6 +2083,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2073,6 +2139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2126,6 +2195,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2179,6 +2251,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2232,6 +2307,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2285,6 +2363,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2338,6 +2419,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2391,6 +2475,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2444,6 +2531,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2497,6 +2587,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2550,6 +2643,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2603,6 +2699,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2656,6 +2755,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2709,6 +2811,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2810,6 +2915,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2863,6 +2971,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2916,6 +3027,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2969,6 +3083,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3022,6 +3139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3075,6 +3195,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3128,6 +3251,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3285,6 +3411,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3338,6 +3467,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3391,6 +3523,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3595,6 +3730,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3648,6 +3786,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3701,6 +3842,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3754,6 +3898,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3807,6 +3954,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3860,6 +4010,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3913,6 +4066,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3966,6 +4122,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4313,6 +4472,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4532,6 +4694,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4585,6 +4750,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4638,6 +4806,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4805,6 +4976,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4858,6 +5032,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4911,6 +5088,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4964,6 +5144,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5017,6 +5200,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5070,6 +5256,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5123,6 +5312,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5176,6 +5368,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5229,6 +5424,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5706,6 +5904,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5759,6 +5960,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5812,6 +6016,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5865,6 +6072,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5918,6 +6128,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5971,6 +6184,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6024,6 +6240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6077,6 +6296,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6130,6 +6352,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6183,6 +6408,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6236,6 +6464,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6289,6 +6520,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6342,6 +6576,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6395,6 +6632,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6448,6 +6688,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6501,6 +6744,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6554,6 +6800,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6607,6 +6856,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6708,6 +6960,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6761,6 +7016,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6814,6 +7072,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6867,6 +7128,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6920,6 +7184,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6973,6 +7240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7026,6 +7296,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7079,6 +7352,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7132,6 +7408,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7185,6 +7464,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7342,6 +7624,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7395,6 +7680,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -7448,6 +7736,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7501,6 +7792,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7554,6 +7848,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -7607,6 +7904,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7660,6 +7960,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7713,6 +8016,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8060,6 +8366,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8327,6 +8636,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8380,6 +8692,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8433,6 +8748,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8486,6 +8804,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8539,6 +8860,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8592,6 +8916,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8645,6 +8972,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8698,6 +9028,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9045,6 +9378,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9312,6 +9648,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9477,6 +9816,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9582,6 +9924,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9635,6 +9980,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9840,6 +10188,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9893,6 +10244,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9946,6 +10300,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9999,6 +10356,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10052,6 +10412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10105,6 +10468,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10158,6 +10524,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10315,6 +10684,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr b/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr index cf22c24c427..e5dcda8d1a5 100644 --- a/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr +++ b/tests/components/fujitsu_fglair/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/fyta/snapshots/test_sensor.ambr b/tests/components/fyta/snapshots/test_sensor.ambr index 6a835b9697e..5227755d852 100644 --- a/tests/components/fyta/snapshots/test_sensor.ambr +++ b/tests/components/fyta/snapshots/test_sensor.ambr @@ -591,6 +591,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), }), 'original_device_class': , 'original_icon': None, @@ -758,6 +761,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1445,6 +1451,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), }), 'original_device_class': , 'original_icon': None, @@ -1612,6 +1621,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/glances/snapshots/test_sensor.ambr b/tests/components/glances/snapshots/test_sensor.ambr index 536e48bef55..40dd1a00cd1 100644 --- a/tests/components/glances/snapshots/test_sensor.ambr +++ b/tests/components/glances/snapshots/test_sensor.ambr @@ -126,6 +126,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -179,6 +182,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -232,6 +238,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -261,7 +270,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.000000', + 'state': '0.0', }) # --- # name: test_sensor_states[sensor.0_0_0_0_dummy0_tx-entry] @@ -288,6 +297,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -317,7 +329,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.000000', + 'state': '0.0', }) # --- # name: test_sensor_states[sensor.0_0_0_0_err_temp_temperature-entry] @@ -344,6 +356,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -397,6 +412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -426,7 +444,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.03162', + 'state': '0.031624', }) # --- # name: test_sensor_states[sensor.0_0_0_0_eth0_tx-entry] @@ -453,6 +471,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -509,6 +530,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -538,7 +562,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06117', + 'state': '0.061168', }) # --- # name: test_sensor_states[sensor.0_0_0_0_lo_tx-entry] @@ -565,6 +589,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -594,7 +621,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.06117', + 'state': '0.061168', }) # --- # name: test_sensor_states[sensor.0_0_0_0_md1_available-entry] @@ -825,6 +852,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -930,6 +960,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -983,6 +1016,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1088,6 +1124,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1141,6 +1180,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1353,6 +1395,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1406,6 +1451,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1435,7 +1483,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.184320', + 'state': '0.18432', }) # --- # name: test_sensor_states[sensor.0_0_0_0_nvme0n1_disk_write-entry] @@ -1462,6 +1510,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1518,6 +1569,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1574,6 +1628,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1630,6 +1687,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1735,6 +1795,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 15740ffa0ea..56ad9fdcb0e 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -132,7 +132,7 @@ async def test_hddtemp_one_disk(hass: HomeAssistant, telnetmock) -> None: reference = REFERENCE[state.attributes.get("device")] - assert state.state == reference["temperature"] + assert round(float(state.state), 0) == float(reference["temperature"]) assert state.attributes.get("device") == reference["device"] assert state.attributes.get("model") == reference["model"] assert ( diff --git a/tests/components/homee/snapshots/test_sensor.ambr b/tests/components/homee/snapshots/test_sensor.ambr index 52bbe4aae3e..618f2bcfdf6 100644 --- a/tests/components/homee/snapshots/test_sensor.ambr +++ b/tests/components/homee/snapshots/test_sensor.ambr @@ -129,6 +129,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +185,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +294,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +350,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -394,6 +406,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -763,6 +778,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -868,6 +886,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1054,6 +1075,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1160,6 +1184,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1329,6 +1356,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1382,6 +1412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1435,6 +1468,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1488,6 +1524,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1541,6 +1580,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1645,6 +1687,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1698,6 +1743,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1751,6 +1799,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), diff --git a/tests/components/homekit_controller/snapshots/test_init.ambr b/tests/components/homekit_controller/snapshots/test_init.ambr index 3d7b276c472..4540cfd239a 100644 --- a/tests/components/homekit_controller/snapshots/test_init.ambr +++ b/tests/components/homekit_controller/snapshots/test_init.ambr @@ -2714,6 +2714,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2931,6 +2934,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2978,6 +2984,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3025,6 +3034,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3072,6 +3084,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3119,6 +3134,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3166,6 +3184,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3428,6 +3449,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3907,6 +3931,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4079,6 +4106,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4251,6 +4281,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4518,6 +4551,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5817,6 +5853,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -6598,6 +6637,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -7254,6 +7296,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -7517,6 +7562,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -8259,6 +8307,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -8686,6 +8737,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -8858,6 +8912,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -9030,6 +9087,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -9522,6 +9582,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -9788,6 +9851,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -10065,6 +10131,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -10207,6 +10276,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -10340,6 +10412,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -10387,6 +10462,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -10434,6 +10512,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10481,6 +10562,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -12052,6 +12136,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -13769,6 +13856,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -14895,6 +14985,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -17441,6 +17534,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -17617,6 +17713,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -17998,6 +18097,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -19190,6 +19292,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -20231,6 +20336,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20278,6 +20386,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -22588,6 +22699,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -23029,6 +23143,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -23338,6 +23455,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -24180,6 +24300,9 @@ ]), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 4e73968d113..9f95e140edc 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -148,6 +148,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -238,6 +241,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -328,6 +334,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -418,6 +427,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -780,6 +792,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1044,6 +1059,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1134,6 +1152,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1224,6 +1245,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1314,6 +1338,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1404,6 +1431,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1677,6 +1707,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1767,6 +1800,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2031,6 +2067,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2121,6 +2160,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2211,6 +2253,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2301,6 +2346,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2391,6 +2439,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2481,6 +2532,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2571,6 +2625,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2661,6 +2718,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2751,6 +2811,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2841,6 +2904,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2931,6 +2997,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3663,6 +3732,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3753,6 +3825,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3843,6 +3918,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3933,6 +4011,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4023,6 +4104,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4113,6 +4197,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4203,6 +4290,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4465,6 +4555,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4554,6 +4647,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4644,6 +4740,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4734,6 +4833,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4909,6 +5011,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4999,6 +5104,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5089,6 +5197,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5179,6 +5290,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5269,6 +5383,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5359,6 +5476,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5449,6 +5569,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5539,6 +5662,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5629,6 +5755,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5719,6 +5848,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5809,6 +5941,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5982,6 +6117,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6797,6 +6935,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6887,6 +7028,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6977,6 +7121,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7067,6 +7214,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7926,6 +8076,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8012,6 +8165,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8183,6 +8339,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8269,6 +8428,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8357,6 +8519,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -8446,6 +8611,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8536,6 +8704,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8626,6 +8797,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8801,6 +8975,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8891,6 +9068,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -8981,6 +9161,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9071,6 +9254,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9161,6 +9347,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9251,6 +9440,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9341,6 +9533,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9431,6 +9626,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9521,6 +9719,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9611,6 +9812,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -9701,6 +9905,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -9874,6 +10081,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10689,6 +10899,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -10779,6 +10992,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10869,6 +11085,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -10959,6 +11178,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11818,6 +12040,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -11904,6 +12129,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12075,6 +12303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12161,6 +12392,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12249,6 +12483,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -12338,6 +12575,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12428,6 +12668,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12518,6 +12761,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12608,6 +12854,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12698,6 +12947,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12788,6 +13040,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12878,6 +13133,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -12968,6 +13226,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13058,6 +13319,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13148,6 +13412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13238,6 +13505,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13328,6 +13598,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13418,6 +13691,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -13508,6 +13784,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -14140,6 +14419,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -14230,6 +14512,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -14320,6 +14605,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -14410,6 +14698,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -15273,6 +15564,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -15363,6 +15657,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -15813,6 +16110,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -15903,6 +16203,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -15993,6 +16296,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -16083,6 +16389,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -16173,6 +16482,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -16539,6 +16851,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -16629,6 +16944,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -16893,6 +17211,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -17246,6 +17567,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -17336,6 +17660,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -17426,6 +17753,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -17516,6 +17846,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -17606,6 +17939,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -17879,6 +18215,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -17969,6 +18308,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18233,6 +18575,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18323,6 +18668,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18413,6 +18761,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18503,6 +18854,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -18593,6 +18947,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -18683,6 +19040,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -18773,6 +19133,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -18863,6 +19226,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -18953,6 +19319,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -19043,6 +19412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -19133,6 +19505,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -19865,6 +20240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -19955,6 +20333,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20045,6 +20426,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20135,6 +20519,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20225,6 +20612,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20315,6 +20705,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -20405,6 +20798,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index ed46fd4cdd2..23df33703d2 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -@pytest.mark.parametrize(("unit", "temp"), [("C", "5"), ("F", "-15")]) +@pytest.mark.parametrize(("unit", "temp"), [("C", 5), ("F", -15)]) async def test_outdoor_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -32,11 +32,11 @@ async def test_outdoor_sensor( assert temperature_state assert humidity_state - assert temperature_state.state == temp - assert humidity_state.state == "25" + assert float(temperature_state.state) == temp + assert float(humidity_state.state) == 25 -@pytest.mark.parametrize(("unit", "temp"), [("C", "5"), ("F", "-15")]) +@pytest.mark.parametrize(("unit", "temp"), [("C", 5), ("F", -15)]) async def test_indoor_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, @@ -62,5 +62,5 @@ async def test_indoor_sensor( assert temperature_state assert humidity_state - assert temperature_state.state == temp + assert float(temperature_state.state) == temp assert humidity_state.state == "25" diff --git a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr index 526474ec08a..109e6614545 100644 --- a/tests/components/husqvarna_automower/snapshots/test_sensor.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -105,7 +108,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.034', + 'state': '0.0341666666666667', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_downtime-entry] @@ -996,6 +999,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1025,7 +1031,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1204.000', + 'state': '1204.0', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_total_cutting_time-entry] @@ -1052,6 +1058,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1081,7 +1090,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1165.000', + 'state': '1165.0', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_total_drive_distance-entry] @@ -1108,6 +1117,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1164,6 +1176,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1193,7 +1208,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1268.000', + 'state': '1268.0', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_total_searching_time-entry] @@ -1220,6 +1235,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1249,7 +1267,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '103.000', + 'state': '103.0', }) # --- # name: test_sensor_snapshot[sensor.test_mower_1_uptime-entry] diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 3d4922781b4..b1029f5919b 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -53,7 +53,7 @@ async def test_cutting_blade_usage_time_sensor( await setup_integration(hass, mock_config_entry) state = hass.states.get("sensor.test_mower_1_cutting_blade_usage_time") assert state is not None - assert state.state == "0.034" + assert float(state.state) == pytest.approx(0.03416666) @pytest.mark.freeze_time( diff --git a/tests/components/hydrawise/snapshots/test_sensor.ambr b/tests/components/hydrawise/snapshots/test_sensor.ambr index c06442a5269..e2e97da120c 100644 --- a/tests/components/hydrawise/snapshots/test_sensor.ambr +++ b/tests/components/hydrawise/snapshots/test_sensor.ambr @@ -78,6 +78,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -300,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -509,6 +515,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index beead7d251b..d3ae33a6c8b 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +300,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +356,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -394,6 +412,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -447,6 +468,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -500,6 +524,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -553,6 +580,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -606,6 +636,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -659,6 +692,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -712,6 +748,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -765,6 +804,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -818,6 +860,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -871,6 +916,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -924,6 +972,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -977,6 +1028,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1030,6 +1084,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1083,6 +1140,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1136,6 +1196,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1914,6 +1977,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1967,6 +2033,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2020,6 +2089,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2073,6 +2145,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2126,6 +2201,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2179,6 +2257,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2232,6 +2313,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2285,6 +2369,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2338,6 +2425,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2391,6 +2481,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2444,6 +2537,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2497,6 +2593,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2550,6 +2649,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2603,6 +2705,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2656,6 +2761,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2709,6 +2817,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/incomfort/snapshots/test_sensor.ambr b/tests/components/incomfort/snapshots/test_sensor.ambr index c08b7ba9f1e..80dd945d7bf 100644 --- a/tests/components/incomfort/snapshots/test_sensor.ambr +++ b/tests/components/incomfort/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -130,6 +136,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/intellifire/snapshots/test_sensor.ambr b/tests/components/intellifire/snapshots/test_sensor.ambr index c65da4357ef..a641db96ffc 100644 --- a/tests/components/intellifire/snapshots/test_sensor.ambr +++ b/tests/components/intellifire/snapshots/test_sensor.ambr @@ -324,6 +324,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -378,6 +381,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 2d22f48c4a1..39dda49d313 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -180,6 +186,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -233,6 +242,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -284,6 +296,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -646,6 +661,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -699,6 +717,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/lcn/snapshots/test_sensor.ambr b/tests/components/lcn/snapshots/test_sensor.ambr index 7cec584ca48..e96f6ccd643 100644 --- a/tests/components/lcn/snapshots/test_sensor.ambr +++ b/tests/components/lcn/snapshots/test_sensor.ambr @@ -117,6 +117,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -167,6 +170,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/lektrico/snapshots/test_sensor.ambr b/tests/components/lektrico/snapshots/test_sensor.ambr index e2ae997d423..569c6af4c04 100644 --- a/tests/components/lektrico/snapshots/test_sensor.ambr +++ b/tests/components/lektrico/snapshots/test_sensor.ambr @@ -21,6 +21,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -73,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -124,6 +130,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -174,6 +183,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -226,6 +238,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -355,6 +370,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -384,7 +402,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0000', + 'state': '0.0', }) # --- # name: test_all_entities[sensor.1p7k_500006_state-entry] @@ -483,6 +501,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -534,6 +555,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/letpot/snapshots/test_sensor.ambr b/tests/components/letpot/snapshots/test_sensor.ambr index 415a1ae8b32..12669bb4110 100644 --- a/tests/components/letpot/snapshots/test_sensor.ambr +++ b/tests/components/letpot/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/lg_thinq/snapshots/test_sensor.ambr b/tests/components/lg_thinq/snapshots/test_sensor.ambr index f5e8fb79d06..d561c4c6fc9 100644 --- a/tests/components/lg_thinq/snapshots/test_sensor.ambr +++ b/tests/components/lg_thinq/snapshots/test_sensor.ambr @@ -282,6 +282,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -332,6 +335,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/madvr/snapshots/test_sensor.ambr b/tests/components/madvr/snapshots/test_sensor.ambr index ac5cbe24d5c..c6c680260d3 100644 --- a/tests/components/madvr/snapshots/test_sensor.ambr +++ b/tests/components/madvr/snapshots/test_sensor.ambr @@ -215,6 +215,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -268,6 +271,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -321,6 +327,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -834,6 +843,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index ec3cb30ea83..3af00db623e 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -617,6 +617,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -670,6 +673,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1160,6 +1166,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1266,6 +1275,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1564,6 +1576,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2133,6 +2148,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2235,6 +2253,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2264,7 +2285,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3.050', + 'state': '3.05', }) # --- # name: test_sensors[eve_weather_sensor][sensor.eve_weather_battery-entry] @@ -2453,6 +2474,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2506,6 +2530,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3024,6 +3051,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3077,6 +3107,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3130,6 +3163,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3301,6 +3337,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3406,6 +3445,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3459,6 +3501,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4849,6 +4894,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5062,6 +5110,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -5091,7 +5142,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.000', + 'state': '0.0', }) # --- # name: test_sensors[solar_power][sensor.solarpower_current-entry] @@ -5354,6 +5405,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5407,6 +5461,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/meteo_france/snapshots/test_sensor.ambr b/tests/components/meteo_france/snapshots/test_sensor.ambr index 553f82c2a8e..2d048112bbb 100644 --- a/tests/components/meteo_france/snapshots/test_sensor.ambr +++ b/tests/components/meteo_france/snapshots/test_sensor.ambr @@ -177,6 +177,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -384,6 +387,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -540,6 +546,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -645,6 +654,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': 'mdi:weather-windy-variant', @@ -700,6 +712,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/metoffice/const.py b/tests/components/metoffice/const.py index 2485b308981..59061f12ddc 100644 --- a/tests/components/metoffice/const.py +++ b/tests/components/metoffice/const.py @@ -35,7 +35,7 @@ METOFFICE_CONFIG_KINGSLYNN = { KINGSLYNN_SENSOR_RESULTS = { "weather": "rainy", - "temperature": "7.87", + "temperature": "7.9", "uv_index": "1", "probability_of_precipitation": "67", "pressure": "998.20", @@ -44,7 +44,7 @@ KINGSLYNN_SENSOR_RESULTS = { WAVERTREE_SENSOR_RESULTS = { "weather": "rainy", - "temperature": "9.28", + "temperature": "9.3", "uv_index": "1", "probability_of_precipitation": "61", "pressure": "987.50", diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 15a2acbf20b..dd2824e91b9 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -24,13 +24,14 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, get_sensor_display_state, load_fixture @pytest.mark.freeze_time(datetime.datetime(2024, 11, 23, 12, tzinfo=datetime.UTC)) async def test_one_sensor_site_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, requests_mock: requests_mock.Mocker, ) -> None: """Test the Met Office sensor platform.""" @@ -69,7 +70,9 @@ async def test_one_sensor_site_running( sensor_id = re.search("met_office_wavertree_(.+?)$", running_id).group(1) sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] - assert sensor.state == sensor_value + assert ( + get_sensor_display_state(hass, entity_registry, running_id) == sensor_value + ) assert sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING assert sensor.attributes.get("attribution") == ATTRIBUTION @@ -78,6 +81,7 @@ async def test_one_sensor_site_running( async def test_two_sensor_sites_running( hass: HomeAssistant, device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, requests_mock: requests_mock.Mocker, ) -> None: """Test we handle two sets of sensors running for two different sites.""" @@ -139,7 +143,10 @@ async def test_two_sensor_sites_running( if "wavertree" in running_id: sensor_id = re.search("met_office_wavertree_(.+?)$", running_id).group(1) sensor_value = WAVERTREE_SENSOR_RESULTS[sensor_id] - assert sensor.state == sensor_value + assert ( + get_sensor_display_state(hass, entity_registry, running_id) + == sensor_value + ) assert ( sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) @@ -148,7 +155,10 @@ async def test_two_sensor_sites_running( else: sensor_id = re.search("met_office_king_s_lynn_(.+?)$", running_id).group(1) sensor_value = KINGSLYNN_SENSOR_RESULTS[sensor_id] - assert sensor.state == sensor_value + assert ( + get_sensor_display_state(hass, entity_registry, running_id) + == sensor_value + ) assert ( sensor.attributes.get("last_update").isoformat() == TEST_DATETIME_STRING ) diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 488996cf363..6984fcc4c50 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -715,6 +715,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -950,6 +953,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1092,6 +1098,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1144,6 +1153,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1522,6 +1534,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1679,6 +1694,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1872,6 +1890,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2107,6 +2128,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2249,6 +2273,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2301,6 +2328,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2679,6 +2709,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2836,6 +2869,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index fb124797523..c12a8f6818b 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -26,8 +26,8 @@ from homeassistant.util.unit_system import ( @pytest.mark.parametrize( ("unit_system", "state_unit", "state1", "state2"), [ - (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), - (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, "212", "253"), + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, 100, 123), + (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, 212, 253.4), ], ) async def test_sensor( @@ -83,7 +83,7 @@ async def test_sensor( assert entity.attributes["state_class"] == "measurement" assert entity.domain == "sensor" assert entity.name == "Test 1 Battery Temperature" - assert entity.state == state1 + assert float(entity.state) == state1 assert ( entity_registry.async_get("sensor.test_1_battery_temperature").entity_category @@ -113,7 +113,7 @@ async def test_sensor( assert json["invalid_state"]["success"] is False updated_entity = hass.states.get("sensor.test_1_battery_temperature") - assert updated_entity.state == state2 + assert float(updated_entity.state) == state2 assert "foo" not in updated_entity.attributes assert len(device_registry.devices) == len(create_registrations) @@ -135,21 +135,21 @@ async def test_sensor( @pytest.mark.parametrize( ("unique_id", "unit_system", "state_unit", "state1", "state2"), [ - ("battery_temperature", METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), + ("battery_temperature", METRIC_SYSTEM, UnitOfTemperature.CELSIUS, 100, 123), ( "battery_temperature", US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, - "212", - "253", + 212, + 253, ), # The unique_id doesn't match that of the mobile app's battery temperature sensor ( "battery_temp", US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, - "212", - "123", + 212, + 123, ), ], ) @@ -205,7 +205,7 @@ async def test_sensor_migration( assert entity.attributes["state_class"] == "measurement" assert entity.domain == "sensor" assert entity.name == "Test 1 Battery Temperature" - assert entity.state == state1 + assert float(entity.state) == state1 # Reload to verify state is restored config_entry = hass.config_entries.async_entries("mobile_app")[1] @@ -244,7 +244,7 @@ async def test_sensor_migration( assert update_resp.status == HTTPStatus.OK updated_entity = hass.states.get("sensor.test_1_battery_temperature") - assert updated_entity.state == state2 + assert round(float(updated_entity.state), 0) == state2 assert "foo" not in updated_entity.attributes diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 74dc94de21e..0bafacfed26 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1515,7 +1515,7 @@ async def test_cleanup_triggers_and_restoring_state( await mqtt_mock_entry() async_fire_mqtt_message(hass, "test-topic1", "100") state = hass.states.get("sensor.test1") - assert state.state == "38" # 100 °F -> 38 °C + assert round(float(state.state)) == 38 # 100 °F -> 38 °C async_fire_mqtt_message(hass, "test-topic2", "200") state = hass.states.get("sensor.test2") @@ -1527,14 +1527,14 @@ async def test_cleanup_triggers_and_restoring_state( await hass.async_block_till_done() state = hass.states.get("sensor.test1") - assert state.state == "38" # 100 °F -> 38 °C + assert round(float(state.state)) == 38 # 100 °F -> 38 °C state = hass.states.get("sensor.test2") assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, "test-topic1", "80") state = hass.states.get("sensor.test1") - assert state.state == "27" # 80 °F -> 27 °C + assert round(float(state.state)) == 27 # 80 °F -> 27 °C async_fire_mqtt_message(hass, "test-topic2", "201") state = hass.states.get("sensor.test2") diff --git a/tests/components/myuplink/snapshots/test_sensor.ambr b/tests/components/myuplink/snapshots/test_sensor.ambr index dc5b4c9fb0d..06b2612da1b 100644 --- a/tests/components/myuplink/snapshots/test_sensor.ambr +++ b/tests/components/myuplink/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -394,6 +415,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -447,6 +471,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -500,6 +527,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -553,6 +583,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -606,6 +639,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -659,6 +695,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -712,6 +751,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -959,6 +1001,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1012,6 +1057,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1569,6 +1617,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1622,6 +1673,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1675,6 +1729,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1728,6 +1785,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1781,6 +1841,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1834,6 +1897,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1887,6 +1953,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1940,6 +2009,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1993,6 +2065,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2046,6 +2121,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2197,6 +2275,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2250,6 +2331,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2303,6 +2387,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2356,6 +2443,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2409,6 +2499,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2462,6 +2555,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2515,6 +2611,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2568,6 +2667,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2853,6 +2955,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2906,6 +3011,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2959,6 +3067,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3012,6 +3123,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3065,6 +3179,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3118,6 +3235,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3171,6 +3291,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3224,6 +3347,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3277,6 +3403,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3330,6 +3459,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3383,6 +3515,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3436,6 +3571,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3819,6 +3957,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3872,6 +4013,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3925,6 +4069,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3978,6 +4125,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4031,6 +4181,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4084,6 +4237,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4137,6 +4293,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4190,6 +4349,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4463,6 +4625,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4516,6 +4681,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4569,6 +4737,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4622,6 +4793,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4675,6 +4849,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4728,6 +4905,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 1016a889155..c0431a6449c 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -264,6 +264,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -816,6 +819,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1279,6 +1285,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1603,6 +1612,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1715,6 +1727,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1830,6 +1845,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2001,6 +2019,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2175,6 +2196,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2287,6 +2311,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2402,6 +2429,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2573,6 +2603,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2747,6 +2780,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2859,6 +2895,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2974,6 +3013,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3145,6 +3187,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3491,6 +3536,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4350,6 +4398,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4910,6 +4961,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5278,6 +5332,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -6518,6 +6575,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6795,6 +6855,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -6905,6 +6968,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7378,6 +7444,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -7489,6 +7558,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index ec9ed256617..1a3fc5618ff 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -12,7 +12,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: await async_init_integration(hass) state = hass.states.get("sensor.nick_office_temperature") - assert state.state == "23" + assert round(float(state.state)) == 23 expected_attributes = { "attribution": "Data provided by Trane Technologies", @@ -65,7 +65,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: ) state = hass.states.get("sensor.master_suite_current_compressor_speed") - assert state.state == "69.0" + assert round(float(state.state)) == 69 expected_attributes = { "attribution": "Data provided by Trane Technologies", @@ -79,7 +79,7 @@ async def test_create_sensors(hass: HomeAssistant) -> None: ) state = hass.states.get("sensor.master_suite_outdoor_temperature") - assert state.state == "30.6" + assert round(float(state.state), 1) == 30.6 expected_attributes = { "attribution": "Data provided by Trane Technologies", diff --git a/tests/components/nextcloud/snapshots/test_sensor.ambr b/tests/components/nextcloud/snapshots/test_sensor.ambr index 4aebb1f21f8..e425716b213 100644 --- a/tests/components/nextcloud/snapshots/test_sensor.ambr +++ b/tests/components/nextcloud/snapshots/test_sensor.ambr @@ -3329,6 +3329,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 1de8f67fbdb..fb00d67d9ff 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -86,28 +86,32 @@ SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { round( TemperatureConverter.convert( 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT - ) + ), + 1, ) ), "temperature": str( round( TemperatureConverter.convert( 10, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT - ) + ), + 1, ) ), "windChill": str( round( TemperatureConverter.convert( 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT - ) + ), + 1, ) ), "heatIndex": str( round( TemperatureConverter.convert( 15, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT - ) + ), + 1, ) ), "relativeHumidity": "10", @@ -115,14 +119,14 @@ SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { round( SpeedConverter.convert( 10, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR - ) + ), ) ), "windGust": str( round( SpeedConverter.convert( 20, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR - ) + ), ) ), "windDirection": "180", @@ -234,5 +238,4 @@ EXPECTED_FORECAST_METRIC = { ), ATTR_FORECAST_HUMIDITY: 75, } - NONE_FORECAST = [dict.fromkeys(DEFAULT_FORECAST[0])] diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index dd69d5ac775..acdccf4f6c7 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -66,7 +66,9 @@ async def test_imperial_metric( assert description.name state = hass.states.get(f"sensor.abc_{slugify(description.name)}") assert state - assert state.state == result_observation[description.key] + assert state.state == result_observation[description.key], ( + f"Failed for {description.key}" + ) assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION diff --git a/tests/components/nyt_games/snapshots/test_sensor.ambr b/tests/components/nyt_games/snapshots/test_sensor.ambr index 261127064f4..5a1aa384f0f 100644 --- a/tests/components/nyt_games/snapshots/test_sensor.ambr +++ b/tests/components/nyt_games/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -438,6 +444,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -491,6 +500,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 38f7d8a68c3..62ff0c1f59f 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -36,14 +36,14 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) ), "average_speed": ( "AverageDownloadRate", - "1.250000", + "1.25", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "download_paused": ("DownloadPaused", "False", None, None), "speed": ( "DownloadRate", - "2.500000", + "2.5", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), @@ -70,7 +70,7 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), "speed_limit": ( "DownloadLimit", - "1.000000", + "1.0", UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), diff --git a/tests/components/ohme/snapshots/test_sensor.ambr b/tests/components/ohme/snapshots/test_sensor.ambr index 20c4e7829c9..c22d43a451b 100644 --- a/tests/components/ohme/snapshots/test_sensor.ambr +++ b/tests/components/ohme/snapshots/test_sensor.ambr @@ -69,6 +69,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -119,6 +122,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -405,6 +411,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/omnilogic/snapshots/test_sensor.ambr b/tests/components/omnilogic/snapshots/test_sensor.ambr index 2bfdc00d6ea..f5de91b4199 100644 --- a/tests/components/omnilogic/snapshots/test_sensor.ambr +++ b/tests/components/omnilogic/snapshots/test_sensor.ambr @@ -21,6 +21,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -48,7 +51,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '21', + 'state': '21.1111111111111', }) # --- # name: test_sensors[sensor.scrubbed_spa_water_temperature-entry] @@ -73,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -100,6 +106,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '22', + 'state': '21.6666666666667', }) # --- diff --git a/tests/components/ondilo_ico/snapshots/test_sensor.ambr b/tests/components/ondilo_ico/snapshots/test_sensor.ambr index 7f8b9374aab..81274bc3a76 100644 --- a/tests/components/ondilo_ico/snapshots/test_sensor.ambr +++ b/tests/components/ondilo_ico/snapshots/test_sensor.ambr @@ -336,6 +336,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -702,6 +705,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index 4d9ce5c0f07..8b49b7f3d5f 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -78,6 +81,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -133,6 +139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -294,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -349,6 +361,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -404,6 +419,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -459,6 +477,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -514,6 +535,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -569,6 +593,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -624,6 +651,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -679,6 +709,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -734,6 +767,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1119,6 +1155,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1174,6 +1213,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1229,6 +1271,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1284,6 +1329,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1339,6 +1387,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1394,6 +1445,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1449,6 +1503,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1504,6 +1561,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1559,6 +1619,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1614,6 +1677,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1669,6 +1735,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1724,6 +1793,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1779,6 +1851,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1834,6 +1909,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1999,6 +2077,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2054,6 +2135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2109,6 +2193,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2164,6 +2251,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2549,6 +2639,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2604,6 +2697,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2659,6 +2755,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2714,6 +2813,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2769,6 +2871,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2934,6 +3039,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2989,6 +3097,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3044,6 +3155,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index 57a278a498b..58c17754962 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -125,6 +125,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -179,6 +182,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -336,6 +342,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -390,6 +399,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -444,6 +456,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -498,6 +513,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -605,6 +623,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -811,6 +832,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -841,7 +865,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '35.39', + 'state': '35.388', }) # --- # name: test_sensor_states[v3.0][sensor.openweathermap_cloud_coverage-entry] @@ -970,6 +994,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1024,6 +1051,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1181,6 +1211,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1235,6 +1268,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1289,6 +1325,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1343,6 +1382,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1450,6 +1492,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1656,6 +1701,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1686,6 +1734,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '35.39', + 'state': '35.388', }) # --- diff --git a/tests/components/palazzetti/snapshots/test_sensor.ambr b/tests/components/palazzetti/snapshots/test_sensor.ambr index 42f42371dfc..3221430fd23 100644 --- a/tests/components/palazzetti/snapshots/test_sensor.ambr +++ b/tests/components/palazzetti/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -489,6 +507,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -542,6 +563,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index ed59c21276b..c4022ad786c 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -748,6 +751,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/peblar/snapshots/test_sensor.ambr b/tests/components/peblar/snapshots/test_sensor.ambr index 34d109797e0..2963693d77d 100644 --- a/tests/components/peblar/snapshots/test_sensor.ambr +++ b/tests/components/peblar/snapshots/test_sensor.ambr @@ -406,6 +406,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -459,6 +462,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -512,6 +518,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -565,6 +574,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -794,6 +806,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -847,6 +862,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -900,6 +918,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/ping/snapshots/test_sensor.ambr b/tests/components/ping/snapshots/test_sensor.ambr index cbba01ef272..f09bfe61065 100644 --- a/tests/components/ping/snapshots/test_sensor.ambr +++ b/tests/components/ping/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -75,6 +78,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -133,6 +139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/plaato/snapshots/test_sensor.ambr b/tests/components/plaato/snapshots/test_sensor.ambr index 8b7f2111365..a64fe5f1b71 100644 --- a/tests/components/plaato/snapshots/test_sensor.ambr +++ b/tests/components/plaato/snapshots/test_sensor.ambr @@ -565,6 +565,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/poolsense/snapshots/test_sensor.ambr b/tests/components/poolsense/snapshots/test_sensor.ambr index 706e466d0cf..07ea998d902 100644 --- a/tests/components/poolsense/snapshots/test_sensor.ambr +++ b/tests/components/poolsense/snapshots/test_sensor.ambr @@ -420,6 +420,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/powerfox/snapshots/test_sensor.ambr b/tests/components/powerfox/snapshots/test_sensor.ambr index 9be211ecd94..54976dfaa79 100644 --- a/tests/components/powerfox/snapshots/test_sensor.ambr +++ b/tests/components/powerfox/snapshots/test_sensor.ambr @@ -21,6 +21,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -126,6 +129,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -179,6 +185,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -232,6 +241,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -285,6 +297,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -338,6 +353,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -391,6 +409,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -444,6 +465,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -497,6 +521,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -550,6 +577,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/rainforest_raven/snapshots/test_sensor.ambr b/tests/components/rainforest_raven/snapshots/test_sensor.ambr index f95434e8592..340248f6d8b 100644 --- a/tests/components/rainforest_raven/snapshots/test_sensor.ambr +++ b/tests/components/rainforest_raven/snapshots/test_sensor.ambr @@ -77,6 +77,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -183,6 +186,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -236,6 +242,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/rehlko/snapshots/test_sensor.ambr b/tests/components/rehlko/snapshots/test_sensor.ambr index f63a9106de7..d20b916d3ea 100644 --- a/tests/components/rehlko/snapshots/test_sensor.ambr +++ b/tests/components/rehlko/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -177,6 +183,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -230,6 +239,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -283,6 +295,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -336,6 +351,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -492,6 +510,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -792,6 +813,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -991,6 +1015,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1092,6 +1119,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1145,6 +1175,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1198,6 +1231,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1251,6 +1287,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/renault/snapshots/test_sensor.ambr b/tests/components/renault/snapshots/test_sensor.ambr index d1c5a52d2b6..908b3ab9032 100644 --- a/tests/components/renault/snapshots/test_sensor.ambr +++ b/tests/components/renault/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +132,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +188,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -305,6 +314,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -358,6 +370,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -558,6 +573,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -611,6 +629,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -781,6 +802,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -834,6 +858,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -887,6 +914,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1010,6 +1040,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1063,6 +1096,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1263,6 +1299,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1316,6 +1355,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1433,6 +1475,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1486,6 +1531,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1588,6 +1636,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1737,6 +1788,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1843,6 +1897,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1896,6 +1953,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1949,6 +2009,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2072,6 +2135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2125,6 +2191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2178,6 +2247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2329,6 +2401,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2542,6 +2617,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2648,6 +2726,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2701,6 +2782,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2754,6 +2838,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2877,6 +2964,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3126,6 +3216,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3179,6 +3272,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3349,6 +3445,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3402,6 +3501,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3455,6 +3557,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3578,6 +3683,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3631,6 +3739,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3831,6 +3942,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3884,6 +3998,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4001,6 +4118,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4107,6 +4227,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4160,6 +4283,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4213,6 +4339,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4336,6 +4465,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4585,6 +4717,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4638,6 +4773,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sabnzbd/snapshots/test_sensor.ambr b/tests/components/sabnzbd/snapshots/test_sensor.ambr index 34341b63a4c..3494899990c 100644 --- a/tests/components/sabnzbd/snapshots/test_sensor.ambr +++ b/tests/components/sabnzbd/snapshots/test_sensor.ambr @@ -79,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -132,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -297,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -511,6 +520,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sanix/snapshots/test_sensor.ambr b/tests/components/sanix/snapshots/test_sensor.ambr index 3e227879f01..eadd2db17b4 100644 --- a/tests/components/sanix/snapshots/test_sensor.ambr +++ b/tests/components/sanix/snapshots/test_sensor.ambr @@ -124,6 +124,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sense/snapshots/test_sensor.ambr b/tests/components/sense/snapshots/test_sensor.ambr index 1f96665cb22..d1b0c90aa23 100644 --- a/tests/components/sense/snapshots/test_sensor.ambr +++ b/tests/components/sense/snapshots/test_sensor.ambr @@ -197,6 +197,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': 'mdi:car-electric', @@ -542,6 +545,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': 'mdi:stove', @@ -713,6 +719,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -768,6 +777,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -823,6 +835,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -928,6 +943,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1033,6 +1051,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1088,6 +1109,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1143,6 +1167,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1198,6 +1225,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1303,6 +1333,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1408,6 +1441,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1463,6 +1499,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1517,6 +1556,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1571,6 +1613,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1625,6 +1670,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1680,6 +1728,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1735,6 +1786,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1840,6 +1894,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1945,6 +2002,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2000,6 +2060,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2054,6 +2117,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2109,6 +2175,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2164,6 +2233,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2269,6 +2341,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2374,6 +2449,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2429,6 +2507,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2484,6 +2565,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2539,6 +2623,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2644,6 +2731,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2749,6 +2839,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sensibo/snapshots/test_sensor.ambr b/tests/components/sensibo/snapshots/test_sensor.ambr index 4d2c6b91ee2..98552394ccc 100644 --- a/tests/components/sensibo/snapshots/test_sensor.ambr +++ b/tests/components/sensibo/snapshots/test_sensor.ambr @@ -180,6 +180,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -241,6 +244,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -399,6 +405,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -558,6 +567,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -611,6 +623,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e0fe1713b82..f1d527a2b9b 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -31,12 +31,25 @@ from homeassistant.const import ( PERCENTAGE, STATE_UNKNOWN, EntityCategory, + UnitOfApparentPower, UnitOfArea, + UnitOfBloodGlucoseConcentration, + UnitOfConductivity, UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, + UnitOfEnergyDistance, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, UnitOfLength, UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, UnitOfPressure, + UnitOfReactivePower, + UnitOfSoundPressure, UnitOfSpeed, UnitOfTemperature, UnitOfTime, @@ -78,28 +91,28 @@ TEST_DOMAIN = "test" UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.FAHRENHEIT, 100, - "100", + 100, ), ( US_CUSTOMARY_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT, 38, - "100", + 100.4, ), ( METRIC_SYSTEM, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS, 100, - "38", + pytest.approx(37.77778), ), ( METRIC_SYSTEM, UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 38, - "38", + 38, ), ], ) @@ -125,7 +138,7 @@ async def test_temperature_conversion( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == state_value + assert float(state.state) == state_value assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit @@ -593,6 +606,8 @@ async def test_unit_translation_key_without_platform_raises( "state_unit", "native_value", "custom_state", + "rounded_state", + "suggested_precision", ), [ # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal @@ -602,7 +617,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.INHG, UnitOfPressure.INHG, 1000.0, + pytest.approx(29.52998), "29.53", + 2, ), ( SensorDeviceClass.PRESSURE, @@ -610,7 +627,19 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.HPA, UnitOfPressure.HPA, 1.234, - "12.340", + 12.34, + "12.34", + 2, + ), + ( + SensorDeviceClass.PRESSURE, + UnitOfPressure.HPA, + UnitOfPressure.PA, + UnitOfPressure.PA, + 1.234, + 123.4, + "123", + 0, ), ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -618,7 +647,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.MMHG, UnitOfPressure.MMHG, 1000, - "750", + pytest.approx(750.061575), + "750.06", + 2, ), ( SensorDeviceClass.PRESSURE, @@ -626,7 +657,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.MMHG, UnitOfPressure.MMHG, 1000, - "750", + pytest.approx(750.061575), + "750.06", + 2, ), # Not a supported pressure unit ( @@ -635,7 +668,9 @@ async def test_unit_translation_key_without_platform_raises( "peer_pressure", UnitOfPressure.HPA, 1000, - "1000", + 1000, + "1000.00", + 2, ), ( SensorDeviceClass.TEMPERATURE, @@ -643,7 +678,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.FAHRENHEIT, 37.5, + 99.5, "99.5", + 1, ), ( SensorDeviceClass.TEMPERATURE, @@ -651,7 +688,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfTemperature.CELSIUS, UnitOfTemperature.CELSIUS, 100, - "38", + pytest.approx(37.77777), + "37.8", + 1, ), ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -659,7 +698,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.HPA, UnitOfPressure.HPA, -0.00, - "0.0", + 0.0, + "0.00", + 2, ), ( SensorDeviceClass.ATMOSPHERIC_PRESSURE, @@ -667,7 +708,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfPressure.HPA, UnitOfPressure.HPA, -0.00001, - "0", + pytest.approx(-0.0003386388), + "0.00", + 2, ), ( SensorDeviceClass.VOLUME_FLOW_RATE, @@ -675,7 +718,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, 50.0, - "13.2", + pytest.approx(13.208602), + "13", + 0, ), ( SensorDeviceClass.VOLUME_FLOW_RATE, @@ -683,7 +728,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfVolumeFlowRate.LITERS_PER_MINUTE, UnitOfVolumeFlowRate.LITERS_PER_MINUTE, 13.0, - "49.2", + pytest.approx(49.2103531), + "49", + 0, ), ( SensorDeviceClass.DURATION, @@ -691,7 +738,9 @@ async def test_unit_translation_key_without_platform_raises( UnitOfTime.HOURS, UnitOfTime.HOURS, 5400.0, - "1.5000", + 1.5, + "1.50", + 2, ), ( SensorDeviceClass.DURATION, @@ -699,7 +748,29 @@ async def test_unit_translation_key_without_platform_raises( UnitOfTime.MINUTES, UnitOfTime.MINUTES, 0.5, - "720.0", + 720, + "720.00", + 2, + ), + ( + SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION, + UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, + UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, + UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, + 130, + pytest.approx(7.222222), + "7.2", + 1, + ), + ( + SensorDeviceClass.ENERGY, + UnitOfEnergy.WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, + 1.1, + 0.0011, + "0.00", + 2, ), ], ) @@ -712,6 +783,8 @@ async def test_custom_unit( state_unit, native_value, custom_state, + rounded_state, + suggested_precision, ) -> None: """Test custom unit.""" entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") @@ -734,13 +807,17 @@ async def test_custom_unit( entity_id = entity0.entity_id state = hass.states.get(entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit assert ( - async_rounded_state(hass, entity_id, hass.states.get(entity_id)) == custom_state + async_rounded_state(hass, entity_id, hass.states.get(entity_id)) + == rounded_state ) + entry = entity_registry.async_get(entity0.entity_id) + assert entry.options["sensor"]["suggested_display_precision"] == suggested_precision + @pytest.mark.parametrize( ( @@ -759,8 +836,8 @@ async def test_custom_unit( UnitOfArea.SQUARE_MILES, UnitOfArea.SQUARE_MILES, 1000, - "1000", - "386", + 1000, + pytest.approx(386.102), SensorDeviceClass.AREA, ), ( @@ -768,8 +845,8 @@ async def test_custom_unit( UnitOfArea.SQUARE_INCHES, UnitOfArea.SQUARE_INCHES, 7.24, - "7.24", - "1.12", + 7.24, + pytest.approx(1.1222022), SensorDeviceClass.AREA, ), ( @@ -777,8 +854,8 @@ async def test_custom_unit( "peer_distance", UnitOfArea.SQUARE_KILOMETERS, 1000, - "1000", - "1000", + 1000, + 1000, SensorDeviceClass.AREA, ), # Distance @@ -787,8 +864,8 @@ async def test_custom_unit( UnitOfLength.MILES, UnitOfLength.MILES, 1000, - "1000", - "621", + 1000, + pytest.approx(621.371), SensorDeviceClass.DISTANCE, ), ( @@ -796,8 +873,8 @@ async def test_custom_unit( UnitOfLength.INCHES, UnitOfLength.INCHES, 7.24, - "7.24", - "2.85", + 7.24, + pytest.approx(2.8503937), SensorDeviceClass.DISTANCE, ), ( @@ -805,8 +882,8 @@ async def test_custom_unit( "peer_distance", UnitOfLength.KILOMETERS, 1000, - "1000", - "1000", + 1000, + 1000, SensorDeviceClass.DISTANCE, ), # Energy @@ -815,8 +892,8 @@ async def test_custom_unit( UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, 1000, - "1000", - "1.000", + 1000, + 1.000, SensorDeviceClass.ENERGY, ), ( @@ -824,8 +901,8 @@ async def test_custom_unit( UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, 1000, - "1000", - "278", + 1000, + pytest.approx(277.7778), SensorDeviceClass.ENERGY, ), ( @@ -833,8 +910,8 @@ async def test_custom_unit( "BTU", UnitOfEnergy.KILO_WATT_HOUR, 1000, - "1000", - "1000", + 1000, + 1000, SensorDeviceClass.ENERGY, ), # Power factor @@ -843,8 +920,8 @@ async def test_custom_unit( PERCENTAGE, PERCENTAGE, 1.0, - "1.0", - "100.0", + 1.0, + 100.0, SensorDeviceClass.POWER_FACTOR, ), ( @@ -852,8 +929,8 @@ async def test_custom_unit( None, None, 100, - "100", - "1.00", + 100, + 1.00, SensorDeviceClass.POWER_FACTOR, ), ( @@ -861,8 +938,8 @@ async def test_custom_unit( None, "Cos φ", 1.0, - "1.0", - "1.0", + 1.0, + 1.0, SensorDeviceClass.POWER_FACTOR, ), # Pressure @@ -872,8 +949,8 @@ async def test_custom_unit( UnitOfPressure.INHG, UnitOfPressure.INHG, 1000.0, - "1000.0", - "29.53", + 1000.0, + pytest.approx(29.52998), SensorDeviceClass.PRESSURE, ), ( @@ -881,8 +958,8 @@ async def test_custom_unit( UnitOfPressure.HPA, UnitOfPressure.HPA, 1.234, - "1.234", - "12.340", + 1.234, + 12.340, SensorDeviceClass.PRESSURE, ), ( @@ -890,8 +967,8 @@ async def test_custom_unit( UnitOfPressure.MMHG, UnitOfPressure.MMHG, 1000, - "1000", - "750", + 1000, + pytest.approx(750.0615), SensorDeviceClass.PRESSURE, ), # Not a supported pressure unit @@ -900,8 +977,8 @@ async def test_custom_unit( "peer_pressure", UnitOfPressure.HPA, 1000, - "1000", - "1000", + 1000, + 1000, SensorDeviceClass.PRESSURE, ), # Speed @@ -910,8 +987,8 @@ async def test_custom_unit( UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR, 100, - "100", - "62", + 100, + pytest.approx(62.1371), SensorDeviceClass.SPEED, ), ( @@ -919,8 +996,8 @@ async def test_custom_unit( UnitOfVolumetricFlux.INCHES_PER_HOUR, UnitOfVolumetricFlux.INCHES_PER_HOUR, 78, - "78", - "0.13", + 78, + pytest.approx(0.127952755), SensorDeviceClass.SPEED, ), ( @@ -928,8 +1005,8 @@ async def test_custom_unit( "peer_distance", UnitOfSpeed.KILOMETERS_PER_HOUR, 100, - "100", - "100", + 100, + 100, SensorDeviceClass.SPEED, ), # Volume @@ -938,8 +1015,8 @@ async def test_custom_unit( UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_FEET, 100, - "100", - "3531", + 100, + pytest.approx(3531.4667), SensorDeviceClass.VOLUME, ), ( @@ -947,8 +1024,8 @@ async def test_custom_unit( UnitOfVolume.FLUID_OUNCES, UnitOfVolume.FLUID_OUNCES, 2.3, - "2.3", - "77.8", + 2.3, + pytest.approx(77.77225), SensorDeviceClass.VOLUME, ), ( @@ -956,8 +1033,8 @@ async def test_custom_unit( "peer_distance", UnitOfVolume.CUBIC_METERS, 100, - "100", - "100", + 100, + 100, SensorDeviceClass.VOLUME, ), # Weight @@ -966,8 +1043,8 @@ async def test_custom_unit( UnitOfMass.OUNCES, UnitOfMass.OUNCES, 100, - "100", - "3.5", + 100, + pytest.approx(3.5273962), SensorDeviceClass.WEIGHT, ), ( @@ -975,8 +1052,8 @@ async def test_custom_unit( UnitOfMass.GRAMS, UnitOfMass.GRAMS, 78, - "78", - "2211", + 78, + pytest.approx(2211.262), SensorDeviceClass.WEIGHT, ), ( @@ -984,8 +1061,8 @@ async def test_custom_unit( "peer_distance", UnitOfMass.GRAMS, 100, - "100", - "100", + 100, + 100, SensorDeviceClass.WEIGHT, ), ], @@ -1015,7 +1092,7 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == native_state + assert float(state.state) == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options( @@ -1024,7 +1101,7 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit entity_registry.async_update_entity_options( @@ -1033,14 +1110,14 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == native_state + assert float(state.state) == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options("sensor.test", "sensor", None) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == native_state + assert float(state.state) == native_state assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit @@ -1067,10 +1144,10 @@ async def test_custom_unit_change( UnitOfLength.METERS, UnitOfLength.YARDS, 1000, - "1000", - "621", - "1000000", - "1093613", + 1000, + pytest.approx(621.371), + 1000000, + pytest.approx(1093613), SensorDeviceClass.DISTANCE, ), # Volume Storage (subclass of Volume) @@ -1081,10 +1158,10 @@ async def test_custom_unit_change( UnitOfVolume.GALLONS, UnitOfVolume.FLUID_OUNCES, 1000, - "1000", - "264", - "264", - "33814", + 1000, + pytest.approx(264.172), + pytest.approx(264.172), + pytest.approx(33814.022), SensorDeviceClass.VOLUME_STORAGE, ), ], @@ -1152,34 +1229,36 @@ async def test_unit_conversion_priority( # Registered entity -> Follow automatic unit conversion state = hass.states.get(entity0.entity_id) - assert state.state == automatic_state + assert float(state.state) == automatic_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit # Assert the automatic unit conversion is stored in the registry entry = entity_registry.async_get(entity0.entity_id) assert entry.unit_of_measurement == automatic_unit - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": automatic_unit} - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == automatic_unit + ) # Unregistered entity -> Follow native unit state = hass.states.get(entity1.entity_id) - assert state.state == native_state + assert float(state.state) == native_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit # Registered entity with suggested unit state = hass.states.get(entity2.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity2.entity_id) assert entry.unit_of_measurement == suggested_unit - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": suggested_unit} - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == suggested_unit + ) # Unregistered entity with suggested unit state = hass.states.get(entity3.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Set a custom unit, this should have priority over the automatic unit conversion @@ -1189,7 +1268,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit entity_registry.async_update_entity_options( @@ -1198,7 +1277,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity2.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit @@ -1387,7 +1466,6 @@ async def test_unit_conversion_priority_precision( {"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) @@ -1479,9 +1557,10 @@ async def test_unit_conversion_priority_suggested_unit_change( # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity0.entity_id) assert entry.unit_of_measurement == original_unit - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": original_unit}, - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == original_unit + ) # Registered entity -> Follow suggested unit the first time the entity was seen state = hass.states.get(entity1.entity_id) @@ -1490,9 +1569,10 @@ async def test_unit_conversion_priority_suggested_unit_change( # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity1.entity_id) assert entry.unit_of_measurement == original_unit - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": original_unit}, - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == original_unit + ) @pytest.mark.parametrize( @@ -1574,9 +1654,10 @@ async def test_unit_conversion_priority_suggested_unit_change_2( # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity0.entity_id) assert entry.unit_of_measurement == native_unit_1 - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": native_unit_1}, - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == native_unit_1 + ) # Registered entity -> Follow unit in entity registry state = hass.states.get(entity1.entity_id) @@ -1585,9 +1666,89 @@ async def test_unit_conversion_priority_suggested_unit_change_2( # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity0.entity_id) assert entry.unit_of_measurement == native_unit_1 - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": native_unit_1}, - } + assert ( + entry.options["sensor.private"]["suggested_unit_of_measurement"] + == native_unit_1 + ) + + +@pytest.mark.parametrize( + ( + "device_class", + "native_unit", + "suggested_precision", + ), + [ + (SensorDeviceClass.APPARENT_POWER, UnitOfApparentPower.VOLT_AMPERE, 0), + (SensorDeviceClass.AREA, UnitOfArea.SQUARE_CENTIMETERS, 0), + (SensorDeviceClass.ATMOSPHERIC_PRESSURE, UnitOfPressure.PA, 0), + ( + SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION, + UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, + 0, + ), + (SensorDeviceClass.CONDUCTIVITY, UnitOfConductivity.MICROSIEMENS, 1), + (SensorDeviceClass.CURRENT, UnitOfElectricCurrent.MILLIAMPERE, 0), + (SensorDeviceClass.DATA_RATE, UnitOfDataRate.KILOBITS_PER_SECOND, 0), + (SensorDeviceClass.DATA_SIZE, UnitOfInformation.KILOBITS, 0), + (SensorDeviceClass.DISTANCE, UnitOfLength.CENTIMETERS, 0), + (SensorDeviceClass.DURATION, UnitOfTime.MILLISECONDS, 0), + (SensorDeviceClass.ENERGY, UnitOfEnergy.WATT_HOUR, 0), + ( + SensorDeviceClass.ENERGY_DISTANCE, + UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR, + 0, + ), + (SensorDeviceClass.ENERGY_STORAGE, UnitOfEnergy.WATT_HOUR, 0), + (SensorDeviceClass.FREQUENCY, UnitOfFrequency.HERTZ, 0), + (SensorDeviceClass.GAS, UnitOfVolume.MILLILITERS, 0), + (SensorDeviceClass.IRRADIANCE, UnitOfIrradiance.WATTS_PER_SQUARE_METER, 0), + (SensorDeviceClass.POWER, UnitOfPower.WATT, 0), + (SensorDeviceClass.PRECIPITATION, UnitOfPrecipitationDepth.CENTIMETERS, 0), + ( + SensorDeviceClass.PRECIPITATION_INTENSITY, + UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + 0, + ), + (SensorDeviceClass.PRESSURE, UnitOfPressure.PA, 0), + (SensorDeviceClass.REACTIVE_POWER, UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 0), + (SensorDeviceClass.SOUND_PRESSURE, UnitOfSoundPressure.DECIBEL, 0), + (SensorDeviceClass.SPEED, UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), + (SensorDeviceClass.TEMPERATURE, UnitOfTemperature.KELVIN, 1), + (SensorDeviceClass.VOLTAGE, UnitOfElectricPotential.VOLT, 0), + (SensorDeviceClass.VOLUME, UnitOfVolume.MILLILITERS, 0), + (SensorDeviceClass.VOLUME_FLOW_RATE, UnitOfVolumeFlowRate.LITERS_PER_SECOND, 0), + (SensorDeviceClass.VOLUME_STORAGE, UnitOfVolume.MILLILITERS, 0), + (SensorDeviceClass.WATER, UnitOfVolume.MILLILITERS, 0), + (SensorDeviceClass.WEIGHT, UnitOfMass.GRAMS, 0), + (SensorDeviceClass.WIND_SPEED, UnitOfSpeed.MILLIMETERS_PER_SECOND, 0), + ], +) +async def test_default_precision( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_class: str, + native_unit: str, + suggested_precision: int, +) -> None: + """Test default unit precision.""" + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + await hass.async_block_till_done() + + entity0 = MockSensor( + name="Test", + native_value="123", + native_unit_of_measurement=native_unit, + device_class=device_class, + 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() + + entry = entity_registry.async_get(entity0.entity_id) + assert entry.options["sensor"]["suggested_display_precision"] == suggested_precision @pytest.mark.parametrize( @@ -1756,39 +1917,6 @@ async def test_suggested_precision_option_update( } -async def test_suggested_precision_option_removal( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, -) -> None: - """Test suggested precision stored in the registry is removed.""" - # 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", @@ -1805,7 +1933,7 @@ async def test_suggested_precision_option_removal( UnitOfLength.KILOMETERS, UnitOfLength.MILES, 1000, - 621.0, + 621.3711, SensorDeviceClass.DISTANCE, ), ( @@ -2346,10 +2474,10 @@ async def test_numeric_state_expected_helper( UnitOfLength.METERS, UnitOfLength.YARDS, 1000, - "621", - "1000", - "1000000", - "1093613", + pytest.approx(621.3711), + 1000, + 1000000, + pytest.approx(1093613), SensorDeviceClass.DISTANCE, ), ], @@ -2439,40 +2567,40 @@ async def test_unit_conversion_update( # Registered entity -> Follow automatic unit conversion state = hass.states.get(entity0.entity_id) - assert state.state == automatic_state_1 + assert float(state.state) == automatic_state_1 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_1 # Assert the automatic unit conversion is stored in the registry entry = entity_registry.async_get(entity0.entity_id) - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": automatic_unit_1} + assert entry.options["sensor.private"] == { + "suggested_unit_of_measurement": automatic_unit_1 } state = hass.states.get(entity1.entity_id) - assert state.state == automatic_state_1 + assert float(state.state) == automatic_state_1 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_1 # Assert the automatic unit conversion is stored in the registry entry = entity_registry.async_get(entity1.entity_id) - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": automatic_unit_1} + assert entry.options["sensor.private"] == { + "suggested_unit_of_measurement": automatic_unit_1 } # Registered entity with suggested unit state = hass.states.get(entity2.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity2.entity_id) - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": suggested_unit} + assert entry.options["sensor.private"] == { + "suggested_unit_of_measurement": suggested_unit } state = hass.states.get(entity3.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity3.entity_id) - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": suggested_unit} + assert entry.options["sensor.private"] == { + "suggested_unit_of_measurement": suggested_unit } # Set a custom unit, this should have priority over the automatic unit conversion @@ -2482,7 +2610,7 @@ async def test_unit_conversion_update( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit entity_registry.async_update_entity_options( @@ -2491,7 +2619,7 @@ async def test_unit_conversion_update( await hass.async_block_till_done() state = hass.states.get(entity2.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit # Change unit system, states and units should be unchanged @@ -2499,19 +2627,19 @@ async def test_unit_conversion_update( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit state = hass.states.get(entity1.entity_id) - assert state.state == automatic_state_1 + assert float(state.state) == automatic_state_1 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_1 state = hass.states.get(entity2.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit state = hass.states.get(entity3.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Update suggested unit @@ -2522,39 +2650,37 @@ async def test_unit_conversion_update( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit state = hass.states.get(entity1.entity_id) - assert state.state == automatic_state_2 + assert float(state.state) == automatic_state_2 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2 state = hass.states.get(entity2.entity_id) - assert state.state == custom_state + assert float(state.state) == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit state = hass.states.get(entity3.entity_id) - assert state.state == suggested_state + assert float(state.state) == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Entity 4 still has a pending request to refresh entity options entry = entity_registry.async_get(entity4_entity_id) - assert entry.options == { - "sensor.private": { - "refresh_initial_entity_options": True, - "suggested_unit_of_measurement": automatic_unit_1, - } + assert entry.options["sensor.private"] == { + "refresh_initial_entity_options": True, + "suggested_unit_of_measurement": automatic_unit_1, } # Add entity 4, the pending request to refresh entity options should be handled await entity_platform.async_add_entities((entity4,)) state = hass.states.get(entity4_entity_id) - assert state.state == automatic_state_2 + assert float(state.state) == automatic_state_2 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit_2 entry = entity_registry.async_get(entity4_entity_id) - assert entry.options == {} + assert "sensor.private" not in entry.options class MockFlow(ConfigFlow): @@ -2763,7 +2889,7 @@ async def test_suggested_unit_guard_invalid_unit( UnitOfTemperature.CELSIUS, 10, UnitOfTemperature.KELVIN, - 283, + 283.15, ), ( SensorDeviceClass.DATA_RATE, @@ -2809,8 +2935,8 @@ async def test_suggested_unit_guard_valid_unit( # Assert the suggested unit of measurement is stored in the registry entry = entity_registry.async_get(entity.entity_id) assert entry.unit_of_measurement == suggested_unit - assert entry.options == { - "sensor.private": {"suggested_unit_of_measurement": suggested_unit}, + assert entry.options["sensor.private"] == { + "suggested_unit_of_measurement": suggested_unit } diff --git a/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr b/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr index 80256bfd2ec..7992b82a4d3 100644 --- a/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr +++ b/tests/components/sensorpush_cloud/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -79,6 +82,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -135,6 +141,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -188,6 +197,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -214,7 +226,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_0_humidity-entry] @@ -347,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -373,7 +388,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_0_vapor_pressure-entry] @@ -400,6 +415,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -453,6 +471,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -509,6 +530,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -565,6 +589,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -618,6 +645,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -644,7 +674,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_1_humidity-entry] @@ -777,6 +807,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -803,7 +836,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_1_vapor_pressure-entry] @@ -830,6 +863,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -883,6 +919,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -939,6 +978,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -995,6 +1037,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1048,6 +1093,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1074,7 +1122,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_2_humidity-entry] @@ -1207,6 +1255,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1233,7 +1284,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17.8', + 'state': '-17.7777777777778', }) # --- # name: test_sensors[sensor.test_sensor_name_2_vapor_pressure-entry] @@ -1260,6 +1311,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sfr_box/snapshots/test_sensor.ambr b/tests/components/sfr_box/snapshots/test_sensor.ambr index 4a179146457..cd762a4b2ea 100644 --- a/tests/components/sfr_box/snapshots/test_sensor.ambr +++ b/tests/components/sfr_box/snapshots/test_sensor.ambr @@ -449,6 +449,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -484,6 +487,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/sma/snapshots/test_sensor.ambr b/tests/components/sma/snapshots/test_sensor.ambr index 9d9d876c98e..257f07d1a32 100644 --- a/tests/components/sma/snapshots/test_sensor.ambr +++ b/tests/components/sma/snapshots/test_sensor.ambr @@ -219,6 +219,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -272,6 +275,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -325,6 +331,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -378,6 +387,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -431,6 +443,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -484,6 +499,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -537,6 +555,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -590,6 +611,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -643,6 +667,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -696,6 +723,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -749,6 +779,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -802,6 +835,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -855,6 +891,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -908,6 +947,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -961,6 +1003,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1014,6 +1059,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1067,6 +1115,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1120,6 +1171,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1173,6 +1227,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1226,6 +1283,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1279,6 +1339,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1332,6 +1395,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1645,6 +1711,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1698,6 +1767,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1751,6 +1823,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1804,6 +1879,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1857,6 +1935,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1910,6 +1991,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1963,6 +2047,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2016,6 +2103,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2069,6 +2159,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2122,6 +2215,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2175,6 +2271,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2228,6 +2327,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2281,6 +2383,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2334,6 +2439,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2387,6 +2495,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2440,6 +2551,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2541,6 +2655,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2694,6 +2811,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2747,6 +2867,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2800,6 +2923,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2853,6 +2979,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -2954,6 +3083,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3055,6 +3187,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3156,6 +3291,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3209,6 +3347,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3262,6 +3403,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3315,6 +3459,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3368,6 +3515,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3421,6 +3571,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3474,6 +3627,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3527,6 +3683,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3580,6 +3739,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3633,6 +3795,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3686,6 +3851,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3739,6 +3907,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3792,6 +3963,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3845,6 +4019,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3898,6 +4075,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3951,6 +4131,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4004,6 +4187,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4057,6 +4243,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4110,6 +4299,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4259,6 +4451,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4312,6 +4507,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4365,6 +4563,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4418,6 +4619,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4471,6 +4675,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4524,6 +4731,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4577,6 +4787,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4630,6 +4843,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4683,6 +4899,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4736,6 +4955,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4789,6 +5011,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -4894,6 +5119,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -4947,6 +5175,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5000,6 +5231,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5053,6 +5287,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5106,6 +5343,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5159,6 +5399,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5212,6 +5455,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5265,6 +5511,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5318,6 +5567,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5371,6 +5623,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5472,6 +5727,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -5525,6 +5783,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5578,6 +5839,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -5631,6 +5895,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index a0ea94901cb..e85ec4620e9 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -181,6 +187,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -234,6 +243,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -287,6 +299,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -390,6 +405,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -555,6 +573,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -658,6 +679,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1076,6 +1100,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1746,6 +1773,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2183,6 +2213,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2620,6 +2653,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2880,6 +2916,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3724,6 +3763,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3776,6 +3818,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3802,7 +3847,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-17', + 'state': '-17.2222222222222', }) # --- # name: test_all_entities[da_ks_oven_01061][sensor.oven_completion_time-entry] @@ -4128,6 +4173,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4180,6 +4228,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4590,6 +4641,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4615,7 +4669,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '218', + 'state': '218.333333333333', }) # --- # name: test_all_entities[da_ks_range_0101x][sensor.vulcan_temperature-entry] @@ -4642,6 +4696,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4668,7 +4725,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '218', + 'state': '218.333333333333', }) # --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_energy-entry] @@ -4863,6 +4920,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4889,7 +4949,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-18', + 'state': '-17.7777777777778', }) # --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_fridge_temperature-entry] @@ -4916,6 +4976,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -4942,7 +5005,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '2.77777777777778', }) # --- # name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_power-entry] @@ -5251,6 +5314,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5277,7 +5343,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-18', + 'state': '-17.7777777777778', }) # --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_fridge_temperature-entry] @@ -5304,6 +5370,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5330,7 +5399,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3', + 'state': '2.77777777777778', }) # --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_power-entry] @@ -5639,6 +5708,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -5692,6 +5764,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -10452,6 +10527,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -10700,6 +10778,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -10726,7 +10807,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '22', + 'state': '21.6666666666667', }) # --- # name: test_all_entities[ecobee_thermostat][sensor.main_floor_humidity-entry] @@ -10806,6 +10887,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -10832,7 +10916,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '22', + 'state': '21.6666666666667', }) # --- # name: test_all_entities[ecobee_thermostat_offline][sensor.downstairs_humidity-entry] @@ -10964,6 +11048,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -10993,7 +11080,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '40', + 'state': '39.6435852288', }) # --- # name: test_all_entities[gas_meter][sensor.gas_meter_gas_meter-entry] @@ -11020,6 +11107,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -11274,6 +11364,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11377,6 +11470,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -11430,6 +11526,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -11483,6 +11582,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11586,6 +11688,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -11615,7 +11720,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1000', + 'state': '1000.0', }) # --- # name: test_all_entities[lumi][sensor.outdoor_temp_battery-entry] @@ -11745,6 +11850,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11771,7 +11879,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '24.4', + 'state': '24.4444444444444', }) # --- # name: test_all_entities[multipurpose_sensor][sensor.deck_door_battery-entry] @@ -11848,6 +11956,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -11874,7 +11985,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '19.4', + 'state': '19.4444444444444', }) # --- # name: test_all_entities[multipurpose_sensor][sensor.deck_door_x_coordinate-entry] @@ -12098,6 +12209,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -12124,7 +12238,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '23.6', + 'state': '23.6111111111111', }) # --- # name: test_all_entities[sensibo_airconditioner_1][sensor.office_air_conditioner_mode-entry] @@ -12197,6 +12311,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -12559,6 +12676,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -12585,7 +12705,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '4734.552604985020', + 'state': '4734.55260498502', }) # --- # name: test_all_entities[virtual_water_sensor][sensor.asd_battery-entry] diff --git a/tests/components/smarty/snapshots/test_sensor.ambr b/tests/components/smarty/snapshots/test_sensor.ambr index d62c47235be..232cce177e3 100644 --- a/tests/components/smarty/snapshots/test_sensor.ambr +++ b/tests/components/smarty/snapshots/test_sensor.ambr @@ -21,6 +21,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -169,6 +172,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -219,6 +225,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/smlight/snapshots/test_sensor.ambr b/tests/components/smlight/snapshots/test_sensor.ambr index 63eb97aaf0b..d61872b024c 100644 --- a/tests/components/smlight/snapshots/test_sensor.ambr +++ b/tests/components/smlight/snapshots/test_sensor.ambr @@ -186,6 +186,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -294,6 +297,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/solarlog/snapshots/test_sensor.ambr b/tests/components/solarlog/snapshots/test_sensor.ambr index ba9449f31f1..8f0ee17df44 100644 --- a/tests/components/solarlog/snapshots/test_sensor.ambr +++ b/tests/components/solarlog/snapshots/test_sensor.ambr @@ -82,6 +82,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -194,6 +197,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -247,6 +253,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -356,6 +365,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -757,6 +769,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -859,6 +874,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -912,6 +930,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -965,6 +986,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1018,6 +1042,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1127,6 +1154,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1180,6 +1210,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1410,6 +1443,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1439,7 +1475,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.0230', + 'state': '1.023', }) # --- # name: test_all_entities[sensor.solarlog_yield_yesterday-entry] diff --git a/tests/components/steamist/test_sensor.py b/tests/components/steamist/test_sensor.py index 79592f9fc85..8731e803e0b 100644 --- a/tests/components/steamist/test_sensor.py +++ b/tests/components/steamist/test_sensor.py @@ -16,7 +16,7 @@ async def test_steam_active(hass: HomeAssistant) -> None: """Test that the sensors are setup with the expected values when steam is active.""" await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_ACTIVE) state = hass.states.get("sensor.steam_temperature") - assert state.state == "39" + assert round(float(state.state)) == 39 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "14" @@ -27,7 +27,7 @@ async def test_steam_inactive(hass: HomeAssistant) -> None: """Test that the sensors are setup with the expected values when steam is not active.""" await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_INACTIVE) state = hass.states.get("sensor.steam_temperature") - assert state.state == "21" + assert round(float(state.state)) == 21 assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "0" diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py index 0e15dead33f..c2cebc01c96 100644 --- a/tests/components/subaru/api_responses.py +++ b/tests/components/subaru/api_responses.py @@ -153,21 +153,21 @@ EXPECTED_STATE_EV_IMPERIAL = { EXPECTED_STATE_EV_METRIC = { "AVG_FUEL_CONSUMPTION": "4.6", - "DISTANCE_TO_EMPTY_FUEL": "274", + "DISTANCE_TO_EMPTY_FUEL": "273.59", "EV_CHARGER_STATE_TYPE": "CHARGING", "EV_CHARGE_SETTING_AMPERE_TYPE": "MAXIMUM", "EV_CHARGE_VOLT_TYPE": "CHARGE_LEVEL_1", - "EV_DISTANCE_TO_EMPTY": "2", + "EV_DISTANCE_TO_EMPTY": "1.61", "EV_IS_PLUGGED_IN": "UNLOCKED_CONNECTED", "EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_PERCENT": "20", "EV_TIME_TO_FULLY_CHARGED_UTC": "2020-07-24T03:06:40+00:00", - "ODOMETER": "1986", + "ODOMETER": "1985.93", "TIMESTAMP": 1595560000.0, "TRANSMISSION_MODE": "UNKNOWN", - "TYRE_PRESSURE_FRONT_LEFT": "0.0", - "TYRE_PRESSURE_FRONT_RIGHT": "219.9", - "TYRE_PRESSURE_REAR_LEFT": "224.8", + "TYRE_PRESSURE_FRONT_LEFT": "0.00", + "TYRE_PRESSURE_FRONT_RIGHT": "219.94", + "TYRE_PRESSURE_REAR_LEFT": "224.77", "TYRE_PRESSURE_REAR_RIGHT": "unknown", "VEHICLE_STATE_TYPE": "IGNITION_OFF", "LATITUDE": 40.0, diff --git a/tests/components/subaru/test_sensor.py b/tests/components/subaru/test_sensor.py index a468a2442e1..c8812460e68 100644 --- a/tests/components/subaru/test_sensor.py +++ b/tests/components/subaru/test_sensor.py @@ -27,6 +27,8 @@ from .conftest import ( setup_subaru_config_entry, ) +from tests.common import get_sensor_display_state + async def test_sensors_ev_metric(hass: HomeAssistant, ev_entry) -> None: """Test sensors supporting metric units.""" @@ -141,5 +143,5 @@ def _assert_data(hass: HomeAssistant, expected_state: dict[str, Any]) -> None: expected_states[entity] = expected_state[item.key] for sensor, value in expected_states.items(): - actual = hass.states.get(sensor) - assert actual.state == value + state = get_sensor_display_state(hass, entity_registry, sensor) + assert state == value diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index ffb442694e4..ed05348d924 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -72,6 +72,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/swiss_public_transport/snapshots/test_sensor.ambr b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr index fb16aeae338..1fbd2c17a6c 100644 --- a/tests/components/swiss_public_transport/snapshots/test_sensor.ambr +++ b/tests/components/swiss_public_transport/snapshots/test_sensor.ambr @@ -21,6 +21,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -369,6 +372,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -398,6 +404,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.003', + 'state': '0.00277777777777778', }) # --- diff --git a/tests/components/swiss_public_transport/test_sensor.py b/tests/components/swiss_public_transport/test_sensor.py index 4922941002e..e677be44e3b 100644 --- a/tests/components/swiss_public_transport/test_sensor.py +++ b/tests/components/swiss_public_transport/test_sensor.py @@ -83,7 +83,10 @@ async def test_fetching_data( hass.states.get("sensor.zurich_bern_departure_2").state == "2024-01-06T17:05:00+00:00" ) - assert hass.states.get("sensor.zurich_bern_trip_duration").state == "0.003" + assert ( + round(float(hass.states.get("sensor.zurich_bern_trip_duration").state), 3) + == 0.003 + ) assert hass.states.get("sensor.zurich_bern_platform").state == "0" assert hass.states.get("sensor.zurich_bern_transfers").state == "0" assert hass.states.get("sensor.zurich_bern_delay").state == "0" @@ -139,7 +142,6 @@ async def test_fetching_data_setup_exception( """Test fetching data with setup exception.""" mock_opendata_client.async_get_data.side_effect = raise_error - await setup_integration(hass, swiss_public_transport_config_entry) assert swiss_public_transport_config_entry.state is state diff --git a/tests/components/switchbot_cloud/snapshots/test_sensor.ambr b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr index e6bf75c4b25..83d4fa6b5a3 100644 --- a/tests/components/switchbot_cloud/snapshots/test_sensor.ambr +++ b/tests/components/switchbot_cloud/snapshots/test_sensor.ambr @@ -129,6 +129,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +291,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tasmota/snapshots/test_sensor.ambr b/tests/components/tasmota/snapshots/test_sensor.ambr index af83e6b3872..00b09239b26 100644 --- a/tests/components/tasmota/snapshots/test_sensor.ambr +++ b/tests/components/tasmota/snapshots/test_sensor.ambr @@ -39,6 +39,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -275,6 +278,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -424,6 +430,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -477,6 +486,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -530,6 +542,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -615,6 +630,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -774,6 +792,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -859,6 +880,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -912,6 +936,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1029,6 +1056,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1082,6 +1112,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1199,6 +1232,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1284,6 +1320,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1454,6 +1493,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1603,6 +1645,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1656,6 +1701,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1709,6 +1757,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/technove/snapshots/test_sensor.ambr b/tests/components/technove/snapshots/test_sensor.ambr index f79c70f3364..801cc9fd38e 100644 --- a/tests/components/technove/snapshots/test_sensor.ambr +++ b/tests/components/technove/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -405,6 +420,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tedee/snapshots/test_sensor.ambr b/tests/components/tedee/snapshots/test_sensor.ambr index 7416b51f9f5..dd34c8bdac4 100644 --- a/tests/components/tedee/snapshots/test_sensor.ambr +++ b/tests/components/tedee/snapshots/test_sensor.ambr @@ -76,6 +76,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +185,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tesla_fleet/snapshots/test_sensor.ambr b/tests/components/tesla_fleet/snapshots/test_sensor.ambr index 5aeb6f59d0d..c251468edc4 100644 --- a/tests/components/tesla_fleet/snapshots/test_sensor.ambr +++ b/tests/components/tesla_fleet/snapshots/test_sensor.ambr @@ -2758,6 +2758,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2787,7 +2790,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_charge_rate-statealt] @@ -2803,7 +2806,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_charger_current-entry] @@ -2830,6 +2833,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2899,6 +2905,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2968,6 +2977,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3125,6 +3137,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3154,7 +3169,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.063555', + 'state': '0.063554603904', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] @@ -3170,7 +3185,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-entry] @@ -3771,6 +3786,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3922,6 +3940,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3951,7 +3972,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_speed-statealt] @@ -3967,7 +3988,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] @@ -4489,6 +4510,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tesla_wall_connector/test_init.py b/tests/components/tesla_wall_connector/test_init.py index e16180c328a..fbb3abc1746 100644 --- a/tests/components/tesla_wall_connector/test_init.py +++ b/tests/components/tesla_wall_connector/test_init.py @@ -5,13 +5,15 @@ from tesla_wall_connector.exceptions import WallConnectorConnectionError from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from .conftest import create_wall_connector_entry, get_vitals_mock +from .conftest import create_wall_connector_entry, get_lifetime_mock, get_vitals_mock async def test_init_success(hass: HomeAssistant) -> None: """Test setup and that we get the device info, including firmware version.""" - entry = await create_wall_connector_entry(hass, vitals_data=get_vitals_mock()) + entry = await create_wall_connector_entry( + hass, vitals_data=get_vitals_mock(), lifetime_data=get_lifetime_mock() + ) assert entry.state is ConfigEntryState.LOADED @@ -28,8 +30,9 @@ async def test_init_while_offline(hass: HomeAssistant) -> None: async def test_load_unload(hass: HomeAssistant) -> None: """Config entry can be unloaded.""" - entry = await create_wall_connector_entry(hass, vitals_data=get_vitals_mock()) - + entry = await create_wall_connector_entry( + hass, vitals_data=get_vitals_mock(), lifetime_data=get_lifetime_mock() + ) assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/tesla_wall_connector/test_sensor.py b/tests/components/tesla_wall_connector/test_sensor.py index c6c93006896..56bed9edbb3 100644 --- a/tests/components/tesla_wall_connector/test_sensor.py +++ b/tests/components/tesla_wall_connector/test_sensor.py @@ -33,7 +33,7 @@ async def test_sensors(hass: HomeAssistant) -> None: "sensor.tesla_wall_connector_grid_frequency", "50.021", "49.981" ), EntityAndExpectedValues( - "sensor.tesla_wall_connector_energy", "988.022", "989.000" + "sensor.tesla_wall_connector_energy", "988.022", "989.0" ), EntityAndExpectedValues( "sensor.tesla_wall_connector_phase_a_current", "10", "7" diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 5c3a40ea979..57a0f49d949 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -2831,6 +2831,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2860,7 +2863,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_charge_rate-statealt] @@ -2876,7 +2879,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_charger_current-entry] @@ -2903,6 +2906,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2972,6 +2978,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3041,6 +3050,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -3198,6 +3210,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3227,7 +3242,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.063555', + 'state': '0.063554603904', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] @@ -3243,7 +3258,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '0.0', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-entry] @@ -3844,6 +3859,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3995,6 +4013,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -4562,6 +4583,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tessie/snapshots/test_sensor.ambr b/tests/components/tessie/snapshots/test_sensor.ambr index cad22558519..ca2a379c5f2 100644 --- a/tests/components/tessie/snapshots/test_sensor.ambr +++ b/tests/components/tessie/snapshots/test_sensor.ambr @@ -887,6 +887,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -916,7 +919,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '49.2', + 'state': '49.2459264', }) # --- # name: test_sensors[sensor.test_charger_current-entry] @@ -943,6 +946,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -996,6 +1002,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1049,6 +1058,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1216,6 +1228,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1245,7 +1260,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '75.168198', + 'state': '75.168198306432', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-entry] @@ -1555,6 +1570,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1670,6 +1688,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2113,6 +2134,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/tilt_ble/test_sensor.py b/tests/components/tilt_ble/test_sensor.py index 207e49a22cd..ded46de4ffe 100644 --- a/tests/components/tilt_ble/test_sensor.py +++ b/tests/components/tilt_ble/test_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensor import ATTR_STATE_CLASS, async_rounded_state from homeassistant.components.tilt_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -35,7 +35,10 @@ async def test_sensors(hass: HomeAssistant) -> None: assert temp_sensor is not None temp_sensor_attribtes = temp_sensor.attributes - assert temp_sensor.state == "21" + assert ( + async_rounded_state(hass, "sensor.tilt_green_temperature", temp_sensor) + == "21.1" + ) assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Tilt Green Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py index 43b0e33aed4..31cdca62635 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -8,7 +8,7 @@ from typing import Any from freezegun import freeze_time import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, async_rounded_state from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, _get_unique_id, @@ -142,9 +142,10 @@ async def _setup( def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): """Check the state of a Tomorrow.io sensor.""" - state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name)) + entity_id = CC_SENSOR_ENTITY_ID.format(entity_name) + state = hass.states.get(entity_id) assert state - assert state.state == value + assert async_rounded_state(hass, entity_id, state) == value assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION @@ -168,7 +169,7 @@ async def test_v4_sensor(hass: HomeAssistant) -> None: check_sensor_state(hass, WEED_POLLEN, "none") check_sensor_state(hass, TREE_POLLEN, "none") check_sensor_state(hass, FEELS_LIKE, "101.3") - check_sensor_state(hass, DEW_POINT, "72.82") + check_sensor_state(hass, DEW_POINT, "72.8") check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "29.47") check_sensor_state(hass, GHI, "0") check_sensor_state(hass, CLOUD_BASE, "0.74") @@ -201,8 +202,8 @@ async def test_v4_sensor_imperial(hass: HomeAssistant) -> None: check_sensor_state(hass, WEED_POLLEN, "none") check_sensor_state(hass, TREE_POLLEN, "none") check_sensor_state(hass, FEELS_LIKE, "214.3") - check_sensor_state(hass, DEW_POINT, "163.08") - check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "0.427") + check_sensor_state(hass, DEW_POINT, "163.1") + check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "0.43") check_sensor_state(hass, GHI, "0.0") check_sensor_state(hass, CLOUD_BASE, "0.46") check_sensor_state(hass, CLOUD_COVER, "100") diff --git a/tests/components/tplink/snapshots/test_sensor.ambr b/tests/components/tplink/snapshots/test_sensor.ambr index 47fc5a2bd35..5c22c2f7d83 100644 --- a/tests/components/tplink/snapshots/test_sensor.ambr +++ b/tests/components/tplink/snapshots/test_sensor.ambr @@ -273,6 +273,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -302,7 +305,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.2', + 'state': '0.18580608', }) # --- # name: test_states[sensor.my_device_cleaning_progress-entry] @@ -364,6 +367,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -392,7 +398,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '12.00', + 'state': '12.0', }) # --- # name: test_states[sensor.my_device_current-entry] diff --git a/tests/components/unifi/snapshots/test_sensor.ambr b/tests/components/unifi/snapshots/test_sensor.ambr index 9f0c5f39a9d..c0981d47f1f 100644 --- a/tests/components/unifi/snapshots/test_sensor.ambr +++ b/tests/components/unifi/snapshots/test_sensor.ambr @@ -150,6 +150,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -518,6 +521,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -749,6 +755,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -802,6 +811,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -855,6 +867,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -908,6 +923,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -961,6 +979,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1014,6 +1035,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1067,6 +1091,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1120,6 +1147,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1149,7 +1179,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_1_tx-entry] @@ -1176,6 +1206,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1205,7 +1238,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_poe_power-entry] @@ -1232,6 +1265,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1285,6 +1321,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1314,7 +1353,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_2_tx-entry] @@ -1341,6 +1380,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1370,7 +1412,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_rx-entry] @@ -1397,6 +1439,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1426,7 +1471,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_3_tx-entry] @@ -1453,6 +1498,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1482,7 +1530,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_poe_power-entry] @@ -1509,6 +1557,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1562,6 +1613,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1591,7 +1645,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_port_4_tx-entry] @@ -1618,6 +1672,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1647,7 +1704,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.00000', + 'state': '0.0', }) # --- # name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.mock_name_state-entry] @@ -1852,6 +1909,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1905,6 +1965,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2007,6 +2070,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2060,6 +2126,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 6b58f49f072..8a5b82ff264 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1042,9 +1042,9 @@ async def test_bandwidth_port_sensors( assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 6 # Verify sensor state - assert hass.states.get("sensor.mock_name_port_1_rx").state == "0.00921" - assert hass.states.get("sensor.mock_name_port_1_tx").state == "0.04089" - assert hass.states.get("sensor.mock_name_port_2_rx").state == "0.01229" + assert hass.states.get("sensor.mock_name_port_1_rx").state == "0.009208" + assert hass.states.get("sensor.mock_name_port_1_tx").state == "0.040888" + assert hass.states.get("sensor.mock_name_port_2_rx").state == "0.012288" assert hass.states.get("sensor.mock_name_port_2_tx").state == "0.02892" # Verify state update @@ -1055,8 +1055,8 @@ async def test_bandwidth_port_sensors( mock_websocket_message(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() - assert hass.states.get("sensor.mock_name_port_1_rx").state == "27648.00000" - assert hass.states.get("sensor.mock_name_port_1_tx").state == "63128.00000" + assert hass.states.get("sensor.mock_name_port_1_rx").state == "27648.0" + assert hass.states.get("sensor.mock_name_port_1_tx").state == "63128.0" # Disable option options = config_entry_options.copy() diff --git a/tests/components/v2c/snapshots/test_sensor.ambr b/tests/components/v2c/snapshots/test_sensor.ambr index 32b4e1b6bb4..3ff711383d7 100644 --- a/tests/components/v2c/snapshots/test_sensor.ambr +++ b/tests/components/v2c/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -513,6 +531,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/velbus/snapshots/test_sensor.ambr b/tests/components/velbus/snapshots/test_sensor.ambr index 8aebb226060..dc79663865f 100644 --- a/tests/components/velbus/snapshots/test_sensor.ambr +++ b/tests/components/velbus/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': 'mdi:counter', @@ -234,6 +240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index c31845b80af..64873000c7b 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock import pyvera as pv +from homeassistant.components.sensor import async_rounded_state from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, LIGHT_LUX, PERCENTAGE from homeassistant.core import HomeAssistant @@ -46,7 +47,7 @@ async def run_sensor_test( update_callback(vera_device) await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == state_value + assert async_rounded_state(hass, entity_id, state) == state_value if assert_unit_of_measurement: assert ( state.attributes[ATTR_UNIT_OF_MEASUREMENT] == assert_unit_of_measurement @@ -66,7 +67,7 @@ async def test_temperature_sensor_f( vera_component_factory=vera_component_factory, category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", - assert_states=(("33", "1"), ("44", "7")), + assert_states=(("33", "0.6"), ("44", "6.7")), setup_callback=setup_callback, ) @@ -80,7 +81,7 @@ async def test_temperature_sensor_c( vera_component_factory=vera_component_factory, category=pv.CATEGORY_TEMPERATURE_SENSOR, class_property="temperature", - assert_states=(("33", "33"), ("44", "44")), + assert_states=(("33", "33.0"), ("44", "44.0")), ) diff --git a/tests/components/vesync/snapshots/test_sensor.ambr b/tests/components/vesync/snapshots/test_sensor.ambr index 4ab9a38548a..a47de22f68b 100644 --- a/tests/components/vesync/snapshots/test_sensor.ambr +++ b/tests/components/vesync/snapshots/test_sensor.ambr @@ -913,6 +913,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -948,6 +951,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -983,6 +989,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1018,6 +1027,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1053,6 +1065,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1088,6 +1103,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/vicare/snapshots/test_sensor.ambr b/tests/components/vicare/snapshots/test_sensor.ambr index 561eee3f612..85da1f1d948 100644 --- a/tests/components/vicare/snapshots/test_sensor.ambr +++ b/tests/components/vicare/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -435,6 +438,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -488,6 +494,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -541,6 +550,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -594,6 +606,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -647,6 +662,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -700,6 +718,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -957,6 +978,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1010,6 +1034,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1063,6 +1090,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1116,6 +1146,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1320,6 +1353,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1373,6 +1409,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1426,6 +1465,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1479,6 +1521,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1532,6 +1577,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1585,6 +1633,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1638,6 +1689,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -1691,6 +1745,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1744,6 +1801,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1797,6 +1857,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1850,6 +1913,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1903,6 +1969,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2059,6 +2128,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2112,6 +2184,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2165,6 +2240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2371,6 +2449,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2476,6 +2557,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2817,6 +2901,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -2923,6 +3010,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/watergate/snapshots/test_sensor.ambr b/tests/components/watergate/snapshots/test_sensor.ambr index a399d36cc5f..9ba7bbd3024 100644 --- a/tests/components/watergate/snapshots/test_sensor.ambr +++ b/tests/components/watergate/snapshots/test_sensor.ambr @@ -234,6 +234,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -287,6 +290,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -340,6 +346,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -393,6 +402,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -446,6 +458,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr index 5f8d0037bfb..f9819f39dca 100644 --- a/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr +++ b/tests/components/weatherflow_cloud/snapshots/test_sensor.ambr @@ -406,6 +406,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/weheat/snapshots/test_sensor.ambr b/tests/components/weheat/snapshots/test_sensor.ambr index 91614d0a608..8631f0ab6bf 100644 --- a/tests/components/weheat/snapshots/test_sensor.ambr +++ b/tests/components/weheat/snapshots/test_sensor.ambr @@ -589,6 +589,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -866,6 +869,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/withings/snapshots/test_sensor.ambr b/tests/components/withings/snapshots/test_sensor.ambr index f53bd645728..446956c12a8 100644 --- a/tests/components/withings/snapshots/test_sensor.ambr +++ b/tests/components/withings/snapshots/test_sensor.ambr @@ -139,6 +139,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -169,7 +172,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.530', + 'state': '0.529722222222222', }) # --- # name: test_all_entities[sensor.henk_average_heart_rate-entry] @@ -300,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -512,6 +518,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -541,7 +550,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.617', + 'state': '1.61666666666667', }) # --- # name: test_all_entities[sensor.henk_diastolic_blood_pressure-entry] @@ -875,6 +884,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -927,6 +939,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -981,6 +996,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1869,6 +1887,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1922,6 +1943,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -1979,6 +2003,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -2030,6 +2057,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2285,6 +2315,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2314,7 +2347,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.900', + 'state': '2.9', }) # --- # name: test_all_entities[sensor.henk_maximum_heart_rate-entry] @@ -2549,6 +2582,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2579,7 +2615,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '24.8', + 'state': '24.7833333333333', }) # --- # name: test_all_entities[sensor.henk_muscle_mass-entry] @@ -2940,6 +2976,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -2995,6 +3034,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -3048,6 +3090,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3077,7 +3122,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.667', + 'state': '0.666666666666667', }) # --- # name: test_all_entities[sensor.henk_skin_temperature-entry] @@ -3104,6 +3149,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3157,6 +3205,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3186,7 +3237,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '8.000', + 'state': '8.0', }) # --- # name: test_all_entities[sensor.henk_sleep_score-entry] @@ -3265,6 +3316,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3372,6 +3426,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3402,7 +3459,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '25.3', + 'state': '25.2666666666667', }) # --- # name: test_all_entities[sensor.henk_spo2-entry] @@ -3638,6 +3695,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, @@ -3691,6 +3751,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3720,7 +3783,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.150', + 'state': '0.15', }) # --- # name: test_all_entities[sensor.henk_time_to_wakeup-entry] @@ -3747,6 +3810,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -3776,7 +3842,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.317', + 'state': '0.316666666666667', }) # --- # name: test_all_entities[sensor.henk_total_calories_burnt_today-entry] @@ -4059,6 +4125,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), 'sensor.private': dict({ 'suggested_unit_of_measurement': , }), @@ -4088,7 +4157,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.850', + 'state': '0.85', }) # --- # name: test_all_entities[sensor.henk_weight-entry] @@ -4171,6 +4240,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/wolflink/snapshots/test_sensor.ambr b/tests/components/wolflink/snapshots/test_sensor.ambr index a7289e669fc..c5b23cc8e79 100644 --- a/tests/components/wolflink/snapshots/test_sensor.ambr +++ b/tests/components/wolflink/snapshots/test_sensor.ambr @@ -54,6 +54,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -107,6 +110,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -160,6 +166,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -369,6 +378,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -422,6 +434,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -581,6 +596,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/youless/snapshots/test_sensor.ambr b/tests/components/youless/snapshots/test_sensor.ambr index a4008bab8de..d4b7a1f4e5c 100644 --- a/tests/components/youless/snapshots/test_sensor.ambr +++ b/tests/components/youless/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -129,6 +135,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -182,6 +191,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -235,6 +247,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -288,6 +303,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -341,6 +359,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -394,6 +415,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -447,6 +471,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -500,6 +527,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -553,6 +583,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -606,6 +639,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -659,6 +695,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -712,6 +751,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -823,6 +865,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -876,6 +921,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -929,6 +977,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -982,6 +1033,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1035,6 +1089,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, @@ -1088,6 +1145,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -1141,6 +1201,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, diff --git a/tests/components/zeversolar/snapshots/test_sensor.ambr b/tests/components/zeversolar/snapshots/test_sensor.ambr index 393b46d3709..0c696dba5cb 100644 --- a/tests/components/zeversolar/snapshots/test_sensor.ambr +++ b/tests/components/zeversolar/snapshots/test_sensor.ambr @@ -23,6 +23,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), }), 'original_device_class': , 'original_icon': None, @@ -76,6 +79,9 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), }), 'original_device_class': , 'original_icon': None, From 8ce3ead7825701511a909986e51c5a2eb2201d5d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 26 May 2025 19:44:22 +0200 Subject: [PATCH 0938/1175] Update frontend to 20250526.0 (#145628) --- 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 5c5feca98b7..fe445ae6b28 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==20250516.0"] + "requirements": ["home-assistant-frontend==20250526.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98349ca1d66..7da421526de 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250516.0 +home-assistant-frontend==20250526.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d0069cc4b8d..99891963fab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250516.0 +home-assistant-frontend==20250526.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00fd5b080bb..de79a8efa50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250516.0 +home-assistant-frontend==20250526.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From cfa4d37909aa6c0bd76387848718a36dc50fb2d5 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 26 May 2025 19:44:31 +0200 Subject: [PATCH 0939/1175] Add icons for ZHA fan modes (#145634) --- homeassistant/components/zha/icons.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/zha/icons.json b/homeassistant/components/zha/icons.json index e487f2ee24f..5caa1dec373 100644 --- a/homeassistant/components/zha/icons.json +++ b/homeassistant/components/zha/icons.json @@ -5,6 +5,18 @@ "default": "mdi:hand-wave" } }, + "fan": { + "fan": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "mdi:fan-auto", + "smart": "mdi:fan-auto" + } + } + } + } + }, "light": { "light": { "state_attributes": { From c3dec7fb2f623c9c26bc080fe78fb6c0ff55eb3b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 19:45:26 +0200 Subject: [PATCH 0940/1175] Add ability to set exceptions in dependency version checks (#145442) * Add ability to set exceptions in dependency version checks * Fix message * Improve * Auto-load from requirements.txt * Revert "Auto-load from requirements.txt" This reverts commit f893d4611a4b6ebedccaa639622c3f8f4ea64005. --- script/hassfest/requirements.py | 69 +++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 944724fb2cb..09052de9829 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -24,9 +24,9 @@ from .model import Config, Integration PACKAGE_CHECK_VERSION_RANGE = { "aiohttp": "SemVer", - # https://github.com/iMicknl/python-overkiz-api/issues/1644 - # "attrs": "CalVer" + "attrs": "CalVer", "grpcio": "SemVer", + "httpx": "SemVer", "mashumaro": "SemVer", "pydantic": "SemVer", "pyjwt": "SemVer", @@ -34,6 +34,20 @@ PACKAGE_CHECK_VERSION_RANGE = { "typing_extensions": "SemVer", "yarl": "SemVer", } +PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { + # In the form dict("domain": {"package": {"dependency1", "dependency2"}}) + # - domain is the integration domain + # - package is the package (can be transitive) referencing the dependency + # - dependencyX should be the name of the referenced dependency + "ollama": { + # https://github.com/ollama/ollama-python/pull/445 (not yet released) + "ollama": {"httpx"} + }, + "overkiz": { + # https://github.com/iMicknl/python-overkiz-api/issues/1644 (not yet released) + "pyoverkiz": {"attrs"}, + }, +} PACKAGE_REGEX = re.compile( r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" @@ -399,6 +413,11 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: ) needs_forbidden_package_exceptions = False + package_version_check_exceptions = PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS.get( + integration.domain, {} + ) + needs_package_version_check_exception = False + while to_check: package = to_check.popleft() @@ -433,7 +452,14 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: "requirements", f"Package {pkg} should {reason} in {package}", ) - check_dependency_version_range(integration, package, pkg, version) + if not check_dependency_version_range( + integration, + package, + pkg, + version, + package_version_check_exceptions.get(package, set()), + ): + needs_package_version_check_exception = True to_check.extend(dependencies) @@ -443,27 +469,48 @@ def get_requirements(integration: Integration, packages: set[str]) -> set[str]: f"Integration {integration.domain} runtime dependency exceptions " "have been resolved, please remove from `FORBIDDEN_PACKAGE_EXCEPTIONS`", ) + if package_version_check_exceptions and not needs_package_version_check_exception: + integration.add_error( + "requirements", + f"Integration {integration.domain} version restrictions checks have been " + "resolved, please remove from `PACKAGE_CHECK_VERSION_RANGE_EXCEPTIONS`", + ) + return all_requirements def check_dependency_version_range( - integration: Integration, source: str, pkg: str, version: str -) -> None: + integration: Integration, + source: str, + pkg: str, + version: str, + package_exceptions: set[str], +) -> bool: """Check requirement version range. We want to avoid upper version bounds that are too strict for common packages. """ - if version == "Any" or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None: - return - - if not all( - _is_dependency_version_range_valid(version_part, convention) - for version_part in version.split(";", 1)[0].split(",") + if ( + version == "Any" + or (convention := PACKAGE_CHECK_VERSION_RANGE.get(pkg)) is None + or all( + _is_dependency_version_range_valid(version_part, convention) + for version_part in version.split(";", 1)[0].split(",") + ) ): + return True + + if pkg in package_exceptions: + integration.add_warning( + "requirements", + f"Version restrictions for {pkg} are too strict ({version}) in {source}", + ) + else: integration.add_error( "requirements", f"Version restrictions for {pkg} are too strict ({version}) in {source}", ) + return False def _is_dependency_version_range_valid(version_part: str, convention: str) -> bool: From 6003f3d135cfa8acebf390fae640bd7107a2f4ec Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 26 May 2025 20:47:46 +0300 Subject: [PATCH 0941/1175] Add action exceptions to UptimeRobot integration (#143587) * Add action exceptions to UptimeRobot integration * fix tests and strings --- .../components/uptimerobot/quality_scale.yaml | 4 +- .../components/uptimerobot/strings.json | 5 ++ .../components/uptimerobot/switch.py | 18 ++++--- tests/components/uptimerobot/test_switch.py | 48 +++++++++++-------- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/uptimerobot/quality_scale.yaml b/homeassistant/components/uptimerobot/quality_scale.yaml index 43076320b8f..1244d6a4c19 100644 --- a/homeassistant/components/uptimerobot/quality_scale.yaml +++ b/homeassistant/components/uptimerobot/quality_scale.yaml @@ -26,9 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: todo - comment: we should not swallow the exception in switch.py + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: done docs-installation-parameters: done diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 6bcd1554b16..ffee6769c69 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -45,5 +45,10 @@ } } } + }, + "exceptions": { + "api_exception": { + "message": "Could not turn on/off monitoring: {error}" + } } } diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py index 9b25570393a..5d80903ed02 100644 --- a/homeassistant/components/uptimerobot/switch.py +++ b/homeassistant/components/uptimerobot/switch.py @@ -12,9 +12,10 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import API_ATTR_OK, LOGGER +from .const import API_ATTR_OK, DOMAIN from .coordinator import UptimeRobotConfigEntry from .entity import UptimeRobotEntity @@ -57,16 +58,21 @@ class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): try: response = await self.api.async_edit_monitor(**kwargs) except UptimeRobotAuthenticationException: - LOGGER.debug("API authentication error, calling reauth") self.coordinator.config_entry.async_start_reauth(self.hass) return except UptimeRobotException as exception: - LOGGER.error("API exception: %s", exception) - return + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_exception", + translation_placeholders={"error": repr(exception)}, + ) from exception if response.status != API_ATTR_OK: - LOGGER.error("API exception: %s", response.error.message, exc_info=True) - return + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="api_exception", + translation_placeholders={"error": response.error.message}, + ) await self.coordinator.async_request_refresh() diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py index 8c2cffe504a..48e9da05720 100644 --- a/tests/components/uptimerobot/test_switch.py +++ b/tests/components/uptimerobot/test_switch.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from pyuptimerobot import UptimeRobotAuthenticationException +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .common import ( MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, @@ -128,18 +129,20 @@ async def test_authentication_error( assert config_entry_reauth.assert_called -async def test_refresh_data( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test authentication error turning switch on/off.""" +async def test_action_execution_failure(hass: HomeAssistant) -> None: + """Test turning switch on/off failure.""" await setup_uptimerobot_integration(hass) entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) assert entity.state == STATE_ON - with patch( - "homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_request_refresh" - ) as coordinator_refresh: + with ( + patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + side_effect=UptimeRobotException, + ), + pytest.raises(HomeAssistantError) as exc_info, + ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -147,12 +150,14 @@ async def test_refresh_data( blocking=True, ) - assert coordinator_refresh.assert_called + assert exc_info.value.translation_domain == "uptimerobot" + assert exc_info.value.translation_key == "api_exception" + assert exc_info.value.translation_placeholders == { + "error": "UptimeRobotException()" + } -async def test_switch_api_failure( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_switch_api_failure(hass: HomeAssistant) -> None: """Test general exception turning switch on/off.""" await setup_uptimerobot_integration(hass) @@ -163,11 +168,16 @@ async def test_switch_api_failure( "pyuptimerobot.UptimeRobot.async_edit_monitor", return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), ): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, - blocking=True, - ) + with pytest.raises(HomeAssistantError) as exc_info: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) - assert "API exception" in caplog.text + assert exc_info.value.translation_domain == "uptimerobot" + assert exc_info.value.translation_key == "api_exception" + assert exc_info.value.translation_placeholders == { + "error": "test error from API." + } From 27b0488f05b25c71232826bce6a2cc76d28f8bcb Mon Sep 17 00:00:00 2001 From: Florian von Garrel Date: Mon, 26 May 2025 19:53:54 +0200 Subject: [PATCH 0942/1175] Update Paperless strings (#145633) * minor changed * Update snapshots --- homeassistant/components/paperless_ngx/__init__.py | 5 ----- homeassistant/components/paperless_ngx/strings.json | 4 ++-- tests/components/paperless_ngx/snapshots/test_sensor.ambr | 8 ++++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/paperless_ngx/__init__.py b/homeassistant/components/paperless_ngx/__init__.py index 22c05d798e8..c6147d5ff95 100644 --- a/homeassistant/components/paperless_ngx/__init__.py +++ b/homeassistant/components/paperless_ngx/__init__.py @@ -50,11 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: PaperlessConfigEntry) -> statistics=statistics_coordinator, ) - entry.runtime_data = PaperlessData( - status=status_coordinator, - statistics=statistics_coordinator, - ) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/paperless_ngx/strings.json b/homeassistant/components/paperless_ngx/strings.json index 4cceeb37a5a..33d806463d1 100644 --- a/homeassistant/components/paperless_ngx/strings.json +++ b/homeassistant/components/paperless_ngx/strings.json @@ -103,7 +103,7 @@ } }, "celery_status": { - "name": "Status celery", + "name": "Status Celery", "state": { "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", @@ -111,7 +111,7 @@ } }, "redis_status": { - "name": "Status redis", + "name": "Status Redis", "state": { "ok": "[%key:component::paperless_ngx::entity::sensor::database_status::state::ok%]", "warning": "[%key:component::paperless_ngx::entity::sensor::database_status::state::warning%]", diff --git a/tests/components/paperless_ngx/snapshots/test_sensor.ambr b/tests/components/paperless_ngx/snapshots/test_sensor.ambr index c4022ad786c..ed023f75726 100644 --- a/tests/components/paperless_ngx/snapshots/test_sensor.ambr +++ b/tests/components/paperless_ngx/snapshots/test_sensor.ambr @@ -242,7 +242,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Status celery', + 'original_name': 'Status Celery', 'platform': 'paperless_ngx', 'previous_unique_id': None, 'suggested_object_id': None, @@ -256,7 +256,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'enum', - 'friendly_name': 'Paperless-ngx Status celery', + 'friendly_name': 'Paperless-ngx Status Celery', 'options': list([ 'ok', 'error', @@ -482,7 +482,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Status redis', + 'original_name': 'Status Redis', 'platform': 'paperless_ngx', 'previous_unique_id': None, 'suggested_object_id': None, @@ -496,7 +496,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'enum', - 'friendly_name': 'Paperless-ngx Status redis', + 'friendly_name': 'Paperless-ngx Status Redis', 'options': list([ 'ok', 'error', From 670e8dd4344e9d0e21304dacbcddd9afabfa83da Mon Sep 17 00:00:00 2001 From: David Poll Date: Mon, 26 May 2025 11:22:45 -0700 Subject: [PATCH 0943/1175] Add as_function to allow macros to return values (#142033) --- homeassistant/helpers/template.py | 26 ++++++++++++++++++++++++++ tests/helpers/test_template.py | 17 +++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e3267d2933b..9079d6af300 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2024,6 +2024,29 @@ def apply(value, fn, *args, **kwargs): return fn(value, *args, **kwargs) +def as_function(macro: jinja2.runtime.Macro) -> Callable[..., Any]: + """Turn a macro with a 'returns' keyword argument into a function that returns what that argument is called with.""" + + def wrapper(value, *args, **kwargs): + return_value = None + + def returns(value): + nonlocal return_value + return_value = value + return value + + # Call the callable with the value and other args + macro(value, *args, **kwargs, returns=returns) + return return_value + + # Remove "macro_" from the macro's name to avoid confusion in the wrapper's name + trimmed_name = macro.name.removeprefix("macro_") + + wrapper.__name__ = trimmed_name + wrapper.__qualname__ = trimmed_name + return wrapper + + def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" try: @@ -3069,9 +3092,11 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): str | jinja2.nodes.Template, CodeType | None ] = weakref.WeakValueDictionary() self.add_extension("jinja2.ext.loopcontrols") + self.add_extension("jinja2.ext.do") self.globals["acos"] = arc_cosine self.globals["as_datetime"] = as_datetime + self.globals["as_function"] = as_function self.globals["as_local"] = dt_util.as_local self.globals["as_timedelta"] = as_timedelta self.globals["as_timestamp"] = forgiving_as_timestamp @@ -3124,6 +3149,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["add"] = add self.filters["apply"] = apply self.filters["as_datetime"] = as_datetime + self.filters["as_function"] = as_function self.filters["as_local"] = dt_util.as_local self.filters["as_timedelta"] = as_timedelta self.filters["as_timestamp"] = forgiving_as_timestamp diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 8d2f8c7cc60..8e6e7643df3 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -828,6 +828,23 @@ def test_apply_macro_with_arguments(hass: HomeAssistant) -> None: ).async_render() == ["Hey, Alice!", "Hey, Bob!"] +def test_as_function(hass: HomeAssistant) -> None: + """Test as_function.""" + assert ( + template.Template( + """ + {%- macro macro_double(num, returns) -%} + {%- do returns(num * 2) -%} + {%- endmacro -%} + {%- set double = macro_double | as_function -%} + {{ double(5) }} + """, + hass, + ).async_render() + == 10 + ) + + def test_logarithm(hass: HomeAssistant) -> None: """Test logarithm.""" tests = [ From b8a96d2a76314cdb79783ff22efd9663b9a55a61 Mon Sep 17 00:00:00 2001 From: thargor Date: Mon, 26 May 2025 20:23:41 +0200 Subject: [PATCH 0944/1175] update pyfronius to 0.8.0 (#141984) --- homeassistant/components/fronius/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 661d808ad23..3928860711a 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -12,5 +12,5 @@ "iot_class": "local_polling", "loggers": ["pyfronius"], "quality_scale": "platinum", - "requirements": ["PyFronius==0.7.7"] + "requirements": ["PyFronius==0.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 99891963fab..4e2ca9a2713 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ PyFlick==1.1.3 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.7 +PyFronius==0.8.0 # homeassistant.components.pyload PyLoadAPI==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de79a8efa50..da2ef7e146e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,7 +51,7 @@ PyFlick==1.1.3 PyFlume==0.6.5 # homeassistant.components.fronius -PyFronius==0.7.7 +PyFronius==0.8.0 # homeassistant.components.pyload PyLoadAPI==1.4.2 From a2b02537a67809ca3e5285dfb1b2a8e5346663d3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 20:45:12 +0200 Subject: [PATCH 0945/1175] Add deprecation issues for supervised and core installation methods (#145323) Co-authored-by: Martin Hjelmare --- .../components/homeassistant/__init__.py | 94 ++++++++- .../components/homeassistant/strings.json | 24 +++ tests/components/homeassistant/test_init.py | 187 +++++++++++++++++- tests/components/smartthings/test_switch.py | 4 - 4 files changed, 302 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index dc33b0c63e3..5f012c6a054 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -31,14 +31,22 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser -from homeassistant.helpers import config_validation as cv, recorder, restore_state +from homeassistant.helpers import ( + config_validation as cv, + issue_registry as ir, + recorder, + restore_state, +) from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.importlib import async_import_module +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, async_register_admin_service, ) from homeassistant.helpers.signal import KEY_HA_STOP +from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.template import async_load_custom_templates from homeassistant.helpers.typing import ConfigType @@ -81,6 +89,11 @@ SCHEMA_RESTART = vol.Schema({vol.Optional(ATTR_SAFE_MODE, default=False): bool}) SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART) +DEPRECATION_URL = ( + "https://www.home-assistant.io/blog/2025/05/22/" + "deprecating-core-and-supervised-installation-methods-and-32-bit-systems/" +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" @@ -386,6 +399,83 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities async_set_stop_handler(hass, _async_stop) + info = await async_get_system_info(hass) + + installation_type = info["installation_type"][15:] + deprecated_method = installation_type in { + "Core", + "Supervised", + } + arch = info["arch"] + if arch == "armv7": + if installation_type == "OS": + # Local import to avoid circular dependencies + # We use the import helper because hassio + # may not be loaded yet and we don't want to + # do blocking I/O in the event loop to import it. + if TYPE_CHECKING: + # pylint: disable-next=import-outside-toplevel + from homeassistant.components import hassio + else: + hassio = await async_import_module( + hass, "homeassistant.components.hassio" + ) + os_info = hassio.get_os_info(hass) + assert os_info is not None + issue_id = "deprecated_os_" + board = os_info.get("board") + if board in {"rpi3", "rpi4"}: + issue_id += "aarch64" + elif board in {"tinker", "odroid-xu4", "rpi2"}: + issue_id += "armv7" + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_guide": "https://www.home-assistant.io/installation/", + }, + ) + elif installation_type == "Container": + ir.async_create_issue( + hass, + DOMAIN, + "deprecated_container_armv7", + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_container_armv7", + ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method): + deprecated_architecture = True + if deprecated_method or deprecated_architecture: + issue_id = "deprecated" + if deprecated_method: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": installation_type, + "arch": arch, + }, + ) + return True diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 0987461b4dc..e4c3e19cf7c 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -90,6 +90,30 @@ } } } + }, + "deprecated_method": { + "title": "Deprecation notice: Installation method", + "description": "This system is using the {installation_type} installation type, which has been deprecated and will become unsupported following the release of Home Assistant 2025.12. While you can continue using your current setup after that point, we strongly recommend migrating to a supported installation method." + }, + "deprecated_method_architecture": { + "title": "Deprecation notice", + "description": "This system is using the {installation_type} installation type, and 32-bit hardware (`{arch}`), both of which have been deprecated and will no longer be supported after the release of Home Assistant 2025.12." + }, + "deprecated_architecture": { + "title": "Deprecation notice: 32-bit architecture", + "description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware." + }, + "deprecated_container_armv7": { + "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", + "description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." + }, + "deprecated_os_aarch64": { + "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", + "description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. To continue using Home Assistant on this hardware, you will need to install a 64-bit operating system. Please refer to our [installation guide]({installation_guide})." + }, + "deprecated_os_armv7": { + "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", + "description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware." } }, "system_health": { diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 4facd1695c5..fe5d2155f58 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -10,6 +10,7 @@ from homeassistant import config, core as ha from homeassistant.components.homeassistant import ( ATTR_ENTRY_ID, ATTR_SAFE_MODE, + DOMAIN as HOMEASSISTANT_DOMAIN, SERVICE_CHECK_CONFIG, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, @@ -32,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers import entity, entity_registry as er +from homeassistant.helpers import entity, entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import ( @@ -637,3 +638,187 @@ async def test_reload_all( assert len(core_config) == 1 assert len(themes) == 1 assert len(jinja) == 1 + + +@pytest.mark.parametrize( + "installation_type", + [ + "Home Assistant Core", + "Home Assistant Supervised", + ], +) +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_method( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + installation_type: str, + arch: str, +) -> None: + """Test deprecated installation issue.""" + with patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": installation_type, + "arch": arch, + }, + ): + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_method_architecture" + ) + assert issue.domain == HOMEASSISTANT_DOMAIN + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": installation_type[15:], + "arch": arch, + } + + +@pytest.mark.parametrize( + "installation_type", + [ + "Home Assistant Container", + "Home Assistant OS", + ], +) +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + ], +) +async def test_deprecated_installation_issue_32bit( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + installation_type: str, + arch: str, +) -> None: + """Test deprecated installation issue.""" + with patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": installation_type, + "arch": arch, + }, + ): + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_architecture" + ) + assert issue.domain == HOMEASSISTANT_DOMAIN + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": installation_type[15:], + "arch": arch, + } + + +@pytest.mark.parametrize( + "installation_type", + [ + "Home Assistant Core", + "Home Assistant Supervised", + ], +) +async def test_deprecated_installation_issue_method( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + installation_type: str, +) -> None: + """Test deprecated installation issue.""" + with patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": installation_type, + "arch": "generic-x86-64", + }, + ): + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_method") + assert issue.domain == HOMEASSISTANT_DOMAIN + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": installation_type[15:], + "arch": "generic-x86-64", + } + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi3", "deprecated_os_aarch64"), + ("rpi4", "deprecated_os_aarch64"), + ("tinker", "deprecated_os_armv7"), + ("odroid-xu4", "deprecated_os_armv7"), + ("rpi2", "deprecated_os_armv7"), + ], +) +async def test_deprecated_installation_issue_aarch64( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + board: str, + issue_id: str, +) -> None: + """Test deprecated installation issue.""" + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + ): + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) + assert issue.domain == HOMEASSISTANT_DOMAIN + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_guide": "https://www.home-assistant.io/installation/", + } + + +async def test_deprecated_installation_issue_armv7_container( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test deprecated installation issue.""" + with patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Container", + "arch": "armv7", + }, + ): + assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, "deprecated_container_armv7" + ) + assert issue.domain == HOMEASSISTANT_DOMAIN + assert issue.severity == ir.IssueSeverity.WARNING diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 59790abe07d..524e5988de6 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -288,7 +288,6 @@ async def test_create_issue_with_items( assert automations_with_entity(hass, entity_id)[0] == "automation.test" assert scripts_with_entity(hass, entity_id)[0] == "script.test" - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_switch_{issue_string}_scripts" @@ -308,7 +307,6 @@ async def test_create_issue_with_items( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 @pytest.mark.parametrize( @@ -413,7 +411,6 @@ async def test_create_issue( assert hass.states.get(entity_id).state in [STATE_OFF, STATE_ON] - assert len(issue_registry.issues) == 1 issue = issue_registry.async_get_issue(DOMAIN, issue_id) assert issue is not None assert issue.translation_key == f"deprecated_switch_{issue_string}" @@ -433,7 +430,6 @@ async def test_create_issue( # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) - assert len(issue_registry.issues) == 0 @pytest.mark.parametrize("device_fixture", ["c2c_arlo_pro_3_switch"]) From e2a916ff9d2277f50c304944e5658e06852fda0a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 20:48:07 +0200 Subject: [PATCH 0946/1175] Make sure we can set up OAuth based integrations via discovery (#145144) --- .../components/home_connect/strings.json | 3 ++ homeassistant/components/lyric/strings.json | 3 ++ homeassistant/components/miele/strings.json | 3 ++ homeassistant/components/nest/config_flow.py | 7 +++ homeassistant/components/spotify/strings.json | 3 ++ .../components/withings/strings.json | 3 ++ .../helpers/config_entry_oauth2_flow.py | 43 +++++++++++++++++++ .../home_connect/test_config_flow.py | 18 ++++++++ tests/components/miele/test_config_flow.py | 10 +++++ tests/components/spotify/test_config_flow.py | 22 +++++++--- tests/components/withings/test_config_flow.py | 9 ++++ .../helpers/test_config_entry_oauth2_flow.py | 13 ++++++ 12 files changed, 130 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 3fc509e79f3..37ef37a2839 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -11,6 +11,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Home Connect integration needs to re-authenticate your account" + }, + "oauth_discovery": { + "description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect." } }, "abort": { diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index bc48a791e70..41598dfbdd0 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -7,6 +7,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Lyric integration needs to re-authenticate your account." + }, + "oauth_discovery": { + "description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric." } }, "abort": { diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 55d1769daf8..6774d813e44 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -13,6 +13,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Miele integration needs to re-authenticate your account" + }, + "oauth_discovery": { + "description": "Home Assistant has found a Miele device on your network. Press **Submit** to continue setting up Miele." } }, "abort": { diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 0b249db7a4b..6ed43066fe3 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -31,6 +31,7 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, SelectSelectorMode, ) +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.util import get_random_string from . import api @@ -440,3 +441,9 @@ class NestFlowHandler( if self._structure_config_title: title = self._structure_config_title return self.async_create_entry(title=title, data=self._data) + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle a flow initialized by discovery.""" + return await self.async_step_user() diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 66d837c503f..303942803be 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -7,6 +7,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" + }, + "oauth_discovery": { + "description": "Home Assistant has found Spotify on your network. Press **Submit** to continue setting up Spotify." } }, "abort": { diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 746fa244c8e..8eb4293c637 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -7,6 +7,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Withings integration needs to re-authenticate your account" + }, + "oauth_discovery": { + "description": "Home Assistant has found a Withings device on your network. Press **Submit** to continue setting up Withings." } }, "error": { diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 1cff90031c2..1671e8e2cc2 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -22,6 +22,7 @@ import time from typing import Any, cast from aiohttp import ClientError, ClientResponseError, client, web +from habluetooth import BluetoothServiceInfoBleak import jwt import voluptuous as vol from yarl import URL @@ -34,6 +35,9 @@ from homeassistant.util.hass_dict import HassKey from . import http from .aiohttp_client import async_get_clientsession from .network import NoURLAvailableError +from .service_info.dhcp import DhcpServiceInfo +from .service_info.ssdp import SsdpServiceInfo +from .service_info.zeroconf import ZeroconfServiceInfo _LOGGER = logging.getLogger(__name__) @@ -493,6 +497,45 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """Handle a flow start.""" return await self.async_step_pick_implementation(user_input) + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by Bluetooth discovery.""" + return await self.async_step_oauth_discovery() + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by DHCP discovery.""" + return await self.async_step_oauth_discovery() + + async def async_step_homekit( + self, discovery_info: ZeroconfServiceInfo + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by Homekit discovery.""" + return await self.async_step_oauth_discovery() + + async def async_step_ssdp( + self, discovery_info: SsdpServiceInfo + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by SSDP discovery.""" + return await self.async_step_oauth_discovery() + + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by Zeroconf discovery.""" + return await self.async_step_oauth_discovery() + + async def async_step_oauth_discovery( + self, user_input: dict[str, Any] | None = None + ) -> config_entries.ConfigFlowResult: + """Handle a flow initialized by a discovery method.""" + if user_input is not None: + return await self.async_step_user() + await self._async_handle_discovery_without_unique_id() + return self.async_show_form(step_id="oauth_discovery") + @classmethod def async_register_implementation( cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 3d239d63bd0..ad35f890528 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -279,6 +279,15 @@ async def test_zeroconf_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -351,6 +360,15 @@ async def test_dhcp_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_discovery ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/miele/test_config_flow.py b/tests/components/miele/test_config_flow.py index 78478bc0e9d..bbe5844c1cd 100644 --- a/tests/components/miele/test_config_flow.py +++ b/tests/components/miele/test_config_flow.py @@ -225,6 +225,16 @@ async def test_zeroconf_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF} ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 24c0e1d41d9..0f48002e5db 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -38,13 +38,6 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: 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"] is FlowResultType.ABORT - assert result["reason"] == "missing_credentials" - async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: """Check zeroconf flow aborts when an entry already exist.""" @@ -265,3 +258,18 @@ async def test_reauth_account_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" + + +async def test_zeroconf(hass: HomeAssistant) -> None: + """Check zeroconf flow aborts when an entry already exist.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "missing_credentials" diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index b61a54150e4..4c9e2bef0d6 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -312,6 +312,15 @@ async def test_dhcp( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=service_info ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 5d16a9a62fd..f250f97cfd4 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -397,6 +397,14 @@ async def test_step_discovery( data=data_entry_flow.BaseServiceInfo(), ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" @@ -418,6 +426,11 @@ async def test_abort_discovered_multiple( data=data_entry_flow.BaseServiceInfo(), ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" From c4485c181495738c1806b32d3f21e133563e4573 Mon Sep 17 00:00:00 2001 From: Chuck Deal <106625807+chuckdeal97@users.noreply.github.com> Date: Mon, 26 May 2025 14:58:11 -0400 Subject: [PATCH 0947/1175] Add Sunbeam Dual Zone Heated Bedding to Tuya integration (#135405) --- homeassistant/components/tuya/const.py | 5 +++ homeassistant/components/tuya/select.py | 22 ++++++++++++ homeassistant/components/tuya/strings.json | 14 ++++++++ homeassistant/components/tuya/switch.py | 40 ++++++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 518e49f2636..a546495cc1a 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -218,6 +218,8 @@ class DPCode(StrEnum): LED_TYPE_2 = "led_type_2" LED_TYPE_3 = "led_type_3" LEVEL = "level" + LEVEL_1 = "level_1" + LEVEL_2 = "level_2" LEVEL_CURRENT = "level_current" LIGHT = "light" # Light LIGHT_MODE = "light_mode" @@ -256,6 +258,9 @@ class DPCode(StrEnum): POWDER_SET = "powder_set" # Powder POWER = "power" POWER_GO = "power_go" + PREHEAT = "preheat" + PREHEAT_1 = "preheat_1" + PREHEAT_2 = "preheat_2" POWER_TOTAL = "power_total" PRESENCE_STATE = "presence_state" PRESSURE_STATE = "pressure_state" diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 553191b7d45..21f88156236 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -316,6 +316,28 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Electric Blanket + # https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p + "dr": ( + SelectEntityDescription( + key=DPCode.LEVEL, + name="Level", + icon="mdi:thermometer-lines", + translation_key="blanket_level", + ), + SelectEntityDescription( + key=DPCode.LEVEL_1, + name="Side A Level", + icon="mdi:thermometer-lines", + translation_key="blanket_level", + ), + SelectEntityDescription( + key=DPCode.LEVEL_2, + name="Side B Level", + icon="mdi:thermometer-lines", + translation_key="blanket_level", + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index fc27aa65ce5..a3b997959f6 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -462,6 +462,20 @@ "144h": "144h", "168h": "168h" } + }, + "blanket_level": { + "state": { + "level_1": "Low", + "level_2": "Level 2", + "level_3": "Level 3", + "level_4": "Level 4", + "level_5": "Level 5", + "level_6": "Level 6", + "level_7": "Level 7", + "level_8": "Level 8", + "level_9": "Level 9", + "level_10": "High" + } } }, "sensor": { diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 4000e8d9b24..b0936dcaade 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -729,6 +729,46 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { translation_key="switch", ), ), + # Electric Blanket + # https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p + "dr": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Power", + icon="mdi:power", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Side A Power", + icon="mdi:alpha-a", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Side B Power", + icon="mdi:alpha-b", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key=DPCode.PREHEAT, + name="Preheat", + icon="mdi:radiator", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key=DPCode.PREHEAT_1, + name="Side A Preheat", + icon="mdi:radiator", + device_class=SwitchDeviceClass.SWITCH, + ), + SwitchEntityDescription( + key=DPCode.PREHEAT_2, + name="Side B Preheat", + icon="mdi:radiator", + device_class=SwitchDeviceClass.SWITCH, + ), + ), } # Socket (duplicate of `pc`) From c36f8c38aec1fecc0dbc2febbc332a30a8d3758f Mon Sep 17 00:00:00 2001 From: Speak to the Geek <4546972+sOckhamSter@users.noreply.github.com> Date: Mon, 26 May 2025 19:59:07 +0100 Subject: [PATCH 0948/1175] YouTube Component - Enable SensorStateClass for Long Term Statistic Support (#142670) * Youtube Component Support SensorStateClass in sensor.py Added support for long term statistics by including the appropriate state class type for subscriber and view counts. * Update sensor.py * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/youtube/sensor.py | 8 +++++++- tests/components/youtube/snapshots/test_sensor.ambr | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/youtube/sensor.py b/homeassistant/components/youtube/sensor.py index 128c23f7082..224ace3d405 100644 --- a/homeassistant/components/youtube/sensor.py +++ b/homeassistant/components/youtube/sensor.py @@ -6,7 +6,11 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant @@ -54,6 +58,7 @@ SENSOR_TYPES = [ key="subscribers", translation_key="subscribers", native_unit_of_measurement="subscribers", + state_class=SensorStateClass.MEASUREMENT, available_fn=lambda _: True, value_fn=lambda channel: channel[ATTR_SUBSCRIBER_COUNT], entity_picture_fn=lambda channel: channel[ATTR_ICON], @@ -63,6 +68,7 @@ SENSOR_TYPES = [ key="views", translation_key="views", native_unit_of_measurement="views", + state_class=SensorStateClass.TOTAL_INCREASING, available_fn=lambda _: True, value_fn=lambda channel: channel[ATTR_TOTAL_VIEWS], entity_picture_fn=lambda channel: channel[ATTR_ICON], diff --git a/tests/components/youtube/snapshots/test_sensor.ambr b/tests/components/youtube/snapshots/test_sensor.ambr index f4549e89c8c..feddd644cee 100644 --- a/tests/components/youtube/snapshots/test_sensor.ambr +++ b/tests/components/youtube/snapshots/test_sensor.ambr @@ -20,6 +20,7 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Subscribers', + 'state_class': , 'unit_of_measurement': 'subscribers', }), 'context': , @@ -35,6 +36,7 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Views', + 'state_class': , 'unit_of_measurement': 'views', }), 'context': , @@ -63,6 +65,7 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Subscribers', + 'state_class': , 'unit_of_measurement': 'subscribers', }), 'context': , @@ -78,6 +81,7 @@ 'attributes': ReadOnlyDict({ 'entity_picture': 'https://yt3.ggpht.com/fca_HuJ99xUxflWdex0XViC3NfctBFreIl8y4i9z411asnGTWY-Ql3MeH_ybA4kNaOjY7kyA=s800-c-k-c0x00ffffff-no-rj', 'friendly_name': 'Google for Developers Views', + 'state_class': , 'unit_of_measurement': 'views', }), 'context': , From d6375a79a10a8ba0a6065a971807b7aaff534a65 Mon Sep 17 00:00:00 2001 From: Dave Ingram Date: Mon, 26 May 2025 20:01:45 +0100 Subject: [PATCH 0949/1175] Expose filter/pump timers for Tuya pet fountains (#131863) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/const.py | 6 ++++- homeassistant/components/tuya/sensor.py | 30 ++++++++++++++++++++++ homeassistant/components/tuya/strings.json | 12 +++++++++ homeassistant/components/tuya/switch.py | 2 +- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a546495cc1a..508c47443ca 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -199,7 +199,8 @@ class DPCode(StrEnum): FEED_REPORT = "feed_report" FEED_STATE = "feed_state" FILTER = "filter" - FILTER_LIFE = "filter" + FILTER_DURATION = "filter_life" # Filter duration (hours) + FILTER_LIFE = "filter" # Filter life (percentage) FILTER_RESET = "filter_reset" # Filter (cartridge) reset FLOODLIGHT_LIGHTNESS = "floodlight_lightness" FLOODLIGHT_SWITCH = "floodlight_switch" @@ -267,6 +268,7 @@ class DPCode(StrEnum): PRESSURE_VALUE = "pressure_value" PUMP = "pump" PUMP_RESET = "pump_reset" # Water pump reset + PUMP_TIME = "pump_time" # Water pump duration OXYGEN = "oxygen" # Oxygen bar RECORD_MODE = "record_mode" RECORD_SWITCH = "record_switch" # Recording switch @@ -374,6 +376,7 @@ class DPCode(StrEnum): UPPER_TEMP = "upper_temp" UPPER_TEMP_F = "upper_temp_f" UV = "uv" # UV sterilization + UV_RUNTIME = "uv_runtime" # UV runtime VA_BATTERY = "va_battery" VA_HUMIDITY = "va_humidity" VA_TEMPERATURE = "va_temperature" @@ -387,6 +390,7 @@ class DPCode(StrEnum): WATER = "water" WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level + WATER_TIME = "water_time" # Water usage duration WATERSENSOR_STATE = "watersensor_state" WEATHER_DELAY = "weather_delay" WET = "wet" # Humidification diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index d9be940bddd..912632c074b 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -305,6 +305,36 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), + # Pet Fountain + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r0as4ln + "cwysj": ( + TuyaSensorEntityDescription( + key=DPCode.UV_RUNTIME, + translation_key="uv_runtime", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + TuyaSensorEntityDescription( + key=DPCode.PUMP_TIME, + translation_key="pump_time", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.FILTER_DURATION, + translation_key="filter_duration", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.WATER_TIME, + translation_key="water_time", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + ), # Air Quality Monitor # https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv "hjjcy": ( diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index a3b997959f6..ff67ac19806 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -668,6 +668,18 @@ "level_5": "Level 5", "level_6": "Level 6" } + }, + "uv_runtime": { + "name": "UV runtime" + }, + "pump_time": { + "name": "Water pump duration" + }, + "filter_duration": { + "name": "Filter duration" + }, + "water_time": { + "name": "Water usage duration" } }, "switch": { diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index b0936dcaade..a1d90c6ec2b 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -80,7 +80,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), - # Pet Water Feeder + # Pet Fountain # https://developer.tuya.com/en/docs/iot/f?id=K9gf46aewxem5 "cwysj": ( SwitchEntityDescription( From 2dc2b0ffacb655ad61e58ef6252d349b09d441f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Mon, 26 May 2025 21:02:27 +0200 Subject: [PATCH 0950/1175] Delete Home Connect program switches related strings (#144610) --- .../components/home_connect/strings.json | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 37ef37a2839..9d33f1d3ffd 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -159,28 +159,6 @@ } } }, - "deprecated_program_switch_in_automations_scripts": { - "title": "Deprecated program switch detected in some automations or scripts", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]", - "description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue." - } - } - } - }, - "deprecated_program_switch": { - "title": "Deprecated program switch entities", - "fix_flow": { - "step": { - "confirm": { - "title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]", - "description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead." - } - } - } - }, "deprecated_set_program_and_option_actions": { "title": "The executed action is deprecated", "fix_flow": { From b667fb2728e9c441efbbab1fa4728840a5b7a5c1 Mon Sep 17 00:00:00 2001 From: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com> Date: Mon, 26 May 2025 21:04:38 +0200 Subject: [PATCH 0951/1175] Fix NaN values in Modbus slaves sensors (#139969) * Fix NaN values in Modbus slaves sensors * fixXbdraco --- homeassistant/components/modbus/entity.py | 6 +-- homeassistant/components/modbus/sensor.py | 35 +++++++++----- tests/components/modbus/test_sensor.py | 58 ++++++++++++++++++----- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 4684c2f2b8a..53c3e8f8709 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -285,10 +285,10 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): v_result = [] for entry in val: v_temp = self.__process_raw_value(entry) - if v_temp is None: - v_result.append("0") - else: + if self._data_type != DataType.CUSTOM: v_result.append(str(v_temp)) + else: + v_result.append(str(v_temp) if v_temp is not None else "0") return ",".join(map(str, v_result)) # Apply scale, precision, limits to floats and ints diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 2c2efb70d5a..490aece587c 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -73,7 +73,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): super().__init__(hass, hub, entry) if slave_count: self._count = self._count * (slave_count + 1) - self._coordinator: DataUpdateCoordinator[list[float] | None] | None = None + self._coordinator: DataUpdateCoordinator[list[float | None] | None] | None = ( + None + ) self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_device_class = entry.get(CONF_DEVICE_CLASS) @@ -120,37 +122,45 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreSensor, SensorEntity): self._coordinator.async_set_updated_data(None) self.async_write_ha_state() return - + self._attr_available = True result = self.unpack_structure_result(raw_result.registers) if self._coordinator: + result_array: list[float | None] = [] if result: - result_array = list( - map( - float if not self._value_is_int else int, - result.split(","), - ) - ) + for i in result.split(","): + if i != "None": + result_array.append( + float(i) if not self._value_is_int else int(i) + ) + else: + result_array.append(None) + self._attr_native_value = result_array[0] self._coordinator.async_set_updated_data(result_array) else: self._attr_native_value = None - self._coordinator.async_set_updated_data(None) + result_array = (self._slave_count + 1) * [None] + self._coordinator.async_set_updated_data(result_array) else: self._attr_native_value = result - self._attr_available = self._attr_native_value is not None self.async_write_ha_state() class SlaveSensor( - CoordinatorEntity[DataUpdateCoordinator[list[float] | None]], + CoordinatorEntity[DataUpdateCoordinator[list[float | None] | None]], RestoreSensor, SensorEntity, ): """Modbus slave register sensor.""" + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._attr_available + def __init__( self, - coordinator: DataUpdateCoordinator[list[float] | None], + coordinator: DataUpdateCoordinator[list[float | None] | None], idx: int, entry: dict[str, Any], ) -> None: @@ -178,4 +188,5 @@ class SlaveSensor( """Handle updated data from the coordinator.""" result = self.coordinator.data self._attr_native_value = result[self._idx] if result else None + self._attr_available = result is not None super()._handle_coordinator_update() diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index fc63a300c5c..4910b4df065 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -428,7 +428,7 @@ async def test_config_wrong_struct_sensor( }, [0x89AB], False, - STATE_UNAVAILABLE, + STATE_UNKNOWN, ), ( { @@ -631,7 +631,7 @@ async def test_config_wrong_struct_sensor( }, [0x8000, 0x0000], False, - STATE_UNAVAILABLE, + STATE_UNKNOWN, ), ( { @@ -742,7 +742,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: int.from_bytes(struct.pack(">f", float("nan"))[2:4]), ], False, - ["34899771392.0", "0.0"], + ["34899771392.0", STATE_UNKNOWN], ), ( { @@ -757,7 +757,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: int.from_bytes(struct.pack(">f", float("nan"))[2:4]), ], False, - ["34899771392.0", "0.0"], + ["34899771392.0", STATE_UNKNOWN], ), ( { @@ -802,7 +802,11 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: }, [0x0102, 0x0304, 0x0403, 0x0201, 0x0403], False, - [STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNKNOWN], + [ + STATE_UNKNOWN, + STATE_UNKNOWN, + STATE_UNKNOWN, + ], ), ( { @@ -857,7 +861,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: }, [0x0102, 0x0304, 0x0403, 0x0201], True, - [STATE_UNAVAILABLE, STATE_UNKNOWN], + [STATE_UNAVAILABLE, STATE_UNAVAILABLE], ), ( { @@ -866,7 +870,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: }, [0x0102, 0x0304, 0x0403, 0x0201], True, - [STATE_UNAVAILABLE, STATE_UNKNOWN], + [STATE_UNAVAILABLE, STATE_UNAVAILABLE], ), ( { @@ -875,7 +879,7 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: }, [], False, - [STATE_UNAVAILABLE, STATE_UNKNOWN], + [STATE_UNKNOWN, STATE_UNKNOWN], ), ( { @@ -884,7 +888,35 @@ async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None: }, [], False, - [STATE_UNAVAILABLE, STATE_UNKNOWN], + [STATE_UNKNOWN, STATE_UNKNOWN], + ), + ( + { + CONF_VIRTUAL_COUNT: 4, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, + CONF_DATA_TYPE: DataType.INT32, + CONF_NAN_VALUE: "0x800000", + }, + [ + 0x0, + 0x35, + 0x0, + 0x38, + 0x80, + 0x0, + 0x80, + 0x0, + 0xFFFF, + 0xFFF6, + ], + False, + [ + "53", + "56", + STATE_UNKNOWN, + STATE_UNKNOWN, + "-10", + ], ), ], ) @@ -1103,7 +1135,7 @@ async def test_virtual_swap_sensor( ) async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: """Run test for sensor.""" - assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + assert hass.states.get(ENTITY_ID).state == STATE_UNKNOWN @pytest.mark.parametrize( @@ -1131,14 +1163,14 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: int.from_bytes(struct.pack(">f", float("nan"))[0:2]), int.from_bytes(struct.pack(">f", float("nan"))[2:4]), ], - STATE_UNAVAILABLE, + STATE_UNKNOWN, ), ( { CONF_DATA_TYPE: DataType.FLOAT32, }, [0x6E61, 0x6E00], - STATE_UNAVAILABLE, + STATE_UNKNOWN, ), ( { @@ -1147,7 +1179,7 @@ async def test_wrong_unpack(hass: HomeAssistant, mock_do_cycle) -> None: CONF_STRUCTURE: "4s", }, [0x6E61, 0x6E00], - STATE_UNAVAILABLE, + STATE_UNKNOWN, ), ( { From 0b6ea36e24b9182240e6c1e5d45567c46160132e Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Mon, 26 May 2025 21:04:46 +0200 Subject: [PATCH 0952/1175] Add Tado user agent (#145637) --- homeassistant/components/tado/__init__.py | 13 +++++++++++-- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index d1994075f12..74768ee01fa 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -8,7 +8,13 @@ import PyTado.exceptions from PyTado.interface import Tado from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + APPLICATION_NAME, + CONF_PASSWORD, + CONF_USERNAME, + Platform, + __version__ as HA_VERSION, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -74,7 +80,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool def create_tado_instance() -> tuple[Tado, str]: """Create a Tado instance, this time with a previously obtained refresh token.""" - tado = Tado(saved_refresh_token=entry.data[CONF_REFRESH_TOKEN]) + tado = Tado( + saved_refresh_token=entry.data[CONF_REFRESH_TOKEN], + user_agent=f"{APPLICATION_NAME}/{HA_VERSION}", + ) return tado, tado.device_activation_status() try: diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index b252a396689..8350f300c03 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.18.14"] + "requirements": ["python-tado==0.18.15"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e2ca9a2713..3d7865d048a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2507,7 +2507,7 @@ python-snoo==0.6.6 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.14 +python-tado==0.18.15 # homeassistant.components.technove python-technove==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da2ef7e146e..92f9b3e03ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2047,7 +2047,7 @@ python-snoo==0.6.6 python-songpal==0.16.2 # homeassistant.components.tado -python-tado==0.18.14 +python-tado==0.18.15 # homeassistant.components.technove python-technove==2.0.0 From fd4dafaac5fda3cf5f075892bf21fd2e861ed435 Mon Sep 17 00:00:00 2001 From: asafhas <121308170+asafhas@users.noreply.github.com> Date: Mon, 26 May 2025 22:05:09 +0300 Subject: [PATCH 0953/1175] Fix trigger condition and alarm message in Tuya Alarm (#132963) Co-authored-by: Franck Nijhof --- .../components/tuya/alarm_control_panel.py | 55 +++++++++++++++++-- homeassistant/components/tuya/const.py | 2 + 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 96f7d3a1e1c..4972fe88339 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -2,6 +2,8 @@ from __future__ import annotations +from base64 import b64decode +from dataclasses import dataclass from enum import StrEnum from tuya_sharing import CustomerDevice, Manager @@ -18,7 +20,15 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import TuyaConfigEntry from .const import TUYA_DISCOVERY_NEW, DPCode, DPType -from .entity import TuyaEntity +from .entity import EnumTypeData, TuyaEntity + + +@dataclass(frozen=True) +class TuyaAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription): + """Describe a Tuya Alarm Control Panel entity.""" + + master_state: DPCode | None = None + alarm_msg: DPCode | None = None class Mode(StrEnum): @@ -30,6 +40,13 @@ class Mode(StrEnum): SOS = "sos" +class State(StrEnum): + """Alarm states.""" + + NORMAL = "normal" + ALARM = "alarm" + + STATE_MAPPING: dict[str, AlarmControlPanelState] = { Mode.DISARMED: AlarmControlPanelState.DISARMED, Mode.ARM: AlarmControlPanelState.ARMED_AWAY, @@ -40,12 +57,14 @@ STATE_MAPPING: dict[str, AlarmControlPanelState] = { # All descriptions can be found here: # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -ALARM: dict[str, tuple[AlarmControlPanelEntityDescription, ...]] = { +ALARM: dict[str, tuple[TuyaAlarmControlPanelEntityDescription, ...]] = { # Alarm Host # https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf "mal": ( - AlarmControlPanelEntityDescription( + TuyaAlarmControlPanelEntityDescription( key=DPCode.MASTER_MODE, + master_state=DPCode.MASTER_STATE, + alarm_msg=DPCode.ALARM_MSG, name="Alarm", ), ) @@ -86,12 +105,14 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): _attr_name = None _attr_code_arm_required = False + _master_state: EnumTypeData | None = None + _alarm_msg_dpcode: DPCode | None = None def __init__( self, device: CustomerDevice, device_manager: Manager, - description: AlarmControlPanelEntityDescription, + description: TuyaAlarmControlPanelEntityDescription, ) -> None: """Init Tuya Alarm.""" super().__init__(device, device_manager) @@ -111,13 +132,39 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): if Mode.SOS in supported_modes.range: self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER + # Determine master state + if enum_type := self.find_dpcode( + description.master_state, dptype=DPType.ENUM, prefer_function=True + ): + self._master_state = enum_type + + # Determine alarm message + if dp_code := self.find_dpcode(description.alarm_msg, prefer_function=True): + self._alarm_msg_dpcode = dp_code + @property def alarm_state(self) -> AlarmControlPanelState | None: """Return the state of the device.""" + # When the alarm is triggered, only its 'state' is changing. From 'normal' to 'alarm'. + # The 'mode' doesn't change, and stays as 'arm' or 'home'. + if self._master_state is not None: + if self.device.status.get(self._master_state.dpcode) == State.ALARM: + return AlarmControlPanelState.TRIGGERED + if not (status := self.device.status.get(self.entity_description.key)): return None return STATE_MAPPING.get(status) + @property + def changed_by(self) -> str | None: + """Last change triggered by.""" + if self._master_state is not None and self._alarm_msg_dpcode is not None: + if self.device.status.get(self._master_state.dpcode) == State.ALARM: + encoded_msg = self.device.status.get(self._alarm_msg_dpcode) + if encoded_msg: + return b64decode(encoded_msg).decode("utf-16be") + return None + def alarm_disarm(self, code: str | None = None) -> None: """Send Disarm command.""" self._send_command( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 508c47443ca..a40468fdc8f 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -102,6 +102,7 @@ class DPCode(StrEnum): ALARM_TIME = "alarm_time" # Alarm time ALARM_VOLUME = "alarm_volume" # Alarm volume ALARM_MESSAGE = "alarm_message" + ALARM_MSG = "alarm_msg" ANGLE_HORIZONTAL = "angle_horizontal" ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit @@ -226,6 +227,7 @@ class DPCode(StrEnum): LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock MASTER_MODE = "master_mode" # alarm mode + MASTER_STATE = "master_state" # alarm state MACH_OPERATE = "mach_operate" MANUAL_FEED = "manual_feed" MATERIAL = "material" # Material From 848eb797e05b549164380cda0259364da1189b5b Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 26 May 2025 12:08:30 -0700 Subject: [PATCH 0954/1175] Add read_only selectors to Filter Options Flow (#145526) --- .../components/filter/config_flow.py | 11 ++- homeassistant/components/filter/strings.json | 96 ++++++++++++++----- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/filter/config_flow.py b/homeassistant/components/filter/config_flow.py index dac2d8995bf..7bbfb9f6f0a 100644 --- a/homeassistant/components/filter/config_flow.py +++ b/homeassistant/components/filter/config_flow.py @@ -105,9 +105,18 @@ DATA_SCHEMA_SETUP = vol.Schema( ) BASE_OPTIONS_SCHEMA = { + vol.Optional(CONF_ENTITY_ID): EntitySelector(EntitySelectorConfig(read_only=True)), + vol.Optional(CONF_FILTER_NAME): SelectSelector( + SelectSelectorConfig( + options=FILTERS, + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_FILTER_NAME, + read_only=True, + ) + ), vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): NumberSelector( NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) - ) + ), } OUTLIER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/filter/strings.json b/homeassistant/components/filter/strings.json index 689cf730023..faa1de8b9df 100644 --- a/homeassistant/components/filter/strings.json +++ b/homeassistant/components/filter/strings.json @@ -23,12 +23,16 @@ "data": { "window_size": "Window size", "precision": "Precision", - "radius": "Radius" + "radius": "Radius", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "Size of the window of previous states.", "precision": "Defines the number of decimal places of the calculated sensor value.", - "radius": "Band radius from median of previous states." + "radius": "Band radius from median of previous states.", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "lowpass": { @@ -36,12 +40,16 @@ "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data::precision%]", - "time_constant": "Time constant" + "time_constant": "Time constant", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", - "time_constant": "Loosely relates to the amount of time it takes for a state to influence the output." + "time_constant": "Loosely relates to the amount of time it takes for a state to influence the output.", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "range": { @@ -49,12 +57,16 @@ "data": { "precision": "[%key:component::filter::config::step::outlier::data::precision%]", "lower_bound": "Lower bound", - "upper_bound": "Upper bound" + "upper_bound": "Upper bound", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", "lower_bound": "Lower bound for filter range.", - "upper_bound": "Upper bound for filter range." + "upper_bound": "Upper bound for filter range.", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "time_simple_moving_average": { @@ -62,34 +74,46 @@ "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data::precision%]", - "type": "Type" + "type": "Type", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", - "type": "Defines the type of Simple Moving Average." + "type": "Defines the type of Simple Moving Average.", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "throttle": { "description": "[%key:component::filter::config::step::outlier::description%]", "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "time_throttle": { "description": "[%key:component::filter::config::step::outlier::description%]", "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } } } @@ -104,12 +128,16 @@ "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data::precision%]", - "radius": "[%key:component::filter::config::step::outlier::data::radius%]" + "radius": "[%key:component::filter::config::step::outlier::data::radius%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", - "radius": "[%key:component::filter::config::step::outlier::data_description::radius%]" + "radius": "[%key:component::filter::config::step::outlier::data_description::radius%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "lowpass": { @@ -117,12 +145,16 @@ "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data::precision%]", - "time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]" + "time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", - "time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]" + "time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "range": { @@ -130,12 +162,16 @@ "data": { "precision": "[%key:component::filter::config::step::outlier::data::precision%]", "lower_bound": "[%key:component::filter::config::step::range::data::lower_bound%]", - "upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]" + "upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", "lower_bound": "[%key:component::filter::config::step::range::data_description::lower_bound%]", - "upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]" + "upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "time_simple_moving_average": { @@ -143,34 +179,46 @@ "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data::precision%]", - "type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]" + "type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", - "type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]" + "type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "throttle": { "description": "[%key:component::filter::config::step::outlier::description%]", "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } }, "time_throttle": { "description": "[%key:component::filter::config::step::outlier::description%]", "data": { "window_size": "[%key:component::filter::config::step::outlier::data::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data::filter%]" }, "data_description": { "window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]", - "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]" + "precision": "[%key:component::filter::config::step::outlier::data_description::precision%]", + "entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]", + "filter": "[%key:component::filter::config::step::user::data_description::filter%]" } } } From 001164ce1b0ee8ff7d456756de1a016b5202c992 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 27 May 2025 05:11:35 +1000 Subject: [PATCH 0955/1175] Remove available property for streaming in Teslemetry (#145352) --- homeassistant/components/teslemetry/entity.py | 5 ----- .../components/teslemetry/snapshots/test_device_tracker.ambr | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/teslemetry/entity.py b/homeassistant/components/teslemetry/entity.py index 588bf0b1b65..762678736a5 100644 --- a/homeassistant/components/teslemetry/entity.py +++ b/homeassistant/components/teslemetry/entity.py @@ -262,8 +262,3 @@ class TeslemetryVehicleStreamEntity(TeslemetryRootEntity): self._attr_translation_key = key self._attr_unique_id = f"{data.vin}-{key}" self._attr_device_info = data.device - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.stream.connected diff --git a/tests/components/teslemetry/snapshots/test_device_tracker.ambr b/tests/components/teslemetry/snapshots/test_device_tracker.ambr index c71f818479a..9da463501b7 100644 --- a/tests/components/teslemetry/snapshots/test_device_tracker.ambr +++ b/tests/components/teslemetry/snapshots/test_device_tracker.ambr @@ -147,7 +147,7 @@ 'unknown' # --- # name: test_device_tracker_streaming[device_tracker.test_origin-state] - 'unknown' + 'unavailable' # --- # name: test_device_tracker_streaming[device_tracker.test_route-restore] 'not_home' From 2ef0a8557f3093f58e25b6ebeb18508fd6a12bd8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 26 May 2025 12:12:05 -0700 Subject: [PATCH 0956/1175] Bump ical to 9.2.5 (#145636) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d6f2ee76615..398ff8768a9 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"] + "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.5"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 07de4a82244..fc636d75482 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.4"] + "requirements": ["ical==9.2.5"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 367c75d5755..cd19090f400 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==9.2.4"] + "requirements": ["ical==9.2.5"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 9cf39b7ce45..60b5e15e8fb 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.4"] + "requirements": ["ical==9.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3d7865d048a..ca82c4b74a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.4 +ical==9.2.5 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92f9b3e03ce..6bfd354f921 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.4 +ical==9.2.5 # homeassistant.components.caldav icalendar==6.1.0 From db489a50698f42588d81a5b08b005226855651f3 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 27 May 2025 05:12:39 +1000 Subject: [PATCH 0957/1175] Improve device tracker platform in Teslemetry (#145268) --- .../components/teslemetry/device_tracker.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/teslemetry/device_tracker.py b/homeassistant/components/teslemetry/device_tracker.py index 6a3df6ecb6a..eb2c220ebbd 100644 --- a/homeassistant/components/teslemetry/device_tracker.py +++ b/homeassistant/components/teslemetry/device_tracker.py @@ -47,19 +47,25 @@ DESCRIPTIONS: tuple[TeslemetryDeviceTrackerEntityDescription, ...] = ( TeslemetryDeviceTrackerEntityDescription( key="location", polling_prefix="drive_state", - value_listener=lambda x, y: x.listen_Location(y), + value_listener=lambda vehicle, callback: vehicle.listen_Location(callback), streaming_firmware="2024.26", ), TeslemetryDeviceTrackerEntityDescription( key="route", polling_prefix="drive_state_active_route", - value_listener=lambda x, y: x.listen_DestinationLocation(y), - name_listener=lambda x, y: x.listen_DestinationName(y), + value_listener=lambda vehicle, callback: vehicle.listen_DestinationLocation( + callback + ), + name_listener=lambda vehicle, callback: vehicle.listen_DestinationName( + callback + ), streaming_firmware="2024.26", ), TeslemetryDeviceTrackerEntityDescription( key="origin", - value_listener=lambda x, y: x.listen_OriginLocation(y), + value_listener=lambda vehicle, callback: vehicle.listen_OriginLocation( + callback + ), streaming_firmware="2024.26", entity_registry_enabled_default=False, ), @@ -152,7 +158,6 @@ class TeslemetryStreamingDeviceTrackerEntity( """Handle entity which will be added.""" await super().async_added_to_hass() if (state := await self.async_get_last_state()) is not None: - self._attr_state = state.state self._attr_latitude = state.attributes.get("latitude") self._attr_longitude = state.attributes.get("longitude") self._attr_location_name = state.attributes.get("location_name") @@ -170,12 +175,8 @@ class TeslemetryStreamingDeviceTrackerEntity( def _location_callback(self, location: TeslaLocation | None) -> None: """Update the value of the entity.""" - if location is None: - self._attr_available = False - else: - self._attr_available = True - self._attr_latitude = location.latitude - self._attr_longitude = location.longitude + self._attr_latitude = None if location is None else location.latitude + self._attr_longitude = None if location is None else location.longitude self.async_write_ha_state() def _name_callback(self, name: str | None) -> None: From 84305563ab4f034fb432beaa60d0f83b175dd964 Mon Sep 17 00:00:00 2001 From: Gaylord GIRARD <44167278+GGI1982@users.noreply.github.com> Date: Mon, 26 May 2025 21:13:35 +0200 Subject: [PATCH 0958/1175] Add state class measurement to Freebox rate sensors (#142757) * Update sensor.py Update sensor.py to add state_class=SensorStateClass.MEASUREMENT as per long-term-statistics requierment * Update sensor.py remove duplicate import of SensorStateClass in freebox sensor to satisfy ruff * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/freebox/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 7a176ca5fa7..33af56a1f9e 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature from homeassistant.core import HomeAssistant, callback @@ -28,6 +29,7 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = ( key="rate_down", name="Freebox download speed", device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, icon="mdi:download-network", ), @@ -35,6 +37,7 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = ( key="rate_up", name="Freebox upload speed", device_class=SensorDeviceClass.DATA_RATE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, icon="mdi:upload-network", ), From 9b9d4d7dab31a211cea93ea5b7b29176e6535bc6 Mon Sep 17 00:00:00 2001 From: Jason Mahdjoub Date: Mon, 26 May 2025 21:13:47 +0200 Subject: [PATCH 0959/1175] Set correct state_class for battery_stored and increase timeout to prevent Imeon integration disconnections (#144925) --- homeassistant/components/imeon_inverter/const.py | 2 +- homeassistant/components/imeon_inverter/sensor.py | 2 +- tests/components/imeon_inverter/snapshots/test_sensor.ambr | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/imeon_inverter/const.py b/homeassistant/components/imeon_inverter/const.py index c71a8c72d11..fd08955c038 100644 --- a/homeassistant/components/imeon_inverter/const.py +++ b/homeassistant/components/imeon_inverter/const.py @@ -3,7 +3,7 @@ from homeassistant.const import Platform DOMAIN = "imeon_inverter" -TIMEOUT = 20 +TIMEOUT = 30 PLATFORMS = [ Platform.SENSOR, ] diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index b7a01c3cf17..a2f6ded5ab3 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -69,7 +69,7 @@ ENTITY_DESCRIPTIONS = ( translation_key="battery_stored", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, device_class=SensorDeviceClass.ENERGY_STORAGE, - state_class=SensorStateClass.TOTAL, + state_class=SensorStateClass.MEASUREMENT, ), # Grid SensorEntityDescription( diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index d3ae33a6c8b..8816889f049 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -282,7 +282,7 @@ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -321,7 +321,7 @@ 'attributes': ReadOnlyDict({ 'device_class': 'energy_storage', 'friendly_name': 'Imeon inverter Battery stored', - 'state_class': , + 'state_class': , 'unit_of_measurement': , }), 'context': , From 072d0dc567ef6d0108bb7dd2fa22735704cd36c8 Mon Sep 17 00:00:00 2001 From: peteS-UK <64092177+peteS-UK@users.noreply.github.com> Date: Mon, 26 May 2025 20:14:15 +0100 Subject: [PATCH 0960/1175] Update coordinator logging levels for Squeezebox (#144620) --- homeassistant/components/squeezebox/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py index 7792ec96e0d..6582f143e79 100644 --- a/homeassistant/components/squeezebox/coordinator.py +++ b/homeassistant/components/squeezebox/coordinator.py @@ -112,7 +112,7 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.player.async_update() if self.player.connected is False: - _LOGGER.debug("Player %s is not available", self.name) + _LOGGER.info("Player %s is not available", self.name) self.available = False # start listening for restored players @@ -133,6 +133,6 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Make a player available again.""" if unique_id == self.player.player_id and connected: self.available = True - _LOGGER.debug("Player %s is available again", self.name) + _LOGGER.info("Player %s is available again", self.name) if self._remove_dispatcher: self._remove_dispatcher() From 0ab7d46d7ce1658dbe00e9afb05a2fd51b53d26d Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Mon, 26 May 2025 14:15:40 -0500 Subject: [PATCH 0961/1175] Support AprilAire humidifier auto mode (#144647) --- .../components/aprilaire/humidifier.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/aprilaire/humidifier.py b/homeassistant/components/aprilaire/humidifier.py index fdb9233a0e3..a58f8c43001 100644 --- a/homeassistant/components/aprilaire/humidifier.py +++ b/homeassistant/components/aprilaire/humidifier.py @@ -62,6 +62,8 @@ async def async_setup_entry( target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT, min_humidity=10, max_humidity=50, + auto_status_key=Attribute.HUMIDIFICATION_AVAILABLE, + auto_status_value=1, default_humidity=30, set_humidity_fn=coordinator.client.set_humidification_setpoint, ) @@ -77,6 +79,8 @@ async def async_setup_entry( action_map=DEHUMIDIFIER_ACTION_MAP, current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE, target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT, + auto_status_key=None, + auto_status_value=None, min_humidity=40, max_humidity=90, default_humidity=60, @@ -100,6 +104,8 @@ class AprilaireHumidifierDescription(HumidifierEntityDescription): target_humidity_key: str min_humidity: int max_humidity: int + auto_status_key: str | None + auto_status_value: int | None default_humidity: int set_humidity_fn: Callable[[int], Awaitable] @@ -163,14 +169,31 @@ class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity): def min_humidity(self) -> float: """Return the minimum humidity.""" + if self.is_auto_humidity_mode(): + return 1 + return self.entity_description.min_humidity @property def max_humidity(self) -> float: """Return the maximum humidity.""" + if self.is_auto_humidity_mode(): + return 7 + return self.entity_description.max_humidity + def is_auto_humidity_mode(self) -> bool: + """Return whether the humidifier is in auto mode.""" + + if self.entity_description.auto_status_key is None: + return False + + return ( + self.coordinator.data.get(self.entity_description.auto_status_key) + == self.entity_description.auto_status_value + ) + async def async_set_humidity(self, humidity: int) -> None: """Set the humidity.""" From 987af8f7df2d027b27f69d49203bac57972703db Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Mon, 26 May 2025 20:16:11 +0100 Subject: [PATCH 0962/1175] squeezebox Better result for testing (#144622) --- tests/components/squeezebox/conftest.py | 1 + tests/components/squeezebox/test_media_player.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index a3adf05f5f0..0108dacb00a 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -327,6 +327,7 @@ def mock_pysqueezebox_server( mock_lms.async_status = AsyncMock( return_value={"uuid": format_mac(uuid), "version": FAKE_VERSION} ) + mock_lms.async_prepared_status = mock_lms.async_status return mock_lms diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index f71a7db23ba..1890cde5293 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -831,8 +831,6 @@ async def test_squeezebox_server_discovery( """Mock the async_discover function of pysqueezebox.""" return callback(lms_factory(2)) - lms.async_prepared_status.return_value = {} - with ( patch( "homeassistant.components.squeezebox.Server", From 5f63612b6633cc457e7f79777d4e790e88c31dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Mon, 26 May 2025 21:20:18 +0200 Subject: [PATCH 0963/1175] Increase resolution of sun updates around sunrise/sundown (#140403) --- homeassistant/components/sun/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sun/entity.py b/homeassistant/components/sun/entity.py index 925845c8b4d..4070190e52a 100644 --- a/homeassistant/components/sun/entity.py +++ b/homeassistant/components/sun/entity.py @@ -74,8 +74,8 @@ PHASE_DAY = "day" _PHASE_UPDATES = { PHASE_NIGHT: timedelta(minutes=4 * 5), PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_TWILIGHT: timedelta(minutes=4), + PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4), + PHASE_TWILIGHT: timedelta(minutes=2), PHASE_SMALL_DAY: timedelta(minutes=2), PHASE_DAY: timedelta(minutes=4), } From e857db281f13614f8b9095fbdf23d5f7abb2bb6c Mon Sep 17 00:00:00 2001 From: jukrebs <76174575+MaestroOnICe@users.noreply.github.com> Date: Mon, 26 May 2025 21:21:35 +0200 Subject: [PATCH 0964/1175] Set new IOmeter datacoordinator debouncer cooldown (#143665) --- homeassistant/components/iometer/coordinator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/iometer/coordinator.py b/homeassistant/components/iometer/coordinator.py index 4050341151b..e5d2b554a89 100644 --- a/homeassistant/components/iometer/coordinator.py +++ b/homeassistant/components/iometer/coordinator.py @@ -9,6 +9,7 @@ from iometer import IOmeterClient, IOmeterConnectionError, Reading, Status from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -48,6 +49,9 @@ class IOMeterCoordinator(DataUpdateCoordinator[IOmeterData]): config_entry=config_entry, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=1.0, immediate=False + ), ) self.client = client self.identifier = config_entry.entry_id From b1403838bb0711c9408f3fc74c677229cf24d7f2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 May 2025 21:22:10 +0200 Subject: [PATCH 0965/1175] Add translation string and references for sensor Measurement Angle state class (#145639) --- homeassistant/components/mqtt/strings.json | 1 + homeassistant/components/scrape/strings.json | 1 + homeassistant/components/sensor/strings.json | 1 + homeassistant/components/sql/strings.json | 1 + homeassistant/components/template/strings.json | 1 + 5 files changed, 5 insertions(+) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 3bb467affd6..8fc97362857 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -922,6 +922,7 @@ "state_class": { "options": { "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]", "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" } diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 27115836157..d46f63c9516 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -197,6 +197,7 @@ "state_class": { "options": { "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]", "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 4ad6597692c..2268d2797e4 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -135,6 +135,7 @@ "name": "State class", "state": { "measurement": "Measurement", + "measurement_angle": "Measurement Angle", "total": "Total", "total_increasing": "Total increasing" } diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 486fb5946b4..f9b8044e992 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -128,6 +128,7 @@ "state_class": { "options": { "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]", "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" } diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 729f76a84ec..7f285b4929b 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -349,6 +349,7 @@ "sensor_state_class": { "options": { "measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]", + "measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]", "total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]", "total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]" } From 16394061cb451ba9740846d4c9e915fef05454b2 Mon Sep 17 00:00:00 2001 From: tdfountain <174762217+tdfountain@users.noreply.github.com> Date: Mon, 26 May 2025 12:34:15 -0700 Subject: [PATCH 0966/1175] Add additional outlet sensors to NUT (#143309) Add outlet sensors for current, power, and real powre --- homeassistant/components/nut/sensor.py | 27 +++++++++++++++++++++++ homeassistant/components/nut/strings.json | 3 +++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index ce8c10f8f41..11b646f86a1 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -610,6 +610,33 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "outlet.current": SensorEntityDescription( + key="outlet.current", + translation_key="outlet_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "outlet.power": SensorEntityDescription( + key="outlet.power", + translation_key="outlet_power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "outlet.realpower": SensorEntityDescription( + key="outlet.realpower", + translation_key="outlet_realpower", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), "outlet.voltage": SensorEntityDescription( key="outlet.voltage", translation_key="outlet_voltage", diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index a9a3b470cca..8f993d5fbb1 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -240,6 +240,9 @@ "outlet_number_desc": { "name": "Outlet {outlet_name} description" }, "outlet_number_power": { "name": "Outlet {outlet_name} power" }, "outlet_number_realpower": { "name": "Outlet {outlet_name} real power" }, + "outlet_current": { "name": "Outlet current" }, + "outlet_power": { "name": "Outlet apparent power" }, + "outlet_realpower": { "name": "Outlet real power" }, "outlet_voltage": { "name": "Outlet voltage" }, "output_current": { "name": "Output current" }, "output_current_nominal": { "name": "Nominal output current" }, From b17d62177c836fb7ab921f67451fb3e22b9a1399 Mon Sep 17 00:00:00 2001 From: wittypluck Date: Mon, 26 May 2025 21:34:48 +0200 Subject: [PATCH 0967/1175] Add Air Pollution support to OpenWeatherMap (#137949) Co-authored-by: Joostlek --- .../components/openweathermap/__init__.py | 10 +- .../components/openweathermap/const.py | 12 + .../components/openweathermap/coordinator.py | 79 +++- .../components/openweathermap/sensor.py | 79 +++- .../components/openweathermap/weather.py | 31 +- tests/components/openweathermap/conftest.py | 17 + .../openweathermap/snapshots/test_sensor.ambr | 431 ++++++++++++++++++ .../components/openweathermap/test_sensor.py | 5 +- 8 files changed, 634 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 40ddf0ff37e..737e4fb8e4f 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_MODE, CONF_NAM from homeassistant.core import HomeAssistant from .const import CONFIG_FLOW_VERSION, DEFAULT_OWM_MODE, OWM_MODES, PLATFORMS -from .coordinator import WeatherUpdateCoordinator +from .coordinator import OWMUpdateCoordinator, get_owm_update_coordinator from .repairs import async_create_issue, async_delete_issue from .utils import build_data_and_options @@ -27,7 +27,7 @@ class OpenweathermapData: name: str mode: str - coordinator: WeatherUpdateCoordinator + coordinator: OWMUpdateCoordinator async def async_setup_entry( @@ -45,13 +45,13 @@ async def async_setup_entry( async_delete_issue(hass, entry.entry_id) owm_client = create_owm_client(api_key, mode, lang=language) - weather_coordinator = WeatherUpdateCoordinator(hass, entry, owm_client) + owm_coordinator = get_owm_update_coordinator(mode)(hass, entry, owm_client) - await weather_coordinator.async_config_entry_first_refresh() + await owm_coordinator.async_config_entry_first_refresh() entry.async_on_unload(entry.add_update_listener(async_update_options)) - entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator) + entry.runtime_data = OpenweathermapData(name, mode, owm_coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 0bc804a5b42..9ede24ed1af 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -51,16 +51,28 @@ ATTR_API_CURRENT = "current" ATTR_API_MINUTE_FORECAST = "minute_forecast" ATTR_API_HOURLY_FORECAST = "hourly_forecast" ATTR_API_DAILY_FORECAST = "daily_forecast" +ATTR_API_AIRPOLLUTION_AQI = "aqi" +ATTR_API_AIRPOLLUTION_CO = "co" +ATTR_API_AIRPOLLUTION_NO = "no" +ATTR_API_AIRPOLLUTION_NO2 = "no2" +ATTR_API_AIRPOLLUTION_O3 = "o3" +ATTR_API_AIRPOLLUTION_SO2 = "so2" +ATTR_API_AIRPOLLUTION_PM2_5 = "pm2_5" +ATTR_API_AIRPOLLUTION_PM10 = "pm10" +ATTR_API_AIRPOLLUTION_NH3 = "nh3" + UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] OWM_MODE_FREE_CURRENT = "current" OWM_MODE_FREE_FORECAST = "forecast" OWM_MODE_V30 = "v3.0" +OWM_MODE_AIRPOLLUTION = "air_pollution" OWM_MODES = [ OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, + OWM_MODE_AIRPOLLUTION, ] DEFAULT_OWM_MODE = OWM_MODE_V30 diff --git a/homeassistant/components/openweathermap/coordinator.py b/homeassistant/components/openweathermap/coordinator.py index 994949b5e03..614bf3f193a 100644 --- a/homeassistant/components/openweathermap/coordinator.py +++ b/homeassistant/components/openweathermap/coordinator.py @@ -1,12 +1,13 @@ -"""Weather data coordinator for the OpenWeatherMap (OWM) service.""" +"""Data coordinator for the OpenWeatherMap (OWM) service.""" from __future__ import annotations from datetime import timedelta import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pyopenweathermap import ( + CurrentAirPollution, CurrentWeather, DailyWeatherForecast, HourlyWeatherForecast, @@ -31,6 +32,15 @@ if TYPE_CHECKING: from . import OpenweathermapConfigEntry from .const import ( + ATTR_API_AIRPOLLUTION_AQI, + ATTR_API_AIRPOLLUTION_CO, + ATTR_API_AIRPOLLUTION_NH3, + ATTR_API_AIRPOLLUTION_NO, + ATTR_API_AIRPOLLUTION_NO2, + ATTR_API_AIRPOLLUTION_O3, + ATTR_API_AIRPOLLUTION_PM2_5, + ATTR_API_AIRPOLLUTION_PM10, + ATTR_API_AIRPOLLUTION_SO2, ATTR_API_CLOUDS, ATTR_API_CONDITION, ATTR_API_CURRENT, @@ -57,16 +67,20 @@ from .const import ( ATTR_API_WIND_SPEED, CONDITION_MAP, DOMAIN, + OWM_MODE_AIRPOLLUTION, + OWM_MODE_FREE_CURRENT, + OWM_MODE_FREE_FORECAST, + OWM_MODE_V30, WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT, ) _LOGGER = logging.getLogger(__name__) -WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) +OWM_UPDATE_INTERVAL = timedelta(minutes=10) -class WeatherUpdateCoordinator(DataUpdateCoordinator): - """Weather data update coordinator.""" +class OWMUpdateCoordinator(DataUpdateCoordinator): + """OWM data update coordinator.""" config_entry: OpenweathermapConfigEntry @@ -86,9 +100,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): _LOGGER, config_entry=config_entry, name=DOMAIN, - update_interval=WEATHER_UPDATE_INTERVAL, + update_interval=OWM_UPDATE_INTERVAL, ) + +class WeatherUpdateCoordinator(OWMUpdateCoordinator): + """Weather data update coordinator.""" + async def _async_update_data(self): """Update the data.""" try: @@ -248,3 +266,52 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return ATTR_CONDITION_CLEAR_NIGHT return CONDITION_MAP.get(weather_code) + + +class AirPollutionUpdateCoordinator(OWMUpdateCoordinator): + """Air Pollution data update coordinator.""" + + async def _async_update_data(self) -> dict[str, Any]: + """Update the data.""" + try: + air_pollution_report = await self._owm_client.get_air_pollution( + self._latitude, self._longitude + ) + except RequestError as error: + raise UpdateFailed(error) from error + current_air_pollution = ( + self._get_current_air_pollution_data(air_pollution_report.current) + if air_pollution_report.current is not None + else {} + ) + + return { + ATTR_API_CURRENT: current_air_pollution, + } + + def _get_current_air_pollution_data( + self, current_air_pollution: CurrentAirPollution + ) -> dict[str, Any]: + return { + ATTR_API_AIRPOLLUTION_AQI: current_air_pollution.aqi, + ATTR_API_AIRPOLLUTION_CO: current_air_pollution.co, + ATTR_API_AIRPOLLUTION_NO: current_air_pollution.no, + ATTR_API_AIRPOLLUTION_NO2: current_air_pollution.no2, + ATTR_API_AIRPOLLUTION_O3: current_air_pollution.o3, + ATTR_API_AIRPOLLUTION_SO2: current_air_pollution.so2, + ATTR_API_AIRPOLLUTION_PM2_5: current_air_pollution.pm2_5, + ATTR_API_AIRPOLLUTION_PM10: current_air_pollution.pm10, + ATTR_API_AIRPOLLUTION_NH3: current_air_pollution.nh3, + } + + +def get_owm_update_coordinator(mode: str) -> type[OWMUpdateCoordinator]: + """Create coordinator with a factory.""" + coordinators = { + OWM_MODE_V30: WeatherUpdateCoordinator, + OWM_MODE_FREE_CURRENT: WeatherUpdateCoordinator, + OWM_MODE_FREE_FORECAST: WeatherUpdateCoordinator, + OWM_MODE_AIRPOLLUTION: AirPollutionUpdateCoordinator, + } + + return coordinators[mode] diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index e37ff678708..789e9647f77 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -9,6 +9,8 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, DEGREE, PERCENTAGE, UV_INDEX, @@ -23,10 +25,17 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import OpenweathermapConfigEntry from .const import ( + ATTR_API_AIRPOLLUTION_AQI, + ATTR_API_AIRPOLLUTION_CO, + ATTR_API_AIRPOLLUTION_NO, + ATTR_API_AIRPOLLUTION_NO2, + ATTR_API_AIRPOLLUTION_O3, + ATTR_API_AIRPOLLUTION_PM2_5, + ATTR_API_AIRPOLLUTION_PM10, + ATTR_API_AIRPOLLUTION_SO2, ATTR_API_CLOUDS, ATTR_API_CONDITION, ATTR_API_CURRENT, @@ -47,8 +56,10 @@ from .const import ( ATTRIBUTION, DOMAIN, MANUFACTURER, + OWM_MODE_AIRPOLLUTION, OWM_MODE_FREE_FORECAST, ) +from .coordinator import OWMUpdateCoordinator WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -151,6 +162,56 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) +AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_AQI, + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_CO, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_NO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.NITROGEN_MONOXIDE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_NO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_O3, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.OZONE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_SO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_PM2_5, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_API_AIRPOLLUTION_PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -162,7 +223,7 @@ async def async_setup_entry( name = domain_data.name unique_id = config_entry.unique_id assert unique_id is not None - weather_coordinator = domain_data.coordinator + coordinator = domain_data.coordinator if domain_data.mode == OWM_MODE_FREE_FORECAST: entity_registry = er.async_get(hass) @@ -171,13 +232,23 @@ async def async_setup_entry( ) for entry in entries: entity_registry.async_remove(entry.entity_id) + elif domain_data.mode == OWM_MODE_AIRPOLLUTION: + async_add_entities( + OpenWeatherMapSensor( + name, + unique_id, + description, + coordinator, + ) + for description in AIRPOLLUTION_SENSOR_TYPES + ) else: async_add_entities( OpenWeatherMapSensor( name, unique_id, description, - weather_coordinator, + coordinator, ) for description in WEATHER_SENSOR_TYPES ) @@ -195,7 +266,7 @@ class AbstractOpenWeatherMapSensor(SensorEntity): name: str, unique_id: str, description: SensorEntityDescription, - coordinator: DataUpdateCoordinator, + coordinator: OWMUpdateCoordinator, ) -> None: """Initialize the sensor.""" self.entity_description = description diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index d6cdee77ce9..f182b083b90 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -41,10 +41,11 @@ from .const import ( DEFAULT_NAME, DOMAIN, MANUFACTURER, + OWM_MODE_AIRPOLLUTION, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, ) -from .coordinator import WeatherUpdateCoordinator +from .coordinator import OWMUpdateCoordinator SERVICE_GET_MINUTE_FORECAST = "get_minute_forecast" @@ -58,23 +59,25 @@ async def async_setup_entry( domain_data = config_entry.runtime_data name = domain_data.name mode = domain_data.mode - weather_coordinator = domain_data.coordinator - unique_id = f"{config_entry.unique_id}" - owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) + if mode != OWM_MODE_AIRPOLLUTION: + weather_coordinator = domain_data.coordinator - async_add_entities([owm_weather], False) + unique_id = f"{config_entry.unique_id}" + owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - name=SERVICE_GET_MINUTE_FORECAST, - schema=None, - func="async_get_minute_forecast", - supports_response=SupportsResponse.ONLY, - ) + async_add_entities([owm_weather], False) + + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + name=SERVICE_GET_MINUTE_FORECAST, + schema=None, + func="async_get_minute_forecast", + supports_response=SupportsResponse.ONLY, + ) -class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]): +class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]): """Implementation of an OpenWeatherMap sensor.""" _attr_attribution = ATTRIBUTION @@ -93,7 +96,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina name: str, unique_id: str, mode: str, - weather_coordinator: WeatherUpdateCoordinator, + weather_coordinator: OWMUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(weather_coordinator) diff --git a/tests/components/openweathermap/conftest.py b/tests/components/openweathermap/conftest.py index 44f4b971bd8..f7de53b8f97 100644 --- a/tests/components/openweathermap/conftest.py +++ b/tests/components/openweathermap/conftest.py @@ -5,6 +5,8 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock from pyopenweathermap import ( + AirPollutionReport, + CurrentAirPollution, CurrentWeather, DailyTemperature, DailyWeatherForecast, @@ -132,6 +134,21 @@ def owm_client_mock() -> Generator[AsyncMock]: client.get_weather.return_value = WeatherReport( current_weather, minutely_weather_forecast, [], [daily_weather_forecast] ) + current_air_pollution = CurrentAirPollution( + date_time=datetime.fromtimestamp(1714063537, tz=UTC), + aqi=3, + co=125.55, + no=0.11, + no2=0.78, + o3=101.98, + so2=0.59, + pm2_5=4.48, + pm10=4.77, + nh3=4.62, + ) + client.get_air_pollution.return_value = AirPollutionReport( + current_air_pollution, [] + ) client.validate_key.return_value = True with ( patch( diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index 58c17754962..cbd86f14676 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -1,4 +1,435 @@ # serializer version: 1 +# name: test_sensor_states[air_pollution][sensor.openweathermap_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_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': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-aqi', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'aqi', + 'friendly_name': 'openweathermap Air quality index', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.openweathermap_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_carbon_monoxide', + '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 monoxide', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-co', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'carbon_monoxide', + 'friendly_name': 'openweathermap Carbon monoxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_carbon_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '125.55', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_nitrogen_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': 'Nitrogen dioxide', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-no2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'nitrogen_dioxide', + 'friendly_name': 'openweathermap Nitrogen dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_nitrogen_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.78', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_nitrogen_monoxide', + '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 monoxide', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-no', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_nitrogen_monoxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'nitrogen_monoxide', + 'friendly_name': 'openweathermap Nitrogen monoxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_nitrogen_monoxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.11', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_ozone', + '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', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-o3', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_ozone-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'ozone', + 'friendly_name': 'openweathermap Ozone', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_ozone', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '101.98', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_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': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-pm10', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_pm10-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'pm10', + 'friendly_name': 'openweathermap PM10', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_pm10', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.77', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_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': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-pm2_5', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_pm2_5-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'pm25', + 'friendly_name': 'openweathermap PM2.5', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_pm2_5', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.48', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.openweathermap_sulphur_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': 'Sulphur dioxide', + 'platform': 'openweathermap', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '12.34-56.78-so2', + 'unit_of_measurement': 'µg/m³', + }) +# --- +# name: test_sensor_states[air_pollution][sensor.openweathermap_sulphur_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by OpenWeatherMap', + 'device_class': 'sulphur_dioxide', + 'friendly_name': 'openweathermap Sulphur dioxide', + 'state_class': , + 'unit_of_measurement': 'µg/m³', + }), + 'context': , + 'entity_id': 'sensor.openweathermap_sulphur_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.59', + }) +# --- # name: test_sensor_states[current][sensor.openweathermap_cloud_coverage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/openweathermap/test_sensor.py b/tests/components/openweathermap/test_sensor.py index fdf21ec71fe..78d45bbcc47 100644 --- a/tests/components/openweathermap/test_sensor.py +++ b/tests/components/openweathermap/test_sensor.py @@ -6,6 +6,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.openweathermap.const import ( + OWM_MODE_AIRPOLLUTION, OWM_MODE_FREE_CURRENT, OWM_MODE_FREE_FORECAST, OWM_MODE_V30, @@ -19,7 +20,9 @@ from . import setup_platform from tests.common import MockConfigEntry, snapshot_platform -@pytest.mark.parametrize("mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT], indirect=True) +@pytest.mark.parametrize( + "mode", [OWM_MODE_V30, OWM_MODE_FREE_CURRENT, OWM_MODE_AIRPOLLUTION], indirect=True +) async def test_sensor_states( hass: HomeAssistant, snapshot: SnapshotAssertion, From cdd3ce428fcd5ca5aaed7d5576ca34f115d3ce55 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Tue, 27 May 2025 04:37:05 +0900 Subject: [PATCH 0968/1175] Add select for ventilator's control (#140849) * Add select for ventilator's control * Removed wind_strength and it will be provided by fan --------- Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/icons.json | 3 +++ homeassistant/components/lg_thinq/select.py | 6 ++++++ homeassistant/components/lg_thinq/strings.json | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/homeassistant/components/lg_thinq/icons.json b/homeassistant/components/lg_thinq/icons.json index 3b0baaaaf75..02af1dec155 100644 --- a/homeassistant/components/lg_thinq/icons.json +++ b/homeassistant/components/lg_thinq/icons.json @@ -166,6 +166,9 @@ "monitoring_enabled": { "default": "mdi:monitor-eye" }, + "current_job_mode_ventilator": { + "default": "mdi:format-list-bulleted" + }, "current_job_mode": { "default": "mdi:format-list-bulleted" }, diff --git a/homeassistant/components/lg_thinq/select.py b/homeassistant/components/lg_thinq/select.py index 3f29ee9e5c8..80dcc4a40da 100644 --- a/homeassistant/components/lg_thinq/select.py +++ b/homeassistant/components/lg_thinq/select.py @@ -121,6 +121,12 @@ DEVICE_TYPE_SELECT_MAP: dict[DeviceType, tuple[SelectEntityDescription, ...]] = ), DeviceType.REFRIGERATOR: (SELECT_DESC[ThinQProperty.FRESH_AIR_FILTER],), DeviceType.STYLER: (OPERATION_SELECT_DESC[ThinQProperty.STYLER_OPERATION_MODE],), + DeviceType.VENTILATOR: ( + SelectEntityDescription( + key=ThinQProperty.CURRENT_JOB_MODE, + translation_key="current_job_mode_ventilator", + ), + ), DeviceType.WASHCOMBO_MAIN: ( OPERATION_SELECT_DESC[ThinQProperty.WASHER_OPERATION_MODE], ), diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index a5fb81e3818..0ef3116f063 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -901,6 +901,14 @@ "always": "[%key:component::lg_thinq::entity::sensor::monitoring_enabled::state::always%]" } }, + "current_job_mode_ventilator": { + "name": "Operating mode", + "state": { + "vent_auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "vent_nature": "Bypass", + "vent_heat_exchange": "Heat exchange" + } + }, "current_job_mode": { "name": "Operating mode", "state": { From 393ea0251b4f27152af4c3a67c14a14f6db7ff19 Mon Sep 17 00:00:00 2001 From: Cerallin <66366855+Cerallin@users.noreply.github.com> Date: Tue, 27 May 2025 03:40:12 +0800 Subject: [PATCH 0969/1175] Add add_package action to seventeentrack (#144488) * Fix schema name, add_packages -> get_packages * Add "add_package" service * Update description * Update descriptions --- .../components/seventeentrack/const.py | 2 + .../components/seventeentrack/icons.json | 3 ++ .../components/seventeentrack/services.py | 37 ++++++++++++++++++- .../components/seventeentrack/services.yaml | 16 ++++++++ .../components/seventeentrack/strings.json | 18 +++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/seventeentrack/const.py b/homeassistant/components/seventeentrack/const.py index 19e2d3083c9..988a01f0022 100644 --- a/homeassistant/components/seventeentrack/const.py +++ b/homeassistant/components/seventeentrack/const.py @@ -42,8 +42,10 @@ NOTIFICATION_DELIVERED_MESSAGE = ( VALUE_DELIVERED = "Delivered" SERVICE_GET_PACKAGES = "get_packages" +SERVICE_ADD_PACKAGE = "add_package" SERVICE_ARCHIVE_PACKAGE = "archive_package" ATTR_PACKAGE_STATE = "package_state" ATTR_PACKAGE_TRACKING_NUMBER = "package_tracking_number" +ATTR_PACKAGE_FRIENDLY_NAME = "package_friendly_name" ATTR_CONFIG_ENTRY_ID = "config_entry_id" diff --git a/homeassistant/components/seventeentrack/icons.json b/homeassistant/components/seventeentrack/icons.json index c48e147e973..5ddfaacc8ac 100644 --- a/homeassistant/components/seventeentrack/icons.json +++ b/homeassistant/components/seventeentrack/icons.json @@ -31,6 +31,9 @@ "get_packages": { "service": "mdi:package" }, + "add_package": { + "service": "mdi:package" + }, "archive_package": { "service": "mdi:archive" } diff --git a/homeassistant/components/seventeentrack/services.py b/homeassistant/components/seventeentrack/services.py index 54c23e6d619..5ba0b569b19 100644 --- a/homeassistant/components/seventeentrack/services.py +++ b/homeassistant/components/seventeentrack/services.py @@ -23,6 +23,7 @@ from .const import ( ATTR_DESTINATION_COUNTRY, ATTR_INFO_TEXT, ATTR_ORIGIN_COUNTRY, + ATTR_PACKAGE_FRIENDLY_NAME, ATTR_PACKAGE_STATE, ATTR_PACKAGE_TRACKING_NUMBER, ATTR_PACKAGE_TYPE, @@ -31,11 +32,12 @@ from .const import ( ATTR_TRACKING_INFO_LANGUAGE, ATTR_TRACKING_NUMBER, DOMAIN, + SERVICE_ADD_PACKAGE, SERVICE_ARCHIVE_PACKAGE, SERVICE_GET_PACKAGES, ) -SERVICE_ADD_PACKAGES_SCHEMA: Final = vol.Schema( +SERVICE_GET_PACKAGES_SCHEMA: Final = vol.Schema( { vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, vol.Optional(ATTR_PACKAGE_STATE): selector.SelectSelector( @@ -52,6 +54,14 @@ SERVICE_ADD_PACKAGES_SCHEMA: Final = vol.Schema( } ) +SERVICE_ADD_PACKAGE_SCHEMA: Final = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, + vol.Required(ATTR_PACKAGE_TRACKING_NUMBER): cv.string, + vol.Required(ATTR_PACKAGE_FRIENDLY_NAME): cv.string, + } +) + SERVICE_ARCHIVE_PACKAGE_SCHEMA: Final = vol.Schema( { vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, @@ -87,6 +97,22 @@ def setup_services(hass: HomeAssistant) -> None: ] } + async def add_package(call: ServiceCall) -> None: + """Add a new package to 17Track.""" + config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + tracking_number = call.data[ATTR_PACKAGE_TRACKING_NUMBER] + friendly_name = call.data[ATTR_PACKAGE_FRIENDLY_NAME] + + await _validate_service(config_entry_id) + + seventeen_coordinator: SeventeenTrackCoordinator = hass.data[DOMAIN][ + config_entry_id + ] + + await seventeen_coordinator.client.profile.add_package( + tracking_number, friendly_name + ) + async def archive_package(call: ServiceCall) -> None: config_entry_id = call.data[ATTR_CONFIG_ENTRY_ID] tracking_number = call.data[ATTR_PACKAGE_TRACKING_NUMBER] @@ -138,10 +164,17 @@ def setup_services(hass: HomeAssistant) -> None: DOMAIN, SERVICE_GET_PACKAGES, get_packages, - schema=SERVICE_ADD_PACKAGES_SCHEMA, + schema=SERVICE_GET_PACKAGES_SCHEMA, supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + SERVICE_ADD_PACKAGE, + add_package, + schema=SERVICE_ADD_PACKAGE_SCHEMA, + ) + hass.services.async_register( DOMAIN, SERVICE_ARCHIVE_PACKAGE, diff --git a/homeassistant/components/seventeentrack/services.yaml b/homeassistant/components/seventeentrack/services.yaml index 45d7c0a530a..2ea5658b149 100644 --- a/homeassistant/components/seventeentrack/services.yaml +++ b/homeassistant/components/seventeentrack/services.yaml @@ -18,6 +18,22 @@ get_packages: selector: config_entry: integration: seventeentrack +add_package: + fields: + package_tracking_number: + required: true + selector: + text: + package_friendly_name: + required: true + selector: + text: + config_entry_id: + required: true + selector: + config_entry: + integration: seventeentrack + archive_package: fields: package_tracking_number: diff --git a/homeassistant/components/seventeentrack/strings.json b/homeassistant/components/seventeentrack/strings.json index c95a553ae7b..bffb21cbfbd 100644 --- a/homeassistant/components/seventeentrack/strings.json +++ b/homeassistant/components/seventeentrack/strings.json @@ -80,6 +80,24 @@ } } }, + "add_package": { + "name": "Add a package", + "description": "Adds a package using the 17track API.", + "fields": { + "package_tracking_number": { + "name": "Package tracking number to add", + "description": "The package with the tracking number will be added." + }, + "package_friendly_name": { + "name": "Package friendly name", + "description": "The friendly name of the package to be added." + }, + "config_entry_id": { + "name": "17Track service", + "description": "The selected service to add the package to." + } + } + }, "archive_package": { "name": "Archive package", "description": "Archives a package using the 17track API.", From 405725f8eec39588ada5eac0d7071577b5bb080f Mon Sep 17 00:00:00 2001 From: ngolf <74095787+ngolf@users.noreply.github.com> Date: Mon, 26 May 2025 20:43:55 +0100 Subject: [PATCH 0970/1175] Add last update to aquacell (#143661) --- homeassistant/components/aquacell/icons.json | 3 ++ homeassistant/components/aquacell/sensor.py | 11 ++++- .../components/aquacell/strings.json | 3 ++ .../aquacell/snapshots/test_sensor.ambr | 48 +++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aquacell/icons.json b/homeassistant/components/aquacell/icons.json index d7383f54d72..255a964d218 100644 --- a/homeassistant/components/aquacell/icons.json +++ b/homeassistant/components/aquacell/icons.json @@ -1,6 +1,9 @@ { "entity": { "sensor": { + "last_update": { + "default": "mdi:update" + }, "salt_left_side_percentage": { "default": "mdi:basket-fill" }, diff --git a/homeassistant/components/aquacell/sensor.py b/homeassistant/components/aquacell/sensor.py index 77cd3cdd60a..58d3548284e 100644 --- a/homeassistant/components/aquacell/sensor.py +++ b/homeassistant/components/aquacell/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime from aioaquacell import Softener @@ -28,7 +29,7 @@ PARALLEL_UPDATES = 1 class SoftenerSensorEntityDescription(SensorEntityDescription): """Describes Softener sensor entity.""" - value_fn: Callable[[Softener], StateType] + value_fn: Callable[[Softener], StateType | datetime] SENSORS: tuple[SoftenerSensorEntityDescription, ...] = ( @@ -77,6 +78,12 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = ( "low", ], ), + SoftenerSensorEntityDescription( + key="last_update", + translation_key="last_update", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda softener: softener.lastUpdate, + ), ) @@ -111,6 +118,6 @@ class SoftenerSensor(AquacellEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" return self.entity_description.value_fn(self.softener) diff --git a/homeassistant/components/aquacell/strings.json b/homeassistant/components/aquacell/strings.json index e07adf3c199..d2052fbd08e 100644 --- a/homeassistant/components/aquacell/strings.json +++ b/homeassistant/components/aquacell/strings.json @@ -21,6 +21,9 @@ }, "entity": { "sensor": { + "last_update": { + "name": "Last update" + }, "salt_left_side_percentage": { "name": "Salt left side percentage" }, diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr index ec89cb34bca..f512b2a824d 100644 --- a/tests/components/aquacell/snapshots/test_sensor.ambr +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -49,6 +49,54 @@ 'state': '40', }) # --- +# name: test_sensors[sensor.aquacell_name_last_update-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.aquacell_name_last_update', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last update', + 'platform': 'aquacell', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'last_update', + 'unique_id': 'DSN-last_update', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.aquacell_name_last_update-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'AquaCell name Last update', + }), + 'context': , + 'entity_id': 'sensor.aquacell_name_last_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2024-05-10T07:44:30+00:00', + }) +# --- # name: test_sensors[sensor.aquacell_name_salt_left_side_percentage-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From a7919c5ce7c6ec22396ad296c42c4c03dc18ad9b Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 26 May 2025 21:44:45 +0200 Subject: [PATCH 0971/1175] Move coordinator and getting data closer together in devolo Home Network (#144814) --- .../devolo_home_network/__init__.py | 210 ++----------- .../devolo_home_network/binary_sensor.py | 3 +- .../components/devolo_home_network/button.py | 2 +- .../devolo_home_network/config_flow.py | 2 +- .../devolo_home_network/coordinator.py | 289 +++++++++++++++++- .../devolo_home_network/device_tracker.py | 3 +- .../devolo_home_network/diagnostics.py | 2 +- .../components/devolo_home_network/entity.py | 3 +- .../components/devolo_home_network/image.py | 3 +- .../components/devolo_home_network/sensor.py | 3 +- .../components/devolo_home_network/switch.py | 3 +- .../components/devolo_home_network/update.py | 3 +- 12 files changed, 312 insertions(+), 214 deletions(-) diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 7f6784f2404..79d00ee50be 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -2,27 +2,13 @@ from __future__ import annotations -from asyncio import Semaphore -from dataclasses import dataclass import logging from typing import Any from devolo_plc_api import Device -from devolo_plc_api.device_api import ( - ConnectedStationInfo, - NeighborAPInfo, - UpdateFirmwareCheck, - WifiGuestAccessGet, -) -from devolo_plc_api.exceptions.device import ( - DeviceNotFound, - DevicePasswordProtected, - DeviceUnavailable, -) -from devolo_plc_api.plcnet_api import LogicalNetwork +from devolo_plc_api.exceptions.device import DeviceNotFound from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, @@ -30,38 +16,34 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.update_coordinator import UpdateFailed from .const import ( CONNECTED_PLC_DEVICES, CONNECTED_WIFI_CLIENTS, DOMAIN, - FIRMWARE_UPDATE_INTERVAL, LAST_RESTART, - LONG_UPDATE_INTERVAL, NEIGHBORING_WIFI_NETWORKS, REGULAR_FIRMWARE, - SHORT_UPDATE_INTERVAL, SWITCH_GUEST_WIFI, SWITCH_LEDS, ) -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import ( + DevoloDataUpdateCoordinator, + DevoloFirmwareUpdateCoordinator, + DevoloHomeNetworkConfigEntry, + DevoloHomeNetworkData, + DevoloLedSettingsGetCoordinator, + DevoloLogicalNetworkCoordinator, + DevoloUptimeGetCoordinator, + DevoloWifiConnectedStationsGetCoordinator, + DevoloWifiGuestAccessGetCoordinator, + DevoloWifiNeighborAPsGetCoordinator, +) _LOGGER = logging.getLogger(__name__) -type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData] - - -@dataclass -class DevoloHomeNetworkData: - """The devolo Home Network data.""" - - device: Device - coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] - async def async_setup_entry( hass: HomeAssistant, entry: DevoloHomeNetworkConfigEntry @@ -69,8 +51,6 @@ async def async_setup_entry( """Set up devolo Home Network from a config entry.""" zeroconf_instance = await zeroconf.async_get_async_instance(hass) async_client = get_async_client(hass) - device_registry = dr.async_get(hass) - semaphore = Semaphore(1) try: device = Device( @@ -90,177 +70,52 @@ async def async_setup_entry( entry.runtime_data = DevoloHomeNetworkData(device=device, coordinators={}) - async def async_update_firmware_available() -> UpdateFirmwareCheck: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_check_firmware_available() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - - async def async_update_connected_plc_devices() -> LogicalNetwork: - """Fetch data from API endpoint.""" - assert device.plcnet - update_sw_version(device_registry, device) - try: - return await device.plcnet.async_get_network_overview() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - - async def async_update_guest_wifi_status() -> WifiGuestAccessGet: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_get_wifi_guest_access() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - except DevicePasswordProtected as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, translation_key="password_wrong" - ) from err - - async def async_update_led_status() -> bool: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_get_led_setting() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - - async def async_update_last_restart() -> int: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_uptime() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - except DevicePasswordProtected as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, translation_key="password_wrong" - ) from err - - async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_get_wifi_connected_station() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - - async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]: - """Fetch data from API endpoint.""" - assert device.device - update_sw_version(device_registry, device) - try: - return await device.device.async_get_wifi_neighbor_access_points() - except DeviceUnavailable as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": str(err)}, - ) from err - async def disconnect(event: Event) -> None: """Disconnect from device.""" await device.async_disconnect() coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {} if device.plcnet: - coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator( + coordinators[CONNECTED_PLC_DEVICES] = DevoloLogicalNetworkCoordinator( hass, _LOGGER, config_entry=entry, - name=CONNECTED_PLC_DEVICES, - semaphore=semaphore, - update_method=async_update_connected_plc_devices, - update_interval=LONG_UPDATE_INTERVAL, ) if device.device and "led" in device.device.features: - coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator( + coordinators[SWITCH_LEDS] = DevoloLedSettingsGetCoordinator( hass, _LOGGER, config_entry=entry, - name=SWITCH_LEDS, - semaphore=semaphore, - update_method=async_update_led_status, - update_interval=SHORT_UPDATE_INTERVAL, ) if device.device and "restart" in device.device.features: - coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator( + coordinators[LAST_RESTART] = DevoloUptimeGetCoordinator( hass, _LOGGER, config_entry=entry, - name=LAST_RESTART, - semaphore=semaphore, - update_method=async_update_last_restart, - update_interval=SHORT_UPDATE_INTERVAL, ) if device.device and "update" in device.device.features: - coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator( + coordinators[REGULAR_FIRMWARE] = DevoloFirmwareUpdateCoordinator( hass, _LOGGER, config_entry=entry, - name=REGULAR_FIRMWARE, - semaphore=semaphore, - update_method=async_update_firmware_available, - update_interval=FIRMWARE_UPDATE_INTERVAL, ) if device.device and "wifi1" in device.device.features: - coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - name=CONNECTED_WIFI_CLIENTS, - semaphore=semaphore, - update_method=async_update_wifi_connected_station, - update_interval=SHORT_UPDATE_INTERVAL, + coordinators[CONNECTED_WIFI_CLIENTS] = ( + DevoloWifiConnectedStationsGetCoordinator( + hass, + _LOGGER, + config_entry=entry, + ) ) - coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator( + coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloWifiNeighborAPsGetCoordinator( hass, _LOGGER, config_entry=entry, - name=NEIGHBORING_WIFI_NETWORKS, - semaphore=semaphore, - update_method=async_update_wifi_neighbor_access_points, - update_interval=LONG_UPDATE_INTERVAL, ) - coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator( + coordinators[SWITCH_GUEST_WIFI] = DevoloWifiGuestAccessGetCoordinator( hass, _LOGGER, config_entry=entry, - name=SWITCH_GUEST_WIFI, - semaphore=semaphore, - update_method=async_update_guest_wifi_status, - update_interval=SHORT_UPDATE_INTERVAL, ) for coordinator in coordinators.values(): @@ -303,16 +158,3 @@ def platforms(device: Device) -> set[Platform]: if device.device and "update" in device.device.features: supported_platforms.add(Platform.UPDATE) return supported_platforms - - -@callback -def update_sw_version(device_registry: dr.DeviceRegistry, device: Device) -> None: - """Update device registry with new firmware version.""" - if ( - device_entry := device_registry.async_get_device( - identifiers={(DOMAIN, str(device.serial_number))} - ) - ) and device_entry.sw_version != device.firmware_version: - device_registry.async_update_device( - device_id=device_entry.id, sw_version=device.firmware_version - ) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 2c258d758da..3b1debe42c5 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -16,9 +16,8 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DevoloHomeNetworkConfigEntry from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry from .entity import DevoloCoordinatorEntity PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/button.py b/homeassistant/components/devolo_home_network/button.py index fe6b1786363..53de2945d00 100644 --- a/homeassistant/components/devolo_home_network/button.py +++ b/homeassistant/components/devolo_home_network/button.py @@ -18,8 +18,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS +from .coordinator import DevoloHomeNetworkConfigEntry from .entity import DevoloEntity PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index ad21289ff28..125559eefe4 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo -from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE +from .coordinator import DevoloHomeNetworkConfigEntry _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/devolo_home_network/coordinator.py b/homeassistant/components/devolo_home_network/coordinator.py index c0af9668279..d23aa0e935e 100644 --- a/homeassistant/components/devolo_home_network/coordinator.py +++ b/homeassistant/components/devolo_home_network/coordinator.py @@ -1,13 +1,44 @@ """Base coordinator.""" from asyncio import Semaphore -from collections.abc import Awaitable, Callable +from dataclasses import dataclass from datetime import timedelta from logging import Logger +from typing import Any + +from devolo_plc_api import Device +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + UpdateFirmwareCheck, + WifiGuestAccessGet, +) +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONNECTED_PLC_DEVICES, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + FIRMWARE_UPDATE_INTERVAL, + LAST_RESTART, + LONG_UPDATE_INTERVAL, + NEIGHBORING_WIFI_NETWORKS, + REGULAR_FIRMWARE, + SHORT_UPDATE_INTERVAL, + SWITCH_GUEST_WIFI, + SWITCH_LEDS, +) + +SEMAPHORE = Semaphore(1) + +type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData] class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): @@ -18,11 +49,62 @@ class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): hass: HomeAssistant, logger: Logger, *, - config_entry: ConfigEntry, + config_entry: DevoloHomeNetworkConfigEntry, name: str, - semaphore: Semaphore, - update_interval: timedelta, - update_method: Callable[[], Awaitable[_DataT]], + update_interval: timedelta | None = None, + ) -> None: + """Initialize global data updater.""" + self.device = config_entry.runtime_data.device + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + + async def _async_update_data(self) -> _DataT: + """Fetch the latest data from the source.""" + self.update_sw_version() + async with SEMAPHORE: + try: + return await super()._async_update_data() + except DeviceUnavailable as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_failed", + translation_placeholders={"error": str(err)}, + ) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, translation_key="password_wrong" + ) from err + + @callback + def update_sw_version(self) -> None: + """Update device registry with new firmware version, if it changed at runtime.""" + device_registry = dr.async_get(self.hass) + if ( + device_entry := device_registry.async_get_device( + identifiers={(DOMAIN, self.device.serial_number)} + ) + ) and device_entry.sw_version != self.device.firmware_version: + device_registry.async_update_device( + device_id=device_entry.id, sw_version=self.device.firmware_version + ) + + +class DevoloFirmwareUpdateCoordinator(DevoloDataUpdateCoordinator[UpdateFirmwareCheck]): + """Class to manage fetching data from the UpdateFirmwareCheck endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = REGULAR_FIRMWARE, + update_interval: timedelta | None = FIRMWARE_UPDATE_INTERVAL, ) -> None: """Initialize global data updater.""" super().__init__( @@ -31,11 +113,192 @@ class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): config_entry=config_entry, name=name, update_interval=update_interval, - update_method=update_method, ) - self._semaphore = semaphore + self.update_method = self.async_update_firmware_available - async def _async_update_data(self) -> _DataT: - """Fetch the latest data from the source.""" - async with self._semaphore: - return await super()._async_update_data() + async def async_update_firmware_available(self) -> UpdateFirmwareCheck: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_check_firmware_available() + + +class DevoloLedSettingsGetCoordinator(DevoloDataUpdateCoordinator[bool]): + """Class to manage fetching data from the LedSettingsGet endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = SWITCH_LEDS, + update_interval: timedelta | None = SHORT_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_update_led_status + + async def async_update_led_status(self) -> bool: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_get_led_setting() + + +class DevoloLogicalNetworkCoordinator(DevoloDataUpdateCoordinator[LogicalNetwork]): + """Class to manage fetching data from the GetNetworkOverview endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = CONNECTED_PLC_DEVICES, + update_interval: timedelta | None = LONG_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_update_connected_plc_devices + + async def async_update_connected_plc_devices(self) -> LogicalNetwork: + """Fetch data from API endpoint.""" + assert self.device.plcnet + return await self.device.plcnet.async_get_network_overview() + + +class DevoloUptimeGetCoordinator(DevoloDataUpdateCoordinator[int]): + """Class to manage fetching data from the UptimeGet endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = LAST_RESTART, + update_interval: timedelta | None = SHORT_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_update_last_restart + + async def async_update_last_restart(self) -> int: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_uptime() + + +class DevoloWifiConnectedStationsGetCoordinator( + DevoloDataUpdateCoordinator[list[ConnectedStationInfo]] +): + """Class to manage fetching data from the WifiGuestAccessGet endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = CONNECTED_WIFI_CLIENTS, + update_interval: timedelta | None = SHORT_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_get_wifi_connected_station + + async def async_get_wifi_connected_station(self) -> list[ConnectedStationInfo]: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_get_wifi_connected_station() + + +class DevoloWifiGuestAccessGetCoordinator( + DevoloDataUpdateCoordinator[WifiGuestAccessGet] +): + """Class to manage fetching data from the WifiGuestAccessGet endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = SWITCH_GUEST_WIFI, + update_interval: timedelta | None = SHORT_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_update_guest_wifi_status + + async def async_update_guest_wifi_status(self) -> WifiGuestAccessGet: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_get_wifi_guest_access() + + +class DevoloWifiNeighborAPsGetCoordinator( + DevoloDataUpdateCoordinator[list[NeighborAPInfo]] +): + """Class to manage fetching data from the WifiNeighborAPsGet endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + *, + config_entry: ConfigEntry, + name: str = NEIGHBORING_WIFI_NETWORKS, + update_interval: timedelta | None = LONG_UPDATE_INTERVAL, + ) -> None: + """Initialize global data updater.""" + super().__init__( + hass, + logger, + config_entry=config_entry, + name=name, + update_interval=update_interval, + ) + self.update_method = self.async_update_wifi_neighbor_access_points + + async def async_update_wifi_neighbor_access_points(self) -> list[NeighborAPInfo]: + """Fetch data from API endpoint.""" + assert self.device.device + return await self.device.device.async_get_wifi_neighbor_access_points() + + +@dataclass +class DevoloHomeNetworkData: + """The devolo Home Network data.""" + + device: Device + coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index cb726e5954c..15ff0e5ac2a 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -15,9 +15,8 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DevoloHomeNetworkConfigEntry from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/diagnostics.py b/homeassistant/components/devolo_home_network/diagnostics.py index 9cfc8a2c260..1683edb4074 100644 --- a/homeassistant/components/devolo_home_network/diagnostics.py +++ b/homeassistant/components/devolo_home_network/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from . import DevoloHomeNetworkConfigEntry +from .coordinator import DevoloHomeNetworkConfigEntry TO_REDACT = {CONF_PASSWORD} diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index 64d8ff131e8..be437314ae4 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -15,9 +15,8 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry type _DataType = ( LogicalNetwork diff --git a/homeassistant/components/devolo_home_network/image.py b/homeassistant/components/devolo_home_network/image.py index 46a3eb3426a..8dc701a30c9 100644 --- a/homeassistant/components/devolo_home_network/image.py +++ b/homeassistant/components/devolo_home_network/image.py @@ -15,9 +15,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util import dt as dt_util -from . import DevoloHomeNetworkConfigEntry from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry from .entity import DevoloCoordinatorEntity PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index cec1ecc8a81..f4c911bf787 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -22,7 +22,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import utcnow -from . import DevoloHomeNetworkConfigEntry from .const import ( CONNECTED_PLC_DEVICES, CONNECTED_WIFI_CLIENTS, @@ -31,7 +30,7 @@ from .const import ( PLC_RX_RATE, PLC_TX_RATE, ) -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry from .entity import DevoloCoordinatorEntity PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py index b57305a7a77..e709d0f54b4 100644 --- a/homeassistant/components/devolo_home_network/switch.py +++ b/homeassistant/components/devolo_home_network/switch.py @@ -16,9 +16,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry from .entity import DevoloCoordinatorEntity PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/devolo_home_network/update.py b/homeassistant/components/devolo_home_network/update.py index aaaf72af359..ace12f24358 100644 --- a/homeassistant/components/devolo_home_network/update.py +++ b/homeassistant/components/devolo_home_network/update.py @@ -21,9 +21,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from . import DevoloHomeNetworkConfigEntry from .const import DOMAIN, REGULAR_FIRMWARE -from .coordinator import DevoloDataUpdateCoordinator +from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry from .entity import DevoloCoordinatorEntity PARALLEL_UPDATES = 0 From 34f92d584bc50b308a70ac7b32d82502fe072e48 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 26 May 2025 12:48:13 -0700 Subject: [PATCH 0972/1175] Bump gcal_sync to 7.1.0 (#145642) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 398ff8768a9..c5a9d4784bc 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.5"] + "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca82c4b74a9..414153e193e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -989,7 +989,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.1 +gcal-sync==7.1.0 # homeassistant.components.geniushub geniushub-client==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bfd354f921..f858d8e4315 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ gardena-bluetooth==1.6.0 gassist-text==0.0.12 # homeassistant.components.google -gcal-sync==7.0.1 +gcal-sync==7.1.0 # homeassistant.components.geniushub geniushub-client==0.7.1 From 8abbd35c54944f94b39220f15be7527bc51707b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 26 May 2025 21:50:28 +0200 Subject: [PATCH 0973/1175] Add ability to load test fixtures on the executor (#144534) --- tests/common.py | 21 +++++++++++++++++++++ tests/components/easyenergy/conftest.py | 11 +++++------ tests/components/energyzero/conftest.py | 11 +++++------ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/common.py b/tests/common.py index 869291c9463..66129ecc9c3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -575,6 +575,13 @@ def load_fixture(filename: str, integration: str | None = None) -> str: return get_fixture_path(filename, integration).read_text(encoding="utf8") +async def async_load_fixture( + hass: HomeAssistant, filename: str, integration: str | None = None +) -> str: + """Load a fixture.""" + return await hass.async_add_executor_job(load_fixture, filename, integration) + + def load_json_value_fixture( filename: str, integration: str | None = None ) -> JsonValueType: @@ -589,6 +596,13 @@ def load_json_array_fixture( return json_loads_array(load_fixture(filename, integration)) +async def async_load_json_array_fixture( + hass: HomeAssistant, filename: str, integration: str | None = None +) -> JsonArrayType: + """Load a JSON object from a fixture.""" + return json_loads_array(await async_load_fixture(hass, filename, integration)) + + def load_json_object_fixture( filename: str, integration: str | None = None ) -> JsonObjectType: @@ -596,6 +610,13 @@ def load_json_object_fixture( return json_loads_object(load_fixture(filename, integration)) +async def async_load_json_object_fixture( + hass: HomeAssistant, filename: str, integration: str | None = None +) -> JsonObjectType: + """Load a JSON object from a fixture.""" + return json_loads_object(await async_load_fixture(hass, filename, integration)) + + def json_round_trip(obj: Any) -> Any: """Round trip an object to JSON.""" return json_loads(json_dumps(obj)) diff --git a/tests/components/easyenergy/conftest.py b/tests/components/easyenergy/conftest.py index ffe0e36f3d2..f2ed2cf4dbc 100644 --- a/tests/components/easyenergy/conftest.py +++ b/tests/components/easyenergy/conftest.py @@ -1,7 +1,6 @@ """Fixtures for easyEnergy integration tests.""" -from collections.abc import Generator -import json +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from easyenergy import Electricity, Gas @@ -10,7 +9,7 @@ import pytest from homeassistant.components.easyenergy.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, async_load_json_array_fixture @pytest.fixture @@ -34,17 +33,17 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_easyenergy() -> Generator[MagicMock]: +async def mock_easyenergy(hass: HomeAssistant) -> AsyncGenerator[MagicMock]: """Return a mocked easyEnergy client.""" with patch( "homeassistant.components.easyenergy.coordinator.EasyEnergy", autospec=True ) as easyenergy_mock: client = easyenergy_mock.return_value client.energy_prices.return_value = Electricity.from_dict( - json.loads(load_fixture("today_energy.json", DOMAIN)) + await async_load_json_array_fixture(hass, "today_energy.json", DOMAIN) ) client.gas_prices.return_value = Gas.from_dict( - json.loads(load_fixture("today_gas.json", DOMAIN)) + await async_load_json_array_fixture(hass, "today_gas.json", DOMAIN) ) yield client diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py index 3fd93ee31f8..d861e1365f7 100644 --- a/tests/components/energyzero/conftest.py +++ b/tests/components/energyzero/conftest.py @@ -1,7 +1,6 @@ """Fixtures for EnergyZero integration tests.""" -from collections.abc import Generator -import json +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock, patch from energyzero import Electricity, Gas @@ -10,7 +9,7 @@ import pytest from homeassistant.components.energyzero.const import DOMAIN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry, async_load_json_object_fixture @pytest.fixture @@ -35,17 +34,17 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_energyzero() -> Generator[MagicMock]: +async def mock_energyzero(hass: HomeAssistant) -> AsyncGenerator[MagicMock]: """Return a mocked EnergyZero client.""" with patch( "homeassistant.components.energyzero.coordinator.EnergyZero", autospec=True ) as energyzero_mock: client = energyzero_mock.return_value client.energy_prices.return_value = Electricity.from_dict( - json.loads(load_fixture("today_energy.json", DOMAIN)) + await async_load_json_object_fixture(hass, "today_energy.json", DOMAIN) ) client.gas_prices.return_value = Gas.from_dict( - json.loads(load_fixture("today_gas.json", DOMAIN)) + await async_load_json_object_fixture(hass, "today_gas.json", DOMAIN) ) yield client From 4aade14c9ef30ae90afb0af0c1d0ec03f77db965 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 21:55:33 +0200 Subject: [PATCH 0974/1175] Fix CI (#145644) * Fix CI * Fix CI --- homeassistant/components/lg_thinq/strings.json | 2 +- tests/components/teslemetry/snapshots/test_device_tracker.ambr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 0ef3116f063..38ea7b454ae 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -904,7 +904,7 @@ "current_job_mode_ventilator": { "name": "Operating mode", "state": { - "vent_auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", + "vent_auto": "[%key:common::state::auto%]", "vent_nature": "Bypass", "vent_heat_exchange": "Heat exchange" } diff --git a/tests/components/teslemetry/snapshots/test_device_tracker.ambr b/tests/components/teslemetry/snapshots/test_device_tracker.ambr index 9da463501b7..c71f818479a 100644 --- a/tests/components/teslemetry/snapshots/test_device_tracker.ambr +++ b/tests/components/teslemetry/snapshots/test_device_tracker.ambr @@ -147,7 +147,7 @@ 'unknown' # --- # name: test_device_tracker_streaming[device_tracker.test_origin-state] - 'unavailable' + 'unknown' # --- # name: test_device_tracker_streaming[device_tracker.test_route-restore] 'not_home' From 9a7300668161848b720b8b80b228c85494fc92c9 Mon Sep 17 00:00:00 2001 From: Markus Jacobsen Date: Mon, 26 May 2025 22:14:27 +0200 Subject: [PATCH 0975/1175] Simplify Bang & Olufsen testing setup (#139830) * Add and use integration fixture * Simplify WebSocket testing * Remove integration fixture return value --------- Co-authored-by: Joostlek --- tests/components/bang_olufsen/conftest.py | 9 +- .../bang_olufsen/test_diagnostics.py | 6 +- tests/components/bang_olufsen/test_event.py | 11 +- .../bang_olufsen/test_media_player.py | 249 +++--------------- .../components/bang_olufsen/test_websocket.py | 28 +- 5 files changed, 59 insertions(+), 244 deletions(-) diff --git a/tests/components/bang_olufsen/conftest.py b/tests/components/bang_olufsen/conftest.py index 700d085dd11..c7915968cbf 100644 --- a/tests/components/bang_olufsen/conftest.py +++ b/tests/components/bang_olufsen/conftest.py @@ -76,16 +76,17 @@ def mock_config_entry_core() -> MockConfigEntry: ) -@pytest.fixture -async def mock_media_player( +@pytest.fixture(name="integration") +async def integration_fixture( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, mock_mozart_client: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: - """Mock media_player entity.""" + """Set up the Bang & Olufsen integration.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() @pytest.fixture diff --git a/tests/components/bang_olufsen/test_diagnostics.py b/tests/components/bang_olufsen/test_diagnostics.py index fdc22390e64..efa5a0a8680 100644 --- a/tests/components/bang_olufsen/test_diagnostics.py +++ b/tests/components/bang_olufsen/test_diagnostics.py @@ -1,7 +1,5 @@ """Test bang_olufsen config entry diagnostics.""" -from unittest.mock import AsyncMock - from syrupy.assertion import SnapshotAssertion from syrupy.filters import props @@ -19,13 +17,11 @@ async def test_async_get_config_entry_diagnostics( hass: HomeAssistant, entity_registry: EntityRegistry, hass_client: ClientSessionGenerator, + integration: None, mock_config_entry: MockConfigEntry, - mock_mozart_client: AsyncMock, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) # Enable an Event entity entity_registry.async_update_entity(TEST_BUTTON_EVENT_ENTITY_ID, disabled_by=None) diff --git a/tests/components/bang_olufsen/test_event.py b/tests/components/bang_olufsen/test_event.py index 855dab40db1..11f337b715f 100644 --- a/tests/components/bang_olufsen/test_event.py +++ b/tests/components/bang_olufsen/test_event.py @@ -23,17 +23,12 @@ from tests.common import MockConfigEntry async def test_button_event_creation( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_mozart_client: AsyncMock, + integration: None, entity_registry: EntityRegistry, snapshot: SnapshotAssertion, ) -> None: """Test button event entities are created.""" - # Load entry - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - # Add Button Event entity ids entity_ids = [ f"event.beosound_balance_11111111_{underscore(button_type)}".replace( @@ -77,14 +72,12 @@ async def test_button_event_creation_beoconnect_core( async def test_button( hass: HomeAssistant, + integration: None, mock_config_entry: MockConfigEntry, mock_mozart_client: AsyncMock, entity_registry: EntityRegistry, ) -> None: """Test button event entity.""" - # Load entry - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) # Enable the entity entity_registry.async_update_entity(TEST_BUTTON_EVENT_ENTITY_ID, disabled_by=None) diff --git a/tests/components/bang_olufsen/test_media_player.py b/tests/components/bang_olufsen/test_media_player.py index a389f9fa818..33719cb2311 100644 --- a/tests/components/bang_olufsen/test_media_player.py +++ b/tests/components/bang_olufsen/test_media_player.py @@ -190,14 +190,11 @@ async def test_async_update_sources_outdated_api( async def test_async_update_sources_remote( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test _async_update_sources is called when there are new video sources.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - notification_callback = mock_mozart_client.get_notification_notifications.call_args[ 0 ][0] @@ -246,14 +243,10 @@ async def test_async_update_sources_availability( async def test_async_update_playback_metadata( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test _async_update_playback_metadata.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_metadata_callback = ( mock_mozart_client.get_playback_metadata_notifications.call_args[0][0] ) @@ -286,14 +279,10 @@ async def test_async_update_playback_metadata( async def test_async_update_playback_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test _async_update_playback_error.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_error_callback = ( mock_mozart_client.get_playback_error_notifications.call_args[0][0] ) @@ -309,14 +298,10 @@ async def test_async_update_playback_error( async def test_async_update_playback_progress( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test _async_update_playback_progress.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_progress_callback = ( mock_mozart_client.get_playback_progress_notifications.call_args[0][0] ) @@ -337,14 +322,10 @@ async def test_async_update_playback_progress( async def test_async_update_playback_state( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test _async_update_playback_state.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_state_callback = ( mock_mozart_client.get_playback_state_notifications.call_args[0][0] ) @@ -386,18 +367,14 @@ async def test_async_update_playback_state( ) async def test_async_update_source_change( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, source: Source, content_type: MediaType, progress: int, metadata: PlaybackContentMetadata, ) -> None: """Test _async_update_source_change.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_progress_callback = ( mock_mozart_client.get_playback_progress_notifications.call_args[0][0] ) @@ -427,14 +404,11 @@ async def test_async_update_source_change( async def test_async_turn_off( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_turn_off.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_state_callback = ( mock_mozart_client.get_playback_state_notifications.call_args[0][0] ) @@ -458,14 +432,10 @@ async def test_async_turn_off( async def test_async_set_volume_level( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_set_volume_level and _async_update_volume by proxy.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - volume_callback = mock_mozart_client.get_volume_notifications.call_args[0][0] assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) @@ -526,15 +496,11 @@ async def test_async_update_beolink_line_in( async def test_async_update_beolink_listener( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, mock_config_entry_core: MockConfigEntry, ) -> None: """Test _async_update_beolink as a listener.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_metadata_callback = ( mock_mozart_client.get_playback_metadata_notifications.call_args[0][0] ) @@ -612,14 +578,10 @@ async def test_async_update_name_and_beolink( async def test_async_mute_volume( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_mute_volume.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - volume_callback = mock_mozart_client.get_volume_notifications.call_args[0][0] assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) @@ -660,16 +622,12 @@ async def test_async_mute_volume( ) async def test_async_media_play_pause( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, initial_state: RenderingState, command: str, ) -> None: """Test async_media_play_pause.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_state_callback = ( mock_mozart_client.get_playback_state_notifications.call_args[0][0] ) @@ -693,14 +651,10 @@ async def test_async_media_play_pause( async def test_async_media_stop( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_media_stop.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - playback_state_callback = ( mock_mozart_client.get_playback_state_notifications.call_args[0][0] ) @@ -725,14 +679,10 @@ async def test_async_media_stop( async def test_async_media_next_track( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_media_next_track.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, @@ -756,17 +706,13 @@ async def test_async_media_next_track( ) async def test_async_media_seek( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, source: Source, expected_result: AbstractContextManager, seek_called_times: int, ) -> None: """Test async_media_seek.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - source_change_callback = ( mock_mozart_client.get_source_change_notifications.call_args[0][0] ) @@ -791,14 +737,10 @@ async def test_async_media_seek( async def test_async_media_previous_track( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_media_previous_track.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, @@ -811,14 +753,10 @@ async def test_async_media_previous_track( async def test_async_clear_playlist( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_clear_playlist.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, @@ -842,18 +780,14 @@ async def test_async_clear_playlist( ) async def test_async_select_source( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, source: str, expected_result: AbstractContextManager, audio_source_call: int, video_source_call: int, ) -> None: """Test async_select_source with an invalid source.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - with expected_result: await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -871,14 +805,10 @@ async def test_async_select_source( async def test_async_select_sound_mode( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_select_sound_mode.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) assert states.attributes[ATTR_SOUND_MODE] == TEST_ACTIVE_SOUND_MODE_NAME @@ -908,14 +838,10 @@ async def test_async_select_sound_mode( async def test_async_select_sound_mode_invalid( hass: HomeAssistant, - mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, + integration: None, ) -> None: """Test async_select_sound_mode with an invalid sound_mode.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -934,14 +860,10 @@ async def test_async_select_sound_mode_invalid( async def test_async_play_media_invalid_type( hass: HomeAssistant, - mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, + integration: None, ) -> None: """Test async_play_media only accepts valid media types.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - with pytest.raises(ServiceValidationError) as exc_info: await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -961,14 +883,10 @@ async def test_async_play_media_invalid_type( async def test_async_play_media_url( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media URL.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - # Setup media source await async_setup_component(hass, "media_source", {"media_source": {}}) @@ -988,14 +906,11 @@ async def test_async_play_media_url( async def test_async_play_media_overlay_absolute_volume_uri( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media overlay with Home Assistant local URI and absolute volume.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await async_setup_component(hass, "media_source", {"media_source": {}}) await hass.services.async_call( @@ -1022,14 +937,10 @@ async def test_async_play_media_overlay_absolute_volume_uri( async def test_async_play_media_overlay_invalid_offset_volume_tts( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Home Assistant invalid offset volume and B&O tts.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1054,14 +965,10 @@ async def test_async_play_media_overlay_invalid_offset_volume_tts( async def test_async_play_media_overlay_offset_volume_tts( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Home Assistant invalid offset volume and B&O tts.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - volume_callback = mock_mozart_client.get_volume_notifications.call_args[0][0] # Set the volume to enable offset @@ -1087,14 +994,10 @@ async def test_async_play_media_overlay_offset_volume_tts( async def test_async_play_media_tts( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Home Assistant tts.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await async_setup_component(hass, "media_source", {"media_source": {}}) await hass.services.async_call( @@ -1113,14 +1016,10 @@ async def test_async_play_media_tts( async def test_async_play_media_radio( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with B&O radio.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1139,14 +1038,10 @@ async def test_async_play_media_radio( async def test_async_play_media_favourite( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with B&O favourite.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1163,14 +1058,11 @@ async def test_async_play_media_favourite( async def test_async_play_media_deezer_flow( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Deezer flow.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - # Send a service call await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -1191,14 +1083,10 @@ async def test_async_play_media_deezer_flow( async def test_async_play_media_deezer_playlist( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Deezer playlist.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1218,14 +1106,10 @@ async def test_async_play_media_deezer_playlist( async def test_async_play_media_deezer_track( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with Deezer track.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -1244,16 +1128,13 @@ async def test_async_play_media_deezer_track( async def test_async_play_media_invalid_deezer( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media with an invalid/no Deezer login.""" mock_mozart_client.start_deezer_flow.side_effect = TEST_DEEZER_INVALID_FLOW - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -1275,14 +1156,10 @@ async def test_async_play_media_invalid_deezer( async def test_async_play_media_url_m3u( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_play_media URL with the m3u extension.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await async_setup_component(hass, "media_source", {"media_source": {}}) with ( @@ -1349,16 +1226,12 @@ async def test_async_play_media_url_m3u( async def test_async_browse_media( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, - mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, + integration: None, child: dict[str, str | bool | None], present: bool, ) -> None: """Test async_browse_media with audio and video source.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await async_setup_component(hass, "media_source", {"media_source": {}}) client = await hass_ws_client() @@ -1386,18 +1259,14 @@ async def test_async_browse_media( async def test_async_join_players( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, mock_config_entry_core: MockConfigEntry, group_members: list[str], expand_count: int, join_count: int, ) -> None: """Test async_join_players.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - source_change_callback = ( mock_mozart_client.get_source_change_notifications.call_args[0][0] ) @@ -1453,8 +1322,8 @@ async def test_async_join_players( async def test_async_join_players_invalid( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, mock_config_entry_core: MockConfigEntry, source: Source, group_members: list[str], @@ -1462,10 +1331,6 @@ async def test_async_join_players_invalid( error_type: str, ) -> None: """Test async_join_players with an invalid media_player entity.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - source_change_callback = ( mock_mozart_client.get_source_change_notifications.call_args[0][0] ) @@ -1505,14 +1370,10 @@ async def test_async_join_players_invalid( async def test_async_unjoin_player( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_unjoin_player.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_UNJOIN, @@ -1552,16 +1413,12 @@ async def test_async_unjoin_player( async def test_async_beolink_join( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, service_parameters: dict[str, str], method_parameters: dict[str, str], ) -> None: """Test async_beolink_join with defined JID and JID and source.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( DOMAIN, "beolink_join", @@ -1601,16 +1458,12 @@ async def test_async_beolink_join( async def test_async_beolink_join_invalid( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, service_parameters: dict[str, str], expected_result: AbstractContextManager, ) -> None: """Test invalid async_beolink_join calls with defined JID or source ID.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - with expected_result: await hass.services.async_call( DOMAIN, @@ -1665,8 +1518,8 @@ async def test_async_beolink_expand( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, parameter: str, parameter_value: bool | list[str], expand_side_effect: NotFoundException | None, @@ -1676,9 +1529,6 @@ async def test_async_beolink_expand( """Test async_beolink_expand.""" mock_mozart_client.post_beolink_expand.side_effect = expand_side_effect - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - source_change_callback = ( mock_mozart_client.get_source_change_notifications.call_args[0][0] ) @@ -1714,14 +1564,10 @@ async def test_async_beolink_expand( async def test_async_beolink_unexpand( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test test_async_beolink_unexpand.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( DOMAIN, "beolink_unexpand", @@ -1741,14 +1587,10 @@ async def test_async_beolink_unexpand( async def test_async_beolink_allstandby( hass: HomeAssistant, snapshot: SnapshotAssertion, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, ) -> None: """Test async_beolink_allstandby.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.services.async_call( DOMAIN, "beolink_allstandby", @@ -1775,13 +1617,11 @@ async def test_async_beolink_allstandby( ) async def test_async_set_repeat( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, repeat: RepeatMode, ) -> None: """Test async_set_repeat.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) assert ATTR_MEDIA_REPEAT not in states.attributes @@ -1822,14 +1662,11 @@ async def test_async_set_repeat( ) async def test_async_set_shuffle( hass: HomeAssistant, + integration: None, mock_mozart_client: AsyncMock, - mock_config_entry: MockConfigEntry, shuffle: bool, ) -> None: """Test async_set_shuffle.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - assert (states := hass.states.get(TEST_MEDIA_PLAYER_ENTITY_ID)) assert ATTR_MEDIA_SHUFFLE not in states.attributes diff --git a/tests/components/bang_olufsen/test_websocket.py b/tests/components/bang_olufsen/test_websocket.py index ecf5b2d011e..3b812846b7c 100644 --- a/tests/components/bang_olufsen/test_websocket.py +++ b/tests/components/bang_olufsen/test_websocket.py @@ -23,16 +23,13 @@ from tests.common import MockConfigEntry async def test_connection( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_config_entry: MockConfigEntry, + integration: None, mock_mozart_client: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test on_connection and on_connection_lost logs and calls correctly.""" - mock_mozart_client.websocket_connected = True - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - connection_callback = mock_mozart_client.get_on_connection.call_args[0][0] caplog.set_level(logging.DEBUG) @@ -56,14 +53,11 @@ async def test_connection( async def test_connection_lost( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_config_entry: MockConfigEntry, + integration: None, mock_mozart_client: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test on_connection_lost logs and calls correctly.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - connection_lost_callback = mock_mozart_client.get_on_connection_lost.call_args[0][0] mock_connection_lost_callback = Mock() @@ -84,14 +78,11 @@ async def test_connection_lost( async def test_on_software_update_state( hass: HomeAssistant, device_registry: DeviceRegistry, - mock_config_entry: MockConfigEntry, + integration: None, mock_mozart_client: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test software version is updated through on_software_update_state.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - software_update_state_callback = ( mock_mozart_client.get_software_update_state_notifications.call_args[0][0] ) @@ -114,14 +105,11 @@ async def test_on_all_notifications_raw( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, device_registry: DeviceRegistry, - mock_config_entry: MockConfigEntry, + integration: None, mock_mozart_client: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test on_all_notifications_raw logs and fires as expected.""" - - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - all_notifications_raw_callback = ( mock_mozart_client.get_all_notifications_raw.call_args[0][0] ) From 3438a4f063f5a4d04b824531ca4e07c1700bfa4e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 May 2025 20:31:18 +0000 Subject: [PATCH 0976/1175] Bump version to 2025.6.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 5b299fd0187..a36aa88ca84 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 1fc4a28b9da..6a1fa4d49b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0.dev0" +version = "2025.6.0b0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 9483a88ee1fda9f0dfc3c1dca56022432913aff0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 26 May 2025 23:47:53 +0200 Subject: [PATCH 0977/1175] Fix translation for sensor measurement angle state class (#145649) --- homeassistant/components/sensor/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 2268d2797e4..ecaeb2504d9 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -135,7 +135,7 @@ "name": "State class", "state": { "measurement": "Measurement", - "measurement_angle": "Measurement Angle", + "measurement_angle": "Measurement angle", "total": "Total", "total_increasing": "Total increasing" } From 77031d1ae4235c13a7a8e768a954cb84ae0bce44 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 26 May 2025 23:08:07 +0200 Subject: [PATCH 0978/1175] Fix Aquacell snapshot (#145651) --- tests/components/aquacell/snapshots/test_sensor.ambr | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/aquacell/snapshots/test_sensor.ambr b/tests/components/aquacell/snapshots/test_sensor.ambr index f512b2a824d..c24a7f43cfe 100644 --- a/tests/components/aquacell/snapshots/test_sensor.ambr +++ b/tests/components/aquacell/snapshots/test_sensor.ambr @@ -77,6 +77,7 @@ 'original_name': 'Last update', 'platform': 'aquacell', 'previous_unique_id': None, + 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'last_update', 'unique_id': 'DSN-last_update', From f60de45b52987c34f3c2de193b37e6be556be363 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 08:49:25 +0200 Subject: [PATCH 0979/1175] Fix Amazon devices offline handling (#145656) --- .../components/amazon_devices/entity.py | 6 ++- .../amazon_devices/test_binary_sensor.py | 32 +++++++++++++++ .../components/amazon_devices/test_notify.py | 37 +++++++++++++++++- .../components/amazon_devices/test_switch.py | 39 ++++++++++++++++++- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py index 825a63db476..bab8009ceb0 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/amazon_devices/entity.py @@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): @property def available(self) -> bool: """Return True if entity is available.""" - return super().available and self._serial_num in self.coordinator.data + return ( + super().available + and self._serial_num in self.coordinator.data + and self.device.online + ) diff --git a/tests/components/amazon_devices/test_binary_sensor.py b/tests/components/amazon_devices/test_binary_sensor.py index bbe8af17a8e..b31d85e06aa 100644 --- a/tests/components/amazon_devices/test_binary_sensor.py +++ b/tests/components/amazon_devices/test_binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration +from .const import TEST_SERIAL_NUMBER from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @@ -69,3 +70,34 @@ async def test_coordinator_data_update_fails( assert (state := hass.states.get(entity_id)) assert state.state == STATE_UNAVAILABLE + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "binary_sensor.echo_test_connectivity" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/amazon_devices/test_notify.py b/tests/components/amazon_devices/test_notify.py index c1147af94c7..b486380fd07 100644 --- a/tests/components/amazon_devices/test_notify.py +++ b/tests/components/amazon_devices/test_notify.py @@ -6,19 +6,21 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL from homeassistant.components.notify import ( ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, ) -from homeassistant.const import ATTR_ENTITY_ID, Platform +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.util import dt as dt_util from . import setup_integration +from .const import TEST_SERIAL_NUMBER -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") @@ -68,3 +70,34 @@ async def test_notify_send_message( assert (state := hass.states.get(entity_id)) assert state.state == now.isoformat() + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "notify.echo_test_announce" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/amazon_devices/test_switch.py index 004d6cce842..24af96db280 100644 --- a/tests/components/amazon_devices/test_switch.py +++ b/tests/components/amazon_devices/test_switch.py @@ -12,7 +12,13 @@ from homeassistant.components.switch import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -89,3 +95,34 @@ async def test_switch_dnd( assert mock_amazon_devices_client.set_do_not_disturb.call_count == 2 assert (state := hass.states.get(entity_id)) assert state.state == STATE_OFF + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "switch.echo_test_do_not_disturb" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE From 20a6a3f195f62e530c483f6b22bca806b00cd8cd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 00:01:35 +0200 Subject: [PATCH 0980/1175] Handle Google Nest DHCP flows (#145658) * Handle Google Nest DHCP flows * Handle Google Nest DHCP flows --- homeassistant/components/nest/config_flow.py | 1 + tests/components/nest/test_config_flow.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 6ed43066fe3..1513a039407 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -446,4 +446,5 @@ class NestFlowHandler( self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: """Handle a flow initialized by discovery.""" + await self._async_handle_discovery_without_unique_id() return await self.async_step_user() diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 0e6ec290841..3f369f3e127 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -1002,6 +1002,24 @@ async def test_dhcp_discovery( assert result.get("reason") == "missing_credentials" +@pytest.mark.parametrize( + ("nest_test_config", "sdm_managed_topic", "device_access_project_id"), + [(TEST_CONFIG_APP_CREDS, True, "project-id-2")], +) +async def test_dhcp_discovery_already_setup( + hass: HomeAssistant, oauth: OAuthFixture, setup_platform +) -> None: + """Exercise discovery dhcp with existing config entry.""" + await setup_platform() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=FAKE_DHCP_DATA, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize(("sdm_managed_topic"), [(True)]) async def test_dhcp_discovery_with_creds( hass: HomeAssistant, From d6cadc1e3ff0c1d600a475cb4f1311f74324b3f8 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 18:53:45 +0200 Subject: [PATCH 0981/1175] Support addresses with comma in google_travel_time (#145663) Support addresses with comma --- .../components/google_travel_time/helpers.py | 2 +- .../google_travel_time/test_helpers.py | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/components/google_travel_time/test_helpers.py diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 49294455a49..2e36abd62b1 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -37,7 +37,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None: try: formatted_coordinates = coordinates.split(",") vol.Schema(cv.gps(formatted_coordinates)) - except (AttributeError, vol.ExactSequenceInvalid): + except (AttributeError, vol.Invalid): return Waypoint(address=location) return Waypoint( location=Location( diff --git a/tests/components/google_travel_time/test_helpers.py b/tests/components/google_travel_time/test_helpers.py new file mode 100644 index 00000000000..058cb214ed7 --- /dev/null +++ b/tests/components/google_travel_time/test_helpers.py @@ -0,0 +1,46 @@ +"""Tests for google_travel_time.helpers.""" + +from google.maps.routing_v2 import Location, Waypoint +from google.type import latlng_pb2 +import pytest + +from homeassistant.components.google_travel_time import helpers +from homeassistant.core import HomeAssistant + + +@pytest.mark.parametrize( + ("location", "expected_result"), + [ + ( + "12.34,56.78", + Waypoint( + location=Location( + lat_lng=latlng_pb2.LatLng( + latitude=12.34, + longitude=56.78, + ) + ) + ), + ), + ( + "12.34, 56.78", + Waypoint( + location=Location( + lat_lng=latlng_pb2.LatLng( + latitude=12.34, + longitude=56.78, + ) + ) + ), + ), + ("Some Address", Waypoint(address="Some Address")), + ("Some Street 1, 12345 City", Waypoint(address="Some Street 1, 12345 City")), + ], +) +def test_convert_to_waypoint_coordinates( + hass: HomeAssistant, location: str, expected_result: Waypoint +) -> None: + """Test convert_to_waypoint returns correct Waypoint for coordinates or address.""" + waypoint = helpers.convert_to_waypoint(hass, location) + + assert waypoint == expected_result From bfdba7713e6e6f022cbb71ef2493bfb0da85a297 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 May 2025 21:58:46 -0500 Subject: [PATCH 0982/1175] Bump aiohttp to 3.12.2 (#145671) --- 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 7da421526de..ae59ce94200 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.1 +aiohttp==3.12.2 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 6a1fa4d49b6..7df9230b7ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.1", + "aiohttp==3.12.2", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index b89c164188e..e4d1cc5ba30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.1 +aiohttp==3.12.2 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From f09c28e61f338d2f34f369a8daef52f640434cf5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 27 May 2025 08:48:06 +0200 Subject: [PATCH 0983/1175] Fix justnimbus CI test (#145681) --- tests/components/justnimbus/test_config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index 330b05bf48c..cc3a7a88285 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -1,6 +1,6 @@ """Test the JustNimbus config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from justnimbus.exceptions import InvalidClientID, JustNimbusError import pytest @@ -132,7 +132,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.justnimbus.config_flow.justnimbus.JustNimbusClient.get_data", - return_value=True, + return_value=MagicMock(), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From bfa919d078a5ac81c17763d8dc4c2980669d6367 Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 13:53:30 +0300 Subject: [PATCH 0984/1175] Remove confirm screen after Z-Wave usb discovery (#145682) * Remove confirm screen after Z-Wave usb discovery * Simplify async_step_usb --- .../components/zwave_js/config_flow.py | 30 +----- .../components/zwave_js/strings.json | 3 - tests/components/zwave_js/test_config_flow.py | 91 +++---------------- 3 files changed, 14 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 3e899da0538..e2941b52522 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -170,8 +170,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - _title: str - def __init__(self) -> None: """Set up flow instance.""" self.s0_legacy_key: str | None = None @@ -446,7 +444,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # at least for a short time. return self.async_abort(reason="already_in_progress") if current_config_entries := self._async_current_entries(include_ignore=False): - config_entry = next( + self._reconfigure_config_entry = next( ( entry for entry in current_config_entries @@ -454,7 +452,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ), None, ) - if not config_entry: + if not self._reconfigure_config_entry: return self.async_abort(reason="addon_required") vid = discovery_info.vid @@ -503,31 +501,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) title = human_name.split(" - ")[0].strip() self.context["title_placeholders"] = {CONF_NAME: title} - self._title = title - return await self.async_step_usb_confirm() - - async def async_step_usb_confirm( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Handle USB Discovery confirmation.""" - if user_input is None: - return self.async_show_form( - step_id="usb_confirm", - description_placeholders={CONF_NAME: self._title}, - ) self._usb_discovery = True - if current_config_entries := self._async_current_entries(include_ignore=False): - self._reconfigure_config_entry = next( - ( - entry - for entry in current_config_entries - if entry.data.get(CONF_USE_ADDON) - ), - None, - ) - if not self._reconfigure_config_entry: - return self.async_abort(reason="addon_required") + if current_config_entries: return await self.async_step_intent_migrate() return await self.async_step_installation_type() diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index fbe43af1f6f..ee6efcb2fb6 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -98,9 +98,6 @@ "start_addon": { "title": "The Z-Wave add-on is starting." }, - "usb_confirm": { - "description": "Do you want to set up {name} with the Z-Wave add-on?" - }, "zeroconf_confirm": { "description": "Do you want to add the Z-Wave Server with home ID {home_id} found at {url} to Home Assistant?", "title": "Discovered Z-Wave Server" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index bae8ae55034..c9929759a49 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -585,8 +585,8 @@ async def test_abort_hassio_discovery_with_existing_flow(hass: HomeAssistant) -> context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -664,13 +664,8 @@ async def test_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=usb_discovery_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" assert result["menu_options"] == ["intent_recommended", "intent_custom"] @@ -771,12 +766,8 @@ async def test_usb_discovery_addon_not_running( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" @@ -932,12 +923,8 @@ async def test_usb_discovery_migration( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" @@ -1063,12 +1050,8 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert mock_usb_serial_by_id.call_count == 2 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.FORM assert result["step_id"] == "intent_migrate" @@ -1366,16 +1349,16 @@ async def test_usb_discovery_with_existing_usb_flow(hass: HomeAssistant) -> None data=first_usb_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "installation_type" result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USB}, data=USB_DISCOVERY_INFO, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "usb_confirm" + assert result2["type"] is FlowResultType.MENU + assert result2["step_id"] == "installation_type" usb_flows_in_progress = hass.config_entries.flow.async_progress_by_handler( DOMAIN, match_context={"source": config_entries.SOURCE_USB} @@ -1409,53 +1392,6 @@ async def test_abort_usb_discovery_addon_required(hass: HomeAssistant) -> None: assert result["reason"] == "addon_required" -@pytest.mark.usefixtures( - "supervisor", - "addon_running", -) -async def test_abort_usb_discovery_confirm_addon_required( - hass: HomeAssistant, - addon_options: dict[str, Any], - mock_usb_serial_by_id: MagicMock, -) -> None: - """Test usb discovery confirm aborted when existing entry not using add-on.""" - addon_options["device"] = "/dev/another_device" - entry = MockConfigEntry( - domain=DOMAIN, - data={ - "url": "ws://localhost:3000", - "usb_path": "/dev/another_device", - "use_addon": True, - }, - title=TITLE, - unique_id="1234", - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USB}, - data=USB_DISCOVERY_INFO, - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert mock_usb_serial_by_id.call_count == 2 - - hass.config_entries.async_update_entry( - entry, - data={ - **entry.data, - "use_addon": False, - }, - ) - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "addon_required" - - async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None: """Test usb discovery flow is aborted when there is no supervisor.""" result = await hass.config_entries.flow.async_init( @@ -4635,13 +4571,8 @@ async def test_recommended_usb_discovery( context={"source": config_entries.SOURCE_USB}, data=usb_discovery_info, ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "usb_confirm" - assert result["description_placeholders"] == {"name": discovery_name} + assert mock_usb_serial_by_id.call_count == 1 - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] is FlowResultType.MENU assert result["step_id"] == "installation_type" assert result["menu_options"] == ["intent_recommended", "intent_custom"] From 2830ed61471a8e1adde182d4abe4e320e83ff05a Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Tue, 27 May 2025 11:04:29 +0300 Subject: [PATCH 0985/1175] Change description on recommended/custom Z-Wave install step (#145688) Change description on recommended/custom Z-WaveJS step --- homeassistant/components/zwave_js/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index ee6efcb2fb6..439fc7b1aad 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -131,7 +131,7 @@ }, "installation_type": { "title": "Set up Z-Wave", - "description": "Choose the installation type for your Z-Wave integration.", + "description": "In a few steps, we’re going to set up your Home Assistant Connect ZWA-2. Home Assistant can automatically install and configure the recommended Z-Wave setup, or you can customize it.", "menu_options": { "intent_recommended": "Recommended installation", "intent_custom": "Custom installation" From 9e7dc1d11d702eae57f57a5d65bb472c4a062c0c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 12:33:02 +0200 Subject: [PATCH 0986/1175] Use string type for amazon devices OTP code (#145698) --- homeassistant/components/amazon_devices/config_flow.py | 2 +- tests/components/amazon_devices/const.py | 2 +- tests/components/amazon_devices/test_config_flow.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_devices/config_flow.py b/homeassistant/components/amazon_devices/config_flow.py index 5566c16602b..d0c3d067cee 100644 --- a/homeassistant/components/amazon_devices/config_flow.py +++ b/homeassistant/components/amazon_devices/config_flow.py @@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): ): CountrySelector(), vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CODE): cv.positive_int, + vol.Required(CONF_CODE): cv.string, } ), ) diff --git a/tests/components/amazon_devices/const.py b/tests/components/amazon_devices/const.py index 94b5b7052e6..a2600ba98a6 100644 --- a/tests/components/amazon_devices/const.py +++ b/tests/components/amazon_devices/const.py @@ -1,6 +1,6 @@ """Amazon Devices tests const.""" -TEST_CODE = 123123 +TEST_CODE = "023123" TEST_COUNTRY = "IT" TEST_PASSWORD = "fake_password" TEST_SERIAL_NUMBER = "echo_test_serial_number" diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py index 68ab7f4ffa6..41b65c33bd5 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/amazon_devices/test_config_flow.py @@ -56,6 +56,7 @@ async def test_full_flow( }, } assert result["result"].unique_id == TEST_USERNAME + mock_amazon_devices_client.login_mode_interactive.assert_called_once_with("023123") @pytest.mark.parametrize( From b84850df9f6683fdccbdd57a1f3d3920629a2e9b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 12:54:57 +0200 Subject: [PATCH 0987/1175] Fix error stack trace for HomeAssistantError in websocket service call (#145699) * Add test * Fix error stack trace for HomeAssistantError in websocket service call --- homeassistant/components/websocket_api/commands.py | 4 +++- tests/components/websocket_api/test_commands.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index ddcdd4f1cf8..9c371a8399d 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -300,7 +300,9 @@ async def handle_call_service( translation_placeholders=err.translation_placeholders, ) except HomeAssistantError as err: - connection.logger.exception("Unexpected exception") + connection.logger.error( + "Error during service call to %s.%s: %s", msg["domain"], msg["service"], err + ) connection.send_error( msg["id"], const.ERR_HOME_ASSISTANT_ERROR, diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4ca2098550b..2c9cc19c84b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -514,9 +514,12 @@ async def test_call_service_schema_validation_error( @pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"]) async def test_call_service_error( - hass: HomeAssistant, websocket_client: MockHAClientWebSocket + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + websocket_client: MockHAClientWebSocket, ) -> None: """Test call service command with error.""" + caplog.set_level(logging.ERROR) @callback def ha_error_call(_): @@ -561,6 +564,7 @@ async def test_call_service_error( assert msg["error"]["translation_placeholders"] == {"option": "bla"} assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_domain"] == "test" + assert "Traceback" not in caplog.text await websocket_client.send_json_auto_id( { @@ -578,6 +582,7 @@ async def test_call_service_error( assert msg["error"]["translation_placeholders"] == {"option": "bla"} assert msg["error"]["translation_key"] == "custom_error" assert msg["error"]["translation_domain"] == "test" + assert "Traceback" not in caplog.text await websocket_client.send_json_auto_id( { @@ -592,6 +597,7 @@ async def test_call_service_error( assert msg["success"] is False assert msg["error"]["code"] == "unknown_error" assert msg["error"]["message"] == "value_error" + assert "Traceback" in caplog.text async def test_subscribe_unsubscribe_events( From 923530972a5e25c78065df1e4cc39afb3bf44a24 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 17:35:11 +0200 Subject: [PATCH 0988/1175] Remove static pin code length Matter sensors (#145711) * Remove static Matter sensors * Clean up translation strings --- homeassistant/components/matter/sensor.py | 22 -- homeassistant/components/matter/strings.json | 6 - .../matter/snapshots/test_sensor.ambr | 192 ------------------ 3 files changed, 220 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 2197f81e134..e1fbf1c5a82 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -967,28 +967,6 @@ DISCOVERY_SCHEMAS = [ # don't discover this entry if the supported state list is empty secondary_value_is_not=[], ), - MatterDiscoverySchema( - platform=Platform.SENSOR, - entity_description=MatterSensorEntityDescription( - key="MinPINCodeLength", - translation_key="min_pin_code_length", - entity_category=EntityCategory.DIAGNOSTIC, - device_class=None, - ), - entity_class=MatterSensor, - required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,), - ), - MatterDiscoverySchema( - platform=Platform.SENSOR, - entity_description=MatterSensorEntityDescription( - key="MaxPINCodeLength", - translation_key="max_pin_code_length", - entity_category=EntityCategory.DIAGNOSTIC, - device_class=None, - ), - entity_class=MatterSensor, - required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,), - ), MatterDiscoverySchema( platform=Platform.SENSOR, entity_description=MatterSensorEntityDescription( diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index a04f1d86880..7cae16c5e9b 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -390,12 +390,6 @@ "evse_user_max_charge_current": { "name": "User max charge current" }, - "min_pin_code_length": { - "name": "Min PIN code length" - }, - "max_pin_code_length": { - "name": "Max PIN code length" - }, "window_covering_target_position": { "name": "Target opening position" } diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 3af00db623e..685d3c0022d 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -1307,198 +1307,6 @@ 'state': '180.0', }) # --- -# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Max PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'max_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_max_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Max PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8', - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Min PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'min_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock][sensor.mock_door_lock_min_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Min PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Max PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'max_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MaxPINCodeLength-257-23', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_max_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Max PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_max_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '8', - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Min PIN code length', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'min_pin_code_length', - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-MinPINCodeLength-257-24', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[door_lock_with_unbolt][sensor.mock_door_lock_min_pin_code_length-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Min PIN code length', - }), - 'context': , - 'entity_id': 'sensor.mock_door_lock_min_pin_code_length', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '6', - }) -# --- # name: test_sensors[eve_contact_sensor][sensor.eve_door_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 389becc4f6397886c7a552243bbceac3205e36c0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 27 May 2025 17:46:21 +0200 Subject: [PATCH 0989/1175] Disable advanced window cover position Matter sensor by default (#145713) * Disable advanced window cover position Matter sensor by default * Enanble disabled sensors in snapshot test --- homeassistant/components/matter/sensor.py | 1 + .../matter/snapshots/test_sensor.ambr | 306 ++++++++++++++++++ tests/components/matter/test_sensor.py | 2 +- 3 files changed, 308 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index e1fbf1c5a82..70e4cb238f5 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -972,6 +972,7 @@ DISCOVERY_SCHEMAS = [ entity_description=MatterSensorEntityDescription( key="TargetPositionLiftPercent100ths", entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, translation_key="window_covering_target_position", measurement_to_ha=lambda x: round((10000 - x) / 100), native_unit_of_measurement=PERCENTAGE, diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 685d3c0022d..3a5a937b4a4 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -2425,6 +2425,159 @@ 'state': '0.0', }) # --- +# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Current switch position', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch][sensor.mock_generic_switch_current_switch_position-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Current switch position', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position_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': 'Current switch position (1)', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_current_switch_position_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Current switch position (1)', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_current_switch_position_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.mock_generic_switch_fancy_button', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Fancy Button', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-2-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[generic_switch_multi][sensor.mock_generic_switch_fancy_button-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mock Generic Switch Fancy Button', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.mock_generic_switch_fancy_button', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[humidity_sensor][sensor.mock_humidity_sensor_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2715,6 +2868,159 @@ 'state': 'stopped', }) # --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.inovelli_config', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Config', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-5-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_config-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Config', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_config', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.inovelli_down', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Down', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-4-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_down-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Down', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_down', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.inovelli_up', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Up', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch_current_position', + 'unique_id': '00000000000004D2-00000000000000C5-MatterNodeDevice-3-SwitchCurrentPosition-59-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[multi_endpoint_light][sensor.inovelli_up-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Inovelli Up', + 'state_class': , + }), + 'context': , + 'entity_id': 'sensor.inovelli_up', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensors[oven][sensor.mock_oven_current_phase-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_sensor.py b/tests/components/matter/test_sensor.py index 19697efab71..e15e3f9f53e 100644 --- a/tests/components/matter/test_sensor.py +++ b/tests/components/matter/test_sensor.py @@ -17,7 +17,7 @@ from .common import ( ) -@pytest.mark.usefixtures("matter_devices") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "matter_devices") async def test_sensors( hass: HomeAssistant, entity_registry: er.EntityRegistry, From 8880ab64987920a77cad1bdf1c6008c4de74f2d4 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 18:45:49 +0200 Subject: [PATCH 0990/1175] Catch PermissionDenied(Route API disabled) in google_travel_time (#145722) Catch PermissionDenied(Route API disabled) --- .../google_travel_time/config_flow.py | 9 ++++- .../components/google_travel_time/helpers.py | 39 +++++++++++++++++++ .../components/google_travel_time/sensor.py | 13 ++++++- .../google_travel_time/strings.json | 7 ++++ .../google_travel_time/test_config_flow.py | 13 ++++++- .../google_travel_time/test_sensor.py | 26 ++++++++++++- 6 files changed, 102 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_travel_time/config_flow.py b/homeassistant/components/google_travel_time/config_flow.py index 24ea29aef03..9e07fdefe9d 100644 --- a/homeassistant/components/google_travel_time/config_flow.py +++ b/homeassistant/components/google_travel_time/config_flow.py @@ -50,7 +50,12 @@ from .const import ( UNITS_IMPERIAL, UNITS_METRIC, ) -from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry +from .helpers import ( + InvalidApiKeyException, + PermissionDeniedException, + UnknownException, + validate_config_entry, +) RECONFIGURE_SCHEMA = vol.Schema( { @@ -188,6 +193,8 @@ async def validate_input( user_input[CONF_ORIGIN], user_input[CONF_DESTINATION], ) + except PermissionDeniedException: + return {"base": "permission_denied"} except InvalidApiKeyException: return {"base": "invalid_auth"} except TimeoutError: diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index 2e36abd62b1..70f9300c92f 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -7,6 +7,7 @@ from google.api_core.exceptions import ( Forbidden, GatewayTimeout, GoogleAPIError, + PermissionDenied, Unauthorized, ) from google.maps.routing_v2 import ( @@ -19,10 +20,18 @@ from google.maps.routing_v2 import ( from google.type import latlng_pb2 import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.location import find_coordinates +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -67,6 +76,9 @@ async def validate_config_entry( await client.compute_routes( request, metadata=[("x-goog-fieldmask", field_mask)] ) + except PermissionDenied as permission_error: + _LOGGER.error("Permission denied: %s", permission_error.message) + raise PermissionDeniedException from permission_error except (Unauthorized, Forbidden) as unauthorized_error: _LOGGER.error("Request denied: %s", unauthorized_error.message) raise InvalidApiKeyException from unauthorized_error @@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception): class UnknownException(Exception): """Unknown API Error.""" + + +class PermissionDeniedException(Exception): + """Permission Denied Error.""" + + +def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Create an issue for the Routes API being disabled.""" + async_create_issue( + hass, + DOMAIN, + f"routes_api_disabled_{entry.entry_id}", + learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="routes_api_disabled", + translation_placeholders={ + "entry_title": entry.title, + "enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api", + "api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions", + }, + ) + + +def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Delete the issue for the Routes API being disabled.""" + async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}") diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 7448fc1cb09..6323813b759 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -7,7 +7,7 @@ import logging from typing import TYPE_CHECKING, Any from google.api_core.client_options import ClientOptions -from google.api_core.exceptions import GoogleAPIError +from google.api_core.exceptions import GoogleAPIError, PermissionDenied from google.maps.routing_v2 import ( ComputeRoutesRequest, Route, @@ -58,7 +58,11 @@ from .const import ( TRAVEL_MODES_TO_GOOGLE_SDK_ENUM, UNITS_TO_GOOGLE_SDK_ENUM, ) -from .helpers import convert_to_waypoint +from .helpers import ( + convert_to_waypoint, + create_routes_api_disabled_issue, + delete_routes_api_disabled_issue, +) _LOGGER = logging.getLogger(__name__) @@ -273,6 +277,11 @@ class GoogleTravelTimeSensor(SensorEntity): ) if response is not None and len(response.routes) > 0: self._route = response.routes[0] + delete_routes_api_disabled_issue(self.hass, self._config_entry) + except PermissionDenied: + _LOGGER.error("Routes API is disabled for this API key") + create_routes_api_disabled_issue(self.hass, self._config_entry) + self._route = None except GoogleAPIError as ex: _LOGGER.error("Error getting travel time: %s", ex) self._route = None diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 87bc09eb456..f46d33fda09 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -21,6 +21,7 @@ } }, "error": { + "permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" @@ -100,5 +101,11 @@ "fewer_transfers": "Fewer transfers" } } + }, + "issues": { + "routes_api_disabled": { + "title": "The Routes API must be enabled", + "description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically." + } } } diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 8cdb3c270d0..562ca152ce8 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -2,7 +2,12 @@ from unittest.mock import AsyncMock, patch -from google.api_core.exceptions import GatewayTimeout, GoogleAPIError, Unauthorized +from google.api_core.exceptions import ( + GatewayTimeout, + GoogleAPIError, + PermissionDenied, + Unauthorized, +) import pytest from homeassistant.components.google_travel_time.const import ( @@ -98,6 +103,12 @@ async def test_minimum_fields(hass: HomeAssistant) -> None: (GoogleAPIError("test"), "cannot_connect"), (GatewayTimeout("Timeout error."), "timeout_connect"), (Unauthorized("Invalid API key."), "invalid_auth"), + ( + PermissionDenied( + "Requests to this API routes.googleapis.com method google.maps.routing.v2.Routes.ComputeRoutes are blocked." + ), + "permission_denied", + ), ], ) async def test_errors( diff --git a/tests/components/google_travel_time/test_sensor.py b/tests/components/google_travel_time/test_sensor.py index 58843d8275c..0ab5e38a644 100644 --- a/tests/components/google_travel_time/test_sensor.py +++ b/tests/components/google_travel_time/test_sensor.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from freezegun.api import FrozenDateTimeFactory -from google.api_core.exceptions import GoogleAPIError +from google.api_core.exceptions import GoogleAPIError, PermissionDenied from google.maps.routing_v2 import Units import pytest @@ -20,6 +20,7 @@ from homeassistant.components.google_travel_time.const import ( from homeassistant.components.google_travel_time.sensor import SCAN_INTERVAL from homeassistant.const import CONF_MODE, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.util.unit_system import ( METRIC_SYSTEM, US_CUSTOMARY_SYSTEM, @@ -170,3 +171,26 @@ async def test_sensor_exception( await hass.async_block_till_done() assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN assert "Error getting travel time" in caplog.text + + +@pytest.mark.parametrize( + ("data", "options"), + [(MOCK_CONFIG, DEFAULT_OPTIONS)], +) +async def test_sensor_routes_api_disabled( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + routes_mock: AsyncMock, + mock_config: MockConfigEntry, + freezer: FrozenDateTimeFactory, + issue_registry: ir.IssueRegistry, +) -> None: + """Test that exception gets caught and issue created.""" + routes_mock.compute_routes.side_effect = PermissionDenied("Errormessage") + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get("sensor.google_travel_time").state == STATE_UNKNOWN + assert "Routes API is disabled for this API key" in caplog.text + + assert len(issue_registry.issues) == 1 From 41a140d16c6b78d1c204f6adeb1277315e1b4426 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 27 May 2025 17:44:48 +0200 Subject: [PATCH 0991/1175] Debug log the update response in google_travel_time (#145725) Debug log the update response --- homeassistant/components/google_travel_time/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 6323813b759..1a9b361bd33 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -275,6 +275,7 @@ class GoogleTravelTimeSensor(SensorEntity): response = await self._client.compute_routes( request, metadata=[("x-goog-fieldmask", FIELD_MASK)] ) + _LOGGER.debug("Received response: %s", response) if response is not None and len(response.routes) > 0: self._route = response.routes[0] delete_routes_api_disabled_issue(self.hass, self._config_entry) From 6e6aae2ea3594743d451e63679325cdc89f2669a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 27 May 2025 19:29:04 +0200 Subject: [PATCH 0992/1175] Fix unbound local variable in Acmeda config flow (#145729) --- homeassistant/components/acmeda/config_flow.py | 3 ++- tests/components/acmeda/test_config_flow.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 5024507a7d3..785906ebf2a 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN): entry.unique_id for entry in self._async_current_entries() } + hubs: list[aiopulse.Hub] = [] with suppress(TimeoutError): async with timeout(5): - hubs: list[aiopulse.Hub] = [ + hubs = [ hub async for hub in aiopulse.Hub.discover() if hub.id not in already_configured diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 7b92c1aac3b..6589013d432 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -49,6 +49,21 @@ async def test_show_form_no_hubs(hass: HomeAssistant, mock_hub_discover) -> None assert len(mock_hub_discover.mock_calls) == 1 +async def test_timeout_fetching_hub(hass: HomeAssistant, mock_hub_discover) -> None: + """Test that flow aborts if no hubs are discovered.""" + mock_hub_discover.side_effect = TimeoutError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + # Check we performed the discovery + assert len(mock_hub_discover.mock_calls) == 1 + + @pytest.mark.usefixtures("mock_hub_run") async def test_show_form_one_hub(hass: HomeAssistant, mock_hub_discover) -> None: """Test that a config is created when one hub discovered.""" From 6adb27d17326147ac56e1a54d9a00651655f41fa Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 27 May 2025 21:27:52 +0200 Subject: [PATCH 0993/1175] Tado update mobile devices interval (#145738) Update the mobile devices interval to five minutes --- homeassistant/components/tado/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index 5f3aa1de1e4..09c6ec40208 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) SCAN_INTERVAL = timedelta(minutes=5) -SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30) +SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5) class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): From 3160fe9abcf1961408894cb0bb3a50f077727c97 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 May 2025 22:12:07 +0200 Subject: [PATCH 0994/1175] Update frontend to 20250527.0 (#145741) --- 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 fe445ae6b28..32d243b3431 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==20250526.0"] + "requirements": ["home-assistant-frontend==20250527.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae59ce94200..378e9fdce83 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 home-assistant-intents==2025.5.7 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 414153e193e..a54a12ee9c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f858d8e4315..f381de177dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -994,7 +994,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250526.0 +home-assistant-frontend==20250527.0 # homeassistant.components.conversation home-assistant-intents==2025.5.7 From 10adb57b837906d529b826e2740a741bd4d52db2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 27 May 2025 22:16:13 +0200 Subject: [PATCH 0995/1175] Bump version to 2025.6.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 a36aa88ca84..6b3cbb4f27c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 7df9230b7ee..7e1e98bdb24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b0" +version = "2025.6.0b1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From adddf330fd835fcef52e90ade508feb79b646963 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 28 May 2025 10:16:40 +0200 Subject: [PATCH 0996/1175] Ensure mqtt sensor unit of measurement validation for state class `measurement_angle` (#145648) --- homeassistant/components/mqtt/config_flow.py | 24 +++++++++++++++--- homeassistant/components/mqtt/sensor.py | 12 +++++++++ homeassistant/components/mqtt/strings.json | 1 + tests/components/mqtt/test_config_flow.py | 10 +++++++- tests/components/mqtt/test_sensor.py | 26 ++++++++++++++++++++ 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index bb884d6392f..b41e549093d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -39,6 +39,7 @@ from homeassistant.components.light import ( from homeassistant.components.sensor import ( CONF_STATE_CLASS, DEVICE_CLASS_UNITS, + STATE_CLASS_UNITS, SensorDeviceClass, SensorStateClass, ) @@ -640,6 +641,13 @@ def validate_sensor_platform_config( ): errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom" + if ( + (state_class := config.get(CONF_STATE_CLASS)) is not None + and state_class in STATE_CLASS_UNITS + and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class] + ): + errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class" + return errors @@ -676,11 +684,19 @@ class PlatformField: @callback def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector: """Return a context based unit of measurement selector.""" + + if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS: + return SelectSelector( + SelectSelectorConfig( + options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]], + sort=True, + custom_value=True, + ) + ) + if ( - user_data is None - or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None - or device_class not in DEVICE_CLASS_UNITS - ): + device_class := user_data.get(CONF_DEVICE_CLASS) + ) is None or device_class not in DEVICE_CLASS_UNITS: return TEXT_SELECTOR return SelectSelector( SelectSelectorConfig( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index b27ef68368a..46d475fcee8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_UNITS, DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, + STATE_CLASS_UNITS, STATE_CLASSES_SCHEMA, RestoreSensor, SensorDeviceClass, @@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT f"got `{CONF_DEVICE_CLASS}` '{device_class}'" ) + if ( + (state_class := config.get(CONF_STATE_CLASS)) is not None + and state_class in STATE_CLASS_UNITS + and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) + not in STATE_CLASS_UNITS[state_class] + ): + raise vol.Invalid( + f"The unit of measurement '{unit_of_measurement}' is not valid " + f"together with state class '{state_class}'" + ) + if (device_class := config.get(CONF_DEVICE_CLASS)) is None or ( unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT) ) is None: diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 8fc97362857..9bc6df1b633 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -644,6 +644,7 @@ "invalid_template": "Invalid template", "invalid_supported_color_modes": "Invalid supported color modes selection", "invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", + "invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list", "invalid_url": "Invalid URL", "last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only", "max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value", diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index a43617badb0..e30aa5d50d6 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3038,7 +3038,15 @@ async def test_migrate_of_incompatible_config_entry( { "state_class": "measurement", }, - (), + ( + ( + { + "state_class": "measurement_angle", + "unit_of_measurement": "deg", + }, + {"unit_of_measurement": "invalid_uom_for_state_class"}, + ), + ), { "state_topic": "test-topic", }, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0bafacfed26..ea1b7e186e2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -995,6 +995,32 @@ async def test_invalid_state_class( assert "expected SensorStateClass or one of" in caplog.text +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "state_class": "measurement_angle", + "unit_of_measurement": "deg", + } + } + } + ], +) +async def test_invalid_state_class_with_unit_of_measurement( + mqtt_mock_entry: MqttMockHAClientGenerator, caplog: pytest.LogCaptureFixture +) -> None: + """Test state_class option with invalid unit of measurement.""" + assert await mqtt_mock_entry() + assert ( + "The unit of measurement 'deg' is not valid together with state class 'measurement_angle'" + in caplog.text + ) + + @pytest.mark.parametrize( ("hass_config", "error_logged"), [ From f86bf69ebcd5a13d142b0371469eb857c92b3a60 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 08:13:28 +0200 Subject: [PATCH 0997/1175] Update otp description for amazon_devices (#145701) * Update otp description from amazon_devices * separate * Update strings.json --- homeassistant/components/amazon_devices/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/amazon_devices/strings.json index 8db249b44ed..47e6234cd9c 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/amazon_devices/strings.json @@ -5,7 +5,7 @@ "data_description_country": "The country of your Amazon account.", "data_description_username": "The email address of your Amazon account.", "data_description_password": "The password of your Amazon account.", - "data_description_code": "The one-time password sent to your email address." + "data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported." }, "config": { "flow_title": "{username}", From 0e7a1bb76cf1890c9bcfb6ad36039ec5e7a47345 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 27 May 2025 23:00:52 +0200 Subject: [PATCH 0998/1175] Make async_remove_stale_devices_links_keep_entity_device move entities (#145719) Co-authored-by: Jan Bouwhuis Co-authored-by: Joost Lekkerkerker --- homeassistant/helpers/device.py | 20 ++++++----- tests/helpers/test_device.py | 60 ++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index 16212422236..a7d888900b1 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -64,10 +64,10 @@ def async_remove_stale_devices_links_keep_entity_device( entry_id: str, source_entity_id_or_uuid: str, ) -> None: - """Remove the link between stale devices and a configuration entry. + """Remove entry_id from all devices except that of source_entity_id_or_uuid. - Only the device passed in the source_entity_id_or_uuid parameter - linked to the configuration entry will be maintained. + Also moves all entities linked to the entry_id to the device of + source_entity_id_or_uuid. """ async_remove_stale_devices_links_keep_current_device( @@ -83,13 +83,17 @@ def async_remove_stale_devices_links_keep_current_device( entry_id: str, current_device_id: str | None, ) -> None: - """Remove the link between stale devices and a configuration entry. - - Only the device passed in the current_device_id parameter linked to - the configuration entry will be maintained. - """ + """Remove entry_id from all devices except current_device_id.""" dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + + # Make sure all entities are linked to the correct device + for entity in ent_reg.entities.get_entries_for_config_entry_id(entry_id): + if entity.device_id == current_device_id: + continue + ent_reg.async_update_entity(entity.entity_id, device_id=current_device_id) + # Removes all devices from the config entry that are not the same as the current device for device in dev_reg.devices.get_devices_for_config_entry_id(entry_id): if device.id == current_device_id: diff --git a/tests/helpers/test_device.py b/tests/helpers/test_device.py index 852d418da23..266435ef05d 100644 --- a/tests/helpers/test_device.py +++ b/tests/helpers/test_device.py @@ -118,61 +118,75 @@ async def test_remove_stale_device_links_keep_entity_device( entity_registry: er.EntityRegistry, ) -> None: """Test cleaning works for entity.""" - config_entry = MockConfigEntry(domain="hue") - config_entry.add_to_hass(hass) + helper_config_entry = MockConfigEntry(domain="helper_integration") + helper_config_entry.add_to_hass(hass) + host_config_entry = MockConfigEntry(domain="host_integration") + host_config_entry.add_to_hass(hass) current_device = device_registry.async_get_or_create( identifiers={("test", "current_device")}, connections={("mac", "30:31:32:33:34:00")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) - assert current_device is not None - device_registry.async_get_or_create( + stale_device_1 = device_registry.async_get_or_create( identifiers={("test", "stale_device_1")}, connections={("mac", "30:31:32:33:34:01")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) device_registry.async_get_or_create( identifiers={("test", "stale_device_2")}, connections={("mac", "30:31:32:33:34:02")}, - config_entry_id=config_entry.entry_id, + config_entry_id=helper_config_entry.entry_id, ) - # Source entity registry + # Source entity source_entity = entity_registry.async_get_or_create( "sensor", - "test", + "host_integration", "source", - config_entry=config_entry, + config_entry=host_config_entry, device_id=current_device.id, ) - await hass.async_block_till_done() - assert entity_registry.async_get("sensor.test_source") is not None + assert entity_registry.async_get(source_entity.entity_id) is not None - devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( - config_entry.entry_id + # Helper entity connected to a stale device + helper_entity = entity_registry.async_get_or_create( + "sensor", + "helper_integration", + "helper", + config_entry=helper_config_entry, + device_id=stale_device_1.id, + ) + assert entity_registry.async_get(helper_entity.entity_id) is not None + + devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id ) # 3 devices linked to the config entry are expected (1 current device + 2 stales) - assert len(devices_config_entry) == 3 + assert len(devices_helper_entry) == 3 - # Manual cleanup should unlink stales devices from the config entry + # Manual cleanup should unlink stale devices from the config entry async_remove_stale_devices_links_keep_entity_device( hass, - entry_id=config_entry.entry_id, + entry_id=helper_config_entry.entry_id, source_entity_id_or_uuid=source_entity.entity_id, ) - devices_config_entry = device_registry.devices.get_devices_for_config_entry_id( - config_entry.entry_id + await hass.async_block_till_done() + + devices_helper_entry = device_registry.devices.get_devices_for_config_entry_id( + helper_config_entry.entry_id ) - # After cleanup, only one device is expected to be linked to the config entry - assert len(devices_config_entry) == 1 - - assert current_device in devices_config_entry + # After cleanup, only one device is expected to be linked to the config entry, and + # the entities should exist and be linked to the current device + assert len(devices_helper_entry) == 1 + assert current_device in devices_helper_entry + assert entity_registry.async_get(source_entity.entity_id) is not None + assert entity_registry.async_get(helper_entity.entity_id) is not None async def test_remove_stale_devices_links_keep_current_device( From cd133cbbe38f4042e99e9c49e4c4d8853b382a67 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 May 2025 20:51:27 +0200 Subject: [PATCH 0999/1175] Add level of collections in Immich media source tree (#145734) * add layer for collections in media source tree * re-arange tests, add test for collection layer * fix --- .../components/immich/media_source.py | 46 ++++-- tests/components/immich/test_media_source.py | 149 ++++++++++-------- 2 files changed, 116 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 201076f1295..0d0616875c6 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -43,11 +43,12 @@ class ImmichMediaSourceIdentifier: def __init__(self, identifier: str) -> None: """Split identifier into parts.""" parts = identifier.split("/") - # coonfig_entry.unique_id/album_id/asset_it/filename + # config_entry.unique_id/collection/collection_id/asset_id/file_name self.unique_id = parts[0] - self.album_id = parts[1] if len(parts) > 1 else None - self.asset_id = parts[2] if len(parts) > 2 else None - self.file_name = parts[3] if len(parts) > 2 else None + self.collection = parts[1] if len(parts) > 1 else None + self.collection_id = parts[2] if len(parts) > 2 else None + self.asset_id = parts[3] if len(parts) > 3 else None + self.file_name = parts[4] if len(parts) > 3 else None class ImmichMediaSource(MediaSource): @@ -87,6 +88,7 @@ class ImmichMediaSource(MediaSource): ) -> list[BrowseMediaSource]: """Handle browsing different immich instances.""" if not item.identifier: + LOGGER.debug("Render all Immich instances") return [ BrowseMediaSource( domain=DOMAIN, @@ -108,8 +110,22 @@ class ImmichMediaSource(MediaSource): assert entry immich_api = entry.runtime_data.api - if identifier.album_id is None: - # Get Albums + if identifier.collection is None: + LOGGER.debug("Render all collections for %s", entry.title) + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{identifier.unique_id}/albums", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaClass.IMAGE, + title="albums", + can_play=False, + can_expand=True, + ) + ] + + if identifier.collection_id is None: + LOGGER.debug("Render all albums for %s", entry.title) try: albums = await immich_api.albums.async_get_all_albums() except ImmichError: @@ -118,7 +134,7 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{item.identifier}/{album.album_id}", + identifier=f"{identifier.unique_id}/albums/{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title=album.name, @@ -129,10 +145,14 @@ class ImmichMediaSource(MediaSource): for album in albums ] - # Request items of album + LOGGER.debug( + "Render all assets of album %s for %s", + identifier.collection_id, + entry.title, + ) try: album_info = await immich_api.albums.async_get_album_info( - identifier.album_id + identifier.collection_id ) except ImmichError: return [] @@ -141,8 +161,8 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/" - f"{identifier.album_id}/" + f"{identifier.unique_id}/albums/" + f"{identifier.collection_id}/" f"{asset.asset_id}/" f"{asset.file_name}" ), @@ -161,8 +181,8 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/" - f"{identifier.album_id}/" + f"{identifier.unique_id}/albums/" + f"{identifier.collection_id}/" f"{asset.asset_id}/" f"{asset.file_name}" ), diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index c8da8d94eeb..0f448fbf23d 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -44,8 +44,8 @@ async def test_get_media_source(hass: HomeAssistant) -> None: ("identifier", "exception_msg"), [ ("unique_id", "No file name"), - ("unique_id/album_id", "No file name"), - ("unique_id/album_id/asset_id/filename", "No file extension"), + ("unique_id/albums/album_id", "No file name"), + ("unique_id/albums/album_id/asset_id/filename", "No file extension"), ], ) async def test_resolve_media_bad_identifier( @@ -64,12 +64,12 @@ async def test_resolve_media_bad_identifier( ("identifier", "url", "mime_type"), [ ( - "unique_id/album_id/asset_id/filename.jpg", + "unique_id/albums/album_id/asset_id/filename.jpg", "/immich/unique_id/asset_id/filename.jpg/fullsize", "image/jpeg", ), ( - "unique_id/album_id/asset_id/filename.png", + "unique_id/albums/album_id/asset_id/filename.png", "/immich/unique_id/asset_id/filename.png/fullsize", "image/png", ), @@ -95,13 +95,82 @@ async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: source = await async_get_media_source(hass) item = MediaSourceItem( - hass, DOMAIN, "unique_id/album_id/asset_id/filename.png", None + hass, DOMAIN, "unique_id/albums/album_id/asset_id/filename.png", None ) with pytest.raises(BrowseError, match="Immich is not configured"): await source.async_browse_media(item) -async def test_browse_media_album_error( +async def test_browse_media_get_root( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning root media sources.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + + # get root + item = MediaSourceItem(hass, DOMAIN, "", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "Someone" + assert media_file.media_content_id == ( + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" + ) + + # get collections + item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "albums" + assert media_file.media_content_id == ( + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums" + ) + + +async def test_browse_media_get_albums( + hass: HomeAssistant, + mock_immich: Mock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test browse_media returning albums.""" + assert await async_setup_component(hass, "media_source", {}) + + with patch("homeassistant.components.immich.PLATFORMS", []): + await setup_integration(hass, mock_config_entry) + + source = await async_get_media_source(hass) + item = MediaSourceItem( + hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None + ) + result = await source.async_browse_media(item) + + assert result + assert len(result.children) == 1 + media_file = result.children[0] + assert isinstance(media_file, BrowseMedia) + assert media_file.title == "My Album" + assert media_file.media_content_id == ( + "media-source://immich/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" + ) + + +async def test_browse_media_get_albums_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -124,7 +193,7 @@ async def test_browse_media_album_error( source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, mock_config_entry.unique_id, None) + item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None) result = await source.async_browse_media(item) assert result @@ -132,59 +201,7 @@ async def test_browse_media_album_error( assert len(result.children) == 0 -async def test_browse_media_get_root( - hass: HomeAssistant, - mock_immich: Mock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test browse_media returning root media sources.""" - assert await async_setup_component(hass, "media_source", {}) - - with patch("homeassistant.components.immich.PLATFORMS", []): - await setup_integration(hass, mock_config_entry) - - source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, "", None) - result = await source.async_browse_media(item) - - assert result - assert len(result.children) == 1 - media_file = result.children[0] - assert isinstance(media_file, BrowseMedia) - assert media_file.title == "Someone" - assert media_file.media_content_id == ( - "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e" - ) - - -async def test_browse_media_get_albums( - hass: HomeAssistant, - mock_immich: Mock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test browse_media returning albums.""" - assert await async_setup_component(hass, "media_source", {}) - - with patch("homeassistant.components.immich.PLATFORMS", []): - await setup_integration(hass, mock_config_entry) - - source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", None) - result = await source.async_browse_media(item) - - assert result - assert len(result.children) == 1 - media_file = result.children[0] - assert isinstance(media_file, BrowseMedia) - assert media_file.title == "My Album" - assert media_file.media_content_id == ( - "media-source://immich/" - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" - ) - - -async def test_browse_media_get_items_error( +async def test_browse_media_get_album_items_error( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -202,7 +219,7 @@ async def test_browse_media_get_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -223,7 +240,7 @@ async def test_browse_media_get_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -233,7 +250,7 @@ async def test_browse_media_get_items_error( assert len(result.children) == 0 -async def test_browse_media_get_items( +async def test_browse_media_get_album_items( hass: HomeAssistant, mock_immich: Mock, mock_config_entry: MockConfigEntry, @@ -249,7 +266,7 @@ async def test_browse_media_get_items( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -259,7 +276,7 @@ async def test_browse_media_get_items( media_file = result.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" ) @@ -276,7 +293,7 @@ async def test_browse_media_get_items( media_file = result.children[1] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" ) From 61823ec7e2f9eb840eadbbe4f7d7a6a9ca85d1df Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 27 May 2025 22:17:34 +0200 Subject: [PATCH 1000/1175] Fix dns resolver error in dnsip config flow validation (#145735) Fix dns resolver error in dnsip --- homeassistant/components/dnsip/config_flow.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py index e7b60d5bd6f..6b86f1627bc 100644 --- a/homeassistant/components/dnsip/config_flow.py +++ b/homeassistant/components/dnsip/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib -from typing import Any +from typing import Any, Literal import aiodns from aiodns.error import DNSError @@ -62,16 +62,16 @@ async def async_validate_hostname( """Validate hostname.""" async def async_check( - hostname: str, resolver: str, qtype: str, port: int = 53 + hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53 ) -> bool: """Return if able to resolve hostname.""" - result = False + result: bool = False with contextlib.suppress(DNSError): - result = bool( - await aiodns.DNSResolver( # type: ignore[call-overload] - nameservers=[resolver], udp_port=port, tcp_port=port - ).query(hostname, qtype) + _resolver = aiodns.DNSResolver( + nameservers=[resolver], udp_port=port, tcp_port=port ) + result = bool(await _resolver.query(hostname, qtype)) + return result result: dict[str, bool] = {} From e825bd0bdbbd060192fe6211eda515f5d05fb8f6 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 27 May 2025 22:58:46 +0200 Subject: [PATCH 1001/1175] Bump uiprotect to version 7.10.1 (#145737) Co-authored-by: Jan Bouwhuis --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index f825e0a5eaf..1cf2e4391e2 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a54a12ee9c4..c885e71a969 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.0 +uiprotect==7.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f381de177dd..0ea08f302a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2422,7 +2422,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.0 +uiprotect==7.10.1 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From fb833965221e625f16a383fdc29e69f31c57887c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 28 May 2025 14:56:47 +0100 Subject: [PATCH 1002/1175] Add Shelly zwave virtual integration (#145749) --- homeassistant/brands/shelly.json | 6 ++++++ homeassistant/generated/integrations.json | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 homeassistant/brands/shelly.json diff --git a/homeassistant/brands/shelly.json b/homeassistant/brands/shelly.json new file mode 100644 index 00000000000..94d683157ee --- /dev/null +++ b/homeassistant/brands/shelly.json @@ -0,0 +1,6 @@ +{ + "domain": "shelly", + "name": "shelly", + "integrations": ["shelly"], + "iot_standards": ["zwave"] +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4ae336f3c61..775272f77c4 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5867,10 +5867,18 @@ "iot_class": "local_push" }, "shelly": { - "name": "Shelly", - "integration_type": "device", - "config_flow": true, - "iot_class": "local_push" + "name": "shelly", + "integrations": { + "shelly": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push", + "name": "Shelly" + } + }, + "iot_standards": [ + "zwave" + ] }, "shodan": { "name": "Shodan", From 644a6f5569b8c8feddce1578f0fcff90bb8c0548 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 28 May 2025 08:43:59 +0200 Subject: [PATCH 1003/1175] Add more Amazon Devices DHCP matches (#145754) --- homeassistant/components/amazon_devices/manifest.json | 4 +++- homeassistant/generated/dhcp.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 606dec83150..7593fbd4943 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -13,6 +13,7 @@ { "macaddress": "50D45C*" }, { "macaddress": "50DCE7*" }, { "macaddress": "68F63B*" }, + { "macaddress": "6C0C9A*" }, { "macaddress": "74D637*" }, { "macaddress": "7C6166*" }, { "macaddress": "901195*" }, @@ -22,7 +23,8 @@ { "macaddress": "A8E621*" }, { "macaddress": "C095CF*" }, { "macaddress": "D8BE65*" }, - { "macaddress": "EC2BEB*" } + { "macaddress": "EC2BEB*" }, + { "macaddress": "F02F9E*" } ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 19fa6cc706a..6ef3051a953 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -62,6 +62,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "68F63B*", }, + { + "domain": "amazon_devices", + "macaddress": "6C0C9A*", + }, { "domain": "amazon_devices", "macaddress": "74D637*", @@ -102,6 +106,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "EC2BEB*", }, + { + "domain": "amazon_devices", + "macaddress": "F02F9E*", + }, { "domain": "august", "hostname": "connect", From 6d44daf599c7e969314506e84f1f0e2f62735a01 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 09:39:33 +0200 Subject: [PATCH 1004/1175] Bump pylamarzocco to 2.0.7 (#145763) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 6118e364c15..36a0a489e30 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.6"] + "requirements": ["pylamarzocco==2.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index c885e71a969..d8ffc983546 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.6 +pylamarzocco==2.0.7 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ea08f302a1..8cbe9918eab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1714,7 +1714,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.6 +pylamarzocco==2.0.7 # homeassistant.components.lastfm pylast==5.1.0 From f1ec0b2c596f25b3e84f0876d20669c9a7689886 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 28 May 2025 12:26:28 +0200 Subject: [PATCH 1005/1175] Handle late abort when creating subentry (#145765) * Handle late abort when creating subentry * Move error handling to the base class * Narrow down expected error in test --- homeassistant/data_entry_flow.py | 13 ++- .../components/config/test_config_entries.py | 82 +++++++++++++++++++ tests/test_config_entries.py | 2 +- tests/test_data_entry_flow.py | 31 ++++++- 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 9286f9c78f5..ce1c0806b14 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -543,8 +543,17 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]): flow.cur_step = result return result - # We pass a copy of the result because we're mutating our version - result = await self.async_finish_flow(flow, result.copy()) + try: + # We pass a copy of the result because we're mutating our version + result = await self.async_finish_flow(flow, result.copy()) + except AbortFlow as err: + result = self._flow_result( + type=FlowResultType.ABORT, + flow_id=flow.flow_id, + handler=flow.handler, + reason=err.reason, + description_placeholders=err.description_placeholders, + ) # _async_finish_flow may change result type, check it again if result["type"] == FlowResultType.FORM: diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6784866ea4b..c6e82976bf1 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1526,6 +1526,88 @@ async def test_subentry_reconfigure_flow(hass: HomeAssistant, client) -> None: } +async def test_subentry_flow_abort_duplicate(hass: HomeAssistant, client) -> None: + """Test we can handle a subentry flow raising due to unique_id collision.""" + + class TestFlow(core_ce.ConfigFlow): + class SubentryFlowHandler(core_ce.ConfigSubentryFlow): + async def async_step_user(self, user_input=None): + return await self.async_step_finish() + + async def async_step_finish(self, user_input=None): + if user_input: + return self.async_create_entry( + title="Mock title", data=user_input, unique_id="test" + ) + + return self.async_show_form( + step_id="finish", data_schema=vol.Schema({"enabled": bool}) + ) + + @classmethod + @callback + def async_get_supported_subentry_types( + cls, config_entry: core_ce.ConfigEntry + ) -> dict[str, type[core_ce.ConfigSubentryFlow]]: + return {"test": TestFlow.SubentryFlowHandler} + + mock_integration(hass, MockModule("test")) + mock_platform(hass, "test.config_flow", None) + MockConfigEntry( + domain="test", + entry_id="test1", + source="bla", + subentries_data=[ + core_ce.ConfigSubentryData( + data={}, + subentry_id="mock_id", + subentry_type="test", + title="Title", + unique_id="test", + ) + ], + ).add_to_hass(hass) + entry = hass.config_entries.async_entries()[0] + + with mock_config_flow("test", TestFlow): + url = "/api/config/config_entries/subentries/flow" + resp = await client.post(url, json={"handler": [entry.entry_id, "test"]}) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data.pop("flow_id") + assert data == { + "type": "form", + "handler": ["test1", "test"], + "step_id": "finish", + "data_schema": [{"name": "enabled", "type": "boolean"}], + "description_placeholders": None, + "errors": None, + "last_step": None, + "preview": None, + } + + with mock_config_flow("test", TestFlow): + resp = await client.post( + f"/api/config/config_entries/subentries/flow/{flow_id}", + json={"enabled": True}, + ) + assert resp.status == HTTPStatus.OK + + entries = hass.config_entries.async_entries("test") + assert len(entries) == 1 + + data = await resp.json() + data.pop("flow_id") + assert data == { + "handler": ["test1", "test"], + "reason": "already_configured", + "type": "abort", + "description_placeholders": None, + } + + async def test_subentry_does_not_support_reconfigure( hass: HomeAssistant, client: TestClient ) -> None: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ffff19f2c46..55b8434160e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2226,7 +2226,7 @@ async def test_entry_subentry_no_context( @pytest.mark.parametrize( ("unique_id", "expected_result"), - [(None, does_not_raise()), ("test", pytest.raises(HomeAssistantError))], + [(None, does_not_raise()), ("test", pytest.raises(data_entry_flow.AbortFlow))], ) async def test_entry_subentry_duplicate( hass: HomeAssistant, diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 961afd69c2d..a5908f0feab 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -886,8 +886,8 @@ async def test_show_progress_fires_only_when_changed( ) # change (description placeholder) -async def test_abort_flow_exception(manager: MockFlowManager) -> None: - """Test that the AbortFlow exception works.""" +async def test_abort_flow_exception_step(manager: MockFlowManager) -> None: + """Test that the AbortFlow exception works in a step.""" @manager.mock_reg_handler("test") class TestFlow(data_entry_flow.FlowHandler): @@ -900,6 +900,33 @@ async def test_abort_flow_exception(manager: MockFlowManager) -> None: assert form["description_placeholders"] == {"placeholder": "yo"} +async def test_abort_flow_exception_finish_flow(hass: HomeAssistant) -> None: + """Test that the AbortFlow exception works when finishing a flow.""" + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 1 + + async def async_step_init(self, input): + """Return init form with one input field 'count'.""" + return self.async_create_entry(title="init", data=input) + + class FlowManager(data_entry_flow.FlowManager): + async def async_create_flow(self, handler_key, *, context, data): + """Create a test flow.""" + return TestFlow() + + async def async_finish_flow(self, flow, result): + """Raise AbortFlow.""" + raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) + + manager = FlowManager(hass) + + form = await manager.async_init("test") + assert form["type"] == data_entry_flow.FlowResultType.ABORT + assert form["reason"] == "mock-reason" + assert form["description_placeholders"] == {"placeholder": "yo"} + + async def test_init_unknown_flow(manager: MockFlowManager) -> None: """Test that UnknownFlow is raised when async_create_flow returns None.""" From 13b487972307331a389b12d3b5af64a63f304e00 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 09:58:44 +0200 Subject: [PATCH 1006/1175] Deprecate dlib image processing integrations (#145767) --- .../components/dlib_face_detect/__init__.py | 2 + .../dlib_face_detect/image_processing.py | 23 ++++++++++- .../components/dlib_face_identify/__init__.py | 3 ++ .../dlib_face_identify/image_processing.py | 25 ++++++++++- requirements_test_all.txt | 4 ++ tests/components/dlib_face_detect/__init__.py | 1 + .../dlib_face_detect/test_image_processing.py | 37 +++++++++++++++++ .../components/dlib_face_identify/__init__.py | 1 + .../test_image_processing.py | 41 +++++++++++++++++++ 9 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tests/components/dlib_face_detect/__init__.py create mode 100644 tests/components/dlib_face_detect/test_image_processing.py create mode 100644 tests/components/dlib_face_identify/__init__.py create mode 100644 tests/components/dlib_face_identify/test_image_processing.py diff --git a/homeassistant/components/dlib_face_detect/__init__.py b/homeassistant/components/dlib_face_detect/__init__.py index a732132955f..0de082595ea 100644 --- a/homeassistant/components/dlib_face_detect/__init__.py +++ b/homeassistant/components/dlib_face_detect/__init__.py @@ -1 +1,3 @@ """The dlib_face_detect component.""" + +DOMAIN = "dlib_face_detect" diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 79f03ab3af7..9bd78f89653 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -11,10 +11,17 @@ from homeassistant.components.image_processing import ( ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import DOMAIN + PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA @@ -25,6 +32,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Dlib Face Detect", + }, + ) source: list[dict[str, str]] = config[CONF_SOURCE] add_entities( DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME)) diff --git a/homeassistant/components/dlib_face_identify/__init__.py b/homeassistant/components/dlib_face_identify/__init__.py index 79b9e4ec4bc..0e682d6b839 100644 --- a/homeassistant/components/dlib_face_identify/__init__.py +++ b/homeassistant/components/dlib_face_identify/__init__.py @@ -1 +1,4 @@ """The dlib_face_identify component.""" + +CONF_FACES = "faces" +DOMAIN = "dlib_face_identify" diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index c41dad863d4..c7c512c16d9 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -15,14 +15,20 @@ from homeassistant.components.image_processing import ( ImageProcessingFaceEntity, ) from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import CONF_FACES, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_FACES = "faces" PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend( { @@ -39,6 +45,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Dlib Face detection platform.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Dlib Face Identify", + }, + ) + confidence: float = config[CONF_CONFIDENCE] faces: dict[str, str] = config[CONF_FACES] source: list[dict[str, str]] = config[CONF_SOURCE] diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8cbe9918eab..24bb23a331a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -783,6 +783,10 @@ evolutionhttp==0.0.18 # homeassistant.components.faa_delays faadelays==2023.9.1 +# homeassistant.components.dlib_face_detect +# homeassistant.components.dlib_face_identify +# face-recognition==1.2.3 + # homeassistant.components.fastdotcom fastdotcom==0.0.3 diff --git a/tests/components/dlib_face_detect/__init__.py b/tests/components/dlib_face_detect/__init__.py new file mode 100644 index 00000000000..a732132955f --- /dev/null +++ b/tests/components/dlib_face_detect/__init__.py @@ -0,0 +1 @@ +"""The dlib_face_detect component.""" diff --git a/tests/components/dlib_face_detect/test_image_processing.py b/tests/components/dlib_face_detect/test_image_processing.py new file mode 100644 index 00000000000..e3b82a4cedf --- /dev/null +++ b/tests/components/dlib_face_detect/test_image_processing.py @@ -0,0 +1,37 @@ +"""Dlib Face Identity Image Processing Tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.dlib_face_detect import DOMAIN as DLIB_DOMAIN +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", face_recognition=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + IMAGE_PROCESSING_DOMAIN, + { + IMAGE_PROCESSING_DOMAIN: [ + { + CONF_PLATFORM: DLIB_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}", + ) in issue_registry.issues diff --git a/tests/components/dlib_face_identify/__init__.py b/tests/components/dlib_face_identify/__init__.py new file mode 100644 index 00000000000..79b9e4ec4bc --- /dev/null +++ b/tests/components/dlib_face_identify/__init__.py @@ -0,0 +1 @@ +"""The dlib_face_identify component.""" diff --git a/tests/components/dlib_face_identify/test_image_processing.py b/tests/components/dlib_face_identify/test_image_processing.py new file mode 100644 index 00000000000..f914baeffb9 --- /dev/null +++ b/tests/components/dlib_face_identify/test_image_processing.py @@ -0,0 +1,41 @@ +"""Dlib Face Identity Image Processing Tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.dlib_face_identify import ( + CONF_FACES, + DOMAIN as DLIB_DOMAIN, +) +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", face_recognition=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + IMAGE_PROCESSING_DOMAIN, + { + IMAGE_PROCESSING_DOMAIN: [ + { + CONF_PLATFORM: DLIB_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + CONF_FACES: {"person1": __file__}, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DLIB_DOMAIN}", + ) in issue_registry.issues From 74104cf1073bec7ee37791c7140aa33957d144e1 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 11:16:08 +0200 Subject: [PATCH 1007/1175] Deprecate GStreamer integration (#145768) --- .../components/gstreamer/__init__.py | 2 ++ .../components/gstreamer/media_player.py | 20 +++++++++-- requirements_test_all.txt | 3 ++ tests/components/gstreamer/__init__.py | 1 + .../components/gstreamer/test_media_player.py | 34 +++++++++++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/components/gstreamer/__init__.py create mode 100644 tests/components/gstreamer/test_media_player.py diff --git a/homeassistant/components/gstreamer/__init__.py b/homeassistant/components/gstreamer/__init__.py index 9fb97d25744..d24ac28f25f 100644 --- a/homeassistant/components/gstreamer/__init__.py +++ b/homeassistant/components/gstreamer/__init__.py @@ -1 +1,3 @@ """The gstreamer component.""" + +DOMAIN = "gstreamer" diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index bb78aff8faf..7d830377f1b 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -19,16 +19,18 @@ from homeassistant.components.media_player import ( async_process_play_media_url, ) from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) CONF_PIPELINE = "pipeline" -DOMAIN = "gstreamer" PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string} @@ -48,6 +50,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Gstreamer platform.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "GStreamer", + }, + ) name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24bb23a331a..e14b9109ab7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -945,6 +945,9 @@ growattServer==1.6.0 # homeassistant.components.google_sheets gspread==5.5.0 +# homeassistant.components.gstreamer +gstreamer-player==1.1.2 + # homeassistant.components.profiler guppy3==3.1.5 diff --git a/tests/components/gstreamer/__init__.py b/tests/components/gstreamer/__init__.py new file mode 100644 index 00000000000..56369257098 --- /dev/null +++ b/tests/components/gstreamer/__init__.py @@ -0,0 +1 @@ +"""Gstreamer tests.""" diff --git a/tests/components/gstreamer/test_media_player.py b/tests/components/gstreamer/test_media_player.py new file mode 100644 index 00000000000..9fcf8eb7cfc --- /dev/null +++ b/tests/components/gstreamer/test_media_player.py @@ -0,0 +1,34 @@ +"""Tests for the Gstreamer platform.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.gstreamer import DOMAIN as GSTREAMER_DOMAIN +from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", gsp=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + PLATFORM_DOMAIN, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: GSTREAMER_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{GSTREAMER_DOMAIN}", + ) in issue_registry.issues From 3f172233871327fc80e446ca5d15b80a21f6fcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Wed, 28 May 2025 10:57:01 +0200 Subject: [PATCH 1008/1175] Add more information about possible hostnames at Home Connect (#145770) --- homeassistant/components/home_connect/manifest.json | 4 ++-- homeassistant/generated/dhcp.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index e550d22e0ca..e8a36cd60d9 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -10,11 +10,11 @@ "macaddress": "C8D778*" }, { - "hostname": "(bosch|siemens)-*", + "hostname": "(balay|bosch|neff|siemens)-*", "macaddress": "68A40E*" }, { - "hostname": "siemens-*", + "hostname": "(siemens|neff)-*", "macaddress": "38B4D3*" } ], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 6ef3051a953..0fb8b48b01c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -367,12 +367,12 @@ DHCP: Final[list[dict[str, str | bool]]] = [ }, { "domain": "home_connect", - "hostname": "(bosch|siemens)-*", + "hostname": "(balay|bosch|neff|siemens)-*", "macaddress": "68A40E*", }, { "domain": "home_connect", - "hostname": "siemens-*", + "hostname": "(siemens|neff)-*", "macaddress": "38B4D3*", }, { From eb2728e5b981888c2dcefd628160ea7f2903cf66 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Wed, 28 May 2025 10:15:24 +0200 Subject: [PATCH 1009/1175] Fix uom for prebrew numbers in lamarzocco (#145772) --- homeassistant/components/lamarzocco/number.py | 4 ++-- tests/components/lamarzocco/snapshots/test_number.ambr | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lamarzocco/number.py b/homeassistant/components/lamarzocco/number.py index 7c4fe33a041..980a08c09ae 100644 --- a/homeassistant/components/lamarzocco/number.py +++ b/homeassistant/components/lamarzocco/number.py @@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( key="prebrew_on", translation_key="prebrew_time_on", device_class=NumberDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.MINUTES, + native_unit_of_measurement=UnitOfTime.SECONDS, native_step=PRECISION_TENTHS, native_min_value=0, native_max_value=10, @@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = ( key="prebrew_off", translation_key="prebrew_time_off", device_class=NumberDeviceClass.DURATION, - native_unit_of_measurement=UnitOfTime.MINUTES, + native_unit_of_measurement=UnitOfTime.SECONDS, native_step=PRECISION_TENTHS, native_min_value=0, native_max_value=10, diff --git a/tests/components/lamarzocco/snapshots/test_number.ambr b/tests/components/lamarzocco/snapshots/test_number.ambr index 85892521456..5f451695443 100644 --- a/tests/components/lamarzocco/snapshots/test_number.ambr +++ b/tests/components/lamarzocco/snapshots/test_number.ambr @@ -126,7 +126,7 @@ 'min': 0, 'mode': , 'step': 0.1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.mr012345_prebrew_off_time', @@ -173,7 +173,7 @@ 'supported_features': 0, 'translation_key': 'prebrew_time_off', 'unique_id': 'MR012345_prebrew_off', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_prebrew_on[Linea Micra] @@ -185,7 +185,7 @@ 'min': 0, 'mode': , 'step': 0.1, - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.mr012345_prebrew_on_time', @@ -232,7 +232,7 @@ 'supported_features': 0, 'translation_key': 'prebrew_time_on', 'unique_id': 'MR012345_prebrew_on', - 'unit_of_measurement': , + 'unit_of_measurement': , }) # --- # name: test_preinfusion[Linea Micra] From a53c786fe07ecd8a50c1bc627140d5a0f880ca59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 13:12:55 +0200 Subject: [PATCH 1010/1175] Deprecate pandora integration (#145785) --- homeassistant/components/pandora/__init__.py | 2 ++ .../components/pandora/media_player.py | 20 +++++++++++- requirements_test_all.txt | 5 +++ tests/components/pandora/__init__.py | 1 + tests/components/pandora/test_media_player.py | 31 +++++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/components/pandora/__init__.py create mode 100644 tests/components/pandora/test_media_player.py diff --git a/homeassistant/components/pandora/__init__.py b/homeassistant/components/pandora/__init__.py index 9664730bdab..0850b00553e 100644 --- a/homeassistant/components/pandora/__init__.py +++ b/homeassistant/components/pandora/__init__.py @@ -1 +1,3 @@ """The pandora component.""" + +DOMAIN = "pandora" diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 064b2930971..77564245522 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -27,10 +27,13 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -53,6 +56,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Pandora media player platform.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Pandora", + }, + ) + if not _pianobar_exists(): return pandora = PandoraMediaPlayer("Pandora") diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e14b9109ab7..f03e3c6bc6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1393,6 +1393,11 @@ peco==0.1.2 # homeassistant.components.escea pescea==1.0.12 +# homeassistant.components.aruba +# homeassistant.components.cisco_ios +# homeassistant.components.pandora +pexpect==4.9.0 + # homeassistant.components.modem_callerid phone-modem==0.1.1 diff --git a/tests/components/pandora/__init__.py b/tests/components/pandora/__init__.py new file mode 100644 index 00000000000..6fccecfd679 --- /dev/null +++ b/tests/components/pandora/__init__.py @@ -0,0 +1 @@ +"""Padora component tests.""" diff --git a/tests/components/pandora/test_media_player.py b/tests/components/pandora/test_media_player.py new file mode 100644 index 00000000000..2af72ba2224 --- /dev/null +++ b/tests/components/pandora/test_media_player.py @@ -0,0 +1,31 @@ +"""Pandora media player tests.""" + +from homeassistant.components.media_player import DOMAIN as PLATFORM_DOMAIN +from homeassistant.components.pandora import DOMAIN as PANDORA_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + PLATFORM_DOMAIN, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: PANDORA_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{PANDORA_DOMAIN}", + ) in issue_registry.issues From fbd05a0fcf3e833c5410a75eaa0cf6cd9030a9f8 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 18:28:37 +0200 Subject: [PATCH 1011/1175] Deprecate lirc integration (#145797) --- homeassistant/components/lirc/__init__.py | 17 ++++++++++++- requirements_test_all.txt | 3 +++ tests/components/lirc/__init__.py | 1 + tests/components/lirc/test_init.py | 31 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/components/lirc/__init__.py create mode 100644 tests/components/lirc/test_init.py diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index f5b26743a03..6b8e0d08d52 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -7,8 +7,9 @@ import time import lirc from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LIRC capability.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "LIRC", + }, + ) # blocking=True gives unexpected behavior (multiple responses for 1 press) # also by not blocking, we allow hass to shut down the thread gracefully # on exit. diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f03e3c6bc6a..a67f27dc6b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2012,6 +2012,9 @@ python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay python-linkplay==0.2.8 +# homeassistant.components.lirc +# python-lirc==1.2.3 + # homeassistant.components.matter python-matter-server==7.0.0 diff --git a/tests/components/lirc/__init__.py b/tests/components/lirc/__init__.py new file mode 100644 index 00000000000..f8e11b194a6 --- /dev/null +++ b/tests/components/lirc/__init__.py @@ -0,0 +1 @@ +"""LIRC tests.""" diff --git a/tests/components/lirc/test_init.py b/tests/components/lirc/test_init.py new file mode 100644 index 00000000000..d6fd7975c77 --- /dev/null +++ b/tests/components/lirc/test_init.py @@ -0,0 +1,31 @@ +"""Tests for the LIRC.""" + +from unittest.mock import Mock, patch + +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", lirc=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.lirc import ( # pylint: disable=import-outside-toplevel + DOMAIN as LIRC_DOMAIN, + ) + + assert await async_setup_component( + hass, + LIRC_DOMAIN, + { + LIRC_DOMAIN: {}, + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{LIRC_DOMAIN}", + ) in issue_registry.issues From 74102d03197f32e40773872fc4628c77c65a6ab1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 28 May 2025 16:33:20 +0200 Subject: [PATCH 1012/1175] Bump reolink-aio to 0.13.4 (#145799) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index a6f0b59426a..694dd43a532 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.3"] + "requirements": ["reolink-aio==0.13.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index d8ffc983546..da5d520c1a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.3 +reolink-aio==0.13.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67f27dc6b5..9cb25847a65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2180,7 +2180,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.3 +reolink-aio==0.13.4 # homeassistant.components.rflink rflink==0.0.66 From 83af5ec36bed410748e6574cb0f14c692c0f941f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 17:22:18 +0200 Subject: [PATCH 1013/1175] Deprecate keyboard integration (#145805) --- homeassistant/components/keyboard/__init__.py | 17 ++++++++++- requirements_test_all.txt | 3 ++ tests/components/keyboard/__init__.py | 1 + tests/components/keyboard/test_init.py | 29 +++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/components/keyboard/__init__.py create mode 100644 tests/components/keyboard/test_init.py diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index bf935f119d0..227472ff553 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -11,8 +11,9 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType DOMAIN = "keyboard" @@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Listen for keyboard events.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Keyboard", + }, + ) keyboard = PyKeyboard() keyboard.special_key_assignment() diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cb25847a65..58b27544486 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2098,6 +2098,9 @@ pytrydan==0.8.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 +# homeassistant.components.keyboard +# pyuserinput==0.1.11 + # homeassistant.components.vera pyvera==0.3.15 diff --git a/tests/components/keyboard/__init__.py b/tests/components/keyboard/__init__.py new file mode 100644 index 00000000000..7bc8a91511f --- /dev/null +++ b/tests/components/keyboard/__init__.py @@ -0,0 +1 @@ +"""Keyboard tests.""" diff --git a/tests/components/keyboard/test_init.py b/tests/components/keyboard/test_init.py new file mode 100644 index 00000000000..42a700a3d07 --- /dev/null +++ b/tests/components/keyboard/test_init.py @@ -0,0 +1,29 @@ +"""Keyboard tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", pykeyboard=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.keyboard import ( # pylint:disable=import-outside-toplevel + DOMAIN as KEYBOARD_DOMAIN, + ) + + assert await async_setup_component( + hass, + KEYBOARD_DOMAIN, + {KEYBOARD_DOMAIN: {}}, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{KEYBOARD_DOMAIN}", + ) in issue_registry.issues From 612861061cba5302aa08173a8b025756cfce2bc1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 28 May 2025 17:52:51 +0100 Subject: [PATCH 1014/1175] Fix HOMEASSISTANT_STOP unsubscribe in data update coordinator (#145809) * initial commit * a better approach * Add comment --- homeassistant/helpers/update_coordinator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7130264eb0d..bd85391f98f 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -138,6 +138,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): async def _on_hass_stop(_: Event) -> None: """Shutdown coordinator on HomeAssistant stop.""" + # Already cleared on EVENT_HOMEASSISTANT_STOP, via async_fire_internal + self._unsub_shutdown = None await self.async_shutdown() self._unsub_shutdown = self.hass.bus.async_listen_once( From 12f8ebb3eae35ab945b82442461d4baedec2d5ae Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 28 May 2025 13:14:13 -0500 Subject: [PATCH 1015/1175] Bump intents to 2025.5.28 (#145816) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2955bb96833..6078d73e99b 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==2.2.3", "home-assistant-intents==2025.5.7"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 378e9fdce83..c2c4a20b947 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250527.0 -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 7e1e98bdb24..78d09a61477 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.5.7", + "home-assistant-intents==2025.5.28", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index e4d1cc5ba30..403948d1445 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index da5d520c1a3..2716c6992cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ holidays==0.73 home-assistant-frontend==20250527.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58b27544486..ea2df4c3fe0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ holidays==0.73 home-assistant-frontend==20250527.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.7 +home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud homematicip==2.0.1.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 5ca638ef487..647755d8237 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.7 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 309acb961b4b1b223bf3822c20fdaf9db3739f6e Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 28 May 2025 20:49:20 +0200 Subject: [PATCH 1016/1175] Fix Immich media source browsing with multiple config entries (#145823) fix media source browsing with multiple config entries --- homeassistant/components/immich/media_source.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 0d0616875c6..9304039f297 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -30,11 +30,8 @@ LOGGER = getLogger(__name__) async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Immich media source.""" - entries = hass.config_entries.async_entries( - DOMAIN, include_disabled=False, include_ignore=False - ) hass.http.register_view(ImmichMediaView(hass)) - return ImmichMediaSource(hass, entries) + return ImmichMediaSource(hass) class ImmichMediaSourceIdentifier: @@ -56,18 +53,17 @@ class ImmichMediaSource(MediaSource): name = "Immich" - def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize Immich media source.""" super().__init__(DOMAIN) self.hass = hass - self.entries = entries async def async_browse_media( self, item: MediaSourceItem, ) -> BrowseMediaSource: """Return media.""" - if not self.hass.config_entries.async_loaded_entries(DOMAIN): + if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)): raise BrowseError("Immich is not configured") return BrowseMediaSource( domain=DOMAIN, @@ -79,12 +75,12 @@ class ImmichMediaSource(MediaSource): can_expand=True, children_media_class=MediaClass.DIRECTORY, children=[ - *await self._async_build_immich(item), + *await self._async_build_immich(item, entries), ], ) async def _async_build_immich( - self, item: MediaSourceItem + self, item: MediaSourceItem, entries: list[ConfigEntry] ) -> list[BrowseMediaSource]: """Handle browsing different immich instances.""" if not item.identifier: @@ -99,7 +95,7 @@ class ImmichMediaSource(MediaSource): can_play=False, can_expand=True, ) - for entry in self.entries + for entry in entries ] identifier = ImmichMediaSourceIdentifier(item.identifier) entry: ImmichConfigEntry | None = ( From d0d228d9f4dac8cb1535b45a5580921f6ea85e9f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 May 2025 23:17:14 +0200 Subject: [PATCH 1017/1175] Update frontend to 20250528.0 (#145828) Co-authored-by: Robert Resch --- 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 32d243b3431..3ee40e1ce60 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==20250527.0"] + "requirements": ["home-assistant-frontend==20250528.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c2c4a20b947..33766ec52c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2716c6992cb..2677eedbb39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea2df4c3fe0..185e51d8a48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250527.0 +home-assistant-frontend==20250528.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 From 17a0b4f3d0942f5bd6f60f8da2d87db05579f79e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 28 May 2025 23:18:38 +0200 Subject: [PATCH 1018/1175] Bump version to 2025.6.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 6b3cbb4f27c..5a6ac701771 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 78d09a61477..04dcb3e3057 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b1" +version = "2025.6.0b2" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From e0d3b819e5153073e69321318e52a6e6301f5d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lersveen?= <7195448+lersveen@users.noreply.github.com> Date: Thu, 29 May 2025 03:52:05 +0200 Subject: [PATCH 1019/1175] Set correct nobo_hub max temperature (#145751) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Max temperature 30°C is implemented upstream in pynobo and the Nobø Energy Hub app also stops at 30°C. --- homeassistant/components/nobo_hub/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py index 771da420213..018f3e2b06a 100644 --- a/homeassistant/components/nobo_hub/climate.py +++ b/homeassistant/components/nobo_hub/climate.py @@ -40,7 +40,7 @@ SUPPORT_FLAGS = ( PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] MIN_TEMPERATURE = 7 -MAX_TEMPERATURE = 40 +MAX_TEMPERATURE = 30 async def async_setup_entry( From fa66ea31d398af81f14f2a12aa31a49a5550e603 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 29 May 2025 15:35:35 +0200 Subject: [PATCH 1020/1175] Deprecate tensorflow (#145806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Abílio Costa --- .../components/tensorflow/__init__.py | 3 ++ .../components/tensorflow/image_processing.py | 26 ++++++++++-- requirements_test_all.txt | 9 +++++ tests/components/tensorflow/__init__.py | 1 + .../tensorflow/test_image_processing.py | 40 +++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/components/tensorflow/__init__.py create mode 100644 tests/components/tensorflow/test_image_processing.py diff --git a/homeassistant/components/tensorflow/__init__.py b/homeassistant/components/tensorflow/__init__.py index 00a695d6aa8..7ed20cbe4b6 100644 --- a/homeassistant/components/tensorflow/__init__.py +++ b/homeassistant/components/tensorflow/__init__.py @@ -1 +1,4 @@ """The tensorflow component.""" + +DOMAIN = "tensorflow" +CONF_GRAPH = "graph" diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 0fb069e8da8..05be56d444d 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -26,15 +26,21 @@ from homeassistant.const import ( CONF_SOURCE, EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + split_entity_id, +) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.pil import draw_box +from . import CONF_GRAPH, DOMAIN + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" -DOMAIN = "tensorflow" _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" @@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom" CONF_CATEGORIES = "categories" CONF_CATEGORY = "category" CONF_FILE_OUT = "file_out" -CONF_GRAPH = "graph" CONF_LABELS = "labels" CONF_LABEL_OFFSET = "label_offset" CONF_LEFT = "left" @@ -110,6 +115,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the TensorFlow image processing platform.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Tensorflow", + }, + ) + model_config = config[CONF_MODEL] model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow") labels = model_config.get(CONF_LABELS) or hass.config.path( diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 185e51d8a48..a5a927e94bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1560,6 +1560,9 @@ pybravia==0.3.4 # homeassistant.components.cloudflare pycfdns==3.0.0 +# homeassistant.components.tensorflow +# pycocotools==2.0.6 + # homeassistant.components.comfoconnect pycomfoconnect==0.5.1 @@ -2365,6 +2368,9 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.1 +# homeassistant.components.tensorflow +# tensorflow==2.5.0 + # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie @@ -2382,6 +2388,9 @@ teslemetry-stream==0.7.9 # homeassistant.components.tessie tessie-api==0.1.1 +# homeassistant.components.tensorflow +# tf-models-official==2.5.0 + # homeassistant.components.thermobeacon thermobeacon-ble==0.10.0 diff --git a/tests/components/tensorflow/__init__.py b/tests/components/tensorflow/__init__.py new file mode 100644 index 00000000000..458de30c9fa --- /dev/null +++ b/tests/components/tensorflow/__init__.py @@ -0,0 +1 @@ +"""TensorFlow component tests.""" diff --git a/tests/components/tensorflow/test_image_processing.py b/tests/components/tensorflow/test_image_processing.py new file mode 100644 index 00000000000..06199b9c60c --- /dev/null +++ b/tests/components/tensorflow/test_image_processing.py @@ -0,0 +1,40 @@ +"""Tensorflow test.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.image_processing import DOMAIN as IMAGE_PROCESSING_DOMAINN +from homeassistant.components.tensorflow import CONF_GRAPH, DOMAIN as TENSORFLOW_DOMAIN +from homeassistant.const import CONF_ENTITY_ID, CONF_MODEL, CONF_PLATFORM, CONF_SOURCE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", tensorflow=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + IMAGE_PROCESSING_DOMAINN, + { + IMAGE_PROCESSING_DOMAINN: [ + { + CONF_PLATFORM: TENSORFLOW_DOMAIN, + CONF_SOURCE: [ + {CONF_ENTITY_ID: "camera.test_camera"}, + ], + CONF_MODEL: { + CONF_GRAPH: ".", + }, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{TENSORFLOW_DOMAIN}", + ) in issue_registry.issues From 95fb2a7d7f466e15a7916114aa31d9f16ca677d5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 28 May 2025 23:54:48 +0200 Subject: [PATCH 1021/1175] Deprecate decora integration (#145807) --- homeassistant/components/decora/__init__.py | 2 ++ homeassistant/components/decora/light.py | 19 ++++++++++++ requirements_test_all.txt | 6 ++++ tests/components/decora/__init__.py | 1 + tests/components/decora/test_light.py | 34 +++++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 tests/components/decora/__init__.py create mode 100644 tests/components/decora/test_light.py diff --git a/homeassistant/components/decora/__init__.py b/homeassistant/components/decora/__init__.py index 694ff77fdb3..4ba4fb4dee0 100644 --- a/homeassistant/components/decora/__init__.py +++ b/homeassistant/components/decora/__init__.py @@ -1 +1,3 @@ """The decora component.""" + +DOMAIN = "decora" diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index a7d14b83aca..d0226a24dcc 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -21,7 +21,11 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue + +from . import DOMAIN if TYPE_CHECKING: from homeassistant.core import HomeAssistant @@ -90,6 +94,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up an Decora switch.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Leviton Decora", + }, + ) + lights = [] for address, device_config in config[CONF_DEVICES].items(): device = {} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5a927e94bd..263832d2bc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,6 +561,9 @@ bluecurrent-api==1.2.3 # homeassistant.components.bluemaestro bluemaestro-ble==0.4.1 +# homeassistant.components.decora +# bluepy==1.3.0 + # homeassistant.components.bluetooth bluetooth-adapters==0.21.4 @@ -655,6 +658,9 @@ dbus-fast==2.43.0 # homeassistant.components.debugpy debugpy==1.8.14 +# homeassistant.components.decora +# decora==0.6 + # homeassistant.components.ecovacs deebot-client==13.2.1 diff --git a/tests/components/decora/__init__.py b/tests/components/decora/__init__.py new file mode 100644 index 00000000000..399b353aa0c --- /dev/null +++ b/tests/components/decora/__init__.py @@ -0,0 +1 @@ +"""Decora component tests.""" diff --git a/tests/components/decora/test_light.py b/tests/components/decora/test_light.py new file mode 100644 index 00000000000..6315d6c3986 --- /dev/null +++ b/tests/components/decora/test_light.py @@ -0,0 +1,34 @@ +"""Decora component tests.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.decora import DOMAIN as DECORA_DOMAIN +from homeassistant.components.light import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", {"bluepy": Mock(), "bluepy.btle": Mock(), "decora": Mock()}) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + PLATFORM_DOMAIN, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: DECORA_DOMAIN, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DECORA_DOMAIN}", + ) in issue_registry.issues From 26586b451435e02785e94cc8a3fff764e2bd6eef Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 29 May 2025 15:28:54 +0200 Subject: [PATCH 1022/1175] Fix language selections in workday (#145813) --- .../components/workday/binary_sensor.py | 44 ++++++++++++++++-- .../components/workday/config_flow.py | 17 +------ .../components/workday/test_binary_sensor.py | 46 +++++++++++++++++++ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 6b878db8159..a48e19e59b2 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -94,21 +94,59 @@ def _get_obj_holidays( language=language, categories=set_categories, ) + + supported_languages = obj_holidays.supported_languages + default_language = obj_holidays.default_language + + if default_language and not language: + # If no language is set, use the default language + LOGGER.debug("Changing language from None to %s", default_language) + return country_holidays( # Return default if no language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + if ( - (supported_languages := obj_holidays.supported_languages) + default_language and language + and language not in supported_languages and language.startswith("en") ): + # If language does not match supported languages, use the first English variant + if default_language.startswith("en"): + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) for lang in supported_languages: if lang.startswith("en"): - obj_holidays = country_holidays( + LOGGER.debug("Changing language from %s to %s", language, lang) + return country_holidays( country, subdiv=province, years=year, language=lang, categories=set_categories, ) - LOGGER.debug("Changing language from %s to %s", language, lang) + + if default_language and language and language not in supported_languages: + # If language does not match supported languages, use the default language + LOGGER.debug("Changing language from %s to %s", language, default_language) + return country_holidays( # Return default English if default language + country, + subdiv=province, + years=year, + language=default_language, + categories=set_categories, + ) + return obj_holidays diff --git a/homeassistant/components/workday/config_flow.py b/homeassistant/components/workday/config_flow.py index b0b1e9fcc02..7a8a8181a9f 100644 --- a/homeassistant/components/workday/config_flow.py +++ b/homeassistant/components/workday/config_flow.py @@ -67,8 +67,7 @@ def add_province_and_language_to_schema( _country = country_holidays(country=country) if country_default_language := (_country.default_language): - selectable_languages = _country.supported_languages - new_selectable_languages = list(selectable_languages) + new_selectable_languages = list(_country.supported_languages) language_schema = { vol.Optional( CONF_LANGUAGE, default=country_default_language @@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None: years=year, language=language, ) - if ( - (supported_languages := obj_holidays.supported_languages) - and language - and language.startswith("en") - ): - for lang in supported_languages: - if lang.startswith("en"): - obj_holidays = country_holidays( - country, - subdiv=province, - years=year, - language=lang, - ) + else: obj_holidays = HolidayBase(years=year) diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 212c3e9d305..8f8894e3536 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -461,3 +461,49 @@ async def test_only_repairs_for_current_next_year( assert len(issue_registry.issues) == 2 assert issue_registry.issues == snapshot + + +async def test_missing_language( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when language exist but is empty.""" + config = { + "add_holidays": [], + "country": "AU", + "days_offset": 0, + "excludes": ["sat", "sun", "holiday"], + "language": None, + "name": "Workday Sensor", + "platform": "workday", + "province": "QLD", + "remove_holidays": [ + "Labour Day", + ], + "workdays": ["mon", "tue", "wed", "thu", "fri"], + } + await init_integration(hass, config) + assert "Changing language from None to en_AU" in caplog.text + + +async def test_incorrect_english_variant( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test when language exist but is empty.""" + config = { + "add_holidays": [], + "country": "AU", + "days_offset": 0, + "excludes": ["sat", "sun", "holiday"], + "language": "en_UK", # Incorrect variant + "name": "Workday Sensor", + "platform": "workday", + "province": "QLD", + "remove_holidays": [ + "Labour Day", + ], + "workdays": ["mon", "tue", "wed", "thu", "fri"], + } + await init_integration(hass, config) + assert "Changing language from en_UK to en_AU" in caplog.text From 4d22b35a9f109f914a2210617b32dd24643b9ec3 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Thu, 29 May 2025 09:35:05 +0200 Subject: [PATCH 1023/1175] Bump aiotedee to 0.2.23 (#145822) * Bump aiotedee to 0.2.23 * update snapshot --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tedee/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index bca51f08f93..012e82318ed 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["aiotedee"], "quality_scale": "platinum", - "requirements": ["aiotedee==0.2.20"] + "requirements": ["aiotedee==0.2.23"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2677eedbb39..0aad41a4bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.2 # homeassistant.components.tedee -aiotedee==0.2.20 +aiotedee==0.2.23 # homeassistant.components.tractive aiotractive==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 263832d2bc1..d358800b7f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -387,7 +387,7 @@ aiosyncthing==0.5.1 aiotankerkoenig==0.4.2 # homeassistant.components.tedee -aiotedee==0.2.20 +aiotedee==0.2.23 # homeassistant.components.tractive aiotractive==0.6.0 diff --git a/tests/components/tedee/snapshots/test_diagnostics.ambr b/tests/components/tedee/snapshots/test_diagnostics.ambr index 401c519c215..046a8fd210a 100644 --- a/tests/components/tedee/snapshots/test_diagnostics.ambr +++ b/tests/components/tedee/snapshots/test_diagnostics.ambr @@ -6,6 +6,7 @@ 'duration_pullspring': 2, 'is_charging': False, 'is_connected': True, + 'is_enabled_auto_pullspring': False, 'is_enabled_pullspring': 1, 'lock_id': '**REDACTED**', 'lock_name': 'Lock-1A2B', @@ -18,6 +19,7 @@ 'duration_pullspring': 0, 'is_charging': False, 'is_connected': True, + 'is_enabled_auto_pullspring': False, 'is_enabled_pullspring': 0, 'lock_id': '**REDACTED**', 'lock_name': 'Lock-2C3D', From 0e87d14ca8890c7427432e37914e8588b2fb45d3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 May 2025 10:28:02 +0200 Subject: [PATCH 1024/1175] Use mime type provided by Immich (#145830) use mime type from immich instead of guessing it --- .../components/immich/media_source.py | 68 ++++++++++-------- tests/components/immich/test_media_source.py | 69 +++++++++++-------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index 9304039f297..a7c55f9c572 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -3,7 +3,6 @@ from __future__ import annotations from logging import getLogger -import mimetypes from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse from aioimmich.exceptions import ImmichError @@ -39,13 +38,14 @@ class ImmichMediaSourceIdentifier: def __init__(self, identifier: str) -> None: """Split identifier into parts.""" - parts = identifier.split("/") - # config_entry.unique_id/collection/collection_id/asset_id/file_name + parts = identifier.split("|") + # config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type self.unique_id = parts[0] self.collection = parts[1] if len(parts) > 1 else None self.collection_id = parts[2] if len(parts) > 2 else None self.asset_id = parts[3] if len(parts) > 3 else None self.file_name = parts[4] if len(parts) > 3 else None + self.mime_type = parts[5] if len(parts) > 3 else None class ImmichMediaSource(MediaSource): @@ -111,7 +111,7 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/albums", + identifier=f"{identifier.unique_id}|albums", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title="albums", @@ -130,13 +130,13 @@ class ImmichMediaSource(MediaSource): return [ BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/albums/{album.album_id}", + identifier=f"{identifier.unique_id}|albums|{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, title=album.name, can_play=False, can_expand=True, - thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg", ) for album in albums ] @@ -157,17 +157,18 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/albums/" - f"{identifier.collection_id}/" - f"{asset.asset_id}/" - f"{asset.file_name}" + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.file_name}|" + f"{asset.mime_type}" ), media_class=MediaClass.IMAGE, media_content_type=asset.mime_type, title=asset.file_name, can_play=False, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}", ) for asset in album_info.assets if asset.mime_type.startswith("image/") @@ -177,17 +178,18 @@ class ImmichMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=( - f"{identifier.unique_id}/albums/" - f"{identifier.collection_id}/" - f"{asset.asset_id}/" - f"{asset.file_name}" + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.file_name}|" + f"{asset.mime_type}" ), media_class=MediaClass.VIDEO, media_content_type=asset.mime_type, title=asset.file_name, can_play=True, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", ) for asset in album_info.assets if asset.mime_type.startswith("video/") @@ -197,17 +199,23 @@ class ImmichMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - identifier = ImmichMediaSourceIdentifier(item.identifier) - if identifier.file_name is None: - raise Unresolvable("No file name") - mime_type, _ = mimetypes.guess_type(identifier.file_name) - if not isinstance(mime_type, str): - raise Unresolvable("No file extension") + try: + identifier = ImmichMediaSourceIdentifier(item.identifier) + except IndexError as err: + raise Unresolvable( + f"Could not parse identifier: {item.identifier}" + ) from err + + if identifier.mime_type is None: + raise Unresolvable( + f"Could not resolve identifier that has no mime-type: {item.identifier}" + ) + return PlayMedia( ( - f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize" + f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}" ), - mime_type, + identifier.mime_type, ) @@ -228,10 +236,10 @@ class ImmichMediaView(HomeAssistantView): if not self.hass.config_entries.async_loaded_entries(DOMAIN): raise HTTPNotFound - asset_id, file_name, size = location.split("/") - mime_type, _ = mimetypes.guess_type(file_name) - if not isinstance(mime_type, str): - raise HTTPNotFound + try: + asset_id, size, mime_type_base, mime_type_format = location.split("/") + except ValueError as err: + raise HTTPNotFound from err entry: ImmichConfigEntry | None = ( self.hass.config_entries.async_entry_for_domain_unique_id( @@ -242,7 +250,7 @@ class ImmichMediaView(HomeAssistantView): immich_api = entry.runtime_data.api # stream response for videos - if mime_type.startswith("video/"): + if mime_type_base == "video": try: resp = await immich_api.assets.async_play_video_stream(asset_id) except ImmichError as exc: @@ -259,4 +267,4 @@ class ImmichMediaView(HomeAssistantView): image = await immich_api.assets.async_view_asset(asset_id, size) except ImmichError as exc: raise HTTPNotFound from exc - return Response(body=image, content_type=mime_type) + return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}") diff --git a/tests/components/immich/test_media_source.py b/tests/components/immich/test_media_source.py index 0f448fbf23d..5b396a780cc 100644 --- a/tests/components/immich/test_media_source.py +++ b/tests/components/immich/test_media_source.py @@ -43,9 +43,15 @@ async def test_get_media_source(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("identifier", "exception_msg"), [ - ("unique_id", "No file name"), - ("unique_id/albums/album_id", "No file name"), - ("unique_id/albums/album_id/asset_id/filename", "No file extension"), + ("unique_id", "Could not resolve identifier that has no mime-type"), + ( + "unique_id|albums|album_id", + "Could not resolve identifier that has no mime-type", + ), + ( + "unique_id|albums|album_id|asset_id|filename", + "Could not parse identifier", + ), ], ) async def test_resolve_media_bad_identifier( @@ -64,15 +70,20 @@ async def test_resolve_media_bad_identifier( ("identifier", "url", "mime_type"), [ ( - "unique_id/albums/album_id/asset_id/filename.jpg", - "/immich/unique_id/asset_id/filename.jpg/fullsize", + "unique_id|albums|album_id|asset_id|filename.jpg|image/jpeg", + "/immich/unique_id/asset_id/fullsize/image/jpeg", "image/jpeg", ), ( - "unique_id/albums/album_id/asset_id/filename.png", - "/immich/unique_id/asset_id/filename.png/fullsize", + "unique_id|albums|album_id|asset_id|filename.png|image/png", + "/immich/unique_id/asset_id/fullsize/image/png", "image/png", ), + ( + "unique_id|albums|album_id|asset_id|filename.mp4|video/mp4", + "/immich/unique_id/asset_id/fullsize/video/mp4", + "video/mp4", + ), ], ) async def test_resolve_media_success( @@ -137,7 +148,7 @@ async def test_browse_media_get_root( assert isinstance(media_file, BrowseMedia) assert media_file.title == "albums" assert media_file.media_content_id == ( - "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums" + "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums" ) @@ -154,7 +165,7 @@ async def test_browse_media_get_albums( source = await async_get_media_source(hass) item = MediaSourceItem( - hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None + hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums", None ) result = await source.async_browse_media(item) @@ -165,7 +176,7 @@ async def test_browse_media_get_albums( assert media_file.title == "My Album" assert media_file.media_content_id == ( "media-source://immich/" - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" ) @@ -193,7 +204,7 @@ async def test_browse_media_get_albums_error( source = await async_get_media_source(hass) - item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None) + item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}|albums", None) result = await source.async_browse_media(item) assert result @@ -219,7 +230,7 @@ async def test_browse_media_get_album_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -240,7 +251,7 @@ async def test_browse_media_get_album_items_error( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -266,7 +277,7 @@ async def test_browse_media_get_album_items( item = MediaSourceItem( hass, DOMAIN, - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", None, ) result = await source.async_browse_media(item) @@ -276,9 +287,9 @@ async def test_browse_media_get_album_items( media_file = result.children[0] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4|filename.jpg|image/jpeg" ) assert media_file.title == "filename.jpg" assert media_file.media_class == MediaClass.IMAGE @@ -287,15 +298,15 @@ async def test_browse_media_get_album_items( assert not media_file.can_expand assert media_file.thumbnail == ( "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail" + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg" ) media_file = result.children[1] assert isinstance(media_file, BrowseMedia) assert media_file.identifier == ( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" + "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|" + "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b|filename.mp4|video/mp4" ) assert media_file.title == "filename.mp4" assert media_file.media_class == MediaClass.VIDEO @@ -304,7 +315,7 @@ async def test_browse_media_get_album_items( assert not media_file.can_expand assert media_file.thumbnail == ( "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail.jpg/thumbnail" + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail/image/jpeg" ) @@ -327,12 +338,12 @@ async def test_media_view( with patch("homeassistant.components.immich.PLATFORMS", []): await setup_integration(hass, mock_config_entry) - # wrong url (without file extension) + # wrong url (without mime type) with pytest.raises(web.HTTPNotFound): await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail", ) # exception in async_view_asset() @@ -348,7 +359,7 @@ async def test_media_view( await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) # exception in async_play_video_stream() @@ -364,7 +375,7 @@ async def test_media_view( await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) # success @@ -374,14 +385,14 @@ async def test_media_view( result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg", ) assert isinstance(result, web.Response) with patch.object(tempfile, "tempdir", tmp_path): result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize", + "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/fullsize/image/jpeg", ) assert isinstance(result, web.Response) @@ -393,6 +404,6 @@ async def test_media_view( result = await view.get( request, "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", + "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4", ) assert isinstance(result, web.StreamResponse) From 64b4642c496caf1043a7d24808429242994d8730 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Thu, 29 May 2025 11:58:29 +1000 Subject: [PATCH 1025/1175] Fix Tessie volume max and step (#145835) * Use fixed volume max and step * Update snapshot --- homeassistant/components/tessie/media_player.py | 9 ++++++--- tests/components/tessie/snapshots/test_media_player.ambr | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py index 139ee07ca5b..ecac11587c1 100644 --- a/homeassistant/components/tessie/media_player.py +++ b/homeassistant/components/tessie/media_player.py @@ -20,6 +20,10 @@ STATES = { "Stopped": MediaPlayerState.IDLE, } +# Tesla uses 31 steps, in 0.333 increments up to 10.333 +VOLUME_STEP = 1 / 31 +VOLUME_FACTOR = 31 / 3 # 10.333 + PARALLEL_UPDATES = 0 @@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): """Vehicle Location Media Class.""" _attr_device_class = MediaPlayerDeviceClass.SPEAKER + _attr_volume_step = VOLUME_STEP def __init__( self, @@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity): @property def volume_level(self) -> float: """Volume level of the media player (0..1).""" - return self.get("vehicle_state_media_info_audio_volume", 0) / self.get( - "vehicle_state_media_info_audio_volume_max", 10.333333 - ) + return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR @property def media_duration(self) -> int | None: diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr index ff0f6c794a7..69a5ca4b86b 100644 --- a/tests/components/tessie/snapshots/test_media_player.ambr +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -41,7 +41,7 @@ 'device_class': 'speaker', 'friendly_name': 'Test Media player', 'supported_features': , - 'volume_level': 0.22580323309042688, + 'volume_level': 0.2258032258064516, }), 'context': , 'entity_id': 'media_player.test_media_player', @@ -64,7 +64,7 @@ 'media_title': 'Song', 'source': 'Spotify', 'supported_features': , - 'volume_level': 0.22580323309042688, + 'volume_level': 0.2258032258064516, }), 'context': , 'entity_id': 'media_player.test_media_player', From 097eecd78a28b3f2183de940d2155edfc930c965 Mon Sep 17 00:00:00 2001 From: Matthew FitzGerald-Chamberlain Date: Wed, 28 May 2025 20:43:28 -0500 Subject: [PATCH 1026/1175] Bump pyaprilaire to 0.9.1 (#145836) --- homeassistant/components/aprilaire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aprilaire/manifest.json b/homeassistant/components/aprilaire/manifest.json index 6fe3beae3bc..fa30882f669 100644 --- a/homeassistant/components/aprilaire/manifest.json +++ b/homeassistant/components/aprilaire/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyaprilaire"], - "requirements": ["pyaprilaire==0.9.0"] + "requirements": ["pyaprilaire==0.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0aad41a4bcc..8a9ce0e353a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1832,7 +1832,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.9.0 +pyaprilaire==0.9.1 # homeassistant.components.asuswrt pyasuswrt==0.1.21 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d358800b7f4..142ba2e57e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1531,7 +1531,7 @@ pyairnow==1.2.1 pyairvisual==2023.08.1 # homeassistant.components.aprilaire -pyaprilaire==0.9.0 +pyaprilaire==0.9.1 # homeassistant.components.asuswrt pyasuswrt==0.1.21 From 5cfccb7e1d7dddccc41619bb5cc10462f65ffaea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 19:38:06 -0500 Subject: [PATCH 1027/1175] Bump aiohttp to 3.12.3 (#145837) --- 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 33766ec52c4..89c2d39edd8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.2 +aiohttp==3.12.3 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 04dcb3e3057..4a7022b7b67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.2", + "aiohttp==3.12.3", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 403948d1445..bc044454da4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.2 +aiohttp==3.12.3 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 4317fad79858f23c89c8ef2d74663d4e8936527a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 May 2025 21:08:01 -0500 Subject: [PATCH 1028/1175] Bump aiohttp to 3.12.4 (#145838) --- 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 89c2d39edd8..8dde4a6a654 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.3 +aiohttp==3.12.4 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 4a7022b7b67..331381def64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.3", + "aiohttp==3.12.4", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index bc044454da4..f1ced51637e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.3 +aiohttp==3.12.4 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 0f7379c941feba20e4930b4e1d838fff3d6dfeb3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 29 May 2025 15:31:50 +0200 Subject: [PATCH 1029/1175] Reolink fallback to download command for playback (#145842) --- homeassistant/components/reolink/views.py | 27 ++++++++++++++++++++++- tests/components/reolink/test_views.py | 7 +++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/views.py b/homeassistant/components/reolink/views.py index 44265244b18..7f062055f7e 100644 --- a/homeassistant/components/reolink/views.py +++ b/homeassistant/components/reolink/views.py @@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView): verify_ssl=False, ssl_cipher=SSLCipherList.INSECURE, ) + self._vod_type: str | None = None async def get( self, @@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView): filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8") ch = int(channel) + if self._vod_type is not None: + vod_type = self._vod_type try: host = get_host(self.hass, config_entry_id) except Unresolvable: @@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView): "apolication/octet-stream", ]: err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}" + if ( + reolink_response.content_type == "video/x-flv" + and vod_type == VodRequestType.PLAYBACK.value + ): + # next time use DOWNLOAD immediately + self._vod_type = VodRequestType.DOWNLOAD.value + _LOGGER.debug( + "%s, retrying using download instead of playback cmd", err_str + ) + return await self.get( + request, + config_entry_id, + channel, + stream_res, + self._vod_type, + filename, + retry, + ) + _LOGGER.error(err_str) if reolink_response.content_type == "text/html": text = await reolink_response.text() @@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView): reolink_response.reason, response_headers, ) - response_headers["Content-Type"] = "video/mp4" + if "Content-Type" not in response_headers: + response_headers["Content-Type"] = reolink_response.content_type + if response_headers["Content-Type"] == "apolication/octet-stream": + response_headers["Content-Type"] = "application/octet-stream" response = web.StreamResponse( status=reolink_response.status, diff --git a/tests/components/reolink/test_views.py b/tests/components/reolink/test_views.py index 3521de072b6..992e47f0575 100644 --- a/tests/components/reolink/test_views.py +++ b/tests/components/reolink/test_views.py @@ -58,17 +58,22 @@ def get_mock_session( return mock_session +@pytest.mark.parametrize( + ("content_type"), + [("video/mp4"), ("application/octet-stream"), ("apolication/octet-stream")], +) async def test_playback_proxy( hass: HomeAssistant, reolink_connect: MagicMock, config_entry: MockConfigEntry, hass_client: ClientSessionGenerator, caplog: pytest.LogCaptureFixture, + content_type: str, ) -> None: """Test successful playback proxy URL.""" reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) - mock_session = get_mock_session() + mock_session = get_mock_session(content_type=content_type) with patch( "homeassistant.components.reolink.views.async_get_clientsession", From d46f28792c3f1739d87568a77bb220131b2c1800 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 29 May 2025 15:30:02 +0200 Subject: [PATCH 1030/1175] Bump aioimmich to 0.7.0 (#145845) --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 454adae5501..5b56a7e3e2d 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.6.0"] + "requirements": ["aioimmich==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a9ce0e353a..6aeaf8b077f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.6.0 +aioimmich==0.7.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 142ba2e57e9..ae0b8ee7cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.6.0 +aioimmich==0.7.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From 600ac17a5f459e922b0d89c41aee4a4f5f672c81 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 29 May 2025 14:12:51 +0200 Subject: [PATCH 1031/1175] Deprecate sms integration (#145847) --- .../components/homeassistant/strings.json | 4 ++ homeassistant/components/sms/__init__.py | 24 +++++++- tests/components/sms/test_init.py | 59 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/components/sms/test_init.py diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index e4c3e19cf7c..123e625d0fc 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -18,6 +18,10 @@ "title": "The {integration_title} YAML configuration is being removed", "description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." }, + "deprecated_system_packages_config_flow_integration": { + "title": "The {integration_title} integration is being removed", + "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue." + }, "deprecated_system_packages_yaml_integration": { "title": "The {integration_title} integration is being removed", "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 2d18d44de3a..6c7c5374f7d 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -6,9 +6,14 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.helpers.typing import ConfigType from .const import ( @@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure Gammu state machine.""" + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_config_flow_integration", + translation_placeholders={ + "integration_title": "SMS notifications via GSM-modem", + }, + ) device = entry.data[CONF_DEVICE] connection_mode = "at" @@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY] await gateway.terminate_async() + if not hass.config_entries.async_loaded_entries(DOMAIN): + async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID) + return unload_ok diff --git a/tests/components/sms/test_init.py b/tests/components/sms/test_init.py new file mode 100644 index 00000000000..03cebfe9b52 --- /dev/null +++ b/tests/components/sms/test_init.py @@ -0,0 +1,59 @@ +"""Test init.""" + +from unittest.mock import Mock, patch + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_DEVICE +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +@patch.dict( + "sys.modules", + { + "gammu": Mock(), + "gammu.asyncworker": Mock(), + }, +) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + from homeassistant.components.sms import ( # pylint: disable=import-outside-toplevel + DEPRECATED_ISSUE_ID, + DOMAIN as SMS_DOMAIN, + ) + + with ( + patch("homeassistant.components.sms.create_sms_gateway", autospec=True), + patch("homeassistant.components.sms.PLATFORMS", []), + ): + config_entry = MockConfigEntry( + title="test", + domain=SMS_DOMAIN, + data={ + CONF_DEVICE: "/dev/ttyUSB0", + }, + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + assert ( + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + ) in issue_registry.issues + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert ( + HOMEASSISTANT_DOMAIN, + DEPRECATED_ISSUE_ID, + ) not in issue_registry.issues From 48103bd2446df6793377a07332f28e9efea644ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Fri, 30 May 2025 01:40:11 +0200 Subject: [PATCH 1032/1175] Bump aiohomeconnect to 0.17.1 (#145873) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index e8a36cd60d9..d4b37552fb7 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -21,6 +21,6 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "iot_class": "cloud_push", "loggers": ["aiohomeconnect"], - "requirements": ["aiohomeconnect==0.17.0"], + "requirements": ["aiohomeconnect==0.17.1"], "zeroconf": ["_homeconnect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 6aeaf8b077f..68ec08b1c90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.0 +aiohomeconnect==0.17.1 # homeassistant.components.homekit_controller aiohomekit==3.2.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae0b8ee7cd2..77661e0f624 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioharmony==0.5.2 aiohasupervisor==0.3.1 # homeassistant.components.home_connect -aiohomeconnect==0.17.0 +aiohomeconnect==0.17.1 # homeassistant.components.homekit_controller aiohomekit==3.2.14 From aa8a6058b5b2d5edf758fa2ebab508cedb1934b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 May 2025 12:56:51 +0200 Subject: [PATCH 1033/1175] Bump version to 2025.6.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 5a6ac701771..29095678e48 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 331381def64..dc185344ca8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b2" +version = "2025.6.0b3" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From f0fcef574432f45a971785dce9f3c9cecd9d9ea3 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 31 May 2025 20:25:24 +0200 Subject: [PATCH 1034/1175] Add more Amazon Devices DHCP matches (#145776) --- .../components/amazon_devices/manifest.json | 89 ++++- homeassistant/generated/dhcp.py | 348 ++++++++++++++++++ 2 files changed, 436 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 7593fbd4943..eb9fae6ddbe 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -4,27 +4,114 @@ "codeowners": ["@chemelli74"], "config_flow": true, "dhcp": [ + { "macaddress": "007147*" }, + { "macaddress": "00FC8B*" }, + { "macaddress": "0812A5*" }, + { "macaddress": "086AE5*" }, + { "macaddress": "08849D*" }, + { "macaddress": "089115*" }, { "macaddress": "08A6BC*" }, + { "macaddress": "08C224*" }, + { "macaddress": "0CDC91*" }, + { "macaddress": "0CEE99*" }, + { "macaddress": "1009F9*" }, + { "macaddress": "109693*" }, { "macaddress": "10BF67*" }, + { "macaddress": "10CE02*" }, + { "macaddress": "140AC5*" }, + { "macaddress": "149138*" }, + { "macaddress": "1848BE*" }, + { "macaddress": "1C12B0*" }, + { "macaddress": "1C4D66*" }, + { "macaddress": "1C93C4*" }, + { "macaddress": "1CFE2B*" }, + { "macaddress": "244CE3*" }, + { "macaddress": "24CE33*" }, + { "macaddress": "2873F6*" }, + { "macaddress": "2C71FF*" }, + { "macaddress": "34AFB3*" }, + { "macaddress": "34D270*" }, + { "macaddress": "38F73D*" }, + { "macaddress": "3C5CC4*" }, + { "macaddress": "3CE441*" }, { "macaddress": "440049*" }, + { "macaddress": "40A2DB*" }, + { "macaddress": "40A9CF*" }, + { "macaddress": "40B4CD*" }, { "macaddress": "443D54*" }, + { "macaddress": "44650D*" }, + { "macaddress": "485F2D*" }, + { "macaddress": "48785E*" }, { "macaddress": "48B423*" }, { "macaddress": "4C1744*" }, + { "macaddress": "4CEFC0*" }, + { "macaddress": "5007C3*" }, { "macaddress": "50D45C*" }, { "macaddress": "50DCE7*" }, + { "macaddress": "50F5DA*" }, + { "macaddress": "5C415A*" }, + { "macaddress": "6837E9*" }, + { "macaddress": "6854FD*" }, + { "macaddress": "689A87*" }, + { "macaddress": "68B691*" }, + { "macaddress": "68DBF5*" }, { "macaddress": "68F63B*" }, { "macaddress": "6C0C9A*" }, + { "macaddress": "6C5697*" }, + { "macaddress": "7458F3*" }, + { "macaddress": "74C246*" }, { "macaddress": "74D637*" }, + { "macaddress": "74E20C*" }, + { "macaddress": "74ECB2*" }, + { "macaddress": "786C84*" }, + { "macaddress": "78A03F*" }, { "macaddress": "7C6166*" }, + { "macaddress": "7C6305*" }, + { "macaddress": "7CD566*" }, + { "macaddress": "8871E5*" }, { "macaddress": "901195*" }, + { "macaddress": "90235B*" }, + { "macaddress": "90A822*" }, + { "macaddress": "90F82E*" }, { "macaddress": "943A91*" }, { "macaddress": "98226E*" }, + { "macaddress": "98CCF3*" }, { "macaddress": "9CC8E9*" }, + { "macaddress": "A002DC*" }, + { "macaddress": "A0D2B1*" }, + { "macaddress": "A40801*" }, { "macaddress": "A8E621*" }, + { "macaddress": "AC416A*" }, + { "macaddress": "AC63BE*" }, + { "macaddress": "ACCCFC*" }, + { "macaddress": "B0739C*" }, + { "macaddress": "B0CFCB*" }, + { "macaddress": "B0F7C4*" }, + { "macaddress": "B85F98*" }, + { "macaddress": "C091B9*" }, { "macaddress": "C095CF*" }, + { "macaddress": "C49500*" }, + { "macaddress": "C86C3D*" }, + { "macaddress": "CC9EA2*" }, + { "macaddress": "CCF735*" }, + { "macaddress": "DC54D7*" }, { "macaddress": "D8BE65*" }, + { "macaddress": "D8FBD6*" }, + { "macaddress": "DC91BF*" }, + { "macaddress": "DCA0D0*" }, + { "macaddress": "E0F728*" }, { "macaddress": "EC2BEB*" }, - { "macaddress": "F02F9E*" } + { "macaddress": "EC8AC4*" }, + { "macaddress": "ECA138*" }, + { "macaddress": "F02F9E*" }, + { "macaddress": "F0272D*" }, + { "macaddress": "F0F0A4*" }, + { "macaddress": "F4032A*" }, + { "macaddress": "F854B8*" }, + { "macaddress": "FC492D*" }, + { "macaddress": "FC65DE*" }, + { "macaddress": "FCA183*" }, + { "macaddress": "FCE9D8*" } ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0fb8b48b01c..5285ab7a1db 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,22 +26,158 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, + { + "domain": "amazon_devices", + "macaddress": "007147*", + }, + { + "domain": "amazon_devices", + "macaddress": "00FC8B*", + }, + { + "domain": "amazon_devices", + "macaddress": "0812A5*", + }, + { + "domain": "amazon_devices", + "macaddress": "086AE5*", + }, + { + "domain": "amazon_devices", + "macaddress": "08849D*", + }, + { + "domain": "amazon_devices", + "macaddress": "089115*", + }, { "domain": "amazon_devices", "macaddress": "08A6BC*", }, + { + "domain": "amazon_devices", + "macaddress": "08C224*", + }, + { + "domain": "amazon_devices", + "macaddress": "0CDC91*", + }, + { + "domain": "amazon_devices", + "macaddress": "0CEE99*", + }, + { + "domain": "amazon_devices", + "macaddress": "1009F9*", + }, + { + "domain": "amazon_devices", + "macaddress": "109693*", + }, { "domain": "amazon_devices", "macaddress": "10BF67*", }, + { + "domain": "amazon_devices", + "macaddress": "10CE02*", + }, + { + "domain": "amazon_devices", + "macaddress": "140AC5*", + }, + { + "domain": "amazon_devices", + "macaddress": "149138*", + }, + { + "domain": "amazon_devices", + "macaddress": "1848BE*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C12B0*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C4D66*", + }, + { + "domain": "amazon_devices", + "macaddress": "1C93C4*", + }, + { + "domain": "amazon_devices", + "macaddress": "1CFE2B*", + }, + { + "domain": "amazon_devices", + "macaddress": "244CE3*", + }, + { + "domain": "amazon_devices", + "macaddress": "24CE33*", + }, + { + "domain": "amazon_devices", + "macaddress": "2873F6*", + }, + { + "domain": "amazon_devices", + "macaddress": "2C71FF*", + }, + { + "domain": "amazon_devices", + "macaddress": "34AFB3*", + }, + { + "domain": "amazon_devices", + "macaddress": "34D270*", + }, + { + "domain": "amazon_devices", + "macaddress": "38F73D*", + }, + { + "domain": "amazon_devices", + "macaddress": "3C5CC4*", + }, + { + "domain": "amazon_devices", + "macaddress": "3CE441*", + }, { "domain": "amazon_devices", "macaddress": "440049*", }, + { + "domain": "amazon_devices", + "macaddress": "40A2DB*", + }, + { + "domain": "amazon_devices", + "macaddress": "40A9CF*", + }, + { + "domain": "amazon_devices", + "macaddress": "40B4CD*", + }, { "domain": "amazon_devices", "macaddress": "443D54*", }, + { + "domain": "amazon_devices", + "macaddress": "44650D*", + }, + { + "domain": "amazon_devices", + "macaddress": "485F2D*", + }, + { + "domain": "amazon_devices", + "macaddress": "48785E*", + }, { "domain": "amazon_devices", "macaddress": "48B423*", @@ -50,6 +186,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "4C1744*", }, + { + "domain": "amazon_devices", + "macaddress": "4CEFC0*", + }, + { + "domain": "amazon_devices", + "macaddress": "5007C3*", + }, { "domain": "amazon_devices", "macaddress": "50D45C*", @@ -58,6 +202,34 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "50DCE7*", }, + { + "domain": "amazon_devices", + "macaddress": "50F5DA*", + }, + { + "domain": "amazon_devices", + "macaddress": "5C415A*", + }, + { + "domain": "amazon_devices", + "macaddress": "6837E9*", + }, + { + "domain": "amazon_devices", + "macaddress": "6854FD*", + }, + { + "domain": "amazon_devices", + "macaddress": "689A87*", + }, + { + "domain": "amazon_devices", + "macaddress": "68B691*", + }, + { + "domain": "amazon_devices", + "macaddress": "68DBF5*", + }, { "domain": "amazon_devices", "macaddress": "68F63B*", @@ -66,18 +238,70 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "6C0C9A*", }, + { + "domain": "amazon_devices", + "macaddress": "6C5697*", + }, + { + "domain": "amazon_devices", + "macaddress": "7458F3*", + }, + { + "domain": "amazon_devices", + "macaddress": "74C246*", + }, { "domain": "amazon_devices", "macaddress": "74D637*", }, + { + "domain": "amazon_devices", + "macaddress": "74E20C*", + }, + { + "domain": "amazon_devices", + "macaddress": "74ECB2*", + }, + { + "domain": "amazon_devices", + "macaddress": "786C84*", + }, + { + "domain": "amazon_devices", + "macaddress": "78A03F*", + }, { "domain": "amazon_devices", "macaddress": "7C6166*", }, + { + "domain": "amazon_devices", + "macaddress": "7C6305*", + }, + { + "domain": "amazon_devices", + "macaddress": "7CD566*", + }, + { + "domain": "amazon_devices", + "macaddress": "8871E5*", + }, { "domain": "amazon_devices", "macaddress": "901195*", }, + { + "domain": "amazon_devices", + "macaddress": "90235B*", + }, + { + "domain": "amazon_devices", + "macaddress": "90A822*", + }, + { + "domain": "amazon_devices", + "macaddress": "90F82E*", + }, { "domain": "amazon_devices", "macaddress": "943A91*", @@ -86,30 +310,154 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "amazon_devices", "macaddress": "98226E*", }, + { + "domain": "amazon_devices", + "macaddress": "98CCF3*", + }, { "domain": "amazon_devices", "macaddress": "9CC8E9*", }, + { + "domain": "amazon_devices", + "macaddress": "A002DC*", + }, + { + "domain": "amazon_devices", + "macaddress": "A0D2B1*", + }, + { + "domain": "amazon_devices", + "macaddress": "A40801*", + }, { "domain": "amazon_devices", "macaddress": "A8E621*", }, + { + "domain": "amazon_devices", + "macaddress": "AC416A*", + }, + { + "domain": "amazon_devices", + "macaddress": "AC63BE*", + }, + { + "domain": "amazon_devices", + "macaddress": "ACCCFC*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0739C*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0CFCB*", + }, + { + "domain": "amazon_devices", + "macaddress": "B0F7C4*", + }, + { + "domain": "amazon_devices", + "macaddress": "B85F98*", + }, + { + "domain": "amazon_devices", + "macaddress": "C091B9*", + }, { "domain": "amazon_devices", "macaddress": "C095CF*", }, + { + "domain": "amazon_devices", + "macaddress": "C49500*", + }, + { + "domain": "amazon_devices", + "macaddress": "C86C3D*", + }, + { + "domain": "amazon_devices", + "macaddress": "CC9EA2*", + }, + { + "domain": "amazon_devices", + "macaddress": "CCF735*", + }, + { + "domain": "amazon_devices", + "macaddress": "DC54D7*", + }, { "domain": "amazon_devices", "macaddress": "D8BE65*", }, + { + "domain": "amazon_devices", + "macaddress": "D8FBD6*", + }, + { + "domain": "amazon_devices", + "macaddress": "DC91BF*", + }, + { + "domain": "amazon_devices", + "macaddress": "DCA0D0*", + }, + { + "domain": "amazon_devices", + "macaddress": "E0F728*", + }, { "domain": "amazon_devices", "macaddress": "EC2BEB*", }, + { + "domain": "amazon_devices", + "macaddress": "EC8AC4*", + }, + { + "domain": "amazon_devices", + "macaddress": "ECA138*", + }, { "domain": "amazon_devices", "macaddress": "F02F9E*", }, + { + "domain": "amazon_devices", + "macaddress": "F0272D*", + }, + { + "domain": "amazon_devices", + "macaddress": "F0F0A4*", + }, + { + "domain": "amazon_devices", + "macaddress": "F4032A*", + }, + { + "domain": "amazon_devices", + "macaddress": "F854B8*", + }, + { + "domain": "amazon_devices", + "macaddress": "FC492D*", + }, + { + "domain": "amazon_devices", + "macaddress": "FC65DE*", + }, + { + "domain": "amazon_devices", + "macaddress": "FCA183*", + }, + { + "domain": "amazon_devices", + "macaddress": "FCE9D8*", + }, { "domain": "august", "hostname": "connect", From 9879ecad85565b2fd5d14813d3b522c110c28f01 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sat, 31 May 2025 20:00:34 +0200 Subject: [PATCH 1035/1175] Deprecate snips integration (#145784) --- homeassistant/components/snips/__init__.py | 21 ++++++++++++++++++++- tests/components/snips/test_init.py | 16 ++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 70837b95ec5..293caeaedac 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -7,8 +7,13 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType DOMAIN = "snips" @@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Activate Snips component.""" + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Snips", + }, + ) # Make sure MQTT integration is enabled and the client is available if not await mqtt.async_wait_for_mqtt_client(hass): diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 82dbf1cd281..2be6d769f08 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -7,7 +7,8 @@ import pytest import voluptuous as vol from homeassistant.components import snips -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.intent import ServiceIntentHandler, async_register from homeassistant.setup import async_setup_component @@ -15,9 +16,13 @@ from tests.common import async_fire_mqtt_message, async_mock_intent, async_mock_ from tests.typing import MqttMockHAClient -async def test_snips_config(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None: +async def test_snips_config( + hass: HomeAssistant, + mqtt_mock: MqttMockHAClient, + issue_registry: ir.IssueRegistry, +) -> None: """Test Snips Config.""" - result = await async_setup_component( + assert await async_setup_component( hass, "snips", { @@ -28,7 +33,10 @@ async def test_snips_config(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> } }, ) - assert result + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{snips.DOMAIN}", + ) in issue_registry.issues async def test_snips_no_mqtt( From 306bbdc697f1a4a238e4efa42011677f398b3f3b Mon Sep 17 00:00:00 2001 From: Samuel Xiao <40679757+XiaoLing-git@users.noreply.github.com> Date: Sat, 31 May 2025 02:22:40 +0800 Subject: [PATCH 1036/1175] Bump switchbot-api to 2.4.0 (#145786) * update switchbot-api version to 2.4.0 * debug for test code --- .../components/switchbot_cloud/__init__.py | 12 +++++++++--- .../components/switchbot_cloud/config_flow.py | 10 +++++++--- .../components/switchbot_cloud/coordinator.py | 4 ++-- .../components/switchbot_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot_cloud/test_config_flow.py | 8 ++++---- tests/components/switchbot_cloud/test_init.py | 14 ++++++++++---- 8 files changed, 35 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index c7bf66a5803..7b7f60589f0 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -7,7 +7,13 @@ from dataclasses import dataclass, field from logging import getLogger from aiohttp import web -from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI +from switchbot_api import ( + Device, + Remote, + SwitchBotAPI, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) from homeassistant.components import webhook from homeassistant.config_entries import ConfigEntry @@ -175,12 +181,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SwitchBotAPI(token=token, secret=secret) try: devices = await api.list_devices() - except InvalidAuth as ex: + except SwitchBotAuthenticationError as ex: _LOGGER.error( "Invalid authentication while connecting to SwitchBot API: %s", ex ) return False - except CannotConnect as ex: + except SwitchBotConnectionError as ex: raise ConfigEntryNotReady from ex _LOGGER.debug("Devices: %s", devices) coordinators_by_id: dict[str, SwitchBotCoordinator] = {} diff --git a/homeassistant/components/switchbot_cloud/config_flow.py b/homeassistant/components/switchbot_cloud/config_flow.py index eafe823bc0b..0ba1e0295e0 100644 --- a/homeassistant/components/switchbot_cloud/config_flow.py +++ b/homeassistant/components/switchbot_cloud/config_flow.py @@ -3,7 +3,11 @@ from logging import getLogger from typing import Any -from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI +from switchbot_api import ( + SwitchBotAPI, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -36,9 +40,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN): await SwitchBotAPI( token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY] ).list_devices() - except CannotConnect: + except SwitchBotConnectionError: errors["base"] = "cannot_connect" - except InvalidAuth: + except SwitchBotAuthenticationError: errors["base"] = "invalid_auth" except Exception: _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/switchbot_cloud/coordinator.py b/homeassistant/components/switchbot_cloud/coordinator.py index 4f047145b47..9fc8f64aa68 100644 --- a/homeassistant/components/switchbot_cloud/coordinator.py +++ b/homeassistant/components/switchbot_cloud/coordinator.py @@ -4,7 +4,7 @@ from asyncio import timeout from logging import getLogger from typing import Any -from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI +from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]): status: Status = await self._api.get_status(self._device_id) _LOGGER.debug("Refreshing %s with %s", self._device_id, status) return status - except CannotConnect as err: + except SwitchBotConnectionError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json index 83404aac2ba..e0c49d9e739 100644 --- a/homeassistant/components/switchbot_cloud/manifest.json +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["switchbot_api"], - "requirements": ["switchbot-api==2.3.1"] + "requirements": ["switchbot-api==2.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68ec08b1c90..eae64f1225a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2859,7 +2859,7 @@ surepy==0.9.0 swisshydrodata==0.1.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.3.1 +switchbot-api==2.4.0 # homeassistant.components.synology_srm synology-srm==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77661e0f624..002189807fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2354,7 +2354,7 @@ subarulink==0.7.13 surepy==0.9.0 # homeassistant.components.switchbot_cloud -switchbot-api==2.3.1 +switchbot-api==2.4.0 # homeassistant.components.system_bridge systembridgeconnector==4.1.5 diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py index 1d49b503ef2..5eef1805a5a 100644 --- a/tests/components/switchbot_cloud/test_config_flow.py +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -6,8 +6,8 @@ import pytest from homeassistant import config_entries from homeassistant.components.switchbot_cloud.config_flow import ( - CannotConnect, - InvalidAuth, + SwitchBotAuthenticationError, + SwitchBotConnectionError, ) from homeassistant.components.switchbot_cloud.const import DOMAIN, ENTRY_TITLE from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN @@ -57,8 +57,8 @@ async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @pytest.mark.parametrize( ("error", "message"), [ - (InvalidAuth, "invalid_auth"), - (CannotConnect, "cannot_connect"), + (SwitchBotAuthenticationError, "invalid_auth"), + (SwitchBotConnectionError, "cannot_connect"), (Exception, "unknown"), ], ) diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py index bab9200e7c9..b55106e90d9 100644 --- a/tests/components/switchbot_cloud/test_init.py +++ b/tests/components/switchbot_cloud/test_init.py @@ -3,7 +3,13 @@ from unittest.mock import patch import pytest -from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState, Remote +from switchbot_api import ( + Device, + PowerState, + Remote, + SwitchBotAuthenticationError, + SwitchBotConnectionError, +) from homeassistant.components.switchbot_cloud import SwitchBotAPI from homeassistant.config_entries import ConfigEntryState @@ -127,8 +133,8 @@ async def test_setup_entry_success( @pytest.mark.parametrize( ("error", "state"), [ - (InvalidAuth, ConfigEntryState.SETUP_ERROR), - (CannotConnect, ConfigEntryState.SETUP_RETRY), + (SwitchBotAuthenticationError, ConfigEntryState.SETUP_ERROR), + (SwitchBotConnectionError, ConfigEntryState.SETUP_RETRY), ], ) async def test_setup_entry_fails_when_listing_devices( @@ -162,7 +168,7 @@ async def test_setup_entry_fails_when_refreshing( hubDeviceId="test-hub-id", ) ] - mock_get_status.side_effect = CannotConnect + mock_get_status.side_effect = SwitchBotConnectionError entry = await configure_integration(hass) assert entry.state is ConfigEntryState.SETUP_RETRY From c84ffb54d2fe5b0c9aca802ab269d50dbbd76b8c Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 31 May 2025 04:21:51 +1000 Subject: [PATCH 1037/1175] Bump tesla-fleet-api to 1.1.1. (#145869) bump --- homeassistant/components/tesla_fleet/manifest.json | 2 +- homeassistant/components/teslemetry/manifest.json | 2 +- homeassistant/components/tessie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tesla_fleet/manifest.json b/homeassistant/components/tesla_fleet/manifest.json index 53c8e7d554c..8f5ba1468a5 100644 --- a/homeassistant/components/tesla_fleet/manifest.json +++ b/homeassistant/components/tesla_fleet/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17"] + "requirements": ["tesla-fleet-api==1.1.1"] } diff --git a/homeassistant/components/teslemetry/manifest.json b/homeassistant/components/teslemetry/manifest.json index 855cdc9f364..7fc621eeeae 100644 --- a/homeassistant/components/teslemetry/manifest.json +++ b/homeassistant/components/teslemetry/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/teslemetry", "iot_class": "cloud_polling", "loggers": ["tesla-fleet-api"], - "requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.9"] + "requirements": ["tesla-fleet-api==1.1.1", "teslemetry-stream==0.7.9"] } diff --git a/homeassistant/components/tessie/manifest.json b/homeassistant/components/tessie/manifest.json index 3f71bcb95e3..9ad87e9dbbe 100644 --- a/homeassistant/components/tessie/manifest.json +++ b/homeassistant/components/tessie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/tessie", "iot_class": "cloud_polling", "loggers": ["tessie", "tesla-fleet-api"], - "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"] + "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index eae64f1225a..997184849ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2900,7 +2900,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.17 +tesla-fleet-api==1.1.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 002189807fa..66637db0537 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2380,7 +2380,7 @@ temperusb==1.6.1 # homeassistant.components.tesla_fleet # homeassistant.components.teslemetry # homeassistant.components.tessie -tesla-fleet-api==1.0.17 +tesla-fleet-api==1.1.1 # homeassistant.components.powerwall tesla-powerwall==0.5.2 From fb2d8c640643120bc228951c07ba15b81e601848 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 1 Jun 2025 04:01:10 +1000 Subject: [PATCH 1038/1175] Add streaming to charge cable connected in Teslemetry (#145880) --- homeassistant/components/teslemetry/binary_sensor.py | 3 +++ tests/components/teslemetry/snapshots/test_binary_sensor.ambr | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/binary_sensor.py b/homeassistant/components/teslemetry/binary_sensor.py index 99c21cbe03e..a32c5fea40e 100644 --- a/homeassistant/components/teslemetry/binary_sensor.py +++ b/homeassistant/components/teslemetry/binary_sensor.py @@ -125,6 +125,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = ( key="charge_state_conn_charge_cable", polling=True, polling_value_fn=lambda x: x != "", + streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType( + lambda value: callback(value != "Unknown") + ), entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, ), diff --git a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr index 8bcd837d06f..06ec0a60434 100644 --- a/tests/components/teslemetry/snapshots/test_binary_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_binary_sensor.ambr @@ -673,7 +673,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor[binary_sensor.test_charge_enable_request-entry] @@ -3374,7 +3374,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'on', + 'state': 'unknown', }) # --- # name: test_binary_sensor_refresh[binary_sensor.test_charge_enable_request-statealt] From a6608bd7ea8145fe1f81b353a65a4deb5264cc1b Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Fri, 30 May 2025 20:21:14 +0200 Subject: [PATCH 1039/1175] Bump pyiskra to 0.1.19 (#145889) --- homeassistant/components/iskra/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index caa176ab6b6..3f7c805a917 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.15"] + "requirements": ["pyiskra==0.1.19"] } diff --git a/requirements_all.txt b/requirements_all.txt index 997184849ae..9735bbc48bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.15 +pyiskra==0.1.19 # homeassistant.components.iss pyiss==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66637db0537..18d2be2057e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1699,7 +1699,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.15 +pyiskra==0.1.19 # homeassistant.components.iss pyiss==1.0.1 From 6015f60db4d9554a82169082aa95d3fa052f019f Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 30 May 2025 19:35:08 +0200 Subject: [PATCH 1040/1175] Bump python-linkplay to v0.2.9 (#145892) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index fafc9e66514..eb9b5a87c75 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.8"], + "requirements": ["python-linkplay==0.2.9"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9735bbc48bb..84739a2500c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.8 +python-linkplay==0.2.9 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18d2be2057e..70b1190a7eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2019,7 +2019,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.8 +python-linkplay==0.2.9 # homeassistant.components.lirc # python-lirc==1.2.3 From ddc79a631d2ba39ce786658e514de42a109da5c6 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Fri, 30 May 2025 18:33:03 +0100 Subject: [PATCH 1041/1175] Bump pyprobeplus to 1.0.1 (#145897) --- homeassistant/components/probe_plus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/probe_plus/manifest.json b/homeassistant/components/probe_plus/manifest.json index cf61e394a83..e7db39b8ae4 100644 --- a/homeassistant/components/probe_plus/manifest.json +++ b/homeassistant/components/probe_plus/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["pyprobeplus==1.0.0"] + "requirements": ["pyprobeplus==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 84739a2500c..e4b150bbf89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2251,7 +2251,7 @@ pyplaato==0.0.19 pypoint==3.0.0 # homeassistant.components.probe_plus -pyprobeplus==1.0.0 +pyprobeplus==1.0.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70b1190a7eb..59d4d9a2db3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1869,7 +1869,7 @@ pyplaato==0.0.19 pypoint==3.0.0 # homeassistant.components.probe_plus -pyprobeplus==1.0.0 +pyprobeplus==1.0.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 From d0bf9d9bfb610c84b467167c2bf8fb9626e8b887 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 11:19:32 +0200 Subject: [PATCH 1042/1175] Move server device creation to init in jellyfin (#145910) * Move server device creation to init in jellyfin * move device creation to after coordinator refresh --- homeassistant/components/jellyfin/__init__.py | 13 +++++++++++-- homeassistant/components/jellyfin/entity.py | 8 ++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jellyfin/__init__.py b/homeassistant/components/jellyfin/__init__.py index 1cb6219ada0..d22594070ff 100644 --- a/homeassistant/components/jellyfin/__init__.py +++ b/homeassistant/components/jellyfin/__init__.py @@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input -from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS +from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator @@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) -> coordinator = JellyfinDataUpdateCoordinator( hass, entry, client, server_info, user_id ) - await coordinator.async_config_entry_first_refresh() + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + entry_type=dr.DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.server_id)}, + manufacturer=DEFAULT_NAME, + name=coordinator.server_name, + sw_version=coordinator.server_version, + ) + entry.runtime_data = coordinator entry.async_on_unload(client.stop) diff --git a/homeassistant/components/jellyfin/entity.py b/homeassistant/components/jellyfin/entity.py index 4a3b2b77bb1..107a67d6a89 100644 --- a/homeassistant/components/jellyfin/entity.py +++ b/homeassistant/components/jellyfin/entity.py @@ -4,10 +4,10 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN from .coordinator import JellyfinDataUpdateCoordinator @@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity): """Initialize the Jellyfin entity.""" super().__init__(coordinator) self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.server_id)}, - manufacturer=DEFAULT_NAME, - name=coordinator.server_name, - sw_version=coordinator.server_version, ) From cd905a65934b649d33ff75e10c54b9255a7ef49d Mon Sep 17 00:00:00 2001 From: tronikos Date: Sat, 31 May 2025 02:22:49 -0700 Subject: [PATCH 1043/1175] Bump opower to 0.12.3 (#145918) --- 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 7ac9f4cc943..0aa26dbb4b1 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.12.2"] + "requirements": ["opower==0.12.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4b150bbf89..a1ad1673b67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1617,7 +1617,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.12.2 +opower==0.12.3 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59d4d9a2db3..3a989d3c2b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ openhomedevice==2.2.0 openwebifpy==4.3.1 # homeassistant.components.opower -opower==0.12.2 +opower==0.12.3 # homeassistant.components.oralb oralb-ble==0.17.6 From 532c077ddf985f72c1ef8dfff34c7b5f2353954e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 31 May 2025 04:12:00 -0500 Subject: [PATCH 1044/1175] Bump aiohttp to 3.12.6 (#145919) * Bump aiohttp to 3.12.5 changelog: https://github.com/aio-libs/aiohttp/compare/v3.12.4...v3.12.5 * .6 * fix mock --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/hassio/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8dde4a6a654..3a70b1ff8e8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.4 +aiohttp==3.12.6 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index dc185344ca8..791b616b0c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.4", + "aiohttp==3.12.6", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index f1ced51637e..a9a3e105f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.4 +aiohttp==3.12.6 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index ea38865ac5a..a71ee370b32 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -63,7 +63,7 @@ async def hassio_client_supervisor( @pytest.fixture -def hassio_handler( +async def hassio_handler( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> Generator[HassIO]: """Create mock hassio handler.""" From ef0b3c9f9c482df884e2660b962a4f64d4113c99 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 31 May 2025 17:54:36 +0200 Subject: [PATCH 1045/1175] Update frontend to 20250531.0 (#145933) --- 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 3ee40e1ce60..7282482f329 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==20250528.0"] + "requirements": ["home-assistant-frontend==20250531.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a70b1ff8e8..5aa0d8ae82b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.48.2 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a1ad1673b67..fc61430c302 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a989d3c2b7..81cb7278ec1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 holidays==0.73 # homeassistant.components.frontend -home-assistant-frontend==20250528.0 +home-assistant-frontend==20250531.0 # homeassistant.components.conversation home-assistant-intents==2025.5.28 From 745902bc7e92a2075f46e6958265502fee297c48 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 20:25:47 +0200 Subject: [PATCH 1046/1175] Bump pylamarzocco to 2.0.8 (#145938) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index 36a0a489e30..46a29427264 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -37,5 +37,5 @@ "iot_class": "cloud_push", "loggers": ["pylamarzocco"], "quality_scale": "platinum", - "requirements": ["pylamarzocco==2.0.7"] + "requirements": ["pylamarzocco==2.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index fc61430c302..63ddad956e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2096,7 +2096,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==2.0.7 +pylamarzocco==2.0.8 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81cb7278ec1..c7c1f182cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1735,7 +1735,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.8 # homeassistant.components.lamarzocco -pylamarzocco==2.0.7 +pylamarzocco==2.0.8 # homeassistant.components.lastfm pylast==5.1.0 From 907cebdd6d57107e1c8ee2597d2de03662124198 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 31 May 2025 20:25:57 +0200 Subject: [PATCH 1047/1175] Increase update intervals in lamarzocco (#145939) --- homeassistant/components/lamarzocco/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index f0f64e02c28..b6379f237ae 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=15) -SETTINGS_UPDATE_INTERVAL = timedelta(hours=1) -SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5) +SETTINGS_UPDATE_INTERVAL = timedelta(hours=8) +SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30) STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15) _LOGGER = logging.getLogger(__name__) From 06d869aaa5efdfddd2a10648ac9c94172871cb37 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 31 May 2025 21:25:06 +0200 Subject: [PATCH 1048/1175] Bump version to 2025.6.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 29095678e48..edbdba419f3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 791b616b0c3..aa9de97d73b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b3" +version = "2025.6.0b4" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From ea6b9e5260c74aa00ad172fb65e54a67ce1f74fc Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 3 Jun 2025 12:12:56 +0200 Subject: [PATCH 1049/1175] SMA add missing strings for DHCP (#145782) --- homeassistant/components/sma/config_flow.py | 1 + homeassistant/components/sma/strings.json | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index c920b4b0a3a..f43c851d04a 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -218,5 +218,6 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required(CONF_PASSWORD): cv.string, } ), + description_placeholders={CONF_HOST: self._data[CONF_HOST]}, errors=errors, ) diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index 3a7c87acfcc..e19acf20cf8 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -32,6 +32,16 @@ }, "description": "Enter your SMA device information.", "title": "Set up SMA Solar" + }, + "discovery_confirm": { + "title": "[%key:component::sma::config::step::user::title]", + "description": "Do you want to setup the discovered SMA ({host})?", + "data": { + "group": "[%key:component::sma::config::step::user::data::group]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } } } } From b1d35de8e4612266d722de36b23492f0be1b3df9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 4 Jun 2025 10:00:50 +0200 Subject: [PATCH 1050/1175] Deprecate eddystone temperature integration (#145833) --- .../eddystone_temperature/__init__.py | 5 +++ .../eddystone_temperature/sensor.py | 24 ++++++++-- requirements_test_all.txt | 3 ++ .../eddystone_temperature/__init__.py | 1 + .../eddystone_temperature/test_sensor.py | 45 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/components/eddystone_temperature/__init__.py create mode 100644 tests/components/eddystone_temperature/test_sensor.py diff --git a/homeassistant/components/eddystone_temperature/__init__.py b/homeassistant/components/eddystone_temperature/__init__.py index 2d6f92498bd..af37eb629b5 100644 --- a/homeassistant/components/eddystone_temperature/__init__.py +++ b/homeassistant/components/eddystone_temperature/__init__.py @@ -1 +1,6 @@ """The eddystone_temperature component.""" + +DOMAIN = "eddystone_temperature" +CONF_BEACONS = "beacons" +CONF_INSTANCE = "instance" +CONF_NAMESPACE = "namespace" diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 1047c52e111..7b8e726cf45 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -23,17 +23,18 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_BEACONS = "beacons" CONF_BT_DEVICE_ID = "bt_device_id" -CONF_INSTANCE = "instance" -CONF_NAMESPACE = "namespace" + BEACON_SCHEMA = vol.Schema( { @@ -58,6 +59,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Validate configuration, create devices and start monitoring thread.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Eddystone", + }, + ) + bt_device_id: int = config[CONF_BT_DEVICE_ID] beacons: dict[str, dict[str, str]] = config[CONF_BEACONS] diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7c1f182cd2..e3f5788e5d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -533,6 +533,9 @@ babel==2.15.0 # homeassistant.components.homekit base36==0.1.1 +# homeassistant.components.eddystone_temperature +# beacontools[scan]==2.1.0 + # homeassistant.components.scrape beautifulsoup4==4.13.3 diff --git a/tests/components/eddystone_temperature/__init__.py b/tests/components/eddystone_temperature/__init__.py new file mode 100644 index 00000000000..af67530c946 --- /dev/null +++ b/tests/components/eddystone_temperature/__init__.py @@ -0,0 +1 @@ +"""Tests for eddystone temperature.""" diff --git a/tests/components/eddystone_temperature/test_sensor.py b/tests/components/eddystone_temperature/test_sensor.py new file mode 100644 index 00000000000..056681fdb90 --- /dev/null +++ b/tests/components/eddystone_temperature/test_sensor.py @@ -0,0 +1,45 @@ +"""Tests for eddystone temperature.""" + +from unittest.mock import Mock, patch + +from homeassistant.components.eddystone_temperature import ( + CONF_BEACONS, + CONF_INSTANCE, + CONF_NAMESPACE, + DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +@patch.dict("sys.modules", beacontools=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component( + hass, + PLATFORM_DOMAIN, + { + PLATFORM_DOMAIN: [ + { + CONF_PLATFORM: DOMAIN, + CONF_BEACONS: { + "living_room": { + CONF_NAMESPACE: "112233445566778899AA", + CONF_INSTANCE: "000000000001", + } + }, + } + ], + }, + ) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + ) in issue_registry.issues From 03f028b7e290c8ed5e1449573b77491fc5ede746 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 2 Jun 2025 09:45:14 +0200 Subject: [PATCH 1051/1175] Deprecate hddtemp (#145850) --- homeassistant/components/hddtemp/__init__.py | 2 ++ homeassistant/components/hddtemp/sensor.py | 20 ++++++++++++++++++- tests/components/hddtemp/test_sensor.py | 21 ++++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hddtemp/__init__.py b/homeassistant/components/hddtemp/__init__.py index 121238df9fe..66a819f1e8d 100644 --- a/homeassistant/components/hddtemp/__init__.py +++ b/homeassistant/components/hddtemp/__init__.py @@ -1 +1,3 @@ """The hddtemp component.""" + +DOMAIN = "hddtemp" diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index 4d9bbeb9516..192ddffd330 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -22,11 +22,14 @@ from homeassistant.const import ( CONF_PORT, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) ATTR_DEVICE = "device" @@ -56,6 +59,21 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the HDDTemp sensor.""" + create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + breaks_in_ha_version="2025.12.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_system_packages_yaml_integration", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "hddtemp", + }, + ) + name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 56ad9fdcb0e..62882c7df8b 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,12 +1,15 @@ """The tests for the hddtemp platform.""" import socket -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest +from homeassistant.components.hddtemp import DOMAIN +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import UnitOfTemperature -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 VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -192,3 +195,17 @@ async def test_hddtemp_host_unreachable(hass: HomeAssistant, telnetmock) -> None assert await async_setup_component(hass, "sensor", VALID_CONFIG_HOST_UNREACHABLE) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +@patch.dict("sys.modules", gsp=Mock()) +async def test_repair_issue_is_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test repair issue is created.""" + assert await async_setup_component(hass, PLATFORM_DOMAIN, VALID_CONFIG_MINIMAL) + await hass.async_block_till_done() + assert ( + HOMEASSISTANT_DOMAIN, + f"deprecated_system_packages_yaml_integration_{DOMAIN}", + ) in issue_registry.issues From 1e1b0424d74c8168226b38d1c3da20975b2f1c9c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 2 Jun 2025 09:52:02 +0200 Subject: [PATCH 1052/1175] Fix removal of devices during Z-Wave migration (#145867) --- homeassistant/components/zwave_js/__init__.py | 8 +- .../components/zwave_js/config_flow.py | 21 +++ homeassistant/components/zwave_js/const.py | 1 + tests/components/zwave_js/test_config_flow.py | 130 +++++++++++++++--- 4 files changed, 135 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 6e76b2f89cf..abbf10fb494 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -94,6 +94,7 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, CONF_INSTALLER_MODE, CONF_INTEGRATION_CREATED_ADDON, + CONF_KEEP_OLD_DEVICES, CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_LR_S2_AUTHENTICATED_KEY, CONF_NETWORK_KEY, @@ -405,9 +406,10 @@ class DriverEvents: # Devices that are in the device registry that are not known by the controller # can be removed - for device in stored_devices: - if device not in known_devices and device not in provisioned_devices: - self.dev_reg.async_remove_device(device.id) + if not self.config_entry.data.get(CONF_KEEP_OLD_DEVICES): + for device in stored_devices: + if device not in known_devices and device not in provisioned_devices: + self.dev_reg.async_remove_device(device.id) # run discovery on controller node if controller.own_node: diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index e2941b52522..08c9ec2e2b2 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -56,6 +56,7 @@ from .const import ( CONF_ADDON_S2_AUTHENTICATED_KEY, CONF_ADDON_S2_UNAUTHENTICATED_KEY, CONF_INTEGRATION_CREATED_ADDON, + CONF_KEEP_OLD_DEVICES, CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_LR_S2_AUTHENTICATED_KEY, CONF_S0_LEGACY_KEY, @@ -1383,9 +1384,20 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): config_entry = self._reconfigure_config_entry assert config_entry is not None + # Make sure we keep the old devices + # so that user customizations are not lost, + # when loading the config entry. + self.hass.config_entries.async_update_entry( + config_entry, data=config_entry.data | {CONF_KEEP_OLD_DEVICES: True} + ) + # Reload the config entry to reconnect the client after the addon restart await self.hass.config_entries.async_reload(config_entry.entry_id) + data = config_entry.data.copy() + data.pop(CONF_KEEP_OLD_DEVICES, None) + self.hass.config_entries.async_update_entry(config_entry, data=data) + @callback def forward_progress(event: dict) -> None: """Forward progress events to frontend.""" @@ -1436,6 +1448,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): config_entry, unique_id=str(version_info.home_id) ) await self.hass.config_entries.async_reload(config_entry.entry_id) + + # Reload the config entry two times to clean up + # the stale device entry. + # Since both the old and the new controller have the same node id, + # but different hardware identifiers, the integration + # will create a new device for the new controller, on the first reload, + # but not immediately remove the old device. + await self.hass.config_entries.async_reload(config_entry.entry_id) + finally: for unsub in unsubs: unsub() diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 31cfb144e2a..6d5cbb98902 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -27,6 +27,7 @@ CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key" CONF_ADDON_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key" CONF_INSTALLER_MODE = "installer_mode" CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_KEEP_OLD_DEVICES = "keep_old_devices" CONF_NETWORK_KEY = "network_key" CONF_S0_LEGACY_KEY = "s0_legacy_key" CONF_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index c9929759a49..fc01c9b29b1 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -15,6 +15,7 @@ import pytest from serial.tools.list_ports_common import ListPortInfo from voluptuous import InInvalid from zwave_js_server.exceptions import FailedCommand +from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo from homeassistant import config_entries, data_entry_flow @@ -40,6 +41,7 @@ from homeassistant.components.zwave_js.const import ( from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.usb import UsbServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo @@ -969,7 +971,7 @@ async def test_usb_discovery_migration( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -983,6 +985,7 @@ async def test_usb_discovery_migration( assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data assert entry.unique_id == "5678" @@ -1097,7 +1100,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -1108,9 +1111,10 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == USB_DISCOVERY_INFO.device + assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_installed") @@ -3422,6 +3426,7 @@ async def test_reconfigure_migrate_no_addon( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_required" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("mock_sdk_version") @@ -3446,6 +3451,7 @@ async def test_reconfigure_migrate_low_sdk_version( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_low_sdk_version" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -3457,15 +3463,22 @@ async def test_reconfigure_migrate_low_sdk_version( "final_unique_id", ), [ - (None, "4321", None, "8765"), - (aiohttp.ClientError("Boom"), "1234", None, "8765"), + (None, "4321", None, "3245146787"), + (aiohttp.ClientError("Boom"), "3245146787", None, "3245146787"), (None, "4321", aiohttp.ClientError("Boom"), "5678"), - (aiohttp.ClientError("Boom"), "1234", aiohttp.ClientError("Boom"), "5678"), + ( + aiohttp.ClientError("Boom"), + "3245146787", + aiohttp.ClientError("Boom"), + "5678", + ), ], ) async def test_reconfigure_migrate_with_addon( hass: HomeAssistant, client: MagicMock, + device_registry: dr.DeviceRegistry, + multisensor_6: Node, integration: MockConfigEntry, restart_addon: AsyncMock, addon_options: dict[str, Any], @@ -3482,9 +3495,9 @@ async def test_reconfigure_migrate_with_addon( version_info.home_id = 4321 entry = integration assert client.connect.call_count == 1 + assert client.driver.controller.home_id == 3245146787 hass.config_entries.async_update_entry( entry, - unique_id="1234", data={ "url": "ws://localhost:3000", "use_addon": True, @@ -3493,6 +3506,39 @@ async def test_reconfigure_migrate_with_addon( ) addon_options["device"] = "/dev/ttyUSB0" + controller_node = client.driver.controller.own_node + controller_device_id = ( + f"{client.driver.controller.home_id}-{controller_node.node_id}" + ) + controller_device_id_ext = ( + f"{controller_device_id}-{controller_node.manufacturer_id}:" + f"{controller_node.product_type}:{controller_node.product_id}" + ) + + assert len(device_registry.devices) == 2 + # Verify there's a device entry for the controller. + device = device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id)} + ) + assert device + assert device == device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id_ext)} + ) + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW090" + assert device.name == "Z‐Stick Gen5 USB Controller" + # Verify there's a device entry for the multisensor. + sensor_device_id = f"{client.driver.controller.home_id}-{multisensor_6.node_id}" + device = device_registry.async_get_device(identifiers={(DOMAIN, sensor_device_id)}) + assert device + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + assert device.name == "Multisensor 6" + # Customize the sensor device name. + device_registry.async_update_device( + device.id, name_by_user="Custom Sensor Device Name" + ) + async def mock_backup_nvm_raw(): await asyncio.sleep(0) client.driver.controller.emit( @@ -3521,6 +3567,7 @@ async def test_reconfigure_migrate_with_addon( "nvm restore progress", {"event": "nvm restore progress", "bytesWritten": 100, "total": 200}, ) + client.driver.controller.data["homeId"] = 3245146787 client.driver.emit( "driver ready", {"event": "driver ready", "source": "driver"} ) @@ -3591,6 +3638,17 @@ async def test_reconfigure_migrate_with_addon( "core_zwave_js", AddonsOptions(config={"device": "/test"}) ) + # Simulate the new connected controller hardware labels. + # This will cause a new device entry to be created + # when the config entry is loaded before restoring NVM. + controller_node = client.driver.controller.own_node + controller_node.data["manufacturerId"] = 999 + controller_node.data["productId"] = 999 + controller_node.device_config.data["description"] = "New Device Name" + controller_node.device_config.data["label"] = "New Device Model" + controller_node.device_config.data["manufacturer"] = "New Device Manufacturer" + client.driver.controller.data["homeId"] = 5678 + await hass.async_block_till_done() assert restart_addon.call_args == call("core_zwave_js") @@ -3599,14 +3657,14 @@ async def test_reconfigure_migrate_with_addon( assert entry.unique_id == "5678" get_server_version.side_effect = restore_server_version_side_effect - version_info.home_id = 8765 + version_info.home_id = 3245146787 assert result["type"] is FlowResultType.SHOW_PROGRESS assert result["step_id"] == "restore_nvm" assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3620,8 +3678,29 @@ async def test_reconfigure_migrate_with_addon( assert entry.data["url"] == "ws://host1:3001" assert entry.data["usb_path"] == "/test" assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data assert entry.unique_id == final_unique_id + assert len(device_registry.devices) == 2 + controller_device_id_ext = ( + f"{controller_device_id}-{controller_node.manufacturer_id}:" + f"{controller_node.product_type}:{controller_node.product_id}" + ) + device = device_registry.async_get_device( + identifiers={(DOMAIN, controller_device_id_ext)} + ) + assert device + assert device.manufacturer == "New Device Manufacturer" + assert device.model == "New Device Model" + assert device.name == "New Device Name" + device = device_registry.async_get_device(identifiers={(DOMAIN, sensor_device_id)}) + assert device + assert device.manufacturer == "AEON Labs" + assert device.model == "ZW100" + assert device.name == "Multisensor 6" + assert device.name_by_user == "Custom Sensor Device Name" + assert client.driver.controller.home_id == 3245146787 + @pytest.mark.usefixtures("supervisor", "addon_running") async def test_reconfigure_migrate_reset_driver_ready_timeout( @@ -3755,7 +3834,7 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3770,6 +3849,7 @@ async def test_reconfigure_migrate_reset_driver_ready_timeout( assert entry.data["usb_path"] == "/test" assert entry.data["use_addon"] is True assert entry.unique_id == "5678" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -3895,7 +3975,7 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert client.connect.call_count == 2 await hass.async_block_till_done() - assert client.connect.call_count == 3 + assert client.connect.call_count == 4 assert entry.state is config_entries.ConfigEntryState.LOADED assert client.driver.controller.async_restore_nvm.call_count == 1 assert len(events) == 2 @@ -3906,9 +3986,10 @@ async def test_reconfigure_migrate_restore_driver_ready_timeout( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "migration_successful" - assert integration.data["url"] == "ws://host1:3001" - assert integration.data["usb_path"] == "/test" - assert integration.data["use_addon"] is True + assert entry.data["url"] == "ws://host1:3001" + assert entry.data["usb_path"] == "/test" + assert entry.data["use_addon"] is True + assert "keep_old_devices" not in entry.data async def test_reconfigure_migrate_backup_failure( @@ -3942,6 +4023,7 @@ async def test_reconfigure_migrate_backup_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" + assert "keep_old_devices" not in entry.data async def test_reconfigure_migrate_backup_file_failure( @@ -3988,6 +4070,7 @@ async def test_reconfigure_migrate_backup_file_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "backup_failed" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running") @@ -4073,6 +4156,7 @@ async def test_reconfigure_migrate_start_addon_failure( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "addon_start_failed" + assert "keep_old_devices" not in entry.data @pytest.mark.usefixtures("supervisor", "addon_running", "restart_addon") @@ -4187,6 +4271,7 @@ async def test_reconfigure_migrate_restore_failure( hass.config_entries.flow.async_abort(result["flow_id"]) assert len(hass.config_entries.flow.async_progress()) == 0 + assert "keep_old_devices" not in entry.data async def test_get_driver_failure_intent_migrate( @@ -4196,13 +4281,13 @@ async def test_get_driver_failure_intent_migrate( """Test get driver failure in intent migrate step.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU assert result["step_id"] == "reconfigure" - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": "intent_migrate"} @@ -4210,6 +4295,7 @@ async def test_get_driver_failure_intent_migrate( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "config_entry_not_loaded" + assert "keep_old_devices" not in entry.data async def test_get_driver_failure_instruct_unplug( @@ -4231,7 +4317,7 @@ async def test_get_driver_failure_instruct_unplug( ) entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) result = await entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.MENU @@ -4254,7 +4340,7 @@ async def test_get_driver_failure_instruct_unplug( assert client.driver.controller.async_backup_nvm_raw.call_count == 1 assert mock_file.call_count == 1 - await hass.config_entries.async_unload(integration.entry_id) + await hass.config_entries.async_unload(entry.entry_id) result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -4270,7 +4356,7 @@ async def test_hard_reset_failure( """Test hard reset failure.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) async def mock_backup_nvm_raw(): @@ -4320,7 +4406,7 @@ async def test_choose_serial_port_usb_ports_failure( """Test choose serial port usb ports failure.""" entry = integration hass.config_entries.async_update_entry( - integration, unique_id="1234", data={**integration.data, "use_addon": True} + entry, unique_id="1234", data={**entry.data, "use_addon": True} ) async def mock_backup_nvm_raw(): From d302e817c821bf241e5778cacad1979876274808 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 3 Jun 2025 04:20:14 -0700 Subject: [PATCH 1053/1175] NextBus: Bump py_nextbusnext to 2.2.0 (#145904) --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index a4f6d54f58c..4b7057f7142 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nextbus", "iot_class": "cloud_polling", "loggers": ["py_nextbus"], - "requirements": ["py-nextbusnext==2.1.2"] + "requirements": ["py-nextbusnext==2.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 63ddad956e9..4408673f575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1762,7 +1762,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.1.2 +py-nextbusnext==2.2.0 # homeassistant.components.nightscout py-nightscout==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3f5788e5d8..6f527a9c393 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1485,7 +1485,7 @@ py-madvr2==1.6.32 py-melissa-climate==2.1.4 # homeassistant.components.nextbus -py-nextbusnext==2.1.2 +py-nextbusnext==2.2.0 # homeassistant.components.nightscout py-nightscout==1.2.2 From 1a21e01f851631ecaff2c4852e644baccadf2c64 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:14:19 +0200 Subject: [PATCH 1054/1175] Bump aioimmich to 0.8.0 (#145908) --- homeassistant/components/immich/manifest.json | 2 +- .../components/immich/media_source.py | 28 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/immich/conftest.py | 118 +++++++++++------- tests/components/immich/const.py | 109 ++++++++++++---- .../immich/snapshots/test_diagnostics.ambr | 46 ++++--- .../immich/snapshots/test_sensor.ambr | 4 +- 8 files changed, 206 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 5b56a7e3e2d..b7c8176356f 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.7.0"] + "requirements": ["aioimmich==0.8.0"] } diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index a7c55f9c572..c636fda879a 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -133,10 +133,10 @@ class ImmichMediaSource(MediaSource): identifier=f"{identifier.unique_id}|albums|{album.album_id}", media_class=MediaClass.DIRECTORY, media_content_type=MediaClass.IMAGE, - title=album.name, + title=album.album_name, can_play=False, can_expand=True, - thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg", + thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg", ) for album in albums ] @@ -160,18 +160,19 @@ class ImmichMediaSource(MediaSource): f"{identifier.unique_id}|albums|" f"{identifier.collection_id}|" f"{asset.asset_id}|" - f"{asset.file_name}|" - f"{asset.mime_type}" + f"{asset.original_file_name}|" + f"{mime_type}" ), media_class=MediaClass.IMAGE, - media_content_type=asset.mime_type, - title=asset.file_name, + media_content_type=mime_type, + title=asset.original_file_name, can_play=False, can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}", + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{mime_type}", ) for asset in album_info.assets - if asset.mime_type.startswith("image/") + if (mime_type := asset.original_mime_type) + and mime_type.startswith("image/") ] ret.extend( @@ -181,18 +182,19 @@ class ImmichMediaSource(MediaSource): f"{identifier.unique_id}|albums|" f"{identifier.collection_id}|" f"{asset.asset_id}|" - f"{asset.file_name}|" - f"{asset.mime_type}" + f"{asset.original_file_name}|" + f"{mime_type}" ), media_class=MediaClass.VIDEO, - media_content_type=asset.mime_type, - title=asset.file_name, + media_content_type=mime_type, + title=asset.original_file_name, can_play=True, can_expand=False, thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", ) for asset in album_info.assets - if asset.mime_type.startswith("video/") + if (mime_type := asset.original_mime_type) + and mime_type.startswith("video/") ) return ret diff --git a/requirements_all.txt b/requirements_all.txt index 4408673f575..a7aeda1007b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.7.0 +aioimmich==0.8.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f527a9c393..10121e31eb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.7.0 +aioimmich==0.8.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/tests/components/immich/conftest.py b/tests/components/immich/conftest.py index 1b9a7df8df7..f8f959e0b0a 100644 --- a/tests/components/immich/conftest.py +++ b/tests/components/immich/conftest.py @@ -1,7 +1,6 @@ """Common fixtures for the Immich tests.""" from collections.abc import AsyncGenerator, Generator -from datetime import datetime from unittest.mock import AsyncMock, patch from aioimmich import ImmichAlbums, ImmichAssests, ImmichServer, ImmichUsers @@ -10,7 +9,7 @@ from aioimmich.server.models import ( ImmichServerStatistics, ImmichServerStorage, ) -from aioimmich.users.models import AvatarColor, ImmichUser, UserStatus +from aioimmich.users.models import ImmichUserObject import pytest from homeassistant.components.immich.const import DOMAIN @@ -78,36 +77,58 @@ def mock_immich_assets() -> AsyncMock: def mock_immich_server() -> AsyncMock: """Mock the Immich server.""" mock = AsyncMock(spec=ImmichServer) - mock.async_get_about_info.return_value = ImmichServerAbout( - "v1.132.3", - "some_url", - False, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, + mock.async_get_about_info.return_value = ImmichServerAbout.from_dict( + { + "version": "v1.132.3", + "versionUrl": "https://github.com/immich-app/immich/releases/tag/v1.132.3", + "licensed": False, + "build": "14709928600", + "buildUrl": "https://github.com/immich-app/immich/actions/runs/14709928600", + "buildImage": "v1.132.3", + "buildImageUrl": "https://github.com/immich-app/immich/pkgs/container/immich-server", + "repository": "immich-app/immich", + "repositoryUrl": "https://github.com/immich-app/immich", + "sourceRef": "v1.132.3", + "sourceCommit": "02994883fe3f3972323bb6759d0170a4062f5236", + "sourceUrl": "https://github.com/immich-app/immich/commit/02994883fe3f3972323bb6759d0170a4062f5236", + "nodejs": "v22.14.0", + "exiftool": "13.00", + "ffmpeg": "7.0.2-7", + "libvips": "8.16.1", + "imagemagick": "7.1.1-47", + } ) - mock.async_get_storage_info.return_value = ImmichServerStorage( - "294.2 GiB", - "142.9 GiB", - "136.3 GiB", - 315926315008, - 153400434688, - 146402975744, - 48.56, + mock.async_get_storage_info.return_value = ImmichServerStorage.from_dict( + { + "diskSize": "294.2 GiB", + "diskUse": "142.9 GiB", + "diskAvailable": "136.3 GiB", + "diskSizeRaw": 315926315008, + "diskUseRaw": 153400406016, + "diskAvailableRaw": 146403004416, + "diskUsagePercentage": 48.56, + } ) - mock.async_get_server_statistics.return_value = ImmichServerStatistics( - 27038, 1836, 119525451912, 54291170551, 65234281361 + mock.async_get_server_statistics.return_value = ImmichServerStatistics.from_dict( + { + "photos": 27038, + "videos": 1836, + "usage": 119525451912, + "usagePhotos": 54291170551, + "usageVideos": 65234281361, + "usageByUser": [ + { + "userId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "userName": "admin", + "photos": 27038, + "videos": 1836, + "usage": 119525451912, + "usagePhotos": 54291170551, + "usageVideos": 65234281361, + "quotaSizeInBytes": None, + } + ], + } ) return mock @@ -116,23 +137,26 @@ def mock_immich_server() -> AsyncMock: def mock_immich_user() -> AsyncMock: """Mock the Immich server.""" mock = AsyncMock(spec=ImmichUsers) - mock.async_get_my_user.return_value = ImmichUser( - "e7ef5713-9dab-4bd4-b899-715b0ca4379e", - "user@immich.local", - "user", - "", - AvatarColor.PRIMARY, - datetime.fromisoformat("2025-05-11T10:07:46.866Z"), - "user", - False, - True, - datetime.fromisoformat("2025-05-11T10:07:46.866Z"), - None, - None, - "", - None, - None, - UserStatus.ACTIVE, + mock.async_get_my_user.return_value = ImmichUserObject.from_dict( + { + "id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "email": "user@immich.local", + "name": "user", + "profileImagePath": "", + "avatarColor": "primary", + "profileChangedAt": "2025-05-11T10:07:46.866Z", + "storageLabel": "user", + "shouldChangePassword": True, + "isAdmin": True, + "createdAt": "2025-05-11T10:07:46.866Z", + "deletedAt": None, + "updatedAt": "2025-05-18T00:59:55.547Z", + "oauthId": "", + "quotaSizeInBytes": None, + "quotaUsageInBytes": 119526467534, + "status": "active", + "license": None, + } ) return mock diff --git a/tests/components/immich/const.py b/tests/components/immich/const.py index ac0b221f721..97721bc7dbc 100644 --- a/tests/components/immich/const.py +++ b/tests/components/immich/const.py @@ -1,7 +1,6 @@ """Constants for the Immich integration tests.""" from aioimmich.albums.models import ImmichAlbum -from aioimmich.assets.models import ImmichAsset from homeassistant.const import ( CONF_API_KEY, @@ -26,27 +25,91 @@ MOCK_CONFIG_ENTRY_DATA = { CONF_VERIFY_SSL: False, } -MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum( - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", - "My Album", - "This is my first great album", - "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", - 1, - [], -) +ALBUM_DATA = { + "id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", + "albumName": "My Album", + "albumThumbnailAssetId": "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", + "albumUsers": [], + "assetCount": 1, + "assets": [], + "createdAt": "2025-05-11T10:13:22.799Z", + "hasSharedLink": False, + "isActivityEnabled": False, + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "owner": { + "id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "email": "admin@immich.local", + "name": "admin", + "profileImagePath": "", + "avatarColor": "primary", + "profileChangedAt": "2025-05-11T10:07:46.866Z", + }, + "shared": False, + "updatedAt": "2025-05-17T11:26:03.696Z", +} -MOCK_ALBUM_WITH_ASSETS = ImmichAlbum( - "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", - "My Album", - "This is my first great album", - "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5", - 1, - [ - ImmichAsset( - "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg" - ), - ImmichAsset( - "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", "filename.mp4", "video/mp4" - ), - ], +MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum.from_dict(ALBUM_DATA) + +MOCK_ALBUM_WITH_ASSETS = ImmichAlbum.from_dict( + { + **ALBUM_DATA, + "assets": [ + { + "id": "2e94c203-50aa-4ad2-8e29-56dd74e0eff4", + "deviceAssetId": "web-filename.jpg-1675185639000", + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "deviceId": "WEB", + "libraryId": None, + "type": "IMAGE", + "originalPath": "upload/upload/e7ef5713-9dab-4bd4-b899-715b0ca4379e/b4/b8/b4b8ef00-8a6d-4056-91ff-7f86dc66e427.jpg", + "originalFileName": "filename.jpg", + "originalMimeType": "image/jpeg", + "thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==", + "fileCreatedAt": "2023-01-31T17:20:37.085+00:00", + "fileModifiedAt": "2023-01-31T17:20:39+00:00", + "localDateTime": "2023-01-31T18:20:37.085+00:00", + "updatedAt": "2025-05-11T10:13:49.590401+00:00", + "isFavorite": False, + "isArchived": False, + "isTrashed": False, + "duration": "0:00:00.00000", + "exifInfo": {}, + "livePhotoVideoId": None, + "people": [], + "checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=", + "isOffline": False, + "hasMetadata": True, + "duplicateId": None, + "resized": True, + }, + { + "id": "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", + "deviceAssetId": "web-filename.mp4-1675185639000", + "ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e", + "deviceId": "WEB", + "libraryId": None, + "type": "IMAGE", + "originalPath": "upload/upload/e7ef5713-9dab-4bd4-b899-715b0ca4379e/b4/b8/b4b8ef00-8a6d-4056-eeff-7f86dc66e427.mp4", + "originalFileName": "filename.mp4", + "originalMimeType": "video/mp4", + "thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==", + "fileCreatedAt": "2023-01-31T17:20:37.085+00:00", + "fileModifiedAt": "2023-01-31T17:20:39+00:00", + "localDateTime": "2023-01-31T18:20:37.085+00:00", + "updatedAt": "2025-05-11T10:13:49.590401+00:00", + "isFavorite": False, + "isArchived": False, + "isTrashed": False, + "duration": "0:00:00.00000", + "exifInfo": {}, + "livePhotoVideoId": None, + "people": [], + "checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=", + "isOffline": False, + "hasMetadata": True, + "duplicateId": None, + "resized": True, + }, + ], + } ) diff --git a/tests/components/immich/snapshots/test_diagnostics.ambr b/tests/components/immich/snapshots/test_diagnostics.ambr index 3216de2fabd..b3dd3c47db6 100644 --- a/tests/components/immich/snapshots/test_diagnostics.ambr +++ b/tests/components/immich/snapshots/test_diagnostics.ambr @@ -3,36 +3,48 @@ dict({ 'data': dict({ 'server_about': dict({ - 'build': None, - 'build_image': None, - 'build_image_url': None, - 'build_url': None, - 'exiftool': None, - 'ffmpeg': None, - 'imagemagick': None, - 'libvips': None, + 'build': '14709928600', + 'build_image': 'v1.132.3', + 'build_image_url': 'https://github.com/immich-app/immich/pkgs/container/immich-server', + 'build_url': 'https://github.com/immich-app/immich/actions/runs/14709928600', + 'exiftool': '13.00', + 'ffmpeg': '7.0.2-7', + 'imagemagick': '7.1.1-47', + 'libvips': '8.16.1', 'licensed': False, - 'nodejs': None, - 'repository': None, - 'repository_url': None, - 'source_commit': None, - 'source_ref': None, - 'source_url': None, + 'nodejs': 'v22.14.0', + 'repository': 'immich-app/immich', + 'repository_url': 'https://github.com/immich-app/immich', + 'source_commit': '02994883fe3f3972323bb6759d0170a4062f5236', + 'source_ref': 'v1.132.3', + 'source_url': 'https://github.com/immich-app/immich/commit/02994883fe3f3972323bb6759d0170a4062f5236', 'version': 'v1.132.3', - 'version_url': 'some_url', + 'version_url': 'https://github.com/immich-app/immich/releases/tag/v1.132.3', }), 'server_storage': dict({ 'disk_available': '136.3 GiB', - 'disk_available_raw': 146402975744, + 'disk_available_raw': 146403004416, 'disk_size': '294.2 GiB', 'disk_size_raw': 315926315008, 'disk_usage_percentage': 48.56, 'disk_use': '142.9 GiB', - 'disk_use_raw': 153400434688, + 'disk_use_raw': 153400406016, }), 'server_usage': dict({ 'photos': 27038, 'usage': 119525451912, + 'usage_by_user': list([ + dict({ + 'photos': 27038, + 'quota_size_in_bytes': None, + 'usage': 119525451912, + 'usage_photos': 54291170551, + 'usage_videos': 65234281361, + 'user_id': 'e7ef5713-9dab-4bd4-b899-715b0ca4379e', + 'user_name': 'admin', + 'videos': 1836, + }), + ]), 'usage_photos': 54291170551, 'usage_videos': 65234281361, 'videos': 1836, diff --git a/tests/components/immich/snapshots/test_sensor.ambr b/tests/components/immich/snapshots/test_sensor.ambr index d1ae9a8be8d..590e7d9ad5c 100644 --- a/tests/components/immich/snapshots/test_sensor.ambr +++ b/tests/components/immich/snapshots/test_sensor.ambr @@ -55,7 +55,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '136.34839630127', + 'state': '136.34842300415', }) # --- # name: test_sensors[sensor.someone_disk_size-entry] @@ -225,7 +225,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '142.865287780762', + 'state': '142.865261077881', }) # --- # name: test_sensors[sensor.someone_disk_used_by_photos-entry] From 88f2c3abd3909f23d00c168de622c805997f1da5 Mon Sep 17 00:00:00 2001 From: TimL Date: Sun, 1 Jun 2025 11:14:08 +1000 Subject: [PATCH 1055/1175] Bump pysmlight to v0.2.5 (#145949) --- homeassistant/components/smlight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index b2a03a737fc..f47960a65bd 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -12,7 +12,7 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "silver", - "requirements": ["pysmlight==0.2.4"], + "requirements": ["pysmlight==0.2.5"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index a7aeda1007b..bcb2237124b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2353,7 +2353,7 @@ pysmhi==1.0.2 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.2.4 +pysmlight==0.2.5 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10121e31eb9..cb0e7035770 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1953,7 +1953,7 @@ pysmhi==1.0.2 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.2.4 +pysmlight==0.2.5 # homeassistant.components.snmp pysnmp==6.2.6 From 7e851370126fcf64fd47a66ecf4cf6b08e7a0abd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 2 Jun 2025 00:47:05 -0700 Subject: [PATCH 1056/1175] Bump ical to 10.0.0 (#145954) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/remote_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index c5a9d4784bc..fecd245869a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"] + "requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index fc636d75482..e0b08313d63 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index cd19090f400..c8e80e4f91b 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==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/homeassistant/components/remote_calendar/manifest.json b/homeassistant/components/remote_calendar/manifest.json index 60b5e15e8fb..7bdc5362ae7 100644 --- a/homeassistant/components/remote_calendar/manifest.json +++ b/homeassistant/components/remote_calendar/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["ical"], "quality_scale": "silver", - "requirements": ["ical==9.2.5"] + "requirements": ["ical==10.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index bcb2237124b..68241b5d5ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,7 +1203,7 @@ ibmiotf==0.3.4 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.5 +ical==10.0.0 # homeassistant.components.caldav icalendar==6.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0e7035770..e8176cbd41f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.local_calendar # homeassistant.components.local_todo # homeassistant.components.remote_calendar -ical==9.2.5 +ical==10.0.0 # homeassistant.components.caldav icalendar==6.1.0 From f280032dcf8deffcc152359721e82b7c97b5999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noah=20Gro=C3=9F?= Date: Tue, 3 Jun 2025 10:57:59 +0200 Subject: [PATCH 1057/1175] Bump python-picnic-api2 to 1.3.1 (#145962) --- homeassistant/components/picnic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/picnic/manifest.json b/homeassistant/components/picnic/manifest.json index 251964c15d0..e7623c5eb03 100644 --- a/homeassistant/components/picnic/manifest.json +++ b/homeassistant/components/picnic/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/picnic", "iot_class": "cloud_polling", "loggers": ["python_picnic_api2"], - "requirements": ["python-picnic-api2==1.2.4"] + "requirements": ["python-picnic-api2==1.3.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 68241b5d5ff..ca3ef670783 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ python-otbr-api==2.7.0 python-overseerr==0.7.1 # homeassistant.components.picnic -python-picnic-api2==1.2.4 +python-picnic-api2==1.3.1 # homeassistant.components.rabbitair python-rabbitair==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8176cbd41f..297e3ff5958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2056,7 +2056,7 @@ python-otbr-api==2.7.0 python-overseerr==0.7.1 # homeassistant.components.picnic -python-picnic-api2==1.2.4 +python-picnic-api2==1.3.1 # homeassistant.components.rabbitair python-rabbitair==0.0.8 From d729eed7c2f3b31a557601ea2aca38e7de368504 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 2 Jun 2025 10:48:42 +0300 Subject: [PATCH 1058/1175] Add diagnostics to Amazon devices (#145964) --- .../components/amazon_devices/diagnostics.py | 66 +++++++++++++++++ .../snapshots/test_diagnostics.ambr | 74 +++++++++++++++++++ .../amazon_devices/test_diagnostics.py | 70 ++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 homeassistant/components/amazon_devices/diagnostics.py create mode 100644 tests/components/amazon_devices/snapshots/test_diagnostics.ambr create mode 100644 tests/components/amazon_devices/test_diagnostics.py diff --git a/homeassistant/components/amazon_devices/diagnostics.py b/homeassistant/components/amazon_devices/diagnostics.py new file mode 100644 index 00000000000..e9a0773cd3f --- /dev/null +++ b/homeassistant/components/amazon_devices/diagnostics.py @@ -0,0 +1,66 @@ +"""Diagnostics support for Amazon Devices integration.""" + +from __future__ import annotations + +from typing import Any + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry + +from .coordinator import AmazonConfigEntry + +TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: AmazonConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + coordinator = entry.runtime_data + + devices: list[dict[str, dict[str, Any]]] = [ + build_device_data(device) for device in coordinator.data.values() + ] + + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": { + "last_update success": coordinator.last_update_success, + "last_exception": repr(coordinator.last_exception), + "devices": devices, + }, + } + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + + coordinator = entry.runtime_data + + assert device_entry.serial_number + + return build_device_data(coordinator.data[device_entry.serial_number]) + + +def build_device_data(device: AmazonDevice) -> dict[str, Any]: + """Build device data for diagnostics.""" + return { + "account name": device.account_name, + "capabilities": device.capabilities, + "device family": device.device_family, + "device type": device.device_type, + "device cluster members": device.device_cluster_members, + "online": device.online, + "serial number": device.serial_number, + "software version": device.software_version, + "do not disturb": device.do_not_disturb, + "response style": device.response_style, + "bluetooth state": device.bluetooth_state, + } diff --git a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr b/tests/components/amazon_devices/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..0b5164418aa --- /dev/null +++ b/tests/components/amazon_devices/snapshots/test_diagnostics.ambr @@ -0,0 +1,74 @@ +# serializer version: 1 +# name: test_device_diagnostics + dict({ + 'account name': 'Echo Test', + 'bluetooth state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device cluster members': list([ + 'echo_test_serial_number', + ]), + 'device family': 'mine', + 'device type': 'echo', + 'do not disturb': False, + 'online': True, + 'response style': None, + 'serial number': 'echo_test_serial_number', + 'software version': 'echo_test_software_version', + }) +# --- +# name: test_entry_diagnostics + dict({ + 'device_info': dict({ + 'devices': list([ + dict({ + 'account name': 'Echo Test', + 'bluetooth state': True, + 'capabilities': list([ + 'AUDIO_PLAYER', + 'MICROPHONE', + ]), + 'device cluster members': list([ + 'echo_test_serial_number', + ]), + 'device family': 'mine', + 'device type': 'echo', + 'do not disturb': False, + 'online': True, + 'response style': None, + 'serial number': 'echo_test_serial_number', + 'software version': 'echo_test_software_version', + }), + ]), + 'last_exception': 'None', + 'last_update success': True, + }), + 'entry': dict({ + 'data': dict({ + 'country': 'IT', + 'login_data': dict({ + 'session': 'test-session', + }), + 'password': '**REDACTED**', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'discovery_keys': dict({ + }), + 'domain': 'amazon_devices', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'subentries': list([ + ]), + 'title': '**REDACTED**', + 'unique_id': 'fake_email@gmail.com', + 'version': 1, + }), + }) +# --- diff --git a/tests/components/amazon_devices/test_diagnostics.py b/tests/components/amazon_devices/test_diagnostics.py new file mode 100644 index 00000000000..e548702650b --- /dev/null +++ b/tests/components/amazon_devices/test_diagnostics.py @@ -0,0 +1,70 @@ +"""Tests for Amazon Devices diagnostics platform.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration +from .const import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) +from tests.typing import ClientSessionGenerator + + +async def test_entry_diagnostics( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test Amazon config entry diagnostics.""" + await setup_integration(hass, mock_config_entry) + + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) == snapshot( + exclude=props( + "entry_id", + "created_at", + "modified_at", + ) + ) + + +async def test_device_diagnostics( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test Amazon device diagnostics.""" + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device( + identifiers={(DOMAIN, TEST_SERIAL_NUMBER)} + ) + assert device, repr(device_registry.devices) + + assert await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, device + ) == snapshot( + exclude=props( + "entry_id", + "created_at", + "modified_at", + ) + ) From 6defed2915ce53f08d3b111608fb6d8ff86071b9 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 1 Jun 2025 22:15:18 +0300 Subject: [PATCH 1059/1175] Bump aioamazondevices to 3.0.4 (#145971) --- homeassistant/components/amazon_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index eb9fae6ddbe..a24671298d9 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==2.1.1"] + "requirements": ["aioamazondevices==3.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca3ef670783..a5eada9ccc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.1.1 +aioamazondevices==3.0.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297e3ff5958..3611eaac1f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==2.1.1 +aioamazondevices==3.0.4 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 9e1d8c2fc6f58d8d909356ec17be3c8913828981 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jun 2025 15:11:56 +0200 Subject: [PATCH 1060/1175] Bump reolink-aio to 0.13.5 (#145974) * Add debug logging * Bump reolink-aio to 0.13.5 * Revert "Add debug logging" This reverts commit f96030a6c8dccca7888b6d1274d5ed3a251ac03c. --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 694dd43a532..5ae8b0305e4 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -19,5 +19,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.13.4"] + "requirements": ["reolink-aio==0.13.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index a5eada9ccc2..28188b36675 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2652,7 +2652,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.4 +reolink-aio==0.13.5 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3611eaac1f7..9c895522d12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2195,7 +2195,7 @@ renault-api==0.3.1 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.13.4 +reolink-aio==0.13.5 # homeassistant.components.rflink rflink==0.0.66 From 76269333528940ccae6a37c41a57f781578cb59a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 2 Jun 2025 20:48:40 +0200 Subject: [PATCH 1061/1175] Bump go2rtc-client to 0.2.1 (#146019) * Bump go2rtc-client to 0.2.0 * Bump go2rtc-client to 0.2.1 * Clean up hassfest exception --------- Co-authored-by: Martin Hjelmare --- homeassistant/components/go2rtc/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/go2rtc/manifest.json b/homeassistant/components/go2rtc/manifest.json index 09f7b3fd74c..dd50b4ba076 100644 --- a/homeassistant/components/go2rtc/manifest.json +++ b/homeassistant/components/go2rtc/manifest.json @@ -8,6 +8,6 @@ "integration_type": "system", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["go2rtc-client==0.1.3b0"], + "requirements": ["go2rtc-client==0.2.1"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5aa0d8ae82b..21b9d5e064a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ cronsim==2.6 cryptography==45.0.1 dbus-fast==2.43.0 fnv-hash-fast==1.5.0 -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==3.48.2 hass-nabucasa==0.101.0 diff --git a/requirements_all.txt b/requirements_all.txt index 28188b36675..4af2e16aa4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1026,7 +1026,7 @@ gitterpy==0.1.7 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/requirements_test.txt b/requirements_test.txt index 40349402c4d..e97b71fa7dc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ astroid==3.3.10 coverage==7.6.12 freezegun==1.5.1 -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 license-expression==30.4.1 mock-open==1.4.0 mypy-dev==1.16.0a8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c895522d12..2f51d93402b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -890,7 +890,7 @@ gios==6.0.0 glances-api==0.8.0 # homeassistant.components.go2rtc -go2rtc-client==0.1.3b0 +go2rtc-client==0.2.1 # homeassistant.components.goalzero goalzero==0.2.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 647755d8237..981dc3344c0 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.3b0 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 6f0947419359cec70eb09e86d988cd7ed6ea7f29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 Jun 2025 14:04:02 +0100 Subject: [PATCH 1062/1175] Bump grpcio to 1.72.1 (#146029) --- homeassistant/package_constraints.txt | 6 +++--- script/gen_requirements_all.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 21b9d5e064a..598e2834e86 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,9 +88,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.0 -grpcio-status==1.72.0 -grpcio-reflection==1.72.0 +grpcio==1.72.1 +grpcio-status==1.72.1 +grpcio-reflection==1.72.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 082062c53a0..606141e4c65 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,9 +115,9 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.72.0 -grpcio-status==1.72.0 -grpcio-reflection==1.72.0 +grpcio==1.72.1 +grpcio-status==1.72.1 +grpcio-reflection==1.72.1 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 From cf521d4c7c6f17014ddcc7e203eb95d7322dea25 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Jun 2025 15:12:13 +0200 Subject: [PATCH 1063/1175] Improve debug logging Reolink (#146033) Add debug logging --- homeassistant/components/reolink/__init__.py | 4 ++++ homeassistant/components/reolink/config_flow.py | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 57d41c20521..38f8e709b5c 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -150,6 +150,10 @@ async def async_setup_entry( if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED: # Their are new cameras/chimes connected, reload to add them. + _LOGGER.debug( + "Reloading Reolink %s to add new device (capabilities)", + host.api.nvr_name, + ) hass.async_create_task( hass.config_entries.async_reload(config_entry.entry_id) ) diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index 12ccd455be3..659169c3618 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -194,6 +194,13 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN): ) raise AbortFlow("already_configured") + if existing_entry and existing_entry.data[CONF_HOST] != discovery_info.ip: + _LOGGER.debug( + "Reolink DHCP reported new IP '%s', updating from old IP '%s'", + discovery_info.ip, + existing_entry.data[CONF_HOST], + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) self.context["title_placeholders"] = { From e5cb77d1681bbe8afa545d4c3f7566cbd97b8bb3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:05:33 +0200 Subject: [PATCH 1064/1175] Adjust ConnectionFailure logging in SamsungTV (#146044) --- homeassistant/components/samsungtv/bridge.py | 21 +++++++---- .../components/samsungtv/test_media_player.py | 35 +++++++++++++++++-- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 11da83219c7..d8682856752 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -636,14 +636,21 @@ class SamsungTVWSBridge( ) self._remote = None except ConnectionFailure as err: - LOGGER.warning( - ( + error_details = err.args[0] + if "ms.channel.timeOut" in (error_details := repr(err)): + # The websocket was connected, but the TV is probably asleep + LOGGER.debug( + "Channel timeout occurred trying to get remote for %s: %s", + self.host, + error_details, + ) + else: + LOGGER.warning( "Unexpected ConnectionFailure trying to get remote for %s, " - "please report this issue: %s" - ), - self.host, - repr(err), - ) + "please report this issue: %s", + self.host, + error_details, + ) self._remote = None except (WebSocketException, AsyncioTimeoutError, OSError) as err: LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 58797b67423..1bf3c953fc6 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -409,7 +409,7 @@ async def test_update_ws_connection_failure( patch.object( remote_websocket, "start_listening", - side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), + side_effect=ConnectionFailure({"event": "ms.voiceApp.hide"}), ), patch.object(remote_websocket, "is_alive", return_value=False), ): @@ -419,7 +419,7 @@ async def test_update_ws_connection_failure( assert ( "Unexpected ConnectionFailure trying to get remote for fake_host, please " - 'report this issue: ConnectionFailure(\'{"event": "ms.voiceApp.hide"}\')' + "report this issue: ConnectionFailure({'event': 'ms.voiceApp.hide'})" in caplog.text ) @@ -427,6 +427,37 @@ async def test_update_ws_connection_failure( assert state.state == STATE_OFF +@pytest.mark.usefixtures("rest_api") +async def test_update_ws_connection_failure_channel_timeout( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + remote_websocket: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Testing update tv connection failure exception.""" + await setup_samsungtv_entry(hass, MOCK_CONFIGWS) + + with ( + patch.object( + remote_websocket, + "start_listening", + side_effect=ConnectionFailure({"event": "ms.channel.timeOut"}), + ), + patch.object(remote_websocket, "is_alive", return_value=False), + ): + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert ( + "Channel timeout occurred trying to get remote for fake_host: " + "ConnectionFailure({'event': 'ms.channel.timeOut'})" in caplog.text + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + @pytest.mark.usefixtures("rest_api") async def test_update_ws_connection_closed( hass: HomeAssistant, freezer: FrozenDateTimeFactory, remote_websocket: Mock From e15edbd54b3350df4b5ebc483d2a06e5c66d980a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:30:18 +0200 Subject: [PATCH 1065/1175] Adjust SamsungTV on/off logging (#146045) * Adjust SamsungTV on/off logging * Update coordinator.py --- homeassistant/components/samsungtv/coordinator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/coordinator.py b/homeassistant/components/samsungtv/coordinator.py index ed3c24946ab..9b09436be88 100644 --- a/homeassistant/components/samsungtv/coordinator.py +++ b/homeassistant/components/samsungtv/coordinator.py @@ -39,7 +39,7 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ) self.bridge = bridge - self.is_on: bool | None = False + self.is_on: bool | None = None self.async_extra_update: Callable[[], Coroutine[Any, Any, None]] | None = None async def _async_update_data(self) -> None: @@ -52,7 +52,12 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]): else: self.is_on = await self.bridge.async_is_on() if self.is_on != old_state: - LOGGER.debug("TV %s state updated to %s", self.bridge.host, self.is_on) + LOGGER.debug( + "TV %s state updated from %s to %s", + self.bridge.host, + old_state, + self.is_on, + ) if self.async_extra_update: await self.async_extra_update() From 999c9b3dc53b625b5c2521ba8c0637fccc1da1bd Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 2 Jun 2025 19:58:54 +0200 Subject: [PATCH 1066/1175] Don't use multi-line conditionals in immich (#146062) --- .../components/immich/media_source.py | 73 ++++++++----------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/immich/media_source.py b/homeassistant/components/immich/media_source.py index c636fda879a..caf8264895b 100644 --- a/homeassistant/components/immich/media_source.py +++ b/homeassistant/components/immich/media_source.py @@ -153,49 +153,40 @@ class ImmichMediaSource(MediaSource): except ImmichError: return [] - ret = [ - BrowseMediaSource( - domain=DOMAIN, - identifier=( - f"{identifier.unique_id}|albums|" - f"{identifier.collection_id}|" - f"{asset.asset_id}|" - f"{asset.original_file_name}|" - f"{mime_type}" - ), - media_class=MediaClass.IMAGE, - media_content_type=mime_type, - title=asset.original_file_name, - can_play=False, - can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{mime_type}", - ) - for asset in album_info.assets - if (mime_type := asset.original_mime_type) - and mime_type.startswith("image/") - ] + ret: list[BrowseMediaSource] = [] + for asset in album_info.assets: + if not (mime_type := asset.original_mime_type) or not mime_type.startswith( + ("image/", "video/") + ): + continue - ret.extend( - BrowseMediaSource( - domain=DOMAIN, - identifier=( - f"{identifier.unique_id}|albums|" - f"{identifier.collection_id}|" - f"{asset.asset_id}|" - f"{asset.original_file_name}|" - f"{mime_type}" - ), - media_class=MediaClass.VIDEO, - media_content_type=mime_type, - title=asset.original_file_name, - can_play=True, - can_expand=False, - thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg", + if mime_type.startswith("image/"): + media_class = MediaClass.IMAGE + can_play = False + thumb_mime_type = mime_type + else: + media_class = MediaClass.VIDEO + can_play = True + thumb_mime_type = "image/jpeg" + + ret.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=( + f"{identifier.unique_id}|albums|" + f"{identifier.collection_id}|" + f"{asset.asset_id}|" + f"{asset.original_file_name}|" + f"{mime_type}" + ), + media_class=media_class, + media_content_type=mime_type, + title=asset.original_file_name, + can_play=can_play, + can_expand=False, + thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}", + ) ) - for asset in album_info.assets - if (mime_type := asset.original_mime_type) - and mime_type.startswith("video/") - ) return ret From 1e304fad653529d018906d639718416929f3be41 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 2 Jun 2025 22:38:17 +0300 Subject: [PATCH 1067/1175] Fix Shelly BLU TRV calibrate button (#146066) --- homeassistant/components/shelly/button.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index 44f81cc8b36..eab7514514d 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from functools import partial from typing import TYPE_CHECKING, Any, Final -from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY, RPC_GENERATIONS +from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.components.button import ( @@ -62,7 +62,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="self_test", entity_category=EntityCategory.DIAGNOSTIC, press_action="trigger_shelly_gas_self_test", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription[ShellyBlockCoordinator]( key="mute", @@ -70,7 +70,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="mute", entity_category=EntityCategory.CONFIG, press_action="trigger_shelly_gas_mute", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ShellyButtonDescription[ShellyBlockCoordinator]( key="unmute", @@ -78,7 +78,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ translation_key="unmute", entity_category=EntityCategory.CONFIG, press_action="trigger_shelly_gas_unmute", - supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, + supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS, ), ] @@ -89,7 +89,7 @@ BLU_TRV_BUTTONS: Final[list[ShellyButtonDescription]] = [ translation_key="calibrate", entity_category=EntityCategory.CONFIG, press_action="trigger_blu_trv_calibration", - supported=lambda coordinator: coordinator.device.model == MODEL_BLU_GATEWAY, + supported=lambda coordinator: coordinator.model == MODEL_BLU_GATEWAY_G3, ), ] @@ -160,6 +160,7 @@ async def async_setup_entry( ShellyBluTrvButton(coordinator, button, id_) for id_ in blutrv_key_ids for button in BLU_TRV_BUTTONS + if button.supported(coordinator) ) async_add_entities(entities) From 1838a731d60b05e9f8c9b1f1db36dbb5ee71b297 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 3 Jun 2025 01:18:49 +0300 Subject: [PATCH 1068/1175] Bump aioamazondevices to 3.0.5 (#146073) --- homeassistant/components/amazon_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index a24671298d9..bd9bc701d3e 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.0.4"] + "requirements": ["aioamazondevices==3.0.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4af2e16aa4a..890dc1cb1cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.4 +aioamazondevices==3.0.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f51d93402b..5e472b51a61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.4 +aioamazondevices==3.0.5 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 415858119a48dd3b282f7a650b0d726042816f5e Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Tue, 3 Jun 2025 11:23:52 +0200 Subject: [PATCH 1069/1175] Add state class measurement to Freebox temperature sensors (#146074) --- homeassistant/components/freebox/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 33af56a1f9e..45fe18db95a 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -84,6 +84,7 @@ async def async_setup_entry( name=f"Freebox {sensor_name}", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), ) for sensor_name in router.sensors_temperature From 010c5cab8783a8151d70722591737245cdf93f06 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:11:02 +0800 Subject: [PATCH 1070/1175] Fix nightlatch option for all switchbot locks (#146090) --- homeassistant/components/switchbot/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 04b4e20b7ce..82e6e43130b 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -367,7 +367,9 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): ), ): int } - if self.config_entry.data.get(CONF_SENSOR_TYPE) == SupportedModels.LOCK_PRO: + if self.config_entry.data.get(CONF_SENSOR_TYPE, "").startswith( + SupportedModels.LOCK + ): options.update( { vol.Optional( From 81cbb6e5cfcea2dc170c691c67587228c7fb0a65 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 3 Jun 2025 18:40:20 +1000 Subject: [PATCH 1071/1175] Fix BMS and Charge states in Teslemetry (#146091) Fix BMS and Charge states --- homeassistant/components/teslemetry/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index ab075d18132..8ddd7e186cb 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -205,7 +205,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( key="charge_state_charging_state", polling=True, streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState( - lambda value: None if value is None else callback(value.lower()) + lambda value: callback(None if value is None else CHARGE_STATES.get(value)) ), polling_value_fn=lambda value: CHARGE_STATES.get(str(value)), options=list(CHARGE_STATES.values()), @@ -533,7 +533,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = ( TeslemetryVehicleSensorEntityDescription( key="bms_state", streaming_listener=lambda vehicle, callback: vehicle.listen_BMSState( - lambda value: None if value is None else callback(BMS_STATES.get(value)) + lambda value: callback(None if value is None else BMS_STATES.get(value)) ), device_class=SensorDeviceClass.ENUM, options=list(BMS_STATES.values()), From abfd443541a7c6f7a712e2b6b9ae5ce2cb907a7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jun 2025 11:57:58 +0100 Subject: [PATCH 1072/1175] Bump bleak-esphome to 2.16.0 (#146110) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 1f619b2017c..889401ffc3e 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"] + "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"] } diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index d5faacfd1b0..d0bed1fdb4e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -19,7 +19,7 @@ "requirements": [ "aioesphomeapi==31.1.0", "esphome-dashboard-api==1.3.0", - "bleak-esphome==2.15.1" + "bleak-esphome==2.16.0" ], "zeroconf": ["_esphomelib._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 890dc1cb1cb..4021c23edf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ bizkaibus==0.1.1 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.15.1 +bleak-esphome==2.16.0 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e472b51a61..dca4fc91144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome -bleak-esphome==2.15.1 +bleak-esphome==2.16.0 # homeassistant.components.bluetooth bleak-retry-connector==3.9.0 From 1d578d856343739f67769ee63a02612c937c8b4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Jun 2025 15:56:20 +0100 Subject: [PATCH 1073/1175] Bump habluetooth to 3.49.0 (#146111) * Bump habluetooth to 3.49.0 changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.48.2...v3.49.0 * update diag * diag --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_diagnostics.py | 1 + tests/components/esphome/test_diagnostics.py | 1 + tests/components/shelly/test_diagnostics.py | 7 ++++++- 7 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 4fc835e4532..f212f4bdc17 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -21,6 +21,6 @@ "bluetooth-auto-recovery==1.5.2", "bluetooth-data-tools==1.28.1", "dbus-fast==2.43.0", - "habluetooth==3.48.2" + "habluetooth==3.49.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 598e2834e86..9cc65c62c7e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ dbus-fast==2.43.0 fnv-hash-fast==1.5.0 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 -habluetooth==3.48.2 +habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4021c23edf4..5de5b884d5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1121,7 +1121,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.0 # homeassistant.components.bluetooth -habluetooth==3.48.2 +habluetooth==3.49.0 # homeassistant.components.cloud hass-nabucasa==0.101.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dca4fc91144..de8f6f70f0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -979,7 +979,7 @@ ha-silabs-firmware-client==0.2.0 habiticalib==0.4.0 # homeassistant.components.bluetooth -habluetooth==3.48.2 +habluetooth==3.49.0 # homeassistant.components.cloud hass-nabucasa==0.101.0 diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index 80fca88b2de..540bf1bfbd1 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -655,6 +655,7 @@ async def test_diagnostics_remote_adapter( "source": "esp32", "start_time": ANY, "time_since_last_device_detection": {"44:44:33:11:23:45": ANY}, + "raw_advertisement_data": {"44:44:33:11:23:45": None}, "type": "FakeScanner", }, ], diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 84f2243a844..70acf327788 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -95,6 +95,7 @@ async def test_diagnostics_with_bluetooth( "scanning": True, "source": "AA:BB:CC:DD:EE:FC", "start_time": ANY, + "raw_advertisement_data": {}, "time_since_last_device_detection": {}, "type": "ESPHomeScanner", }, diff --git a/tests/components/shelly/test_diagnostics.py b/tests/components/shelly/test_diagnostics.py index 300b67abe75..6bd44fa036a 100644 --- a/tests/components/shelly/test_diagnostics.py +++ b/tests/components/shelly/test_diagnostics.py @@ -103,7 +103,6 @@ async def test_rpc_config_entry_diagnostics( ) result = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert result == { "entry": entry_dict | {"discovery_keys": {}}, "bluetooth": { @@ -152,6 +151,12 @@ async def test_rpc_config_entry_diagnostics( "start_time": ANY, "source": "12:34:56:78:9A:BE", "time_since_last_device_detection": {"AA:BB:CC:DD:EE:FF": ANY}, + "raw_advertisement_data": { + "AA:BB:CC:DD:EE:FF": { + "__type": "", + "repr": "b'\\x02\\x01\\x06\\t\\xffY\\x00\\xd1\\xfb;t\\xc8\\x90\\x11\\x07\\x1b\\xc5\\xd5\\xa5\\x02\\x00\\xb8\\x9f\\xe6\\x11M\"\\x00\\r\\xa2\\xcb\\x06\\x16\\x00\\rH\\x10a'", + } + }, "type": "ShellyBLEScanner", } }, From e8aab396204d4b446e62dee7ef24b77557082d23 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Tue, 3 Jun 2025 21:54:44 +0200 Subject: [PATCH 1074/1175] SMA fix strings (#146112) * Fix * Feedback --- homeassistant/components/sma/strings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index e19acf20cf8..8253d94a749 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -34,10 +34,10 @@ "title": "Set up SMA Solar" }, "discovery_confirm": { - "title": "[%key:component::sma::config::step::user::title]", - "description": "Do you want to setup the discovered SMA ({host})?", + "title": "[%key:component::sma::config::step::user::title%]", + "description": "Do you want to set up the discovered SMA device ({host})?", "data": { - "group": "[%key:component::sma::config::step::user::data::group]", + "group": "[%key:component::sma::config::step::user::data::group%]", "password": "[%key:common::config_flow::data::password%]", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" From f71a1a7a899e48202974789ad35a6f551f800377 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 07:40:10 +0100 Subject: [PATCH 1075/1175] Bump protobuf to 6.31.1 (#146128) changelog: https://github.com/protocolbuffers/protobuf/compare/v30.2...v31.1 --- 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 9cc65c62c7e..6df7d7d1802 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -145,7 +145,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.30.2 +protobuf==6.31.1 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 606141e4c65..0ea69b365a2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -172,7 +172,7 @@ iso4217!=1.10.20220401 # protobuf must be in package constraints for the wheel # builder to build binary wheels -protobuf==6.30.2 +protobuf==6.31.1 # faust-cchardet: Ensure we have a version we can build wheels # 2.1.18 is the first version that works with our wheel builder From bfb140d2e94e25be4662d22ced33a0a3d5c0b635 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 09:34:04 +0100 Subject: [PATCH 1076/1175] Bump aioesphomeapi to 32.0.0 (#146135) --- 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 d0bed1fdb4e..eea0ed060f9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==31.1.0", + "aioesphomeapi==32.0.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5de5b884d5b..ee5f2f3d8d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.1.0 +aioesphomeapi==32.0.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de8f6f70f0c..f544b68f573 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==31.1.0 +aioesphomeapi==32.0.0 # homeassistant.components.flo aioflo==2021.11.0 From 6c098c3e0a7b6ca2017fe9fb63fbac8d8255f5a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Jun 2025 09:02:53 +0000 Subject: [PATCH 1077/1175] Bump version to 2025.6.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 edbdba419f3..25d722ea685 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index aa9de97d73b..7abfb13f248 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b4" +version = "2025.6.0b5" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 78d2bf736c07c37759b5d8583a0e202e412f3ec2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 10 Jun 2025 18:05:55 +0200 Subject: [PATCH 1078/1175] Reolink conserve battery (#145452) --- homeassistant/components/reolink/__init__.py | 59 ++++++++++++--- homeassistant/components/reolink/const.py | 6 ++ homeassistant/components/reolink/entity.py | 4 +- homeassistant/components/reolink/host.py | 46 +++++++++--- tests/components/reolink/test_init.py | 77 +++++++++++++++++++- 5 files changed, 170 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 38f8e709b5c..aa4ee36fc67 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -3,8 +3,10 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from datetime import timedelta import logging +from time import time from typing import Any from reolink_aio.api import RETRY_ATTEMPTS @@ -28,7 +30,13 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN +from .const import ( + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin from .host import ReolinkHost from .services import async_setup_services @@ -220,6 +228,24 @@ async def async_setup_entry( hass.http.register_view(PlaybackProxyView(hass)) + await register_callbacks(host, device_coordinator, hass) + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + + config_entry.async_on_unload( + config_entry.add_update_listener(entry_update_listener) + ) + + return True + + +async def register_callbacks( + host: ReolinkHost, + device_coordinator: DataUpdateCoordinator[None], + hass: HomeAssistant, +) -> None: + """Register update callbacks.""" + async def refresh(*args: Any) -> None: """Request refresh of coordinator.""" await device_coordinator.async_request_refresh() @@ -233,17 +259,29 @@ async def async_setup_entry( host.cancel_refresh_privacy_mode = async_call_later(hass, 2, refresh) host.privacy_mode = host.api.baichuan.privacy_mode() + def generate_async_camera_wake(channel: int) -> Callable[[], None]: + def async_camera_wake() -> None: + """Request update when a battery camera wakes up.""" + if ( + not host.api.sleeping(channel) + and time() - host.last_wake[channel] + > BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + ): + hass.loop.create_task(device_coordinator.async_request_refresh()) + + return async_camera_wake + host.api.baichuan.register_callback( "privacy_mode_change", async_privacy_mode_change, 623 ) - - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) - - config_entry.async_on_unload( - config_entry.add_update_listener(entry_update_listener) - ) - - return True + for channel in host.api.channels: + if host.api.supported(channel, "battery"): + host.api.baichuan.register_callback( + f"camera_{channel}_wake", + generate_async_camera_wake(channel), + 145, + channel, + ) async def entry_update_listener( @@ -262,6 +300,9 @@ async def async_unload_entry( await host.stop() host.api.baichuan.unregister_callback("privacy_mode_change") + for channel in host.api.channels: + if host.api.supported(channel, "battery"): + host.api.baichuan.unregister_callback(f"camera_{channel}_wake") if host.cancel_refresh_privacy_mode is not None: host.cancel_refresh_privacy_mode() diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 026d1219881..bd9c4bb84a2 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -5,3 +5,9 @@ DOMAIN = "reolink" CONF_USE_HTTPS = "use_https" CONF_BC_PORT = "baichuan_port" CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported" + +# Conserve battery by not waking the battery cameras each minute during normal update +# Most props are cached in the Home Hub and updated, but some are skipped +BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL = 3600 # seconds +BATTERY_WAKE_UPDATE_INTERVAL = 6 * BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL +BATTERY_ALL_WAKE_UPDATE_INTERVAL = 2 * BATTERY_WAKE_UPDATE_INTERVAL diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index f2a0b20994a..d7e8817b1b7 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -142,7 +142,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None] async def async_update(self) -> None: """Force full update from the generic entity update service.""" - self._host.last_wake = 0 + for channel in self._host.api.channels: + if self._host.api.supported(channel, "battery"): + self._host.last_wake[channel] = 0 await super().async_update() diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index c3a8d340501..39b58c92ac3 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -34,7 +34,15 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.storage import Store from homeassistant.util.ssl import SSLCipherList -from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN +from .const import ( + BATTERY_ALL_WAKE_UPDATE_INTERVAL, + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + BATTERY_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + CONF_SUPPORTS_PRIVACY_MODE, + CONF_USE_HTTPS, + DOMAIN, +) from .exceptions import ( PasswordIncompatible, ReolinkSetupException, @@ -52,10 +60,6 @@ POLL_INTERVAL_NO_PUSH = 5 LONG_POLL_COOLDOWN = 0.75 LONG_POLL_ERROR_COOLDOWN = 30 -# Conserve battery by not waking the battery cameras each minute during normal update -# Most props are cached in the Home Hub and updated, but some are skipped -BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds - _LOGGER = logging.getLogger(__name__) @@ -95,7 +99,8 @@ class ReolinkHost: bc_port=config.get(CONF_BC_PORT, DEFAULT_BC_PORT), ) - self.last_wake: float = 0 + self.last_wake: defaultdict[int, float] = defaultdict(float) + self.last_all_wake: float = 0 self.update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict( lambda: defaultdict(int) ) @@ -459,15 +464,34 @@ class ReolinkHost: async def update_states(self) -> None: """Call the API of the camera device to update the internal states.""" - wake = False - if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL: + wake: dict[int, bool] = {} + now = time() + for channel in self._api.stream_channels: # wake the battery cameras for a complete update - wake = True - self.last_wake = time() + if not self._api.supported(channel, "battery"): + wake[channel] = True + elif ( + ( + not self._api.sleeping(channel) + and now - self.last_wake[channel] + > BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + ) + or (now - self.last_wake[channel] > BATTERY_WAKE_UPDATE_INTERVAL) + or (now - self.last_all_wake > BATTERY_ALL_WAKE_UPDATE_INTERVAL) + ): + # let a waking update coincide with the camera waking up by itself unless it did not wake for BATTERY_WAKE_UPDATE_INTERVAL + wake[channel] = True + self.last_wake[channel] = now + else: + wake[channel] = False - for channel in self._api.channels: + # check privacy mode if enabled if self._api.baichuan.privacy_mode(channel): await self._api.baichuan.get_privacy_mode(channel) + + if all(wake.values()): + self.last_all_wake = now + if self._api.baichuan.privacy_mode(): return # API is shutdown, no need to check states diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 3551632903f..86c4ed861a1 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -19,7 +19,12 @@ from homeassistant.components.reolink import ( FIRMWARE_UPDATE_INTERVAL, NUM_CRED_ERRORS, ) -from homeassistant.components.reolink.const import CONF_BC_PORT, DOMAIN +from homeassistant.components.reolink.const import ( + BATTERY_ALL_WAKE_UPDATE_INTERVAL, + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL, + CONF_BC_PORT, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, @@ -1111,6 +1116,76 @@ async def test_privacy_mode_change_callback( assert config_entry.state is ConfigEntryState.NOT_LOADED +async def test_camera_wake_callback( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, +) -> None: + """Test camera wake callback.""" + + class callback_mock_class: + callback_func = None + + def register_callback( + self, callback_id: str, callback: Callable[[], None], *args, **key_args + ) -> None: + if callback_id == "camera_0_wake": + self.callback_func = callback + + callback_mock = callback_mock_class() + + reolink_connect.model = TEST_HOST_MODEL + reolink_connect.baichuan.events_active = True + reolink_connect.baichuan.subscribe_events.reset_mock(side_effect=True) + reolink_connect.baichuan.register_callback = callback_mock.register_callback + reolink_connect.sleeping.return_value = True + reolink_connect.audio_record.return_value = True + reolink_connect.get_states = AsyncMock() + + with ( + patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]), + patch( + "homeassistant.components.reolink.host.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + entity_id = f"{Platform.SWITCH}.{TEST_NVR_NAME}_record_audio" + assert hass.states.get(entity_id).state == STATE_ON + + reolink_connect.sleeping.return_value = False + reolink_connect.get_states.reset_mock() + assert reolink_connect.get_states.call_count == 0 + + # simulate a TCP push callback signaling the battery camera woke up + reolink_connect.audio_record.return_value = False + assert callback_mock.callback_func is not None + with ( + patch( + "homeassistant.components.reolink.host.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL + + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + + 5, + ), + patch( + "homeassistant.components.reolink.time", + return_value=BATTERY_ALL_WAKE_UPDATE_INTERVAL + + BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL + + 5, + ), + ): + callback_mock.callback_func() + await hass.async_block_till_done() + + # check that a coordinator update was scheduled. + assert reolink_connect.get_states.call_count >= 1 + assert hass.states.get(entity_id).state == STATE_OFF + + async def test_remove( hass: HomeAssistant, reolink_connect: MagicMock, From 5821b2f03ccd3519b137a77c677592dca26d6f81 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:25:26 +0200 Subject: [PATCH 1079/1175] fix possible mac collision in enphase_envoy (#145549) * fix possible mac collision in enphase_envoy * remove redundant device registry async_get --- .../components/enphase_envoy/coordinator.py | 12 +++- tests/components/enphase_envoy/test_init.py | 58 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 40c690b29ec..cfff0777af5 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) return - device_registry.async_update_device( - device_id=envoy_device.id, - new_connections={connection}, + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + identifiers={ + ( + DOMAIN, + self.envoy_serial_number, + ) + }, + connections={connection}, ) _LOGGER.debug("added connection: %s to %s", connection, self.name) diff --git a/tests/components/enphase_envoy/test_init.py b/tests/components/enphase_envoy/test_init.py index ef071b421fe..560d0719424 100644 --- a/tests/components/enphase_envoy/test_init.py +++ b/tests/components/enphase_envoy/test_init.py @@ -510,7 +510,6 @@ async def test_coordinator_interface_information_no_device( ) # update device to force no device found in mac verification - device_registry = dr.async_get(hass) envoy_device = device_registry.async_get_device( identifiers={ ( @@ -531,3 +530,60 @@ async def test_coordinator_interface_information_no_device( # verify no device found message in log assert "No envoy device found in device registry" in caplog.text + + +@respx.mock +async def test_coordinator_interface_information_mac_also_in_other_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_envoy: AsyncMock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, + device_registry: dr.DeviceRegistry, +) -> None: + """Test coordinator interface mac verification with MAC also in other existing device.""" + await setup_integration(hass, config_entry) + + caplog.set_level(logging.DEBUG) + logging.getLogger("homeassistant.components.enphase_envoy.coordinator").setLevel( + logging.DEBUG + ) + + # add existing device with MAC and sparsely populated i.e. unifi that found envoy + other_config_entry = MockConfigEntry(domain="test", data={}) + other_config_entry.add_to_hass(hass) + device_registry.async_get_or_create( + config_entry_id=other_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55")}, + manufacturer="Enphase Energy", + ) + + envoy_device = device_registry.async_get_device( + identifiers={ + ( + DOMAIN, + mock_envoy.serial_number, + ) + } + ) + assert envoy_device + + # move time forward so interface information is fetched + freezer.tick(MAC_VERIFICATION_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # verify mac was added + assert "added connection: ('mac', '00:11:22:33:44:55') to Envoy 1234" in caplog.text + + # verify connection is now in envoy device + envoy_device_refetched = device_registry.async_get(envoy_device.id) + assert envoy_device_refetched + assert envoy_device_refetched.name == "Envoy 1234" + assert envoy_device_refetched.serial_number == "1234" + assert envoy_device_refetched.connections == { + ( + dr.CONNECTION_NETWORK_MAC, + "00:11:22:33:44:55", + ) + } From 41431282ee938d2dcc410dd744625801b7b88699 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Tue, 10 Jun 2025 16:23:55 +0200 Subject: [PATCH 1080/1175] Add evaporate water program id for Miele oven (#145996) --- homeassistant/components/miele/const.py | 1 + homeassistant/components/miele/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/miele/const.py b/homeassistant/components/miele/const.py index 0d11cbdd0a5..bda276c6d8a 100644 --- a/homeassistant/components/miele/const.py +++ b/homeassistant/components/miele/const.py @@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = { 116: "custom_program_20", 323: "pyrolytic", 326: "descale", + 327: "evaporate_water", 335: "shabbat_program", 336: "yom_tov", 356: "defrost", diff --git a/homeassistant/components/miele/strings.json b/homeassistant/components/miele/strings.json index 6774d813e44..cf01d01e476 100644 --- a/homeassistant/components/miele/strings.json +++ b/homeassistant/components/miele/strings.json @@ -542,6 +542,7 @@ "endive_strips": "Endive (strips)", "espresso": "Espresso", "espresso_macchiato": "Espresso macchiato", + "evaporate_water": "Evaporate water", "express": "Express", "express_20": "Express 20'", "extra_quiet": "Extra quiet", From dfc4889d456409becb7d9015f8554f052de6c319 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 10 Jun 2025 06:03:20 -0700 Subject: [PATCH 1081/1175] Throttle Nextbus if we are reaching the rate limit (#146064) Co-authored-by: Josef Zweck Co-authored-by: Robert Resch --- .../components/nextbus/coordinator.py | 35 ++++++++++--- tests/components/nextbus/conftest.py | 7 +++ tests/components/nextbus/test_sensor.py | 52 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nextbus/coordinator.py b/homeassistant/components/nextbus/coordinator.py index 617669adf2f..e8d7ab06915 100644 --- a/homeassistant/components/nextbus/coordinator.py +++ b/homeassistant/components/nextbus/coordinator.py @@ -1,8 +1,8 @@ """NextBus data update coordinator.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any +from typing import Any, override from py_nextbus import NextBusClient from py_nextbus.client import NextBusFormatError, NextBusHTTPError @@ -15,8 +15,14 @@ from .util import RouteStop _LOGGER = logging.getLogger(__name__) +# At what percentage of the request limit should the coordinator pause making requests +UPDATE_INTERVAL_SECONDS = 30 +THROTTLE_PRECENTAGE = 80 -class NextBusDataUpdateCoordinator(DataUpdateCoordinator): + +class NextBusDataUpdateCoordinator( + DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]] +): """Class to manage fetching NextBus data.""" def __init__(self, hass: HomeAssistant, agency: str) -> None: @@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, config_entry=None, # It is shared between multiple entries name=DOMAIN, - update_interval=timedelta(seconds=30), + update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) self.client = NextBusClient(agency_id=agency) self._agency = agency @@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): """Check if this coordinator is tracking any routes.""" return len(self._route_stops) > 0 - async def _async_update_data(self) -> dict[str, Any]: + @override + async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]: """Fetch data from NextBus.""" + if ( + # If we have predictions, check the rate limit + self._predictions + # If are over our rate limit percentage, we should throttle + and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE + # But only if we have a reset time to unthrottle + and self.client.rate_limit_reset is not None + # Unless we are after the reset time + and datetime.now() < self.client.rate_limit_reset + ): + self.logger.debug( + "Rate limit threshold reached. Skipping updates for. Routes: %s", + str(self._route_stops), + ) + return self._predictions + _stops_to_route_stops: dict[str, set[RouteStop]] = {} for route_stop in self._route_stops: _stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop) @@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator): "Updating data from API. Routes: %s", str(_stops_to_route_stops) ) - def _update_data() -> dict: + def _update_data() -> dict[RouteStop, dict[str, Any]]: """Fetch data from NextBus.""" self.logger.debug("Updating data from API (executor)") predictions: dict[RouteStop, dict[str, Any]] = {} diff --git a/tests/components/nextbus/conftest.py b/tests/components/nextbus/conftest.py index 3f687989313..9891f6ffa49 100644 --- a/tests/components/nextbus/conftest.py +++ b/tests/components/nextbus/conftest.py @@ -137,6 +137,13 @@ def mock_nextbus_lists( def mock_nextbus() -> Generator[MagicMock]: """Create a mock py_nextbus module.""" with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client: + instance = client.return_value + + # Set some mocked rate limit values + instance.rate_limit = 450 + instance.rate_limit_remaining = 225 + instance.rate_limit_percent = 50.0 + yield client diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 04140a17c4f..eacab5cd5c4 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from datetime import datetime, timedelta from unittest.mock import MagicMock from urllib.error import HTTPError @@ -122,6 +123,57 @@ async def test_verify_no_upcoming( assert state.state == "unknown" +async def test_verify_throttle( + hass: HomeAssistant, + mock_nextbus: MagicMock, + mock_nextbus_lists: MagicMock, + mock_nextbus_predictions: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Verify that the sensor coordinator is throttled correctly.""" + + # Set rate limit past threshold, should be ignored for first request + mock_client = mock_nextbus.return_value + mock_client.rate_limit_percent = 99.0 + mock_client.rate_limit_reset = datetime.now() + timedelta(seconds=30) + + # Do a request with the initial config and get predictions + await assert_setup_sensor(hass, CONFIG_BASIC) + + # Validate the predictions are present + 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 + assert state.attributes["route"] == VALID_ROUTE_TITLE + assert state.attributes["stop"] == VALID_STOP_TITLE + assert state.attributes["upcoming"] == "1, 2, 3, 10" + + # Update the predictions mock to return a different result + mock_nextbus_predictions.return_value = NO_UPCOMING + + # Move time forward and bump the rate limit reset time + mock_client.rate_limit_reset = freezer.tick(31) + timedelta(seconds=30) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify that the sensor state is unchanged + state = hass.states.get(SENSOR_ID) + assert state is not None + assert state.state == "2019-03-28T21:09:31+00:00" + + # Move time forward past the rate limit reset time + freezer.tick(31) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + # Verify that the sensor state is updated with the new predictions + state = hass.states.get(SENSOR_ID) + assert state is not None + assert state.attributes["upcoming"] == "No upcoming predictions" + assert state.state == "unknown" + + async def test_unload_entry( hass: HomeAssistant, mock_nextbus: MagicMock, From ce76b5db1657bcb2677a90e5311ea1cf93a5237c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 12:57:40 +0100 Subject: [PATCH 1082/1175] Bump aiohttp to 3.12.8 (#146153) --- 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 6df7d7d1802..3cfcc2e0a9e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.6 +aiohttp==3.12.8 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 7abfb13f248..1921b1550b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.6", + "aiohttp==3.12.8", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index a9a3e105f33..17b01971087 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.6 +aiohttp==3.12.8 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From 38c92a2338f2b2f354459e1e19a6504fd32aa06f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:35:16 +0200 Subject: [PATCH 1083/1175] Bump aioimmich to 0.9.0 (#146154) bump aioimmich to 0.9.0 --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index b7c8176356f..1a4ccc8580c 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.8.0"] + "requirements": ["aioimmich==0.9.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ee5f2f3d8d3..173f7f94c2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.8.0 +aioimmich==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f544b68f573..fcca7761e2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.8.0 +aioimmich==0.9.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 From d8759898662e47c4b51b50f0be934e07204f98f7 Mon Sep 17 00:00:00 2001 From: Iskra kranj <162285659+iskrakranj@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:51:40 +0200 Subject: [PATCH 1084/1175] Bump pyiskra to 0.1.21 (#146156) --- homeassistant/components/iskra/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iskra/manifest.json b/homeassistant/components/iskra/manifest.json index 3f7c805a917..da983db9969 100644 --- a/homeassistant/components/iskra/manifest.json +++ b/homeassistant/components/iskra/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["pyiskra"], - "requirements": ["pyiskra==0.1.19"] + "requirements": ["pyiskra==0.1.21"] } diff --git a/requirements_all.txt b/requirements_all.txt index 173f7f94c2a..217d94bf1a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ pyiqvia==2022.04.0 pyirishrail==0.0.2 # homeassistant.components.iskra -pyiskra==0.1.19 +pyiskra==0.1.21 # homeassistant.components.iss pyiss==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcca7761e2e..e6b68640306 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1702,7 +1702,7 @@ pyipp==0.17.0 pyiqvia==2022.04.0 # homeassistant.components.iskra -pyiskra==0.1.19 +pyiskra==0.1.21 # homeassistant.components.iss pyiss==1.0.1 From 5accc3dec2ae521bfe21451e4261849097287adf Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:32:44 +0200 Subject: [PATCH 1085/1175] Bump uiprotect to 7.11.0 (#146171) Bump uiprotect to version 7.11.0 --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 1cf2e4391e2..64bb278a8e2 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==7.11.0", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 217d94bf1a6..af40384991e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2987,7 +2987,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.1 +uiprotect==7.11.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6b68640306..b5c4c9e7760 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2458,7 +2458,7 @@ typedmonarchmoney==0.4.4 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==7.10.1 +uiprotect==7.11.0 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 8312780c477edd2508fbc2468d808d49a1643bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 Jun 2025 19:12:19 +0100 Subject: [PATCH 1086/1175] Bump aiohttp to 3.12.9 (#146178) --- 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 3cfcc2e0a9e..9b5a389e05b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.2.3 -aiohttp==3.12.8 +aiohttp==3.12.9 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 1921b1550b6..2e0d06707dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.8", + "aiohttp==3.12.9", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.3", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 17b01971087..8c65e9a0b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.8 +aiohttp==3.12.9 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.3 aiohttp-asyncmdnsresolver==0.1.1 From e4140d71abe646e7b58275aaec4a373a2607c1bf Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 10 Jun 2025 23:00:02 +1000 Subject: [PATCH 1087/1175] Prevent energy history returning zero in Teslemetry (#146202) --- homeassistant/components/teslemetry/coordinator.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 406b9cb2d84..c31bdc2a34e 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -195,9 +195,13 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise UpdateFailed(e.message) from e # Add all time periods together - output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0) + output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None) for period in data.get("time_series", []): for key in ENERGY_HISTORY_FIELDS: - output[key] += period.get(key, 0) + if key in period: + if output[key] is None: + output[key] = period[key] + else: + output[key] += period[key] return output From e5dd15da82234c346c709e0983e6e126070233a9 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 6 Jun 2025 00:39:55 +1000 Subject: [PATCH 1088/1175] Fix Export Rule Select Entity in Tessie (#146203) Fix TessieExportRuleSelectEntity --- homeassistant/components/tessie/select.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tessie/select.py b/homeassistant/components/tessie/select.py index 471372a68bd..ce907deb9c8 100644 --- a/homeassistant/components/tessie/select.py +++ b/homeassistant/components/tessie/select.py @@ -168,6 +168,8 @@ class TessieExportRuleSelectEntity(TessieEnergyEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await handle_command(self.api.grid_import_export(option)) + await handle_command( + self.api.grid_import_export(customer_preferred_export_rule=option) + ) self._attr_current_option = option self.async_write_ha_state() From fc8b5129314ac4c7fda5970439c6c3bfbb9d1983 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 5 Jun 2025 18:02:11 +0200 Subject: [PATCH 1089/1175] Remove zeroconf discovery from Spotify (#146213) --- .../components/spotify/manifest.json | 3 +- homeassistant/components/spotify/strings.json | 3 -- homeassistant/generated/zeroconf.py | 5 --- tests/components/spotify/test_config_flow.py | 41 +------------------ 4 files changed, 2 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 27b8da7cecf..80fcc777e73 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -8,6 +8,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["spotifyaio"], - "requirements": ["spotifyaio==0.8.11"], - "zeroconf": ["_spotify-connect._tcp.local."] + "requirements": ["spotifyaio==0.8.11"] } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 303942803be..66d837c503f 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -7,9 +7,6 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" - }, - "oauth_discovery": { - "description": "Home Assistant has found Spotify on your network. Press **Submit** to continue setting up Spotify." } }, "abort": { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ed5ac37c0cd..e675a0bb237 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -865,11 +865,6 @@ ZEROCONF = { "domain": "soundtouch", }, ], - "_spotify-connect._tcp.local.": [ - { - "domain": "spotify", - }, - ], "_ssh._tcp.local.": [ { "domain": "smappee", diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 0f48002e5db..31842253c0c 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,33 +1,21 @@ """Tests for the Spotify config flow.""" from http import HTTPStatus -from ipaddress import ip_address from unittest.mock import MagicMock, patch import pytest from spotifyaio import SpotifyConnectionError from homeassistant.components.spotify.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator -BLANK_ZEROCONF_INFO = ZeroconfServiceInfo( - ip_address=ip_address("1.2.3.4"), - ip_addresses=[ip_address("1.2.3.4")], - hostname="mock_hostname", - name="mock_name", - port=None, - properties={}, - type="mock_type", -) - async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: """Check flow aborts when no configuration is present.""" @@ -39,18 +27,6 @@ async def test_abort_if_no_configuration(hass: HomeAssistant) -> None: assert result["reason"] == "missing_credentials" -async def test_zeroconf_abort_if_existing_entry(hass: HomeAssistant) -> None: - """Check zeroconf flow aborts when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - @pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("setup_credentials") async def test_full_flow( @@ -258,18 +234,3 @@ async def test_reauth_account_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" - - -async def test_zeroconf(hass: HomeAssistant) -> None: - """Check zeroconf flow aborts when an entry already exist.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO - ) - - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "oauth_discovery" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "missing_credentials" From f6a4486c6567fece2091016aaade569e58f47142 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 14:10:35 +0200 Subject: [PATCH 1090/1175] Explain Withings setup (#146216) --- .../components/withings/application_credentials.py | 8 ++++++++ homeassistant/components/withings/strings.json | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/application_credentials.py b/homeassistant/components/withings/application_credentials.py index ce96ed782dd..0939f9c5b82 100644 --- a/homeassistant/components/withings/application_credentials.py +++ b/homeassistant/components/withings/application_credentials.py @@ -75,3 +75,11 @@ class WithingsLocalOAuth2Implementation(AuthImplementation): } ) return {**token, **new_token} + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.withings.com/dashboard/welcome", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 8eb4293c637..14c7bf640e9 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "To be able to login to Withings we require a client ID and secret. To acquire them, please follow the following steps.\n\n1. Go to the [Withings Developer Dashboard]({developer_dashboard_url}) and be sure to select the Public Cloud.\n1. Log in with your Withings account.\n1. Select **Create an application**.\n1. Select the checkbox for **Public API integration**.\n1. Select **Development** as target environment.\n1. Fill in an application name and description of your choice.\n1. Fill in `{redirect_url}` for the registered URL. Make sure that you don't press the button to test it.\n1. Fill in the client ID and secret that are now available." + }, "config": { "step": { "pick_implementation": { @@ -9,7 +12,7 @@ "description": "The Withings integration needs to re-authenticate your account" }, "oauth_discovery": { - "description": "Home Assistant has found a Withings device on your network. Press **Submit** to continue setting up Withings." + "description": "Home Assistant has found a Withings device on your network. Be aware that the setup of Withings is more complicated than many other integrations. Press **Submit** to continue setting up Withings." } }, "error": { From 91e29a3bf125029f215fd51a7a87d5ce3a80e3ef Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:50:19 +0200 Subject: [PATCH 1091/1175] Bump aioimmich to 0.9.1 (#146222) bump aioimmich to 0.9.1 --- homeassistant/components/immich/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/immich/manifest.json b/homeassistant/components/immich/manifest.json index 1a4ccc8580c..36c993e9c8f 100644 --- a/homeassistant/components/immich/manifest.json +++ b/homeassistant/components/immich/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["aioimmich"], "quality_scale": "silver", - "requirements": ["aioimmich==0.9.0"] + "requirements": ["aioimmich==0.9.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index af40384991e..a4b6a12111e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.9.0 +aioimmich==0.9.1 # homeassistant.components.apache_kafka aiokafka==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5c4c9e7760..1def9ca9e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ aiohue==4.7.4 aioimaplib==2.0.1 # homeassistant.components.immich -aioimmich==0.9.0 +aioimmich==0.9.1 # homeassistant.components.apache_kafka aiokafka==0.10.0 From 4d3145e559aa307e4126de2b16d73aeeec88c1a2 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 7 Jun 2025 12:43:16 +1000 Subject: [PATCH 1092/1175] Add missing write state to Teslemetry (#146267) --- homeassistant/components/teslemetry/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/teslemetry/cover.py b/homeassistant/components/teslemetry/cover.py index c58559ab308..f6ff71ab0cc 100644 --- a/homeassistant/components/teslemetry/cover.py +++ b/homeassistant/components/teslemetry/cover.py @@ -441,6 +441,7 @@ class TeslemetryStreamingRearTrunkEntity( """Update the entity attributes.""" self._attr_is_closed = None if value is None else not value + self.async_write_ha_state() class TeslemetrySunroofEntity(TeslemetryVehiclePollingEntity, CoverEntity): From 761c2578fba0022af5626fb26c009308c5d97836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Jun 2025 09:30:43 -0500 Subject: [PATCH 1093/1175] Bump aiohttp-fast-zlib to 0.3.0 (#146285) changelog: https://github.com/Bluetooth-Devices/aiohttp-fast-zlib/compare/v0.2.3...v0.3.0 proper aiohttp 3.12 support --- 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 9b5a389e05b..eaa3d025a5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.7.0 aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 -aiohttp-fast-zlib==0.2.3 +aiohttp-fast-zlib==0.3.0 aiohttp==3.12.9 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 diff --git a/pyproject.toml b/pyproject.toml index 2e0d06707dc..48d8265abab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "aiohasupervisor==0.3.1", "aiohttp==3.12.9", "aiohttp_cors==0.7.0", - "aiohttp-fast-zlib==0.2.3", + "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", "aiozoneinfo==0.2.3", "annotatedyaml==0.4.5", diff --git a/requirements.txt b/requirements.txt index 8c65e9a0b5a..c8c65b783ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp==3.12.9 aiohttp_cors==0.7.0 -aiohttp-fast-zlib==0.2.3 +aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 aiozoneinfo==0.2.3 annotatedyaml==0.4.5 From 79daeb23a99f8b8def5a1fb942b9ac7f9ac509af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 7 Jun 2025 19:18:24 +0200 Subject: [PATCH 1094/1175] Bump holidays to 0.74 (#146290) --- 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 bd6fd51e726..5a5f1daf967 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.73", "babel==2.15.0"] + "requirements": ["holidays==0.74", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 7a03133dd86..9091dd131dd 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.73"] + "requirements": ["holidays==0.74"] } diff --git a/requirements_all.txt b/requirements_all.txt index a4b6a12111e..2742d46bd4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,7 +1161,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.73 +holidays==0.74 # homeassistant.components.frontend home-assistant-frontend==20250531.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1def9ca9e8b..1d12f4f7993 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1007,7 +1007,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.73 +holidays==0.74 # homeassistant.components.frontend home-assistant-frontend==20250531.0 From 21833e7c3128abd134a07b37889e7294d57320c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Jun 2025 12:08:32 -0500 Subject: [PATCH 1095/1175] Bump aiohttp to 3.12.11 (#146298) --- 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 eaa3d025a5b..a994e1d6333 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.9 +aiohttp==3.12.11 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 48d8265abab..7f458bebf39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.9", + "aiohttp==3.12.11", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index c8c65b783ad..88576cd0c4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.9 +aiohttp==3.12.11 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From 1fc05d1a307c0a67ba48a45a391d59be5e51394c Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 8 Jun 2025 19:47:00 +0200 Subject: [PATCH 1096/1175] Do not probe linkplay device if another config entry already contains the host (#146305) * Do not probe if config entry already contains the host * Add unit test * Use common fixture --- .../components/linkplay/config_flow.py | 3 +++ tests/components/linkplay/test_config_flow.py | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/linkplay/config_flow.py b/homeassistant/components/linkplay/config_flow.py index 11e4aabf257..266d2fef857 100644 --- a/homeassistant/components/linkplay/config_flow.py +++ b/homeassistant/components/linkplay/config_flow.py @@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle Zeroconf discovery.""" + # Do not probe the device if the host is already configured + self._async_abort_entries_match({CONF_HOST: discovery_info.host}) + session: ClientSession = await async_get_client_session(self.hass) bridge: LinkPlayBridge | None = None diff --git a/tests/components/linkplay/test_config_flow.py b/tests/components/linkplay/test_config_flow.py index adf6aa601ae..8c0dd4af88b 100644 --- a/tests/components/linkplay/test_config_flow.py +++ b/tests/components/linkplay/test_config_flow.py @@ -220,3 +220,28 @@ async def test_user_flow_errors( CONF_HOST: HOST, } assert result["result"].unique_id == UUID + + +@pytest.mark.usefixtures("mock_linkplay_factory_bridge") +async def test_zeroconf_no_probe_existing_device( + hass: HomeAssistant, mock_linkplay_factory_bridge: AsyncMock +) -> None: + """Test we do not probe the device is the host is already configured.""" + entry = MockConfigEntry( + data={CONF_HOST: HOST}, + domain=DOMAIN, + title=NAME, + unique_id=UUID, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZEROCONF_DISCOVERY, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_linkplay_factory_bridge.mock_calls) == 0 From 5e5431c9f9a70dc1e3a44dc24fffb8440538e1fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:48:11 -0500 Subject: [PATCH 1097/1175] Use entity unique id for ESPHome media player formats (#146318) --- homeassistant/components/esphome/entity.py | 1 + .../components/esphome/media_player.py | 7 +- tests/components/esphome/test_media_player.py | 102 ++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/entity.py b/homeassistant/components/esphome/entity.py index 15ea54422d4..37f8e738aee 100644 --- a/homeassistant/components/esphome/entity.py +++ b/homeassistant/components/esphome/entity.py @@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]): _static_info: _InfoT _state: _StateT _has_state: bool + unique_id: str def __init__( self, diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 3af6c0b2049..f18b5e7bf5c 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -78,7 +78,7 @@ class EsphomeMediaPlayer( if self._static_info.supports_pause: flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY self._attr_supported_features = flags - self._entry_data.media_player_formats[static_info.unique_id] = cast( + self._entry_data.media_player_formats[self.unique_id] = cast( MediaPlayerInfo, static_info ).supported_formats @@ -114,9 +114,8 @@ class EsphomeMediaPlayer( media_id = async_process_play_media_url(self.hass, media_id) announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE) bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY) - supported_formats: list[MediaPlayerSupportedFormat] | None = ( - self._entry_data.media_player_formats.get(self._static_info.unique_id) + self._entry_data.media_player_formats.get(self.unique_id) ) if ( @@ -139,7 +138,7 @@ class EsphomeMediaPlayer( async def async_will_remove_from_hass(self) -> None: """Handle entity being removed.""" await super().async_will_remove_from_hass() - self._entry_data.media_player_formats.pop(self.entity_id, None) + self._entry_data.media_player_formats.pop(self.unique_id, None) def _get_proxy_url( self, diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index 18a997dc09a..3f1e5e99c34 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -430,3 +430,105 @@ async def test_media_player_proxy( mock_async_create_proxy_url.assert_not_called() media_args = mock_client.media_player_command.call_args.kwargs assert media_args["media_url"] == media_url + + +async def test_media_player_formats_reload_preserves_data( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test that media player formats are properly managed on reload.""" + # Create a media player with supported formats + supported_formats = [ + MediaPlayerSupportedFormat( + format="mp3", + sample_rate=48000, + num_channels=2, + purpose=MediaPlayerFormatPurpose.DEFAULT, + ), + MediaPlayerSupportedFormat( + format="wav", + sample_rate=16000, + num_channels=1, + purpose=MediaPlayerFormatPurpose.ANNOUNCEMENT, + sample_bytes=2, + ), + ] + + mock_device = await mock_esphome_device( + mock_client=mock_client, + entity_info=[ + MediaPlayerInfo( + object_id="test_media_player", + key=1, + name="Test Media Player", + unique_id="test_unique_id", + supports_pause=True, + supported_formats=supported_formats, + ) + ], + states=[ + MediaPlayerEntityState( + key=1, volume=50, muted=False, state=MediaPlayerState.IDLE + ) + ], + ) + await hass.async_block_till_done() + + # Verify entity was created + state = hass.states.get("media_player.test_test_media_player") + assert state is not None + assert state.state == "idle" + + # Test that play_media works with proxy URL (which requires formats to be stored) + media_url = "http://127.0.0.1/test.mp3" + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_test_media_player", + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: media_url, + }, + blocking=True, + ) + + # Verify the API was called with a proxy URL (contains /api/esphome/ffmpeg_proxy/) + mock_client.media_player_command.assert_called_once() + call_args = mock_client.media_player_command.call_args + assert "/api/esphome/ffmpeg_proxy/" in call_args.kwargs["media_url"] + assert ".mp3" in call_args.kwargs["media_url"] # Should use mp3 format for default + assert call_args.kwargs["announcement"] is None + + mock_client.media_player_command.reset_mock() + + # Reload the integration + await hass.config_entries.async_reload(mock_device.entry.entry_id) + await hass.async_block_till_done() + + # Verify entity still exists after reload + state = hass.states.get("media_player.test_test_media_player") + assert state is not None + + # Test that play_media still works after reload with announcement + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: "media_player.test_test_media_player", + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_CONTENT_ID: media_url, + ATTR_MEDIA_ANNOUNCE: True, + }, + blocking=True, + ) + + # Verify the API was called with a proxy URL using wav format for announcements + mock_client.media_player_command.assert_called_once() + call_args = mock_client.media_player_command.call_args + assert "/api/esphome/ffmpeg_proxy/" in call_args.kwargs["media_url"] + assert ( + ".wav" in call_args.kwargs["media_url"] + ) # Should use wav format for announcement + assert call_args.kwargs["announcement"] is True From 79919774437dacbfb84d86de1a21831c5b264390 Mon Sep 17 00:00:00 2001 From: Sanjay Govind Date: Mon, 9 Jun 2025 00:35:54 +1200 Subject: [PATCH 1098/1175] Fix bosch alarm areas not correctly subscribing to alarms (#146322) * Fix bosch alarm areas not correctly subscribing to alarms * add test --- .../components/bosch_alarm/alarm_control_panel.py | 2 +- .../components/bosch_alarm/test_alarm_control_panel.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bosch_alarm/alarm_control_panel.py b/homeassistant/components/bosch_alarm/alarm_control_panel.py index 60365070587..b502ee32fca 100644 --- a/homeassistant/components/bosch_alarm/alarm_control_panel.py +++ b/homeassistant/components/bosch_alarm/alarm_control_panel.py @@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity): def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None: """Initialise a Bosch Alarm control panel entity.""" - super().__init__(panel, area_id, unique_id, False, False, True) + super().__init__(panel, area_id, unique_id, True, False, True) self._attr_unique_id = self._area_unique_id @property diff --git a/tests/components/bosch_alarm/test_alarm_control_panel.py b/tests/components/bosch_alarm/test_alarm_control_panel.py index 31d2f928ec5..51767396880 100644 --- a/tests/components/bosch_alarm/test_alarm_control_panel.py +++ b/tests/components/bosch_alarm/test_alarm_control_panel.py @@ -66,6 +66,16 @@ async def test_update_alarm_device( assert hass.states.get(entity_id).state == AlarmControlPanelState.ARMED_AWAY + area.is_triggered.return_value = True + + await call_observable(hass, area.alarm_observer) + + assert hass.states.get(entity_id).state == AlarmControlPanelState.TRIGGERED + + area.is_triggered.return_value = False + + await call_observable(hass, area.alarm_observer) + await hass.services.async_call( ALARM_CONTROL_PANEL_DOMAIN, SERVICE_ALARM_DISARM, From 0eb3714abcdd4bd9c2b80960885318625da256bf Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 8 Jun 2025 11:47:46 -0700 Subject: [PATCH 1099/1175] Allow different manufacturer than Amazon in Amazon Devices (#146333) --- homeassistant/components/amazon_devices/entity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/amazon_devices/entity.py index bab8009ceb0..962e2f55ae6 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/amazon_devices/entity.py @@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): """Initialize the entity.""" super().__init__(coordinator) self._serial_num = serial_num - model_details = coordinator.api.get_model_details(self.device) - model = model_details["model"] if model_details else None + model_details = coordinator.api.get_model_details(self.device) or {} + model = model_details.get("model") self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial_num)}, name=self.device.account_name, model=model, model_id=self.device.device_type, - manufacturer="Amazon", - hw_version=model_details["hw_version"] if model_details else None, + manufacturer=model_details.get("manufacturer", "Amazon"), + hw_version=model_details.get("hw_version"), sw_version=( self.device.software_version if model != SPEAKER_GROUP_MODEL else None ), From 80b09e3212034d9e923014da2a1738fe2af82ac2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 8 Jun 2025 18:02:06 +0200 Subject: [PATCH 1100/1175] Bump py-synologydsm-api to 2.7.3 (#146338) bump py-synologydsm-api to 2.7.3 --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index cd054c7eb74..3022b4c2af9 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.7.2"], + "requirements": ["py-synologydsm-api==2.7.3"], "ssdp": [ { "manufacturer": "Synology", diff --git a/requirements_all.txt b/requirements_all.txt index 2742d46bd4d..fb2a9172301 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1774,7 +1774,7 @@ py-schluter==0.1.7 py-sucks==0.9.11 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.2 +py-synologydsm-api==2.7.3 # homeassistant.components.atome pyAtome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d12f4f7993..e056dfe6700 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1494,7 +1494,7 @@ py-nightscout==1.2.2 py-sucks==0.9.11 # homeassistant.components.synology_dsm -py-synologydsm-api==2.7.2 +py-synologydsm-api==2.7.3 # homeassistant.components.hdmi_cec pyCEC==0.5.2 From b3ee2a8885069da550dbe4e677afb6d063f2e1dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jun 2025 11:15:00 -0500 Subject: [PATCH 1101/1175] Bump aioesphomeapi to 32.2.0 (#146344) --- 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 eea0ed060f9..3ae66838823 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==32.0.0", + "aioesphomeapi==32.2.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index fb2a9172301..fbcffd47dee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.0.0 +aioesphomeapi==32.2.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e056dfe6700..df13b85db26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.0.0 +aioesphomeapi==32.2.0 # homeassistant.components.flo aioflo==2021.11.0 From e97ab1fe3cc4b5e32652be88f9c77bf68ae2cea2 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 10 Jun 2025 14:38:52 +0200 Subject: [PATCH 1102/1175] Change interval for Powerfox integration (#146348) --- homeassistant/components/powerfox/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/powerfox/const.py b/homeassistant/components/powerfox/const.py index 0970e8a1b66..790f241ae8e 100644 --- a/homeassistant/components/powerfox/const.py +++ b/homeassistant/components/powerfox/const.py @@ -8,4 +8,4 @@ from typing import Final DOMAIN: Final = "powerfox" LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(minutes=1) +SCAN_INTERVAL = timedelta(seconds=10) From bfe2eeb8332e7b1da5ed7922f6fd88e7dd2a9ee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 08:30:19 -0500 Subject: [PATCH 1103/1175] Shift ESPHome log parsing to the library (#146349) --- homeassistant/components/esphome/manager.py | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 1b0e4fc8986..b4af39586d4 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -5,7 +5,6 @@ from __future__ import annotations import asyncio from functools import partial import logging -import re from typing import TYPE_CHECKING, Any, NamedTuple from aioesphomeapi import ( @@ -23,6 +22,7 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, UserService, UserServiceArgType, + parse_log_message, ) from awesomeversion import AwesomeVersion import voluptuous as vol @@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = { logging.ERROR: LogLevel.LOG_LEVEL_ERROR, logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR, } -# 7-bit and 8-bit C1 ANSI sequences -# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python -ANSI_ESCAPE_78BIT = re.compile( - rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" -) @callback @@ -387,13 +382,15 @@ class ESPHomeManager: def _async_on_log(self, msg: SubscribeLogsResponse) -> None: """Handle a log message from the API.""" - log: bytes = msg.message - _LOGGER.log( - LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG), - "%s: %s", - self.entry.title, - ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"), - ) + for line in parse_log_message( + msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True + ): + _LOGGER.log( + LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG), + "%s: %s", + self.entry.title, + line, + ) @callback def _async_get_equivalent_log_level(self) -> LogLevel: From 7bd6ec68a888f60d95f2fb3c7c93e79a343c553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 11:41:36 +0200 Subject: [PATCH 1104/1175] Explain Home Connect setup (#146356) * Explain Home Connect setup * Avoid using "we" * Fix login spelling * Fix signup spelling --- .../components/home_connect/application_credentials.py | 10 ++++++++++ homeassistant/components/home_connect/strings.json | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/application_credentials.py b/homeassistant/components/home_connect/application_credentials.py index d66255e6810..20a3a211b6a 100644 --- a/homeassistant/components/home_connect/application_credentials.py +++ b/homeassistant/components/home_connect/application_credentials.py @@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe authorize_url=OAUTH2_AUTHORIZE, token_url=OAUTH2_TOKEN, ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.home-connect.com/", + "applications_url": "https://developer.home-connect.com/applications", + "register_application_url": "https://developer.home-connect.com/application/add", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 9d33f1d3ffd..71a1f1918f6 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and signup for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the sign up process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + }, "common": { "confirmed": "Confirmed", "present": "Present" @@ -13,7 +16,7 @@ "description": "The Home Connect integration needs to re-authenticate your account" }, "oauth_discovery": { - "description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect." + "description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect." } }, "abort": { From d89b99f42b08966512b2c8b9912f7d1477f77ae0 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 10 Jun 2025 14:10:49 +0200 Subject: [PATCH 1105/1175] Improve error logging in trend binary sensor (#146358) --- .../components/trend/binary_sensor.py | 9 +++- tests/components/trend/test_binary_sensor.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 4261f96bbe6..2bc5949b970 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -239,7 +239,14 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: - _LOGGER.error(ex) + _LOGGER.error( + "Error processing sensor state change for " + "entity_id=%s, attribute=%s, state=%s: %s", + self._entity_id, + self._attribute, + new_state.state, + ex, + ) self.async_on_remove( async_track_state_change_event( diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 4a829bb86d2..4f19c7e3427 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -437,3 +437,50 @@ async def test_unavailable_source( await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_trend_sensor").state == "on" + + +async def test_invalid_state_handling( + hass: HomeAssistant, + config_entry: MockConfigEntry, + setup_component: ComponentSetup, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling of invalid states in trend sensor.""" + await setup_component( + { + "sample_duration": 10000, + "min_gradient": 1, + "max_samples": 25, + "min_samples": 5, + }, + ) + + for val in (10, 20, 30, 40, 50, 60): + freezer.tick(timedelta(seconds=2)) + hass.states.async_set("sensor.test_state", val) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == STATE_ON + + # Set an invalid state + hass.states.async_set("sensor.test_state", "invalid") + await hass.async_block_till_done() + + # The trend sensor should handle the invalid state gracefully + assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor")) + assert sensor_state.state == STATE_ON + + # Check if a warning is logged + assert ( + "Error processing sensor state change for entity_id=sensor.test_state, " + "attribute=None, state=invalid: could not convert string to float: 'invalid'" + ) in caplog.text + + # Set a valid state again + hass.states.async_set("sensor.test_state", 50) + await hass.async_block_till_done() + + # The trend sensor should return to a valid state + assert (sensor_state := hass.states.get("binary_sensor.test_trend_sensor")) + assert sensor_state.state == "on" From 0874f1c350741becb68e7acee552f33e29b0ee19 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:43:20 +0200 Subject: [PATCH 1106/1175] Bump python-linkplay to v0.2.10 (#146359) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index eb9b5a87c75..1bbf70ed3ac 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.9"], + "requirements": ["python-linkplay==0.2.10"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index fbcffd47dee..19a71af3641 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.9 +python-linkplay==0.2.10 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df13b85db26..befaeeebbe3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.9 +python-linkplay==0.2.10 # homeassistant.components.lirc # python-lirc==1.2.3 From ca77b5210f5b88a3e56c41de4fcaac895bebe6a8 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Mon, 9 Jun 2025 14:00:37 -0400 Subject: [PATCH 1107/1175] Bump pydrawise to 2025.6.0 (#146369) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 0c355c34a71..03b9dc68a79 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2025.3.0"] + "requirements": ["pydrawise==2025.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19a71af3641..f2778052b4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1925,7 +1925,7 @@ pydiscovergy==3.0.2 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index befaeeebbe3..8ea955cb20e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1603,7 +1603,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.2 # homeassistant.components.hydrawise -pydrawise==2025.3.0 +pydrawise==2025.6.0 # homeassistant.components.android_ip_webcam pydroid-ipcam==3.0.0 From 0b24a9abc30c39128d88ac57c325c61ea1014884 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Mon, 9 Jun 2025 13:53:44 -0400 Subject: [PATCH 1108/1175] Bump env-canada to v0.11.2 (#146371) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index da0be245fcd..a6a6e447426 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.10.2"] + "requirements": ["env-canada==0.11.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f2778052b4e..ef173c1eb9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -878,7 +878,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea955cb20e..9bb4a42d641 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.10.2 +env-canada==0.11.2 # homeassistant.components.season ephem==4.1.6 From e7a7b2417b874d206cf0fd5cdd7924a35cc9907c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 19:25:29 -0500 Subject: [PATCH 1109/1175] Bump aioesphomeapi to 32.2.1 (#146375) --- 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 3ae66838823..9b70aba4de1 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==32.2.0", + "aioesphomeapi==32.2.1", "esphome-dashboard-api==1.3.0", "bleak-esphome==2.16.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index ef173c1eb9d..88e5ab99bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.0 +aioesphomeapi==32.2.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bb4a42d641..c1af6b4f120 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==32.2.0 +aioesphomeapi==32.2.1 # homeassistant.components.flo aioflo==2021.11.0 From f629731930598fd336e3a8cf24b95430fc2b1952 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 9 Jun 2025 20:59:02 +0300 Subject: [PATCH 1110/1175] Bump aioamazondevices to 3.0.6 (#146385) --- homeassistant/components/amazon_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index bd9bc701d3e..37a56486a08 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -118,5 +118,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "bronze", - "requirements": ["aioamazondevices==3.0.5"] + "requirements": ["aioamazondevices==3.0.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88e5ab99bc4..946f5bcfcc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1af6b4f120..ff1899391c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12 aioairzone==1.0.0 # homeassistant.components.amazon_devices -aioamazondevices==3.0.5 +aioamazondevices==3.0.6 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 3d0d70ece65e537be359ce8a2243164721c998d0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jun 2025 13:24:40 +0200 Subject: [PATCH 1111/1175] Fix switch_as_x entity_id tracking (#146386) --- .../components/switch_as_x/__init__.py | 21 +++++-- tests/components/switch_as_x/test_init.py | 63 +++++++++++++------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 71cb9e9c225..b07bf0fdaec 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event @@ -44,10 +44,12 @@ def async_add_to_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - registry = er.async_get(hass) + entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) try: - entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + entity_id = er.async_validate_entity_id( + entity_registry, entry.options[CONF_ENTITY_ID] + ) except vol.Invalid: # The entity is identified by an unknown entity registry ID _LOGGER.error( @@ -68,14 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return if "entity_id" in data["changes"]: - # Entity_id changed, reload the config entry - await hass.config_entries.async_reload(entry.entry_id) + # Entity_id changed, update or reload the config entry + if valid_entity_id(entry.options[CONF_ENTITY_ID]): + # If the entity is pointed to by an entity ID, update the entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: data["entity_id"]}, + ) + else: + await hass.config_entries.async_reload(entry.entry_id) if device_id and "device_id" in data["changes"]: # If the tracked switch is no longer in the device, remove our config entry # from the device if ( - not (entity_entry := registry.async_get(data[CONF_ENTITY_ID])) + not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) or not device_registry.async_get(device_id) or entity_entry.device_id == device_id ): diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index cd80fab69bc..0b965fc2ad1 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -39,6 +39,44 @@ EXPOSE_SETTINGS = { } +@pytest.fixture +def switch_entity_registry_entry( + entity_registry: er.EntityRegistry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", "test", "unique", original_name="ABC" + ) + + +@pytest.fixture +def switch_as_x_config_entry( + hass: HomeAssistant, + switch_entity_registry_entry: er.RegistryEntry, + target_domain: str, + use_entity_registry_id: bool, +) -> MockConfigEntry: + """Fixture to create a switch_as_x config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_registry_entry.id + if use_entity_registry_id + else switch_entity_registry_entry.entity_id, + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str @@ -67,6 +105,7 @@ async def test_config_entry_unregistered_uuid( assert len(hass.states.async_all()) == 0 +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) @pytest.mark.parametrize( ("target_domain", "state_on", "state_off"), [ @@ -81,33 +120,17 @@ async def test_config_entry_unregistered_uuid( async def test_entity_registry_events( hass: HomeAssistant, entity_registry: er.EntityRegistry, + switch_entity_registry_entry: er.RegistryEntry, + switch_as_x_config_entry: MockConfigEntry, target_domain: str, state_on: str, state_off: str, ) -> None: """Test entity registry events are tracked.""" - registry_entry = entity_registry.async_get_or_create( - "switch", "test", "unique", original_name="ABC" - ) - switch_entity_id = registry_entry.entity_id + switch_entity_id = switch_entity_registry_entry.entity_id hass.states.async_set(switch_entity_id, STATE_ON) - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: registry_entry.id, - CONF_INVERT: False, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - version=SwitchAsXConfigFlowHandler.VERSION, - minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(f"{target_domain}.abc").state == state_on From 218864d08c06844f64a2281662fdbea28fb048cc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Jun 2025 17:04:55 +0200 Subject: [PATCH 1112/1175] Update switch_as_x to handle wrapped switch moved to another device (#146387) * Update switch_as_x to handle wrapped switch moved to another device * Reload switch_as_x config entry after updating device * Make sure the switch_as_x entity is not removed --- .../components/switch_as_x/__init__.py | 24 ++- tests/components/switch_as_x/test_init.py | 151 ++++++++++++++++-- 2 files changed, 164 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index b07bf0fdaec..9e40a99299a 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -13,7 +13,7 @@ from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event -from .const import CONF_INVERT, CONF_TARGET_DOMAIN +from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN from .light import LightSwitch __all__ = ["LightSwitch"] @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_reload(entry.entry_id) if device_id and "device_id" in data["changes"]: - # If the tracked switch is no longer in the device, remove our config entry + # Handle the wrapped switch being moved to a different device or removed # from the device if ( not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) @@ -91,10 +91,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # No need to do any cleanup return + # The wrapped switch has been moved to a different device, update the + # switch_as_x entity and the device entry to include our config entry + switch_as_x_entity_id = entity_registry.async_get_entity_id( + entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id + ) + if switch_as_x_entity_id: + # Update the switch_as_x entity to point to the new device (or no device) + entity_registry.async_update_entity( + switch_as_x_entity_id, device_id=entity_entry.device_id + ) + + if entity_entry.device_id is not None: + device_registry.async_update_device( + entity_entry.device_id, add_config_entry_id=entry.entry_id + ) + device_registry.async_update_device( device_id, remove_config_entry_id=entry.entry_id ) + # Reload the config entry so the switch_as_x entity is recreated with + # correct device info + await hass.config_entries.async_reload(entry.entry_id) + entry.async_on_unload( async_track_entity_registry_updated_event( hass, entity_id, async_registry_updated diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 0b965fc2ad1..2c87b0e3a92 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest +from homeassistant.components import switch_as_x from homeassistant.components.homeassistant import exposed_entities from homeassistant.components.lock import LockState from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler @@ -24,8 +25,9 @@ from homeassistant.const import ( EntityCategory, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.setup import async_setup_component from . import PLATFORMS_TO_TEST @@ -222,16 +224,39 @@ async def test_device_registry_config_entry_1( device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id in device_entry.config_entries - # Remove the wrapped switch's config entry from the device - device_registry.async_update_device( - device_entry.id, remove_config_entry_id=switch_config_entry.entry_id - ) - await hass.async_block_till_done() - await hass.async_block_till_done() + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + + # Remove the wrapped switch's config entry from the device, this removes the + # wrapped switch + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=switch_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + # Check that the switch_as_x config entry is removed + assert ( + switch_as_x_config_entry.entry_id not in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == ["remove"] + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_device_registry_config_entry_2( @@ -281,13 +306,121 @@ async def test_device_registry_config_entry_2( device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id in device_entry.config_entries + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + # Remove the wrapped switch from the device - entity_registry.async_update_entity(switch_entity_entry.entity_id, device_id=None) - await hass.async_block_till_done() + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + entity_registry.async_update_entity( + switch_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + # Check that the switch_as_x config entry is not removed + assert switch_as_x_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_device_registry_config_entry_3( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + target_domain: str, +) -> None: + """Test we add our config entry to the tracked switch's device.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + device_entry_2 = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=device_entry.id, + original_name="ABC", + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry.device_id == switch_entity_entry.device_id + + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id in device_entry.config_entries + device_entry_2 = device_registry.async_get(device_entry_2.id) + assert switch_as_x_config_entry.entry_id not in device_entry_2.config_entries + + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_entry.entity_id, add_event) + + # Move the wrapped switch to another device + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + wraps=switch_as_x.async_unload_entry, + ) as mock_setup_entry: + entity_registry.async_update_entity( + switch_entity_entry.entity_id, device_id=device_entry_2.id + ) + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() + + # Check that the switch_as_x config entry is moved to the other device + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + device_entry_2 = device_registry.async_get(device_entry_2.id) + assert switch_as_x_config_entry.entry_id in device_entry_2.config_entries + + # Check that the switch_as_x config entry is not removed + assert switch_as_x_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_entity_id( From a3220ecae6c33c1175d401a0e669cd680dad1346 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 9 Jun 2025 19:51:46 +0200 Subject: [PATCH 1113/1175] Bump pynordpool to 0.3.0 (#146396) --- homeassistant/components/nordpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nordpool/manifest.json b/homeassistant/components/nordpool/manifest.json index b096d2bd506..ca299b470ea 100644 --- a/homeassistant/components/nordpool/manifest.json +++ b/homeassistant/components/nordpool/manifest.json @@ -8,6 +8,6 @@ "iot_class": "cloud_polling", "loggers": ["pynordpool"], "quality_scale": "platinum", - "requirements": ["pynordpool==0.2.4"], + "requirements": ["pynordpool==0.3.0"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 946f5bcfcc2..dd84a358fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2174,7 +2174,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff1899391c1..d0c4252f456 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1804,7 +1804,7 @@ pynina==0.3.6 pynobo==1.8.1 # homeassistant.components.nordpool -pynordpool==0.2.4 +pynordpool==0.3.0 # homeassistant.components.nuki pynuki==1.6.3 From c6ff0e64927d429c7f56e2e7b460442eebfbbe9a Mon Sep 17 00:00:00 2001 From: wittypluck Date: Mon, 9 Jun 2025 19:55:09 +0200 Subject: [PATCH 1114/1175] Fix CO concentration unit in OpenWeatherMap (#146403) --- homeassistant/components/openweathermap/sensor.py | 3 +-- tests/components/openweathermap/snapshots/test_sensor.ambr | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 789e9647f77..87b7860afb5 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, DEGREE, PERCENTAGE, UV_INDEX, @@ -170,7 +169,7 @@ AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key=ATTR_API_AIRPOLLUTION_CO, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/tests/components/openweathermap/snapshots/test_sensor.ambr b/tests/components/openweathermap/snapshots/test_sensor.ambr index cbd86f14676..11a1feb721f 100644 --- a/tests/components/openweathermap/snapshots/test_sensor.ambr +++ b/tests/components/openweathermap/snapshots/test_sensor.ambr @@ -86,7 +86,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': '12.34-56.78-co', - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }) # --- # name: test_sensor_states[air_pollution][sensor.openweathermap_carbon_monoxide-state] @@ -96,7 +96,7 @@ 'device_class': 'carbon_monoxide', 'friendly_name': 'openweathermap Carbon monoxide', 'state_class': , - 'unit_of_measurement': 'ppm', + 'unit_of_measurement': 'µg/m³', }), 'context': , 'entity_id': 'sensor.openweathermap_carbon_monoxide', From 9997fc11b186e8f406d8d314037bf80ae82f7e73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Jun 2025 14:31:18 +0200 Subject: [PATCH 1115/1175] Handle changes to source entity in derivative helper (#146407) * Handle changes to source entity in derivative helper * Rename helper function, improve docstring * Add tests * Improve derivative tests * Deduplicate tests * Rename helpers/helper_entity.py to helpers/helper_integration.py * Rename tests --- .../components/derivative/__init__.py | 27 ++ .../components/switch_as_x/__init__.py | 79 +--- homeassistant/helpers/helper_integration.py | 105 +++++ tests/components/derivative/test_init.py | 279 +++++++++++- tests/helpers/test_helper_integration.py | 424 ++++++++++++++++++ 5 files changed, 847 insertions(+), 67 deletions(-) create mode 100644 homeassistant/helpers/helper_integration.py create mode 100644 tests/helpers/test_helper_integration.py diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 5117663f3c5..5eb499b0efd 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -2,12 +2,18 @@ from __future__ import annotations +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes + +from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +23,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry.entry_id, entry.options[CONF_SOURCE] ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE: source_entity_id}, + ) + + entity_registry = er.async_get(hass) + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + get_helper_entity_id=lambda: entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, entry.entry_id + ), + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE], + ) + ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 9e40a99299a..6e9e3a93b45 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -9,9 +9,9 @@ import voluptuous as vol from homeassistant.components.homeassistant import exposed_entities from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN from .light import LightSwitch @@ -45,7 +45,6 @@ def async_add_to_device( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) try: entity_id = er.async_validate_entity_id( entity_registry, entry.options[CONF_ENTITY_ID] @@ -58,72 +57,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - async def async_registry_updated( - event: Event[er.EventEntityRegistryUpdatedData], - ) -> None: - """Handle entity registry update.""" - data = event.data - if data["action"] == "remove": - await hass.config_entries.async_remove(entry.entry_id) - - if data["action"] != "update": - return - - if "entity_id" in data["changes"]: - # Entity_id changed, update or reload the config entry - if valid_entity_id(entry.options[CONF_ENTITY_ID]): - # If the entity is pointed to by an entity ID, update the entry - hass.config_entries.async_update_entry( - entry, - options={**entry.options, CONF_ENTITY_ID: data["entity_id"]}, - ) - else: - await hass.config_entries.async_reload(entry.entry_id) - - if device_id and "device_id" in data["changes"]: - # Handle the wrapped switch being moved to a different device or removed - # from the device - if ( - not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID])) - or not device_registry.async_get(device_id) - or entity_entry.device_id == device_id - ): - # No need to do any cleanup - return - - # The wrapped switch has been moved to a different device, update the - # switch_as_x entity and the device entry to include our config entry - switch_as_x_entity_id = entity_registry.async_get_entity_id( - entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id - ) - if switch_as_x_entity_id: - # Update the switch_as_x entity to point to the new device (or no device) - entity_registry.async_update_entity( - switch_as_x_entity_id, device_id=entity_entry.device_id - ) - - if entity_entry.device_id is not None: - device_registry.async_update_device( - entity_entry.device_id, add_config_entry_id=entry.entry_id - ) - - device_registry.async_update_device( - device_id, remove_config_entry_id=entry.entry_id - ) - - # Reload the config entry so the switch_as_x entity is recreated with - # correct device info - await hass.config_entries.async_reload(entry.entry_id) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) entry.async_on_unload( - async_track_entity_registry_updated_event( - hass, entity_id, async_registry_updated + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + get_helper_entity_id=lambda: entity_registry.async_get_entity_id( + entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id + ), + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_add_to_device(hass, entry, entity_id), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], ) ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - device_id = async_add_to_device(hass, entry, entity_id) - await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) ) diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py new file mode 100644 index 00000000000..4f39ef4c843 --- /dev/null +++ b/homeassistant/helpers/helper_integration.py @@ -0,0 +1,105 @@ +"""Helpers for helper integrations.""" + +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, valid_entity_id + +from . import device_registry as dr, entity_registry as er +from .event import async_track_entity_registry_updated_event + + +def async_handle_source_entity_changes( + hass: HomeAssistant, + *, + helper_config_entry_id: str, + get_helper_entity_id: Callable[[], str | None], + set_source_entity_id_or_uuid: Callable[[str], None], + source_device_id: str | None, + source_entity_id_or_uuid: str, +) -> CALLBACK_TYPE: + """Handle changes to a helper entity's source entity. + + The following changes are handled: + - Entity removal: If the source entity is removed, the helper config entry + is removed, and the helper entity is cleaned up. + - Entity ID changed: If the source entity's entity ID changes and the source + entity is identified by an entity ID, the set_source_entity_id_or_uuid is + called. If the source entity is identified by a UUID, the helper config entry + is reloaded. + - Source entity moved to another device: The helper entity is updated to link + to the new device, and the helper config entry removed from the old device + and added to the new device. Then the helper config entry is reloaded. + - Source entity removed from the device: The helper entity is updated to link + to no device, and the helper config entry removed from the old device. Then + the helper config entry is reloaded. + """ + + async def async_registry_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + nonlocal source_device_id + + data = event.data + if data["action"] == "remove": + await hass.config_entries.async_remove(helper_config_entry_id) + + if data["action"] != "update": + return + + if "entity_id" in data["changes"]: + # Entity_id changed, update or reload the config entry + if valid_entity_id(source_entity_id_or_uuid): + # If the entity is pointed to by an entity ID, update the entry + set_source_entity_id_or_uuid(data["entity_id"]) + else: + await hass.config_entries.async_reload(helper_config_entry_id) + + if not source_device_id or "device_id" not in data["changes"]: + return + + # Handle the source entity being moved to a different device or removed + # from the device + if ( + not (source_entity_entry := entity_registry.async_get(data["entity_id"])) + or not device_registry.async_get(source_device_id) + or source_entity_entry.device_id == source_device_id + ): + # No need to do any cleanup + return + + # The source entity has been moved to a different device, update the helper + # helper entity to link to the new device and the helper device to include + # the helper config entry + helper_entity_id = get_helper_entity_id() + if helper_entity_id: + # Update the helper entity to link to the new device (or no device) + entity_registry.async_update_entity( + helper_entity_id, device_id=source_entity_entry.device_id + ) + + if source_entity_entry.device_id is not None: + device_registry.async_update_device( + source_entity_entry.device_id, + add_config_entry_id=helper_config_entry_id, + ) + + device_registry.async_update_device( + source_device_id, remove_config_entry_id=helper_config_entry_id + ) + source_device_id = source_entity_entry.device_id + + # Reload the config entry so the helper entity is recreated with + # correct device info + await hass.config_entries.async_reload(helper_config_entry_id) + + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + source_entity_id = er.async_validate_entity_id( + entity_registry, source_entity_id_or_uuid + ) + return async_track_entity_registry_updated_event( + hass, source_entity_id, async_registry_updated + ) diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index 32802080e39..f75d5940da7 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -1,23 +1,103 @@ """Test the Derivative integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import derivative +from homeassistant.components.derivative.config_flow import ConfigFlowHandler from homeassistant.components.derivative.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry -@pytest.mark.parametrize("platform", ["sensor"]) +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def derivative_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a derivative config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": sensor_entity_entry.entity_id, + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, - platform: str, ) -> None: """Test setting up and removing a config entry.""" input_sensor_entity_id = "sensor.input" - derivative_entity_id = f"{platform}.my_derivative" + derivative_entity_id = "sensor.my_derivative" # Setup the config entry config_entry = MockConfigEntry( @@ -147,3 +227,194 @@ async def test_device_cleaning( derivative_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the derivative config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + + # Check that the derivative config entry is removed + assert derivative_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert derivative_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert derivative_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + derivative_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(derivative_config_entry.entry_id) + await hass.async_block_till_done() + + derivative_entity_entry = entity_registry.async_get("sensor.my_derivative") + assert derivative_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, derivative_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.derivative.async_unload_entry", + wraps=derivative.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the derivative config entry is updated with the new entity ID + assert derivative_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert derivative_config_entry.entry_id in sensor_device.config_entries + + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py new file mode 100644 index 00000000000..25d490c27bb --- /dev/null +++ b/tests/helpers/test_helper_integration.py @@ -0,0 +1,424 @@ +"""Tests for the helper entity helpers.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes + +from tests.common import ( + MockConfigEntry, + MockModule, + mock_config_flow, + mock_integration, + mock_platform, +) + +HELPER_DOMAIN = "helper" +SOURCE_DOMAIN = "test" + + +@pytest.fixture +def source_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a source config entry.""" + source_config_entry = MockConfigEntry() + source_config_entry.add_to_hass(hass) + return source_config_entry + + +@pytest.fixture +def source_device( + device_registry: dr.DeviceRegistry, + source_config_entry: ConfigEntry, +) -> dr.DeviceEntry: + """Fixture to create a source device.""" + return device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def source_entity_entry( + entity_registry: er.EntityRegistry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a source entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + SOURCE_DOMAIN, + "unique", + config_entry=source_config_entry, + device_id=source_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def helper_config_entry( + hass: HomeAssistant, + source_entity_entry: er.RegistryEntry, + use_entity_registry_id: bool, +) -> MockConfigEntry: + """Fixture to create a helper config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=HELPER_DOMAIN, + options={ + "name": "My helper", + "round": 1.0, + "source": source_entity_entry.id + if use_entity_registry_id + else source_entity_entry.entity_id, + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My helper", + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +@pytest.fixture +def mock_helper_flow() -> Generator[None]: + """Mock helper config flow.""" + + class MockConfigFlow: + """Mock the helper config flow.""" + + VERSION = 1 + MINOR_VERSION = 1 + + with mock_config_flow(HELPER_DOMAIN, MockConfigFlow): + yield + + +@pytest.fixture +def helper_entity_entry( + entity_registry: er.EntityRegistry, + helper_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a helper entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + HELPER_DOMAIN, + helper_config_entry.entry_id, + config_entry=helper_config_entry, + device_id=source_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def async_remove_entry() -> AsyncMock: + """Fixture to mock async_remove_entry.""" + return AsyncMock(return_value=True) + + +@pytest.fixture +def async_unload_entry() -> AsyncMock: + """Fixture to mock async_unload_entry.""" + return AsyncMock(return_value=True) + + +@pytest.fixture +def set_source_entity_id_or_uuid() -> AsyncMock: + """Fixture to mock async_unload_entry.""" + return Mock() + + +@pytest.fixture +def mock_helper_integration( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Mock the helper integration.""" + + def get_helper_entity_id() -> str | None: + """Get the helper entity ID.""" + return entity_registry.async_get_entity_id( + "sensor", HELPER_DOMAIN, helper_config_entry.entry_id + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Mock setup entry.""" + async_handle_source_entity_changes( + hass, + helper_config_entry_id=helper_config_entry.entry_id, + get_helper_entity_id=get_helper_entity_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=source_entity_entry.device_id, + source_entity_id_or_uuid=helper_config_entry.options["source"], + ) + return True + + mock_integration( + hass, + MockModule( + HELPER_DOMAIN, + async_remove_entry=async_remove_entry, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_platform(hass, f"{HELPER_DOMAIN}.config_flow", None) + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the helper config entry is removed when the source entity is removed.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Remove the source entitys's config entry from the device, this removes the + # source entity + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Check that the helper config entry is unloaded and removed + async_unload_entry.assert_called_once() + async_remove_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + + # Check that the helper config entry is removed + assert helper_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the source entity removed from the source device.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Remove the source entity from the device + entity_registry.async_update_entity(source_entity_entry.entity_id, device_id=None) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + async_unload_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize("use_entity_registry_id", [True, False]) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_config_entry: ConfigEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, +) -> None: + """Test the source entity is moved to another device.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + # Create another device to move the source entity to + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert helper_config_entry.entry_id not in source_device_2.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Move the source entity to another device + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + async_unload_entry.assert_called_once() + set_source_entity_id_or_uuid.assert_not_called() + + # Check that the helper config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert helper_config_entry.entry_id in source_device_2.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.parametrize( + ("use_entity_registry_id", "unload_calls", "set_source_entity_id_calls"), + [(True, 1, 0), (False, 0, 1)], +) +@pytest.mark.usefixtures("mock_helper_flow", "mock_helper_integration") +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + helper_config_entry: MockConfigEntry, + helper_entity_entry: er.RegistryEntry, + source_device: dr.DeviceEntry, + source_entity_entry: er.RegistryEntry, + async_remove_entry: AsyncMock, + async_unload_entry: AsyncMock, + set_source_entity_id_or_uuid: Mock, + unload_calls: int, + set_source_entity_id_calls: int, +) -> None: + """Test the source entity's entity ID is changed.""" + # Add the helper config entry to the source device + device_registry.async_update_device( + source_device.id, add_config_entry_id=helper_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(helper_config_entry.entry_id) + await hass.async_block_till_done() + + # Check preconditions + helper_entity_entry = entity_registry.async_get(helper_entity_entry.entity_id) + assert helper_entity_entry.device_id == source_entity_entry.device_id + + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + events = track_entity_registry_actions(hass, helper_entity_entry.entity_id) + + # Change the source entity's entity ID + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + async_remove_entry.assert_not_called() + assert len(async_unload_entry.mock_calls) == unload_calls + assert len(set_source_entity_id_or_uuid.mock_calls) == set_source_entity_id_calls + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert helper_config_entry.entry_id in source_device.config_entries + + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From ec30b12fd1e668049299eeba47caa0caa9ebc79e Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Tue, 10 Jun 2025 08:35:40 -0700 Subject: [PATCH 1116/1175] Fix initial state of UV protection window (#146408) The `binary_sensor` is created when the config entry is loaded after the `async_config_entry_first_refresh` has completed (during the forward of setup to platforms). Therefore, the update coordinator will already have data and will not trigger the invocation of `_handle_coordinator_update`. Fixing this just means performing the same update at initialization. --- homeassistant/components/openuv/binary_sensor.py | 6 ++++-- homeassistant/components/openuv/entity.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index f45404ce38e..09c9ab75192 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -49,6 +49,10 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): @callback def _handle_coordinator_update(self) -> None: """Update the entity from the latest data.""" + self._update_attrs() + super()._handle_coordinator_update() + + def _update_attrs(self) -> None: data = self.coordinator.data for key in ("from_time", "to_time", "from_uv", "to_uv"): @@ -78,5 +82,3 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt), } ) - - super()._handle_coordinator_update() diff --git a/homeassistant/components/openuv/entity.py b/homeassistant/components/openuv/entity.py index f3015815bf1..2303f21f2b8 100644 --- a/homeassistant/components/openuv/entity.py +++ b/homeassistant/components/openuv/entity.py @@ -31,3 +31,8 @@ class OpenUvEntity(CoordinatorEntity): name="OpenUV", entry_type=DeviceEntryType.SERVICE, ) + + self._update_attrs() + + def _update_attrs(self) -> None: + """Override point for updating attributes during init.""" From 97d91ddddba41ed0457aa4df6e30287924aac07d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:44:34 -0500 Subject: [PATCH 1117/1175] Bump propcache to 0.3.2 (#146418) --- 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 a994e1d6333..b34d994b8a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ orjson==3.10.18 packaging>=23.1 paho-mqtt==2.1.0 Pillow==11.2.1 -propcache==0.3.1 +propcache==0.3.2 psutil-home-assistant==0.0.1 PyJWT==2.10.1 pymicro-vad==1.0.1 diff --git a/pyproject.toml b/pyproject.toml index 7f458bebf39..65157f8452a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ dependencies = [ # PyJWT has loose dependency. We want the latest one. "cryptography==45.0.1", "Pillow==11.2.1", - "propcache==0.3.1", + "propcache==0.3.2", "pyOpenSSL==25.1.0", "orjson==3.10.18", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index 88576cd0c4c..2ded4aeca71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ numpy==2.2.2 PyJWT==2.10.1 cryptography==45.0.1 Pillow==11.2.1 -propcache==0.3.1 +propcache==0.3.2 pyOpenSSL==25.1.0 orjson==3.10.18 packaging>=23.1 From 2b08c4c344583996ad6905eafcdf790a9712c893 Mon Sep 17 00:00:00 2001 From: Jamin Date: Tue, 10 Jun 2025 09:22:53 -0500 Subject: [PATCH 1118/1175] Check hangup error in voip (#146423) Check hangup error Prevent an error where the call end future may have already been set when a hangup is detected. --- homeassistant/components/voip/assist_satellite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/voip/assist_satellite.py b/homeassistant/components/voip/assist_satellite.py index 7b34d7a11ba..ac8065cabf7 100644 --- a/homeassistant/components/voip/assist_satellite.py +++ b/homeassistant/components/voip/assist_satellite.py @@ -336,7 +336,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol if self._run_pipeline_task is not None: _LOGGER.debug("Cancelling running pipeline") self._run_pipeline_task.cancel() - self._call_end_future.set_result(None) + if not self._call_end_future.done(): + self._call_end_future.set_result(None) self.disconnect() break From 41abc8404de78145b45a08e3bcbbd6712eee1182 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 04:26:29 -0500 Subject: [PATCH 1119/1175] Bump yarl to 1.20.1 (#146424) --- 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 b34d994b8a5..ae8b93788b7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -74,7 +74,7 @@ voluptuous-openapi==0.1.0 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 -yarl==1.20.0 +yarl==1.20.1 zeroconf==0.147.0 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index 65157f8452a..51f89f932bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ dependencies = [ "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.1.0", - "yarl==1.20.0", + "yarl==1.20.1", "webrtc-models==0.3.0", "zeroconf==0.147.0", ] diff --git a/requirements.txt b/requirements.txt index 2ded4aeca71..0c5927dd9e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,6 +58,6 @@ uv==0.7.1 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.1.0 -yarl==1.20.0 +yarl==1.20.1 webrtc-models==0.3.0 zeroconf==0.147.0 From 4f0e4bc1ca89516e54a433364813ed08af076171 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 Jun 2025 02:39:53 -0500 Subject: [PATCH 1120/1175] Bump aiohttp to 3.12.12 (#146426) --- 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 ae8b93788b7..f62c1c899ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiodns==3.4.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 -aiohttp==3.12.11 +aiohttp==3.12.12 aiohttp_cors==0.7.0 aiousbwatcher==1.1.1 aiozoneinfo==0.2.3 diff --git a/pyproject.toml b/pyproject.toml index 51f89f932bc..c31deb67dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.3.1", - "aiohttp==3.12.11", + "aiohttp==3.12.12", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.3.0", "aiohttp-asyncmdnsresolver==0.1.1", diff --git a/requirements.txt b/requirements.txt index 0c5927dd9e5..c07d0e282a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.4.0 aiohasupervisor==0.3.1 -aiohttp==3.12.11 +aiohttp==3.12.12 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.3.0 aiohttp-asyncmdnsresolver==0.1.1 From f945defa2b4e601e389cdba1a0b0d44e45dfd467 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jun 2025 11:14:31 +0200 Subject: [PATCH 1121/1175] Reformat Dockerfile to reduce merge conflicts (#146435) --- script/hassfest/docker.py | 7 +++++-- script/hassfest/docker/Dockerfile | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/script/hassfest/docker.py b/script/hassfest/docker.py index 4bf6c3bb0a6..1f112c11b94 100644 --- a/script/hassfest/docker.py +++ b/script/hassfest/docker.py @@ -103,7 +103,10 @@ RUN --mount=from=ghcr.io/astral-sh/uv:{uv},source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree=={pipdeptree} tqdm=={tqdm} ruff=={ruff} \ + stdlib-list==0.10.0 \ + pipdeptree=={pipdeptree} \ + tqdm=={tqdm} \ + ruff=={ruff} \ {required_components_packages} LABEL "name"="hassfest" @@ -169,7 +172,7 @@ def _generate_hassfest_dockerimage( return File( _HASSFEST_TEMPLATE.format( timeout=timeout, - required_components_packages=" ".join(sorted(packages)), + required_components_packages=" \\\n ".join(sorted(packages)), **package_versions, ), config.root / "script/hassfest/docker/Dockerfile", diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 981dc3344c0..830bdc4445e 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -24,8 +24,18 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree==2.26.1 tqdm==4.67.1 ruff==0.11.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.5.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + stdlib-list==0.10.0 \ + pipdeptree==2.26.1 \ + tqdm==4.67.1 \ + ruff==0.11.0 \ + PyTurboJPEG==1.7.5 \ + go2rtc-client==0.2.1 \ + ha-ffmpeg==3.2.2 \ + hassil==2.2.3 \ + home-assistant-intents==2025.5.28 \ + mutagen==1.47.0 \ + pymicro-vad==1.0.1 \ + pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From b222fe5afaa9f46e696b1ab708a293e66c1aaff7 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 10 Jun 2025 06:31:32 -0700 Subject: [PATCH 1122/1175] Handle grpc errors in Google Assistant SDK (#146438) --- .../google_assistant_sdk/helpers.py | 14 +++++++- .../google_assistant_sdk/strings.json | 5 +++ .../google_assistant_sdk/test_init.py | 27 ++++++++++++++- .../google_assistant_sdk/test_notify.py | 34 ++++++++++++++++--- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index ca774bed77e..b319e1e432c 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -12,6 +12,7 @@ import aiohttp from aiohttp import web from gassist_text import TextAssistant from google.oauth2.credentials import Credentials +from grpc import RpcError from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player import ( @@ -25,6 +26,7 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.event import async_call_later @@ -83,7 +85,17 @@ async def async_send_text_commands( ) as assistant: command_response_list = [] for command in commands: - resp = await hass.async_add_executor_job(assistant.assist, command) + try: + resp = await hass.async_add_executor_job(assistant.assist, command) + except RpcError as err: + _LOGGER.error( + "Failed to send command '%s' to Google Assistant: %s", + command, + err, + ) + raise HomeAssistantError( + translation_domain=DOMAIN, translation_key="grpc_error" + ) from err text_response = resp[0] _LOGGER.debug("command: %s\nresponse: %s", command, text_response) audio_response = resp[2] diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 87c93023900..885ff0aad71 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -57,5 +57,10 @@ } } } + }, + "exceptions": { + "grpc_error": { + "message": "Failed to communicate with Google Assistant" + } } } diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index f986497ed29..9bb08c802c2 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -6,6 +6,7 @@ import time from unittest.mock import call, patch import aiohttp +from grpc import RpcError import pytest from homeassistant.components import conversation @@ -13,6 +14,7 @@ from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.config_entries import ConfigEntryState from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -231,11 +233,34 @@ async def test_send_text_command_expired_token_refresh_failure( {"command": "turn on tv"}, blocking=True, ) - await hass.async_block_till_done() assert any(entry.async_get_active_flows(hass, {"reauth"})) == requires_reauth +async def test_send_text_command_grpc_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call send_text_command when RpcError is raised.""" + await setup_integration() + + command = "turn on home assistant unsupported device" + with ( + patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=RpcError(), + ) as mock_assist_call, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + DOMAIN, + "send_text_command", + {"command": command}, + blocking=True, + ) + mock_assist_call.assert_called_once_with(command) + + async def test_send_text_command_media_player( hass: HomeAssistant, setup_integration: ComponentSetup, diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 266846b17e1..ca4162c9e7a 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -2,6 +2,7 @@ from unittest.mock import call, patch +from grpc import RpcError import pytest from homeassistant.components import notify @@ -9,6 +10,7 @@ from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES from homeassistant.components.google_assistant_sdk.notify import broadcast_commands from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .conftest import ComponentSetup, ExpectedCredentials @@ -45,8 +47,8 @@ async def test_broadcast_no_targets( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message}, + blocking=True, ) - await hass.async_block_till_done() mock_text_assistant.assert_called_once_with( ExpectedCredentials(), language_code, audio_out=False ) @@ -54,6 +56,30 @@ async def test_broadcast_no_targets( mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) +async def test_broadcast_grpc_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test broadcast handling when RpcError is raised.""" + await setup_integration() + + with ( + patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=RpcError(), + ) as mock_assist_call, + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + notify.DOMAIN, + DOMAIN, + {notify.ATTR_MESSAGE: "Dinner is served"}, + blocking=True, + ) + + mock_assist_call.assert_called_once_with("broadcast Dinner is served") + + @pytest.mark.parametrize( ("language_code", "message", "target", "expected_command"), [ @@ -103,8 +129,8 @@ async def test_broadcast_one_target( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target]}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_called_once_with(expected_command) @@ -127,8 +153,8 @@ async def test_broadcast_two_targets( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: message, notify.ATTR_TARGET: [target1, target2]}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_has_calls( [call(expected_command1), call(expected_command2)] ) @@ -148,8 +174,8 @@ async def test_broadcast_empty_message( notify.DOMAIN, DOMAIN, {notify.ATTR_MESSAGE: ""}, + blocking=True, ) - await hass.async_block_till_done() mock_assist_call.assert_not_called() From ba19d4f0431ead5b421d8c96c1714029fd900f28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 11:56:24 +0200 Subject: [PATCH 1123/1175] Fix typo at application credentials string at Home Connect integration (#146442) Fix typos --- homeassistant/components/home_connect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 71a1f1918f6..7aadf6b0dde 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and signup for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the sign up process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: {redirect_url}\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." }, "common": { "confirmed": "Confirmed", From b2d25b1883a8ca65218430394f83fcde40fce248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Diego=20Rodr=C3=ADguez=20Royo?= Date: Tue, 10 Jun 2025 14:11:07 +0200 Subject: [PATCH 1124/1175] Improvements for Home Connect application credentials string (#146443) --- homeassistant/components/home_connect/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 7aadf6b0dde..1445a8eae08 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values: \n\t* **Application ID**: Home Assistant (or any other name that makes sense)\n\t* **OAuth Flow**: Authorization Code Grant Flow\n\t* **Redirect URI**: {redirect_url}\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." + "description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**." }, "common": { "confirmed": "Confirmed", From 6f4029983acdbbcdae1b4fdd937f9b788f3c4fc6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 10 Jun 2025 15:00:41 +0200 Subject: [PATCH 1125/1175] Update requests to 2.32.4 (#146445) --- 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 f62c1c899ba..af9b0472bb1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -61,7 +61,7 @@ pyspeex-noise==1.0.2 python-slugify==8.0.4 PyTurboJPEG==1.7.5 PyYAML==6.0.2 -requests==2.32.3 +requests==2.32.4 securetar==2025.2.1 SQLAlchemy==2.0.40 standard-aifc==3.13.0 diff --git a/pyproject.toml b/pyproject.toml index c31deb67dcf..52910c7f319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,7 @@ dependencies = [ # dependencies to stage 0. "PyTurboJPEG==1.7.5", "PyYAML==6.0.2", - "requests==2.32.3", + "requests==2.32.4", "securetar==2025.2.1", "SQLAlchemy==2.0.40", "standard-aifc==3.13.0", diff --git a/requirements.txt b/requirements.txt index c07d0e282a0..bff95490470 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,7 +46,7 @@ pyspeex-noise==1.0.2 python-slugify==8.0.4 PyTurboJPEG==1.7.5 PyYAML==6.0.2 -requests==2.32.3 +requests==2.32.4 securetar==2025.2.1 SQLAlchemy==2.0.40 standard-aifc==3.13.0 From bdbb74aff120b9f6efc234626ca4c564efd0fa28 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 14:52:24 +0200 Subject: [PATCH 1126/1175] Return expected state in SmartThings water heater (#146449) --- .../components/smartthings/strings.json | 9 ----- .../components/smartthings/water_heater.py | 9 +++-- .../snapshots/test_water_heater.ambr | 36 +++++++++---------- .../smartthings/test_water_heater.py | 27 +++++++------- 4 files changed, 39 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 7b5edde2d10..8e972ac8aea 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -619,15 +619,6 @@ "keep_fresh_mode": { "name": "Keep fresh mode" } - }, - "water_heater": { - "water_heater": { - "state": { - "standard": "Standard", - "force": "Forced", - "power": "Power" - } - } } }, "issues": { diff --git a/homeassistant/components/smartthings/water_heater.py b/homeassistant/components/smartthings/water_heater.py index addbfed2ec4..4b1aaaa5549 100644 --- a/homeassistant/components/smartthings/water_heater.py +++ b/homeassistant/components/smartthings/water_heater.py @@ -10,6 +10,9 @@ from homeassistant.components.water_heater import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, STATE_ECO, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, WaterHeaterEntity, WaterHeaterEntityFeature, ) @@ -24,9 +27,9 @@ from .entity import SmartThingsEntity OPERATION_MAP_TO_HA: dict[str, str] = { "eco": STATE_ECO, - "std": "standard", - "force": "force", - "power": "power", + "std": STATE_HEAT_PUMP, + "force": STATE_HIGH_DEMAND, + "power": STATE_PERFORMANCE, } HA_TO_OPERATION_MAP = {v: k for k, v in OPERATION_MAP_TO_HA.items()} diff --git a/tests/components/smartthings/snapshots/test_water_heater.ambr b/tests/components/smartthings/snapshots/test_water_heater.ambr index 3e5afed3b86..d52400b9de2 100644 --- a/tests/components/smartthings/snapshots/test_water_heater.ambr +++ b/tests/components/smartthings/snapshots/test_water_heater.ambr @@ -10,9 +10,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), }), 'config_entry_id': , @@ -55,9 +55,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), 'operation_mode': 'off', 'supported_features': , @@ -84,8 +84,8 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'force', + 'heat_pump', + 'high_demand', ]), }), 'config_entry_id': , @@ -128,8 +128,8 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'force', + 'heat_pump', + 'high_demand', ]), 'operation_mode': 'off', 'supported_features': , @@ -156,9 +156,9 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), }), 'config_entry_id': , @@ -201,11 +201,11 @@ 'operation_list': list([ 'off', 'eco', - 'standard', - 'power', - 'force', + 'heat_pump', + 'performance', + 'high_demand', ]), - 'operation_mode': 'standard', + 'operation_mode': 'heat_pump', 'supported_features': , 'target_temp_high': 57, 'target_temp_low': 40, @@ -216,6 +216,6 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'standard', + 'state': 'heat_pump', }) # --- diff --git a/tests/components/smartthings/test_water_heater.py b/tests/components/smartthings/test_water_heater.py index a12280e5c92..30c85539d3a 100644 --- a/tests/components/smartthings/test_water_heater.py +++ b/tests/components/smartthings/test_water_heater.py @@ -20,6 +20,9 @@ from homeassistant.components.water_heater import ( SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, STATE_ECO, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, WaterHeaterEntityFeature, ) from homeassistant.const import ( @@ -66,9 +69,9 @@ async def test_all_entities( ("operation_mode", "argument"), [ (STATE_ECO, "eco"), - ("standard", "std"), - ("force", "force"), - ("power", "power"), + (STATE_HEAT_PUMP, "std"), + (STATE_HIGH_DEMAND, "force"), + (STATE_PERFORMANCE, "power"), ], ) async def test_set_operation_mode( @@ -299,9 +302,9 @@ async def test_operation_list_update( ] == [ STATE_OFF, STATE_ECO, - "standard", - "power", - "force", + STATE_HEAT_PUMP, + STATE_PERFORMANCE, + STATE_HIGH_DEMAND, ] await trigger_update( @@ -318,8 +321,8 @@ async def test_operation_list_update( ] == [ STATE_OFF, STATE_ECO, - "force", - "power", + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, ] @@ -332,7 +335,7 @@ async def test_current_operation_update( """Test state update.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP await trigger_update( hass, @@ -356,7 +359,7 @@ async def test_switch_update( await setup_integration(hass, mock_config_entry) state = hass.states.get("water_heater.warmepumpe") - assert state.state == "standard" + assert state.state == STATE_HEAT_PUMP assert ( state.attributes[ATTR_SUPPORTED_FEATURES] == WaterHeaterEntityFeature.ON_OFF @@ -516,7 +519,7 @@ async def test_availability( """Test availability.""" await setup_integration(hass, mock_config_entry) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP await trigger_health_update( hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.OFFLINE @@ -528,7 +531,7 @@ async def test_availability( hass, devices, "3810e5ad-5351-d9f9-12ff-000001200000", HealthStatus.ONLINE ) - assert hass.states.get("water_heater.warmepumpe").state == "standard" + assert hass.states.get("water_heater.warmepumpe").state == STATE_HEAT_PUMP @pytest.mark.parametrize("device_fixture", ["da_sac_ehs_000002_sub"]) From fcd71931e75907740743fa532c744e9dec2dcc59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 10 Jun 2025 17:04:22 +0100 Subject: [PATCH 1127/1175] Update wording deprecated system package integration repair (#146450) Co-authored-by: Martin Hjelmare --- homeassistant/components/homeassistant/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 123e625d0fc..93b4105c702 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -20,11 +20,11 @@ }, "deprecated_system_packages_config_flow_integration": { "title": "The {integration_title} integration is being removed", - "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue." + "description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries." }, "deprecated_system_packages_yaml_integration": { "title": "The {integration_title} integration is being removed", - "description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant." }, "historic_currency": { "title": "The configured currency is no longer in use", From 1040646610732cc38a4bc3d87fd9eecbc4aec6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luca=20Schr=C3=B6der?= Date: Tue, 10 Jun 2025 16:20:35 +0200 Subject: [PATCH 1128/1175] Update caldav to 1.6.0 (#146456) Fixes #140798 --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 5c1334c8029..d0e0bd0b1d0 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/caldav", "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"], - "requirements": ["caldav==1.3.9", "icalendar==6.1.0"] + "requirements": ["caldav==1.6.0", "icalendar==6.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd84a358fd4..a459bafdbf3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ buienradar==1.0.6 cached-ipaddress==0.10.0 # homeassistant.components.caldav -caldav==1.3.9 +caldav==1.6.0 # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0c4252f456..f0b1e22519b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ buienradar==1.0.6 cached-ipaddress==0.10.0 # homeassistant.components.caldav -caldav==1.3.9 +caldav==1.6.0 # homeassistant.components.coinbase coinbase-advanced-py==1.2.2 From 1d91ca5716854b2e28dfc46e9a138fb792be8627 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 17:37:21 +0200 Subject: [PATCH 1129/1175] Bump pySmartThings to 3.2.4 (#146459) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 180d4eebed1..481048c3bdb 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -30,5 +30,5 @@ "iot_class": "cloud_push", "loggers": ["pysmartthings"], "quality_scale": "bronze", - "requirements": ["pysmartthings==3.2.3"] + "requirements": ["pysmartthings==3.2.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index a459bafdbf3..67b7173fe98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2341,7 +2341,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.3 +pysmartthings==3.2.4 # homeassistant.components.smarty pysmarty2==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b1e22519b..42d106778b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1941,7 +1941,7 @@ pysmappee==0.2.29 pysmarlaapi==0.8.2 # homeassistant.components.smartthings -pysmartthings==3.2.3 +pysmartthings==3.2.4 # homeassistant.components.smarty pysmarty2==0.10.2 From 18e1a26da1987c57c56d4b96398cb43761562970 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 17:31:30 +0200 Subject: [PATCH 1130/1175] Catch exception before retrying in AirGradient (#146460) --- .../components/airgradient/coordinator.py | 13 ++++++++++--- tests/components/airgradient/test_init.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index 7484c7e85a9..9ee103b3a90 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]): async def _async_setup(self) -> None: """Set up the coordinator.""" - self._current_version = ( - await self.client.get_current_measures() - ).firmware_version + try: + self._current_version = ( + await self.client.get_current_measures() + ).firmware_version + except AirGradientError as error: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_error", + translation_placeholders={"error": str(error)}, + ) from error async def _async_update_data(self) -> AirGradientData: try: diff --git a/tests/components/airgradient/test_init.py b/tests/components/airgradient/test_init.py index a253cb2888a..5732cd526f6 100644 --- a/tests/components/airgradient/test_init.py +++ b/tests/components/airgradient/test_init.py @@ -3,10 +3,12 @@ from datetime import timedelta from unittest.mock import AsyncMock +from airgradient import AirGradientError from freezegun.api import FrozenDateTimeFactory from syrupy.assertion import SnapshotAssertion from homeassistant.components.airgradient.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -54,3 +56,16 @@ async def test_new_firmware_version( ) assert device_entry is not None assert device_entry.sw_version == "3.1.2" + + +async def test_setup_retry( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test retrying setup.""" + mock_airgradient_client.get_current_measures.side_effect = AirGradientError() + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY From 49646210144aba77abc71f5c86c0d4707a4c8ad1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 10 Jun 2025 19:28:48 +0200 Subject: [PATCH 1131/1175] Fix incorrect categories handling in holiday (#146470) --- homeassistant/components/holiday/calendar.py | 16 ++-- tests/components/holiday/test_calendar.py | 80 +++++++++++++++++++- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/holiday/calendar.py b/homeassistant/components/holiday/calendar.py index 1c01319129b..c5b67b7d555 100644 --- a/homeassistant/components/holiday/calendar.py +++ b/homeassistant/components/holiday/calendar.py @@ -25,17 +25,12 @@ def _get_obj_holidays_and_language( selected_categories: list[str] | None, ) -> tuple[HolidayBase, str]: """Get the object for the requested country and year.""" - if selected_categories is None: - categories = [PUBLIC] - else: - categories = [PUBLIC, *selected_categories] - obj_holidays = country_holidays( country, subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=language, - categories=categories, + categories=selected_categories, ) if language == "en": for lang in obj_holidays.supported_languages: @@ -45,7 +40,7 @@ def _get_obj_holidays_and_language( subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=lang, - categories=categories, + categories=selected_categories, ) language = lang break @@ -59,7 +54,7 @@ def _get_obj_holidays_and_language( subdiv=province, years={dt_util.now().year, dt_util.now().year + 1}, language=default_language, - categories=categories, + categories=selected_categories, ) language = default_language @@ -77,6 +72,11 @@ async def async_setup_entry( categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES) language = hass.config.language + if categories is None: + categories = [PUBLIC] + else: + categories = [PUBLIC, *categories] + obj_holidays, language = await hass.async_add_executor_job( _get_obj_holidays_and_language, country, province, language, categories ) diff --git a/tests/components/holiday/test_calendar.py b/tests/components/holiday/test_calendar.py index 6733d38442b..463f8645647 100644 --- a/tests/components/holiday/test_calendar.py +++ b/tests/components/holiday/test_calendar.py @@ -3,13 +3,18 @@ from datetime import datetime, timedelta from freezegun.api import FrozenDateTimeFactory +from holidays import CATHOLIC import pytest from homeassistant.components.calendar import ( DOMAIN as CALENDAR_DOMAIN, SERVICE_GET_EVENTS, ) -from homeassistant.components.holiday.const import CONF_PROVINCE, DOMAIN +from homeassistant.components.holiday.const import ( + CONF_CATEGORIES, + CONF_PROVINCE, + DOMAIN, +) from homeassistant.const import CONF_COUNTRY from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -353,3 +358,76 @@ async def test_language_not_exist( ] } } + + +async def test_categories( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, +) -> None: + """Test if there is no next event.""" + await hass.config.async_set_time_zone("Europe/Berlin") + zone = await dt_util.async_get_time_zone("Europe/Berlin") + freezer.move_to(datetime(2025, 8, 14, 12, tzinfo=zone)) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_COUNTRY: "DE", + CONF_PROVINCE: "BY", + }, + options={ + CONF_CATEGORIES: [CATHOLIC], + }, + title="Germany", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.germany", + "end_date_time": dt_util.now() + timedelta(days=2), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.germany": { + "events": [ + { + "start": "2025-08-15", + "end": "2025-08-16", + "summary": "Assumption Day", + "location": "Germany", + } + ] + } + } + + freezer.move_to(datetime(2025, 12, 23, 12, tzinfo=zone)) + response = await hass.services.async_call( + CALENDAR_DOMAIN, + SERVICE_GET_EVENTS, + { + "entity_id": "calendar.germany", + "end_date_time": dt_util.now() + timedelta(days=2), + }, + blocking=True, + return_response=True, + ) + assert response == { + "calendar.germany": { + "events": [ + { + "start": "2025-12-25", + "end": "2025-12-26", + "summary": "Christmas Day", + "location": "Germany", + } + ] + } + } From 39962a3f48d4d2b5b3f852355044a5b9908b86a5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 10 Jun 2025 20:18:19 +0300 Subject: [PATCH 1132/1175] Avoid closing shared aiohttp session in Vodafone Station (#146471) --- .../components/vodafone_station/__init__.py | 1 - .../vodafone_station/config_flow.py | 1 - .../vodafone_station/coordinator.py | 47 +++++++++---------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/vodafone_station/__init__.py b/homeassistant/components/vodafone_station/__init__.py index 5efc33ca882..17b0fe6e501 100644 --- a/homeassistant/components/vodafone_station/__init__.py +++ b/homeassistant/components/vodafone_station/__init__.py @@ -37,7 +37,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: VodafoneConfigEntry) -> if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): coordinator = entry.runtime_data await coordinator.api.logout() - await coordinator.api.close() return unload_ok diff --git a/homeassistant/components/vodafone_station/config_flow.py b/homeassistant/components/vodafone_station/config_flow.py index b69078b8ce6..c330a93a1a8 100644 --- a/homeassistant/components/vodafone_station/config_flow.py +++ b/homeassistant/components/vodafone_station/config_flow.py @@ -48,7 +48,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, await api.login() finally: await api.logout() - await api.close() return {"title": data[CONF_HOST]} diff --git a/homeassistant/components/vodafone_station/coordinator.py b/homeassistant/components/vodafone_station/coordinator.py index 846d4b042c0..57d39151160 100644 --- a/homeassistant/components/vodafone_station/coordinator.py +++ b/homeassistant/components/vodafone_station/coordinator.py @@ -117,32 +117,29 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]): async def _async_update_data(self) -> UpdateCoordinatorDataType: """Update router data.""" _LOGGER.debug("Polling Vodafone Station host: %s", self._host) + try: - try: - await self.api.login() - raw_data_devices = await self.api.get_devices_data() - data_sensors = await self.api.get_sensor_data() - await self.api.logout() - except exceptions.CannotAuthenticate as err: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="cannot_authenticate", - translation_placeholders={"error": repr(err)}, - ) from err - except ( - exceptions.CannotConnect, - exceptions.AlreadyLogged, - exceptions.GenericLoginError, - JSONDecodeError, - ) as err: - raise UpdateFailed( - translation_domain=DOMAIN, - translation_key="update_failed", - translation_placeholders={"error": repr(err)}, - ) from err - except (ConfigEntryAuthFailed, UpdateFailed): - await self.api.close() - raise + await self.api.login() + raw_data_devices = await self.api.get_devices_data() + data_sensors = await self.api.get_sensor_data() + await self.api.logout() + except exceptions.CannotAuthenticate as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="cannot_authenticate", + translation_placeholders={"error": repr(err)}, + ) from err + except ( + exceptions.CannotConnect, + exceptions.AlreadyLogged, + exceptions.GenericLoginError, + JSONDecodeError, + ) as err: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_failed", + translation_placeholders={"error": repr(err)}, + ) from err utc_point_in_time = dt_util.utcnow() data_devices = { From bf8ef0a767de8ee0696eb1ad18d4dd0b93a3eda8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 10 Jun 2025 20:28:37 +0300 Subject: [PATCH 1133/1175] Fix EntityCategory for binary_sensor platform in Amazon Devices (#146472) * Fix EntityCategory for binary_sensor platform in Amazon Devices * update snapshots --- homeassistant/components/amazon_devices/binary_sensor.py | 3 +++ .../amazon_devices/snapshots/test_binary_sensor.ambr | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/amazon_devices/binary_sensor.py index 2e41983dda4..ab1fadc7548 100644 --- a/homeassistant/components/amazon_devices/binary_sensor.py +++ b/homeassistant/components/amazon_devices/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -34,10 +35,12 @@ BINARY_SENSORS: Final = ( AmazonBinarySensorEntityDescription( key="online", device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda _device: _device.online, ), AmazonBinarySensorEntityDescription( key="bluetooth", + entity_category=EntityCategory.DIAGNOSTIC, translation_key="bluetooth", is_on_fn=lambda _device: _device.bluetooth_state, ), diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr index 0d3a5252a73..e914541d19c 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr @@ -11,7 +11,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.echo_test_bluetooth', 'has_entity_name': True, 'hidden_by': None, @@ -59,7 +59,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'binary_sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'binary_sensor.echo_test_connectivity', 'has_entity_name': True, 'hidden_by': None, From 8949a595fe026cd54321b499df982e966ea84248 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Jun 2025 17:45:26 +0000 Subject: [PATCH 1134/1175] Bump version to 2025.6.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 25d722ea685..b07549d387f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 52910c7f319..58ad46b63e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b5" +version = "2025.6.0b6" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 35580c0849e09ba874c2e792d3feb58e7632b33e Mon Sep 17 00:00:00 2001 From: hahn-th <15319212+hahn-th@users.noreply.github.com> Date: Tue, 10 Jun 2025 20:44:06 +0200 Subject: [PATCH 1135/1175] Bump homematicip to 2.0.4 (#144096) * Bump to 2.0.2 with all necessary changes * bump to prerelease * add addiional tests * Bump to homematicip 2.0.3 * do not delete device * Setup BRAND_SWITCH_MEASURING as light * bump to 2.0.4 * refactor test_remove_obsolete_entities * move test * use const from homematicip lib --- .../components/homematicip_cloud/hap.py | 16 +++++++++++++++ .../components/homematicip_cloud/light.py | 11 ++++++---- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/sensor.py | 13 ++---------- .../components/homematicip_cloud/switch.py | 15 ++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homematicip_cloud/test_hap.py | 20 +++++++++++++++++++ 8 files changed, 54 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 86630c2896c..f3681a89110 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -112,6 +112,7 @@ class HomematicipHAP: self.config_entry = config_entry self._ws_close_requested = False + self._ws_connection_closed = asyncio.Event() self._retry_task: asyncio.Task | None = None self._tries = 0 self._accesspoint_connected = True @@ -218,6 +219,8 @@ class HomematicipHAP: try: await self.home.get_current_state_async() hmip_events = self.home.enable_events() + self.home.set_on_connected_handler(self.ws_connected_handler) + self.home.set_on_disconnected_handler(self.ws_disconnected_handler) tries = 0 await hmip_events except HmipConnectionError: @@ -267,6 +270,18 @@ class HomematicipHAP: "Reset connection to access point id %s", self.config_entry.unique_id ) + async def ws_connected_handler(self) -> None: + """Handle websocket connected.""" + _LOGGER.debug("WebSocket connection to HomematicIP established") + if self._ws_connection_closed.is_set(): + await self.get_state() + self._ws_connection_closed.clear() + + async def ws_disconnected_handler(self) -> None: + """Handle websocket disconnection.""" + _LOGGER.warning("WebSocket connection to HomematicIP closed") + self._ws_connection_closed.set() + async def get_hap( self, hass: HomeAssistant, @@ -290,6 +305,7 @@ class HomematicipHAP: raise HmipcConnectionError from err home.on_update(self.async_update) home.on_create(self.async_create_entity) + hass.loop.create_task(self.async_connect()) return home diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 855f5851d73..d5175e6e647 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -4,16 +4,16 @@ from __future__ import annotations from typing import Any -from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState +from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel from homematicip.device import ( BrandDimmer, - BrandSwitchMeasuring, BrandSwitchNotificationLight, Dimmer, DinRailDimmer3, FullFlushDimmer, PluggableDimmer, + SwitchMeasuring, WiredDimmer3, ) from packaging.version import Version @@ -44,9 +44,12 @@ async def async_setup_entry( hap = config_entry.runtime_data entities: list[HomematicipGenericEntity] = [] for device in hap.home.devices: - if isinstance(device, BrandSwitchMeasuring): + if ( + isinstance(device, SwitchMeasuring) + and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING + ): entities.append(HomematicipLightMeasuring(hap, device)) - elif isinstance(device, BrandSwitchNotificationLight): + if isinstance(device, BrandSwitchNotificationLight): device_version = Version(device.firmwareVersion) entities.append(HomematicipLight(hap, device)) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 15bc24c110f..fc4a1cb831f 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "iot_class": "cloud_push", "loggers": ["homematicip"], - "requirements": ["homematicip==2.0.1.1"] + "requirements": ["homematicip==2.0.4"] } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 4f43e6d6ca7..13f3694de7a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import ( FunctionalChannel, ) from homematicip.device import ( - BrandSwitchMeasuring, EnergySensorsInterface, FloorTerminalBlock6, FloorTerminalBlock10, FloorTerminalBlock12, - FullFlushSwitchMeasuring, HeatingThermostat, HeatingThermostatCompact, HeatingThermostatEvo, @@ -26,9 +24,9 @@ from homematicip.device import ( MotionDetectorOutdoor, MotionDetectorPushButton, PassageDetector, - PlugableSwitchMeasuring, PresenceDetectorIndoor, RoomControlDeviceAnalog, + SwitchMeasuring, TemperatureDifferenceSensor2, TemperatureHumiditySensorDisplay, TemperatureHumiditySensorOutdoor, @@ -143,14 +141,7 @@ async def async_setup_entry( ), ): entities.append(HomematicipIlluminanceSensor(hap, device)) - if isinstance( - device, - ( - PlugableSwitchMeasuring, - BrandSwitchMeasuring, - FullFlushSwitchMeasuring, - ), - ): + if isinstance(device, SwitchMeasuring): entities.append(HomematicipPowerSensor(hap, device)) entities.append(HomematicipEnergySensor(hap, device)) if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 4927d9a32df..66a40229c7e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -4,20 +4,19 @@ from __future__ import annotations from typing import Any +from homematicip.base.enums import DeviceType from homematicip.device import ( BrandSwitch2, - BrandSwitchMeasuring, DinRailSwitch, DinRailSwitch4, FullFlushInputSwitch, - FullFlushSwitchMeasuring, HeatingSwitch2, MultiIOBox, OpenCollector8Module, PlugableSwitch, - PlugableSwitchMeasuring, PrintedCircuitBoardSwitch2, PrintedCircuitBoardSwitchBattery, + SwitchMeasuring, WiredSwitch8, ) from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup @@ -43,12 +42,10 @@ async def async_setup_entry( if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup)) ] for device in hap.home.devices: - if isinstance(device, BrandSwitchMeasuring): - # BrandSwitchMeasuring inherits PlugableSwitchMeasuring - # This entity is implemented in the light platform and will - # not be added in the switch platform - pass - elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)): + if ( + isinstance(device, SwitchMeasuring) + and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING + ): entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance(device, WiredSwitch8): entities.extend( diff --git a/requirements_all.txt b/requirements_all.txt index 67b7173fe98..98b43cb4635 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,7 +1170,7 @@ home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud -homematicip==2.0.1.1 +homematicip==2.0.4 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42d106778b0..23ace366641 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1016,7 +1016,7 @@ home-assistant-frontend==20250531.0 home-assistant-intents==2025.5.28 # homeassistant.components.homematicip_cloud -homematicip==2.0.1.1 +homematicip==2.0.4 # homeassistant.components.remember_the_milk httplib2==0.20.4 diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 13aaa4d83ba..94d6f9d5dd6 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -231,3 +231,23 @@ async def test_auth_create_exception(hass: HomeAssistant, simple_mock_auth) -> N ), ): assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN) + + +async def test_get_state_after_disconnect( + hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home +) -> None: + """Test get state after disconnect.""" + hass.config.components.add(DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + with patch.object(hap, "get_state") as mock_get_state: + assert not hap._ws_connection_closed.is_set() + + await hap.ws_connected_handler() + mock_get_state.assert_not_called() + + await hap.ws_disconnected_handler() + assert hap._ws_connection_closed.is_set() + await hap.ws_connected_handler() + mock_get_state.assert_called_once() From 63e49c5d3c70bab6e98a3bb444857bd056f39c08 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 10:31:08 +0200 Subject: [PATCH 1136/1175] Explain Nest setup (#146217) --- homeassistant/components/nest/config_flow.py | 8 -------- homeassistant/components/nest/strings.json | 3 +++ tests/components/nest/test_config_flow.py | 8 ++++++++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 1513a039407..0b249db7a4b 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -31,7 +31,6 @@ from homeassistant.helpers.selector import ( SelectSelectorConfig, SelectSelectorMode, ) -from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.util import get_random_string from . import api @@ -441,10 +440,3 @@ class NestFlowHandler( if self._structure_config_title: title = self._structure_config_title return self.async_create_entry(title=title, data=self._data) - - async def async_step_dhcp( - self, discovery_info: DhcpServiceInfo - ) -> ConfigFlowResult: - """Handle a flow initialized by discovery.""" - await self._async_handle_discovery_without_unique_id() - return await self.async_step_user() diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 4a8689ff04c..5146d04af0b 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -47,6 +47,9 @@ "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The Nest integration needs to re-authenticate your account" + }, + "oauth_discovery": { + "description": "Home Assistant has found a Google Nest device on your network. Be aware that the set up of Google Nest is more complicated than most other integrations and requires setting up a Nest Device Access project which **requires paying Google a US $5 fee**. Press **Submit** to continue setting up Google Nest." } }, "error": { diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 3f369f3e127..67364aff412 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -995,6 +995,10 @@ async def test_dhcp_discovery( ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "oauth_discovery" + + result = await oauth.async_configure(result, {}) + assert result["type"] is FlowResultType.FORM assert result["step_id"] == "create_cloud_project" result = await oauth.async_configure(result, {}) @@ -1033,6 +1037,10 @@ async def test_dhcp_discovery_with_creds( ) await hass.async_block_till_done() assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "oauth_discovery" + + result = await oauth.async_configure(result, {}) + 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}) From 4147211f948195fb78fbd3d804a70839a9a3b42c Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 11 Jun 2025 05:58:07 -0400 Subject: [PATCH 1137/1175] Add color_temp_kelvin to set_temperature action variables (#146448) --- homeassistant/components/template/light.py | 4 +++- tests/components/template/test_light.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 9fc935bf0ee..c852ee1808d 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -524,8 +524,10 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): ATTR_COLOR_TEMP_KELVIN in kwargs and (script := CONF_TEMPERATURE_ACTION) in self._action_scripts ): + kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN] + common_params[ATTR_COLOR_TEMP_KELVIN] = kelvin common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired( - kwargs[ATTR_COLOR_TEMP_KELVIN] + kelvin ) return (script, common_params) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index f240c2412e0..eaa1708aea7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -79,6 +79,7 @@ OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG = { "action": "set_temperature", "caller": "{{ this.entity_id }}", "color_temp": "{{color_temp}}", + "color_temp_kelvin": "{{color_temp_kelvin}}", }, }, } @@ -1535,6 +1536,7 @@ async def test_temperature_action_no_template( assert calls[-1].data["action"] == "set_temperature" assert calls[-1].data["caller"] == "light.test_template_light" assert calls[-1].data["color_temp"] == 345 + assert calls[-1].data["color_temp_kelvin"] == 2898 state = hass.states.get("light.test_template_light") assert state is not None From 43e16bb913e51927e869290becf06ae0219a79d7 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 11:35:14 +0200 Subject: [PATCH 1138/1175] Split deprecated system issue in 2 places (#146453) --- homeassistant/components/hassio/__init__.py | 67 +++- .../components/homeassistant/__init__.py | 95 ++---- tests/components/hassio/test_init.py | 290 +++++++++++++++++- tests/components/homeassistant/test_init.py | 80 +---- 4 files changed, 391 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eeeedff00bb..041877e3944 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -37,6 +37,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, discovery_flow, + issue_registry as ir, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.deprecation import ( @@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import ( get_supervisor_ip as _get_supervisor_ip, is_hassio as _is_hassio, ) +from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service_info.hassio import ( HassioServiceInfo as _HassioServiceInfo, ) +from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task @@ -109,7 +112,7 @@ from .coordinator import ( get_core_info, # noqa: F401 get_core_stats, # noqa: F401 get_host_info, # noqa: F401 - get_info, # noqa: F401 + get_info, get_issues_info, # noqa: F401 get_os_info, get_supervisor_info, # noqa: F401 @@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial" VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$")) +DEPRECATION_URL = ( + "https://www.home-assistant.io/blog/2025/05/22/" + "deprecating-core-and-supervised-installation-methods-and-32-bit-systems/" +) + def valid_addon(value: Any) -> str: """Validate value is a valid addon slug.""" @@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[ADDONS_COORDINATOR] = coordinator + system_info = await async_get_system_info(hass) + + def deprecated_setup_issue() -> None: + os_info = get_os_info(hass) + info = get_info(hass) + if os_info is None or info is None: + return + is_haos = info.get("hassos") is not None + arch = system_info["arch"] + board = os_info.get("board") + supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"} + if is_haos and arch == "armv7" and supported_board: + issue_id = "deprecated_os_" + if board in {"rpi3", "rpi4"}: + issue_id += "aarch64" + elif board in {"tinker", "odroid-xu4", "rpi2"}: + issue_id += "armv7" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_guide": "https://www.home-assistant.io/installation/", + }, + ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board): + deprecated_architecture = True + if not is_haos or deprecated_architecture: + issue_id = "deprecated" + if not is_haos: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + "homeassistant", + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": "OS" if is_haos else "Supervised", + "arch": arch, + }, + ) + listener() + + listener = coordinator.async_add_listener(deprecated_setup_issue) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 5f012c6a054..1433358b568 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,7 +4,7 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -38,7 +38,6 @@ from homeassistant.helpers import ( restore_state, ) from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service import ( async_extract_config_entry_ids, @@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: info = await async_get_system_info(hass) installation_type = info["installation_type"][15:] - deprecated_method = installation_type in { - "Core", - "Supervised", - } - arch = info["arch"] - if arch == "armv7": - if installation_type == "OS": - # Local import to avoid circular dependencies - # We use the import helper because hassio - # may not be loaded yet and we don't want to - # do blocking I/O in the event loop to import it. - if TYPE_CHECKING: - # pylint: disable-next=import-outside-toplevel - from homeassistant.components import hassio - else: - hassio = await async_import_module( - hass, "homeassistant.components.hassio" - ) - os_info = hassio.get_os_info(hass) - assert os_info is not None - issue_id = "deprecated_os_" - board = os_info.get("board") - if board in {"rpi3", "rpi4"}: - issue_id += "aarch64" - elif board in {"tinker", "odroid-xu4", "rpi2"}: - issue_id += "armv7" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_guide": "https://www.home-assistant.io/installation/", - }, - ) - elif installation_type == "Container": + if installation_type in {"Core", "Container"}: + deprecated_method = installation_type == "Core" + arch = info["arch"] + if arch == "armv7" and installation_type == "Container": ir.async_create_issue( hass, DOMAIN, @@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: severity=IssueSeverity.WARNING, translation_key="deprecated_container_armv7", ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method): - deprecated_architecture = True - if deprecated_method or deprecated_architecture: - issue_id = "deprecated" - if deprecated_method: - issue_id += "_method" - if deprecated_architecture: - issue_id += "_architecture" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2025.12.0", - learn_more_url=DEPRECATION_URL, - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "installation_type": installation_type, - "arch": arch, - }, - ) + deprecated_architecture = False + if arch in {"i386", "armhf"} or ( + arch == "armv7" and installation_type != "Container" + ): + deprecated_architecture = True + if deprecated_method or deprecated_architecture: + issue_id = "deprecated" + if deprecated_method: + issue_id += "_method" + if deprecated_architecture: + issue_id += "_architecture" + ir.async_create_issue( + hass, + DOMAIN, + issue_id, + breaks_in_ha_version="2025.12.0", + learn_more_url=DEPRECATION_URL, + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key=issue_id, + translation_placeholders={ + "installation_type": installation_type, + "arch": arch, + }, + ) return True diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d34aed608fb..f74ed852a49 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, patch from aiohasupervisor import SupervisorError from aiohasupervisor.models import AddonsStats +from freezegun.api import FrozenDateTimeFactory import pytest from voluptuous import Invalid @@ -23,10 +24,13 @@ from homeassistant.components.hassio import ( is_hassio as deprecated_is_hassio, ) from homeassistant.components.hassio.config import STORAGE_KEY -from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY +from homeassistant.components.hassio.const import ( + HASSIO_UPDATE_INTERVAL, + REQUEST_REFRESH_DELAY, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.setup import async_setup_component @@ -1140,3 +1144,285 @@ def test_deprecated_constants( replacement, "2025.11", ) + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi3", "deprecated_os_aarch64"), + ("rpi4", "deprecated_os_aarch64"), + ("tinker", "deprecated_os_armv7"), + ("odroid-xu4", "deprecated_os_armv7"), + ("rpi2", "deprecated_os_armv7"), + ], +) +async def test_deprecated_installation_issue_aarch64( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test deprecated installation issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "armv7", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", issue_id) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_guide": "https://www.home-assistant.io/installation/", + } + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_method( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", "deprecated_architecture") + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == {"installation_type": "OS", "arch": arch} + + +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armhf", + "armv7", + ], +) +async def test_deprecated_installation_issue_32bit_supervised( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "rpi3-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": None} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + "homeassistant", "deprecated_method_architecture" + ) + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": "Supervised", + "arch": arch, + } + + +@pytest.mark.parametrize( + ("board", "issue_id"), + [ + ("rpi5", "deprecated_os_aarch64"), + ], +) +async def test_deprecated_installation_issue_supported_board( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + board: str, + issue_id: str, +) -> None: + """Test no deprecated installation issue for a supported board.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.hassio.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant OS", + "arch": "aarch64", + }, + ), + patch( + "homeassistant.components.hassio.get_os_info", return_value={"board": board} + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": True} + ), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 0 diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index fe5d2155f58..0010422cd28 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -640,13 +640,6 @@ async def test_reload_all( assert len(jinja) == 1 -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) @pytest.mark.parametrize( "arch", [ @@ -658,14 +651,13 @@ async def test_reload_all( async def test_deprecated_installation_issue_32bit_method( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": arch, }, ): @@ -679,18 +671,11 @@ async def test_deprecated_installation_issue_32bit_method( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Container", - "Home Assistant OS", - ], -) @pytest.mark.parametrize( "arch", [ @@ -701,14 +686,13 @@ async def test_deprecated_installation_issue_32bit_method( async def test_deprecated_installation_issue_32bit( hass: HomeAssistant, issue_registry: ir.IssueRegistry, - installation_type: str, arch: str, ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Container", "arch": arch, }, ): @@ -722,28 +706,19 @@ async def test_deprecated_installation_issue_32bit( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Container", "arch": arch, } -@pytest.mark.parametrize( - "installation_type", - [ - "Home Assistant Core", - "Home Assistant Supervised", - ], -) async def test_deprecated_installation_issue_method( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - installation_type: str, + hass: HomeAssistant, issue_registry: ir.IssueRegistry ) -> None: """Test deprecated installation issue.""" with patch( "homeassistant.components.homeassistant.async_get_system_info", return_value={ - "installation_type": installation_type, + "installation_type": "Home Assistant Core", "arch": "generic-x86-64", }, ): @@ -755,52 +730,11 @@ async def test_deprecated_installation_issue_method( assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { - "installation_type": installation_type[15:], + "installation_type": "Core", "arch": "generic-x86-64", } -@pytest.mark.parametrize( - ("board", "issue_id"), - [ - ("rpi3", "deprecated_os_aarch64"), - ("rpi4", "deprecated_os_aarch64"), - ("tinker", "deprecated_os_armv7"), - ("odroid-xu4", "deprecated_os_armv7"), - ("rpi2", "deprecated_os_armv7"), - ], -) -async def test_deprecated_installation_issue_aarch64( - hass: HomeAssistant, - issue_registry: ir.IssueRegistry, - board: str, - issue_id: str, -) -> None: - """Test deprecated installation issue.""" - with ( - patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "armv7", - }, - ), - patch( - "homeassistant.components.hassio.get_os_info", return_value={"board": board} - ), - ): - assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, issue_id) - assert issue.domain == HOMEASSISTANT_DOMAIN - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_placeholders == { - "installation_guide": "https://www.home-assistant.io/installation/", - } - - async def test_deprecated_installation_issue_armv7_container( hass: HomeAssistant, issue_registry: ir.IssueRegistry, From f1df6dcda57b6a7b15003cf9742caffbab1676f6 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 10 Jun 2025 22:25:47 +0300 Subject: [PATCH 1139/1175] Fix Jewish calendar not updating (#146465) --- .../components/jewish_calendar/sensor.py | 9 ++---- .../components/jewish_calendar/test_sensor.py | 31 ++++++++++++++++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 230adef9894..cb38a3797eb 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -225,7 +225,7 @@ async def async_setup_entry( JewishCalendarTimeSensor(config_entry, description) for description in TIME_SENSORS ) - async_add_entities(sensors) + async_add_entities(sensors, update_before_add=True) class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): @@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC - async def async_added_to_hass(self) -> None: - """Call when entity is added to hass.""" - await super().async_added_to_hass() - await self.async_update_data() - - async def async_update_data(self) -> None: + async def async_update(self) -> None: """Update the state of the sensor.""" now = dt_util.now() _LOGGER.debug("Now: %s Location: %r", now, self.data.location) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 0cc1e60efc8..38a3dd12206 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime as dt from typing import Any +from freezegun.api import FrozenDateTimeFactory from hdate.holidays import HolidayDatabase from hdate.parasha import Parasha import pytest @@ -14,7 +15,7 @@ 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 +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.parametrize("language", ["en", "he"]) @@ -542,6 +543,34 @@ async def test_dafyomi_sensor(hass: HomeAssistant, results: str) -> None: assert hass.states.get("sensor.jewish_calendar_daf_yomi").state == results +@pytest.mark.parametrize( + ("test_time", "results"), + [ + ( + dt(2025, 6, 10, 17), + { + "initial_state": "14 Sivan 5785", + "move_to": dt(2025, 6, 10, 23, 0), + "new_state": "15 Sivan 5785", + }, + ), + ], + indirect=True, +) +@pytest.mark.usefixtures("setup_at_time") +async def test_sensor_does_not_update_on_time_change( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, results: dict[str, Any] +) -> None: + """Test that the Jewish calendar sensor does not update after time advances (regression test for update bug).""" + sensor_id = "sensor.jewish_calendar_date" + assert hass.states.get(sensor_id).state == results["initial_state"] + + freezer.move_to(results["move_to"]) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass.states.get(sensor_id).state == results["new_state"] + + async def test_no_discovery_info( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 69ba2aab11308c66e8a3cf1a895d64eb62be199a Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 10 Jun 2025 20:55:00 +0200 Subject: [PATCH 1140/1175] Remove DHCP discovery from Amazon Devices (#146476) --- .../components/amazon_devices/manifest.json | 110 ----- .../amazon_devices/quality_scale.yaml | 4 +- homeassistant/generated/dhcp.py | 432 ------------------ .../amazon_devices/test_config_flow.py | 64 +-- 4 files changed, 4 insertions(+), 606 deletions(-) diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/amazon_devices/manifest.json index 37a56486a08..f63893c1598 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/amazon_devices/manifest.json @@ -3,116 +3,6 @@ "name": "Amazon Devices", "codeowners": ["@chemelli74"], "config_flow": true, - "dhcp": [ - { "macaddress": "007147*" }, - { "macaddress": "00FC8B*" }, - { "macaddress": "0812A5*" }, - { "macaddress": "086AE5*" }, - { "macaddress": "08849D*" }, - { "macaddress": "089115*" }, - { "macaddress": "08A6BC*" }, - { "macaddress": "08C224*" }, - { "macaddress": "0CDC91*" }, - { "macaddress": "0CEE99*" }, - { "macaddress": "1009F9*" }, - { "macaddress": "109693*" }, - { "macaddress": "10BF67*" }, - { "macaddress": "10CE02*" }, - { "macaddress": "140AC5*" }, - { "macaddress": "149138*" }, - { "macaddress": "1848BE*" }, - { "macaddress": "1C12B0*" }, - { "macaddress": "1C4D66*" }, - { "macaddress": "1C93C4*" }, - { "macaddress": "1CFE2B*" }, - { "macaddress": "244CE3*" }, - { "macaddress": "24CE33*" }, - { "macaddress": "2873F6*" }, - { "macaddress": "2C71FF*" }, - { "macaddress": "34AFB3*" }, - { "macaddress": "34D270*" }, - { "macaddress": "38F73D*" }, - { "macaddress": "3C5CC4*" }, - { "macaddress": "3CE441*" }, - { "macaddress": "440049*" }, - { "macaddress": "40A2DB*" }, - { "macaddress": "40A9CF*" }, - { "macaddress": "40B4CD*" }, - { "macaddress": "443D54*" }, - { "macaddress": "44650D*" }, - { "macaddress": "485F2D*" }, - { "macaddress": "48785E*" }, - { "macaddress": "48B423*" }, - { "macaddress": "4C1744*" }, - { "macaddress": "4CEFC0*" }, - { "macaddress": "5007C3*" }, - { "macaddress": "50D45C*" }, - { "macaddress": "50DCE7*" }, - { "macaddress": "50F5DA*" }, - { "macaddress": "5C415A*" }, - { "macaddress": "6837E9*" }, - { "macaddress": "6854FD*" }, - { "macaddress": "689A87*" }, - { "macaddress": "68B691*" }, - { "macaddress": "68DBF5*" }, - { "macaddress": "68F63B*" }, - { "macaddress": "6C0C9A*" }, - { "macaddress": "6C5697*" }, - { "macaddress": "7458F3*" }, - { "macaddress": "74C246*" }, - { "macaddress": "74D637*" }, - { "macaddress": "74E20C*" }, - { "macaddress": "74ECB2*" }, - { "macaddress": "786C84*" }, - { "macaddress": "78A03F*" }, - { "macaddress": "7C6166*" }, - { "macaddress": "7C6305*" }, - { "macaddress": "7CD566*" }, - { "macaddress": "8871E5*" }, - { "macaddress": "901195*" }, - { "macaddress": "90235B*" }, - { "macaddress": "90A822*" }, - { "macaddress": "90F82E*" }, - { "macaddress": "943A91*" }, - { "macaddress": "98226E*" }, - { "macaddress": "98CCF3*" }, - { "macaddress": "9CC8E9*" }, - { "macaddress": "A002DC*" }, - { "macaddress": "A0D2B1*" }, - { "macaddress": "A40801*" }, - { "macaddress": "A8E621*" }, - { "macaddress": "AC416A*" }, - { "macaddress": "AC63BE*" }, - { "macaddress": "ACCCFC*" }, - { "macaddress": "B0739C*" }, - { "macaddress": "B0CFCB*" }, - { "macaddress": "B0F7C4*" }, - { "macaddress": "B85F98*" }, - { "macaddress": "C091B9*" }, - { "macaddress": "C095CF*" }, - { "macaddress": "C49500*" }, - { "macaddress": "C86C3D*" }, - { "macaddress": "CC9EA2*" }, - { "macaddress": "CCF735*" }, - { "macaddress": "DC54D7*" }, - { "macaddress": "D8BE65*" }, - { "macaddress": "D8FBD6*" }, - { "macaddress": "DC91BF*" }, - { "macaddress": "DCA0D0*" }, - { "macaddress": "E0F728*" }, - { "macaddress": "EC2BEB*" }, - { "macaddress": "EC8AC4*" }, - { "macaddress": "ECA138*" }, - { "macaddress": "F02F9E*" }, - { "macaddress": "F0272D*" }, - { "macaddress": "F0F0A4*" }, - { "macaddress": "F4032A*" }, - { "macaddress": "F854B8*" }, - { "macaddress": "FC492D*" }, - { "macaddress": "FC65DE*" }, - { "macaddress": "FCA183*" }, - { "macaddress": "FCE9D8*" } - ], "documentation": "https://www.home-assistant.io/integrations/amazon_devices", "integration_type": "hub", "iot_class": "cloud_polling", diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/amazon_devices/quality_scale.yaml index 23a7cd22a66..881a02bc6d3 100644 --- a/homeassistant/components/amazon_devices/quality_scale.yaml +++ b/homeassistant/components/amazon_devices/quality_scale.yaml @@ -45,7 +45,9 @@ rules: discovery-update-info: status: exempt comment: Network information not relevant - discovery: done + discovery: + status: exempt + comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 5285ab7a1db..349c69358ba 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -26,438 +26,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "airzone", "macaddress": "E84F25*", }, - { - "domain": "amazon_devices", - "macaddress": "007147*", - }, - { - "domain": "amazon_devices", - "macaddress": "00FC8B*", - }, - { - "domain": "amazon_devices", - "macaddress": "0812A5*", - }, - { - "domain": "amazon_devices", - "macaddress": "086AE5*", - }, - { - "domain": "amazon_devices", - "macaddress": "08849D*", - }, - { - "domain": "amazon_devices", - "macaddress": "089115*", - }, - { - "domain": "amazon_devices", - "macaddress": "08A6BC*", - }, - { - "domain": "amazon_devices", - "macaddress": "08C224*", - }, - { - "domain": "amazon_devices", - "macaddress": "0CDC91*", - }, - { - "domain": "amazon_devices", - "macaddress": "0CEE99*", - }, - { - "domain": "amazon_devices", - "macaddress": "1009F9*", - }, - { - "domain": "amazon_devices", - "macaddress": "109693*", - }, - { - "domain": "amazon_devices", - "macaddress": "10BF67*", - }, - { - "domain": "amazon_devices", - "macaddress": "10CE02*", - }, - { - "domain": "amazon_devices", - "macaddress": "140AC5*", - }, - { - "domain": "amazon_devices", - "macaddress": "149138*", - }, - { - "domain": "amazon_devices", - "macaddress": "1848BE*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C12B0*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C4D66*", - }, - { - "domain": "amazon_devices", - "macaddress": "1C93C4*", - }, - { - "domain": "amazon_devices", - "macaddress": "1CFE2B*", - }, - { - "domain": "amazon_devices", - "macaddress": "244CE3*", - }, - { - "domain": "amazon_devices", - "macaddress": "24CE33*", - }, - { - "domain": "amazon_devices", - "macaddress": "2873F6*", - }, - { - "domain": "amazon_devices", - "macaddress": "2C71FF*", - }, - { - "domain": "amazon_devices", - "macaddress": "34AFB3*", - }, - { - "domain": "amazon_devices", - "macaddress": "34D270*", - }, - { - "domain": "amazon_devices", - "macaddress": "38F73D*", - }, - { - "domain": "amazon_devices", - "macaddress": "3C5CC4*", - }, - { - "domain": "amazon_devices", - "macaddress": "3CE441*", - }, - { - "domain": "amazon_devices", - "macaddress": "440049*", - }, - { - "domain": "amazon_devices", - "macaddress": "40A2DB*", - }, - { - "domain": "amazon_devices", - "macaddress": "40A9CF*", - }, - { - "domain": "amazon_devices", - "macaddress": "40B4CD*", - }, - { - "domain": "amazon_devices", - "macaddress": "443D54*", - }, - { - "domain": "amazon_devices", - "macaddress": "44650D*", - }, - { - "domain": "amazon_devices", - "macaddress": "485F2D*", - }, - { - "domain": "amazon_devices", - "macaddress": "48785E*", - }, - { - "domain": "amazon_devices", - "macaddress": "48B423*", - }, - { - "domain": "amazon_devices", - "macaddress": "4C1744*", - }, - { - "domain": "amazon_devices", - "macaddress": "4CEFC0*", - }, - { - "domain": "amazon_devices", - "macaddress": "5007C3*", - }, - { - "domain": "amazon_devices", - "macaddress": "50D45C*", - }, - { - "domain": "amazon_devices", - "macaddress": "50DCE7*", - }, - { - "domain": "amazon_devices", - "macaddress": "50F5DA*", - }, - { - "domain": "amazon_devices", - "macaddress": "5C415A*", - }, - { - "domain": "amazon_devices", - "macaddress": "6837E9*", - }, - { - "domain": "amazon_devices", - "macaddress": "6854FD*", - }, - { - "domain": "amazon_devices", - "macaddress": "689A87*", - }, - { - "domain": "amazon_devices", - "macaddress": "68B691*", - }, - { - "domain": "amazon_devices", - "macaddress": "68DBF5*", - }, - { - "domain": "amazon_devices", - "macaddress": "68F63B*", - }, - { - "domain": "amazon_devices", - "macaddress": "6C0C9A*", - }, - { - "domain": "amazon_devices", - "macaddress": "6C5697*", - }, - { - "domain": "amazon_devices", - "macaddress": "7458F3*", - }, - { - "domain": "amazon_devices", - "macaddress": "74C246*", - }, - { - "domain": "amazon_devices", - "macaddress": "74D637*", - }, - { - "domain": "amazon_devices", - "macaddress": "74E20C*", - }, - { - "domain": "amazon_devices", - "macaddress": "74ECB2*", - }, - { - "domain": "amazon_devices", - "macaddress": "786C84*", - }, - { - "domain": "amazon_devices", - "macaddress": "78A03F*", - }, - { - "domain": "amazon_devices", - "macaddress": "7C6166*", - }, - { - "domain": "amazon_devices", - "macaddress": "7C6305*", - }, - { - "domain": "amazon_devices", - "macaddress": "7CD566*", - }, - { - "domain": "amazon_devices", - "macaddress": "8871E5*", - }, - { - "domain": "amazon_devices", - "macaddress": "901195*", - }, - { - "domain": "amazon_devices", - "macaddress": "90235B*", - }, - { - "domain": "amazon_devices", - "macaddress": "90A822*", - }, - { - "domain": "amazon_devices", - "macaddress": "90F82E*", - }, - { - "domain": "amazon_devices", - "macaddress": "943A91*", - }, - { - "domain": "amazon_devices", - "macaddress": "98226E*", - }, - { - "domain": "amazon_devices", - "macaddress": "98CCF3*", - }, - { - "domain": "amazon_devices", - "macaddress": "9CC8E9*", - }, - { - "domain": "amazon_devices", - "macaddress": "A002DC*", - }, - { - "domain": "amazon_devices", - "macaddress": "A0D2B1*", - }, - { - "domain": "amazon_devices", - "macaddress": "A40801*", - }, - { - "domain": "amazon_devices", - "macaddress": "A8E621*", - }, - { - "domain": "amazon_devices", - "macaddress": "AC416A*", - }, - { - "domain": "amazon_devices", - "macaddress": "AC63BE*", - }, - { - "domain": "amazon_devices", - "macaddress": "ACCCFC*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0739C*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0CFCB*", - }, - { - "domain": "amazon_devices", - "macaddress": "B0F7C4*", - }, - { - "domain": "amazon_devices", - "macaddress": "B85F98*", - }, - { - "domain": "amazon_devices", - "macaddress": "C091B9*", - }, - { - "domain": "amazon_devices", - "macaddress": "C095CF*", - }, - { - "domain": "amazon_devices", - "macaddress": "C49500*", - }, - { - "domain": "amazon_devices", - "macaddress": "C86C3D*", - }, - { - "domain": "amazon_devices", - "macaddress": "CC9EA2*", - }, - { - "domain": "amazon_devices", - "macaddress": "CCF735*", - }, - { - "domain": "amazon_devices", - "macaddress": "DC54D7*", - }, - { - "domain": "amazon_devices", - "macaddress": "D8BE65*", - }, - { - "domain": "amazon_devices", - "macaddress": "D8FBD6*", - }, - { - "domain": "amazon_devices", - "macaddress": "DC91BF*", - }, - { - "domain": "amazon_devices", - "macaddress": "DCA0D0*", - }, - { - "domain": "amazon_devices", - "macaddress": "E0F728*", - }, - { - "domain": "amazon_devices", - "macaddress": "EC2BEB*", - }, - { - "domain": "amazon_devices", - "macaddress": "EC8AC4*", - }, - { - "domain": "amazon_devices", - "macaddress": "ECA138*", - }, - { - "domain": "amazon_devices", - "macaddress": "F02F9E*", - }, - { - "domain": "amazon_devices", - "macaddress": "F0272D*", - }, - { - "domain": "amazon_devices", - "macaddress": "F0F0A4*", - }, - { - "domain": "amazon_devices", - "macaddress": "F4032A*", - }, - { - "domain": "amazon_devices", - "macaddress": "F854B8*", - }, - { - "domain": "amazon_devices", - "macaddress": "FC492D*", - }, - { - "domain": "amazon_devices", - "macaddress": "FC65DE*", - }, - { - "domain": "amazon_devices", - "macaddress": "FCA183*", - }, - { - "domain": "amazon_devices", - "macaddress": "FCE9D8*", - }, { "domain": "august", "hostname": "connect", diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/amazon_devices/test_config_flow.py index 41b65c33bd5..ce1ac44d102 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/amazon_devices/test_config_flow.py @@ -6,22 +6,15 @@ from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect import pytest from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import TEST_CODE, TEST_COUNTRY, TEST_PASSWORD, TEST_USERNAME from tests.common import MockConfigEntry -DHCP_DISCOVERY = DhcpServiceInfo( - ip="1.1.1.1", - hostname="", - macaddress="c095cfebf19f", -) - async def test_full_flow( hass: HomeAssistant, @@ -140,58 +133,3 @@ async def test_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_dhcp_flow( - hass: HomeAssistant, - mock_amazon_devices_client: AsyncMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test full DHCP flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_DHCP}, - data=DHCP_DISCOVERY, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_COUNTRY: TEST_COUNTRY, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - CONF_CODE: TEST_CODE, - }, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == TEST_USERNAME - assert result["data"] == { - CONF_COUNTRY: TEST_COUNTRY, - CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, - CONF_LOGIN_DATA: { - "customer_info": {"user_id": TEST_USERNAME}, - }, - } - assert result["result"].unique_id == TEST_USERNAME - - -async def test_dhcp_already_configured( - hass: HomeAssistant, - mock_amazon_devices_client: AsyncMock, - mock_setup_entry: AsyncMock, - mock_config_entry: MockConfigEntry, -) -> None: - """Test duplicate flow.""" - mock_config_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_DHCP}, - data=DHCP_DISCOVERY, - ) - - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" From 8fd52248b78e9be47ce910455829d29cd4a3a321 Mon Sep 17 00:00:00 2001 From: Felix Schneider Date: Wed, 11 Jun 2025 10:26:01 +0200 Subject: [PATCH 1141/1175] Bump `apsystems` to `2.7.0` (#146485) --- homeassistant/components/apsystems/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apsystems/manifest.json b/homeassistant/components/apsystems/manifest.json index eb1acb40d17..e86b4a8431e 100644 --- a/homeassistant/components/apsystems/manifest.json +++ b/homeassistant/components/apsystems/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["APsystemsEZ1"], - "requirements": ["apsystems-ez1==2.6.0"] + "requirements": ["apsystems-ez1==2.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98b43cb4635..1409a24ab25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -498,7 +498,7 @@ apprise==1.9.1 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.6.0 +apsystems-ez1==2.7.0 # homeassistant.components.aqualogic aqualogic==2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23ace366641..7a283276028 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -471,7 +471,7 @@ apprise==1.9.1 aprslib==0.7.2 # homeassistant.components.apsystems -apsystems-ez1==2.6.0 +apsystems-ez1==2.7.0 # homeassistant.components.aranet aranet4==2.5.1 From 7afc469306f6298a3d8ffc0e9847db69dc6be0b8 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 10 Jun 2025 18:16:18 -0500 Subject: [PATCH 1142/1175] Bump intents to 2025.6.10 (#146491) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 6078d73e99b..5221e89deee 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==2.2.3", "home-assistant-intents==2025.5.28"] + "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index af9b0472bb1..b5d1af412f9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250531.0 -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 Jinja2==3.1.6 diff --git a/pyproject.toml b/pyproject.toml index 58ad46b63e3..b096fcb8040 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ # onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs # to be setup in stage 0, but we don't want to also promote cloud with all its # dependencies to stage 0. - "home-assistant-intents==2025.5.28", + "home-assistant-intents==2025.6.10", "ifaddr==0.2.0", "Jinja2==3.1.6", "lru-dict==1.3.0", diff --git a/requirements.txt b/requirements.txt index bff95490470..e353adac9d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 httpx==0.28.1 home-assistant-bluetooth==1.13.1 -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1409a24ab25..967a6defd14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ holidays==0.74 home-assistant-frontend==20250531.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud homematicip==2.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a283276028..6632e45ed6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1013,7 +1013,7 @@ holidays==0.74 home-assistant-frontend==20250531.0 # homeassistant.components.conversation -home-assistant-intents==2025.5.28 +home-assistant-intents==2025.6.10 # homeassistant.components.homematicip_cloud homematicip==2.0.4 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 830bdc4445e..82150d031a4 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.7.1,source=/uv,target=/bin/uv \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ hassil==2.2.3 \ - home-assistant-intents==2025.5.28 \ + home-assistant-intents==2025.6.10 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ pyspeex-noise==1.0.2 From 671a33b31c88817bea2c21f05d24ddd702a403e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 11:19:44 +0200 Subject: [PATCH 1143/1175] Do not remove derivative config entry when input sensor is removed (#146506) * Do not remove derivative config entry when input sensor is removed * Add comments * Update homeassistant/helpers/helper_integration.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .../components/derivative/__init__.py | 5 ++++ .../components/switch_as_x/__init__.py | 6 ++++ homeassistant/helpers/device.py | 6 ++-- homeassistant/helpers/helper_integration.py | 14 +++++++-- tests/components/derivative/test_init.py | 8 ++--- tests/helpers/test_helper_integration.py | 30 ++++++++++++------- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 5eb499b0efd..6d539817875 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -29,6 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options={**entry.options, CONF_SOURCE: source_entity_id}, ) + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + entity_registry = er.async_get(hass) entry.async_on_unload( async_handle_source_entity_changes( @@ -42,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry.options[CONF_SOURCE] ), source_entity_id_or_uuid=entry.options[CONF_SOURCE], + source_entity_removed=source_entity_removed, ) ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 6e9e3a93b45..7d12ae4aec2 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -63,6 +63,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options={**entry.options, CONF_ENTITY_ID: source_entity_id}, ) + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # switch_as_x does not allow replacing the wrapped entity. + await hass.config_entries.async_remove(entry.entry_id) + entry.async_on_unload( async_handle_source_entity_changes( hass, @@ -73,6 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_add_to_device(hass, entry, entity_id), source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, ) ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py index a7d888900b1..f1404bb068b 100644 --- a/homeassistant/helpers/device.py +++ b/homeassistant/helpers/device.py @@ -62,7 +62,7 @@ def async_device_info_to_link_from_device_id( def async_remove_stale_devices_links_keep_entity_device( hass: HomeAssistant, entry_id: str, - source_entity_id_or_uuid: str, + source_entity_id_or_uuid: str | None, ) -> None: """Remove entry_id from all devices except that of source_entity_id_or_uuid. @@ -73,7 +73,9 @@ def async_remove_stale_devices_links_keep_entity_device( async_remove_stale_devices_links_keep_current_device( hass=hass, entry_id=entry_id, - current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid), + current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid) + if source_entity_id_or_uuid + else None, ) diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 4f39ef4c843..37aa246178e 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -2,7 +2,8 @@ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from typing import Any from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, valid_entity_id @@ -18,6 +19,7 @@ def async_handle_source_entity_changes( set_source_entity_id_or_uuid: Callable[[str], None], source_device_id: str | None, source_entity_id_or_uuid: str, + source_entity_removed: Callable[[], Coroutine[Any, Any, None]], ) -> CALLBACK_TYPE: """Handle changes to a helper entity's source entity. @@ -34,6 +36,14 @@ def async_handle_source_entity_changes( - Source entity removed from the device: The helper entity is updated to link to no device, and the helper config entry removed from the old device. Then the helper config entry is reloaded. + + :param get_helper_entity: A function which returns the helper entity's entity ID, + or None if the helper entity does not exist. + :param set_source_entity_id_or_uuid: A function which updates the source entity + ID or UUID, e.g., in the helper config entry options. + :param source_entity_removed: A function which is called when the source entity + is removed. This can be used to clean up any resources related to the source + entity or ask the user to select a new source entity. """ async def async_registry_updated( @@ -44,7 +54,7 @@ def async_handle_source_entity_changes( data = event.data if data["action"] == "remove": - await hass.config_entries.async_remove(helper_config_entry_id) + await source_entity_removed() if data["action"] != "update": return diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py index f75d5940da7..d237703eb2e 100644 --- a/tests/components/derivative/test_init.py +++ b/tests/components/derivative/test_init.py @@ -268,17 +268,17 @@ async def test_async_handle_source_entity_changes_source_entity_removed( ) await hass.async_block_till_done() await hass.async_block_till_done() - mock_unload_entry.assert_called_once() + mock_unload_entry.assert_not_called() # Check that the derivative config entry is removed from the device sensor_device = device_registry.async_get(sensor_device.id) assert derivative_config_entry.entry_id not in sensor_device.config_entries - # Check that the derivative config entry is removed - assert derivative_config_entry.entry_id not in hass.config_entries.async_entry_ids() + # Check that the derivative config entry is not removed + assert derivative_config_entry.entry_id in hass.config_entries.async_entry_ids() # Check we got the expected events - assert events == ["remove"] + assert events == ["update"] async def test_async_handle_source_entity_changes_source_entity_removed_from_device( diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index 25d490c27bb..12433894dc7 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -132,11 +132,17 @@ def async_unload_entry() -> AsyncMock: @pytest.fixture -def set_source_entity_id_or_uuid() -> AsyncMock: - """Fixture to mock async_unload_entry.""" +def set_source_entity_id_or_uuid() -> Mock: + """Fixture to mock set_source_entity_id_or_uuid.""" return Mock() +@pytest.fixture +def source_entity_removed() -> AsyncMock: + """Fixture to mock source_entity_removed.""" + return AsyncMock() + + @pytest.fixture def mock_helper_integration( hass: HomeAssistant, @@ -146,6 +152,7 @@ def mock_helper_integration( async_remove_entry: AsyncMock, async_unload_entry: AsyncMock, set_source_entity_id_or_uuid: Mock, + source_entity_removed: AsyncMock, ) -> None: """Mock the helper integration.""" @@ -164,6 +171,7 @@ def mock_helper_integration( set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=source_entity_entry.device_id, source_entity_id_or_uuid=helper_config_entry.options["source"], + source_entity_removed=source_entity_removed, ) return True @@ -206,6 +214,7 @@ async def test_async_handle_source_entity_changes_source_entity_removed( async_remove_entry: AsyncMock, async_unload_entry: AsyncMock, set_source_entity_id_or_uuid: Mock, + source_entity_removed: AsyncMock, ) -> None: """Test the helper config entry is removed when the source entity is removed.""" # Add the helper config entry to the source device @@ -238,20 +247,21 @@ async def test_async_handle_source_entity_changes_source_entity_removed( await hass.async_block_till_done() await hass.async_block_till_done() - # Check that the helper config entry is unloaded and removed - async_unload_entry.assert_called_once() - async_remove_entry.assert_called_once() + # Check that the source_entity_removed callback was called + source_entity_removed.assert_called_once() + async_unload_entry.assert_not_called() + async_remove_entry.assert_not_called() set_source_entity_id_or_uuid.assert_not_called() - # Check that the helper config entry is removed from the device + # Check that the helper config entry is not removed from the device source_device = device_registry.async_get(source_device.id) - assert helper_config_entry.entry_id not in source_device.config_entries + assert helper_config_entry.entry_id in source_device.config_entries - # Check that the helper config entry is removed - assert helper_config_entry.entry_id not in hass.config_entries.async_entry_ids() + # Check that the helper config entry is not removed + assert helper_config_entry.entry_id in hass.config_entries.async_entry_ids() # Check we got the expected events - assert events == ["remove"] + assert events == [] @pytest.mark.parametrize("use_entity_registry_id", [True, False]) From 6d1f621e550311eb915ba31dbd9f1037a7b9519f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 11 Jun 2025 09:49:38 +0100 Subject: [PATCH 1144/1175] Bump deebot-client to 13.3.0 (#146507) --- 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 12fd8e01215..8a7388da735 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.11", "deebot-client==13.2.1"] + "requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 967a6defd14..95a5f919add 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,7 +765,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.2.1 +deebot-client==13.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6632e45ed6e..1ed51e54164 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -665,7 +665,7 @@ debugpy==1.8.14 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==13.2.1 +deebot-client==13.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From c8b70cc0fb72ce52e60c01b60f2b252005f642b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Wed, 11 Jun 2025 11:55:28 +0200 Subject: [PATCH 1145/1175] Graceful handling of missing datapoint in myuplink (#146517) --- homeassistant/components/myuplink/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myuplink/sensor.py b/homeassistant/components/myuplink/sensor.py index 3b14cdd4630..0a3f7d2ebb6 100644 --- a/homeassistant/components/myuplink/sensor.py +++ b/homeassistant/components/myuplink/sensor.py @@ -293,8 +293,8 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity): @property def native_value(self) -> StateType: """Sensor state value.""" - device_point = self.coordinator.data.points[self.device_id][self.point_id] - if device_point.value == MARKER_FOR_UNKNOWN_VALUE: + device_point = self.coordinator.data.points[self.device_id].get(self.point_id) + if device_point is None or device_point.value == MARKER_FOR_UNKNOWN_VALUE: return None return device_point.value # type: ignore[no-any-return] From b6c8718ae43c186615095124783528b98ff884de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 10:17:18 +0000 Subject: [PATCH 1146/1175] Bump version to 2025.6.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 b07549d387f..6cbe42d6dbe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index b096fcb8040..f0aa8169054 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b6" +version = "2025.6.0b7" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From 66be2f924020842225fb44ad0d66a16a1b3be09f Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:08:10 -0400 Subject: [PATCH 1147/1175] Fix `delay_on` and `delay_off` restarting when a new trigger occurs during the delay (#145050) --- .../components/template/binary_sensor.py | 24 ++++++-- .../components/template/test_binary_sensor.py | 56 +++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 7ef64e8077b..f0ec64eae2a 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -352,6 +352,8 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity self._to_render_simple.append(key) self._parse_result.add(key) + self._last_delay_from: bool | None = None + self._last_delay_to: bool | None = None self._delay_cancel: CALLBACK_TYPE | None = None self._auto_off_cancel: CALLBACK_TYPE | None = None self._auto_off_time: datetime | None = None @@ -388,6 +390,20 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity """Handle update of the data.""" self._process_data() + raw = self._rendered.get(CONF_STATE) + state = template.result_as_boolean(raw) + + key = CONF_DELAY_ON if state else CONF_DELAY_OFF + delay = self._rendered.get(key) or self._config.get(key) + + if ( + self._delay_cancel + and delay + and self._attr_is_on == self._last_delay_from + and state == self._last_delay_to + ): + return + if self._delay_cancel: self._delay_cancel() self._delay_cancel = None @@ -401,12 +417,6 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity self.async_write_ha_state() return - raw = self._rendered.get(CONF_STATE) - state = template.result_as_boolean(raw) - - key = CONF_DELAY_ON if state else CONF_DELAY_OFF - delay = self._rendered.get(key) or self._config.get(key) - # state without delay. None means rendering failed. if self._attr_is_on == state or state is None or delay is None: self._set_state(state) @@ -422,6 +432,8 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity return # state with delay. Cancelled if new trigger received + self._last_delay_from = self._attr_is_on + self._last_delay_to = state self._delay_cancel = async_call_later( self.hass, delay.total_seconds(), partial(self._set_state, state) ) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index a7ee953bb09..122801e6c59 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1225,6 +1225,62 @@ async def test_template_with_trigger_templated_delay_on(hass: HomeAssistant) -> assert state.state == STATE_OFF +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + ("config", "delay_state"), + [ + ( + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + "delay_on": '{{ ({ "seconds": 10 }) }}', + }, + }, + }, + STATE_ON, + ), + ( + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer != 2 }}", + "device_class": "motion", + "delay_off": '{{ ({ "seconds": 10 }) }}', + }, + }, + }, + STATE_OFF, + ), + ], +) +@pytest.mark.usefixtures("start_ha") +async def test_trigger_template_delay_with_multiple_triggers( + hass: HomeAssistant, delay_state: str +) -> None: + """Test trigger based binary sensor with multiple triggers occurring during the delay.""" + future = dt_util.utcnow() + for _ in range(10): + # State should still be unknown + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNKNOWN + + hass.bus.async_fire("test_event", {"beer": 2}, context=Context()) + await hass.async_block_till_done() + + future += timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == delay_state + + @pytest.mark.parametrize(("count", "domain"), [(1, "template")]) @pytest.mark.parametrize( "config", From 6f4e16eed1a0c90657f107a793d40d42e6bb87b9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 11 Jun 2025 18:17:11 +0200 Subject: [PATCH 1148/1175] Fix stale options in here_travel_time (#145911) --- .../components/here_travel_time/__init__.py | 33 +--- .../here_travel_time/coordinator.py | 144 +++++++++++------- .../components/here_travel_time/model.py | 18 +-- 3 files changed, 94 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 525da15bd74..5393dfa5050 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -5,26 +5,13 @@ from __future__ import annotations from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.start import async_at_started -from homeassistant.util import dt as dt_util -from .const import ( - CONF_ARRIVAL_TIME, - CONF_DEPARTURE_TIME, - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_ROUTE_MODE, - TRAVEL_MODE_PUBLIC, -) +from .const import TRAVEL_MODE_PUBLIC from .coordinator import ( HereConfigEntry, HERERoutingDataUpdateCoordinator, HERETransitDataUpdateCoordinator, ) -from .model import HERETravelTimeConfig PLATFORMS = [Platform.SENSOR] @@ -33,29 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry) """Set up HERE Travel Time from a config entry.""" api_key = config_entry.data[CONF_API_KEY] - arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, "")) - departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, "")) - - here_travel_time_config = HERETravelTimeConfig( - destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE), - destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE), - destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID), - origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE), - origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE), - origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID), - travel_mode=config_entry.data[CONF_MODE], - route_mode=config_entry.options[CONF_ROUTE_MODE], - arrival=arrival, - departure=departure, - ) - cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator] if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}: cls = HERETransitDataUpdateCoordinator else: cls = HERERoutingDataUpdateCoordinator - data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config) + data_coordinator = cls(hass, config_entry, api_key) config_entry.runtime_data = data_coordinator async def _async_update_at_start(_: HomeAssistant) -> None: diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index aa36404c584..447a45f5d2b 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -26,7 +26,7 @@ from here_transit import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfLength +from homeassistant.const import CONF_MODE, UnitOfLength from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.location import find_coordinates @@ -34,8 +34,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import DistanceConverter -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST -from .model import HERETravelTimeConfig, HERETravelTimeData +from .const import ( + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_ROUTE_MODE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ROUTE_MODE_FASTEST, +) +from .model import HERETravelTimeAPIParams, HERETravelTimeData BACKOFF_MULTIPLIER = 1.1 @@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): - """here_routing DataUpdateCoordinator.""" + """HERETravelTime DataUpdateCoordinator for the routing API.""" config_entry: HereConfigEntry @@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] hass: HomeAssistant, config_entry: HereConfigEntry, api_key: str, - config: HERETravelTimeConfig, ) -> None: """Initialize.""" super().__init__( @@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) self._api = HERERoutingApi(api_key) - self.config = config async def _async_update_data(self) -> HERETravelTimeData: """Get the latest data from the HERE Routing API.""" - origin, destination, arrival, departure = prepare_parameters( - self.hass, self.config - ) - - route_mode = ( - RoutingMode.FAST - if self.config.route_mode == ROUTE_MODE_FASTEST - else RoutingMode.SHORT - ) + params = prepare_parameters(self.hass, self.config_entry) _LOGGER.debug( ( "Requesting route for origin: %s, destination: %s, route_mode: %s," " mode: %s, arrival: %s, departure: %s" ), - origin, - destination, - route_mode, - TransportMode(self.config.travel_mode), - arrival, - departure, + params.origin, + params.destination, + params.route_mode, + TransportMode(params.travel_mode), + params.arrival, + params.departure, ) try: response = await self._api.route( - transport_mode=TransportMode(self.config.travel_mode), - origin=here_routing.Place(origin[0], origin[1]), - destination=here_routing.Place(destination[0], destination[1]), - routing_mode=route_mode, - arrival_time=arrival, - departure_time=departure, + transport_mode=TransportMode(params.travel_mode), + origin=here_routing.Place(params.origin[0], params.origin[1]), + destination=here_routing.Place( + params.destination[0], params.destination[1] + ), + routing_mode=params.route_mode, + arrival_time=params.arrival, + departure_time=params.departure, return_values=[Return.POLYINE, Return.SUMMARY], spans=[Spans.NAMES], ) @@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData] class HERETransitDataUpdateCoordinator( DataUpdateCoordinator[HERETravelTimeData | None] ): - """HERETravelTime DataUpdateCoordinator.""" + """HERETravelTime DataUpdateCoordinator for the transit API.""" config_entry: HereConfigEntry @@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator( hass: HomeAssistant, config_entry: HereConfigEntry, api_key: str, - config: HERETravelTimeConfig, ) -> None: """Initialize.""" super().__init__( @@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator( update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) self._api = HERETransitApi(api_key) - self.config = config async def _async_update_data(self) -> HERETravelTimeData | None: """Get the latest data from the HERE Routing API.""" - origin, destination, arrival, departure = prepare_parameters( - self.hass, self.config - ) + params = prepare_parameters(self.hass, self.config_entry) _LOGGER.debug( ( "Requesting transit route for origin: %s, destination: %s, arrival: %s," " departure: %s" ), - origin, - destination, - arrival, - departure, + params.origin, + params.destination, + params.arrival, + params.departure, ) try: response = await self._api.route( - origin=here_transit.Place(latitude=origin[0], longitude=origin[1]), - destination=here_transit.Place( - latitude=destination[0], longitude=destination[1] + origin=here_transit.Place( + latitude=params.origin[0], longitude=params.origin[1] ), - arrival_time=arrival, - departure_time=departure, + destination=here_transit.Place( + latitude=params.destination[0], longitude=params.destination[1] + ), + arrival_time=params.arrival, + departure_time=params.departure, return_values=[ here_transit.Return.POLYLINE, here_transit.Return.TRAVEL_SUMMARY, @@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator( def prepare_parameters( hass: HomeAssistant, - config: HERETravelTimeConfig, -) -> tuple[list[str], list[str], str | None, str | None]: + config_entry: HereConfigEntry, +) -> HERETravelTimeAPIParams: """Prepare parameters for the HERE api.""" def _from_entity_id(entity_id: str) -> list[str]: @@ -305,32 +308,55 @@ def prepare_parameters( return formatted_coordinates # Destination - if config.destination_entity_id is not None: - destination = _from_entity_id(config.destination_entity_id) + if ( + destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID) + ) is not None: + destination = _from_entity_id(str(destination_entity_id)) else: destination = [ - str(config.destination_latitude), - str(config.destination_longitude), + str(config_entry.data[CONF_DESTINATION_LATITUDE]), + str(config_entry.data[CONF_DESTINATION_LONGITUDE]), ] # Origin - if config.origin_entity_id is not None: - origin = _from_entity_id(config.origin_entity_id) + if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None: + origin = _from_entity_id(str(origin_entity_id)) else: origin = [ - str(config.origin_latitude), - str(config.origin_longitude), + str(config_entry.data[CONF_ORIGIN_LATITUDE]), + str(config_entry.data[CONF_ORIGIN_LONGITUDE]), ] # Arrival/Departure - arrival: str | None = None - departure: str | None = None - if config.arrival is not None: - arrival = next_datetime(config.arrival).isoformat() - if config.departure is not None: - departure = next_datetime(config.departure).isoformat() + arrival: datetime | None = None + if ( + conf_arrival := dt_util.parse_time( + config_entry.options.get(CONF_ARRIVAL_TIME, "") + ) + ) is not None: + arrival = next_datetime(conf_arrival) + departure: datetime | None = None + if ( + conf_departure := dt_util.parse_time( + config_entry.options.get(CONF_DEPARTURE_TIME, "") + ) + ) is not None: + departure = next_datetime(conf_departure) - return (origin, destination, arrival, departure) + route_mode = ( + RoutingMode.FAST + if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST + else RoutingMode.SHORT + ) + + return HERETravelTimeAPIParams( + destination=destination, + origin=origin, + travel_mode=config_entry.data[CONF_MODE], + route_mode=route_mode, + arrival=arrival, + departure=departure, + ) def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None: diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index 178c0d8c805..cbac2b1c353 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import time +from datetime import datetime from typing import TypedDict @@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict): @dataclass -class HERETravelTimeConfig: - """Configuration for HereTravelTimeDataUpdateCoordinator.""" +class HERETravelTimeAPIParams: + """Configuration for polling the HERE API.""" - destination_latitude: float | None - destination_longitude: float | None - destination_entity_id: str | None - origin_latitude: float | None - origin_longitude: float | None - origin_entity_id: str | None + destination: list[str] + origin: list[str] travel_mode: str route_mode: str - arrival: time | None - departure: time | None + arrival: datetime | None + departure: datetime | None From 0cff7cbccde6dbc9bb1f2fd613146ec5beddecfa Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 11 Jun 2025 18:39:49 +0300 Subject: [PATCH 1149/1175] Remove stale Shelly BLU TRV devices (#145994) * Remove stale Shelly BLU TRV devices * Add test * Remove config entry from device --- homeassistant/components/shelly/__init__.py | 2 + .../components/shelly/quality_scale.yaml | 4 +- homeassistant/components/shelly/utils.py | 30 ++++++++++++ tests/components/shelly/conftest.py | 49 +++++++++++++++++++ tests/components/shelly/test_init.py | 49 ++++++++++++++++++- 5 files changed, 131 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 3130acff538..75fedf9b16d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -64,6 +64,7 @@ from .utils import ( get_http_port, get_rpc_scripts_event_types, get_ws_context, + remove_stale_blu_trv_devices, ) PLATFORMS: Final = [ @@ -300,6 +301,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry) runtime_data.rpc_script_events = await get_rpc_scripts_event_types( device, ignore_scripts=[BLE_SCRIPT_NAME] ) + remove_stale_blu_trv_devices(hass, device, entry) except (DeviceConnectionError, MacAddressMismatchError, RpcCallError) as err: await device.shutdown() raise ConfigEntryNotReady( diff --git a/homeassistant/components/shelly/quality_scale.yaml b/homeassistant/components/shelly/quality_scale.yaml index 753b2ee4a93..39667b556dd 100644 --- a/homeassistant/components/shelly/quality_scale.yaml +++ b/homeassistant/components/shelly/quality_scale.yaml @@ -61,8 +61,8 @@ rules: reconfiguration-flow: done repair-issues: done stale-devices: - status: todo - comment: BLU TRV needs to be removed when un-paired + status: done + comment: BLU TRV is removed when un-paired # Platinum async-dependency: done diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index eff5c95125c..cc0f2cf75d5 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -16,6 +16,7 @@ from aioshelly.const import ( DEFAULT_COAP_PORT, DEFAULT_HTTP_PORT, MODEL_1L, + MODEL_BLU_GATEWAY_G3, MODEL_DIMMER, MODEL_DIMMER_2, MODEL_EM3, @@ -821,3 +822,32 @@ def get_block_device_info( manufacturer="Shelly", via_device=(DOMAIN, mac), ) + + +@callback +def remove_stale_blu_trv_devices( + hass: HomeAssistant, rpc_device: RpcDevice, entry: ConfigEntry +) -> None: + """Remove stale BLU TRV devices.""" + if rpc_device.model != MODEL_BLU_GATEWAY_G3: + return + + dev_reg = dr.async_get(hass) + devices = dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id) + config = rpc_device.config + blutrv_keys = get_rpc_key_ids(config, BLU_TRV_IDENTIFIER) + trv_addrs = [config[f"{BLU_TRV_IDENTIFIER}:{key}"]["addr"] for key in blutrv_keys] + + for device in devices: + if not device.via_device_id: + # Device is not a sub-device, skip + continue + + if any( + identifier[0] == DOMAIN and identifier[1] in trv_addrs + for identifier in device.identifiers + ): + continue + + LOGGER.debug("Removing stale BLU TRV device %s", device.name) + dev_reg.async_update_device(device.id, remove_config_entry_id=entry.entry_id) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index ac70226a20a..4eccb075b67 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -260,6 +260,33 @@ MOCK_BLU_TRV_REMOTE_CONFIG = { "meta": {}, }, }, + { + "key": "blutrv:201", + "status": { + "id": 201, + "target_C": 17.1, + "current_C": 17.1, + "pos": 0, + "rssi": -60, + "battery": 100, + "packet_id": 58, + "last_updated_ts": 1734967725, + "paired": True, + "rpc": True, + "rsv": 61, + }, + "config": { + "id": 201, + "addr": "f8:44:77:25:f0:de", + "name": "TRV-201", + "key": None, + "trv": "bthomedevice:201", + "temp_sensors": [], + "dw_sensors": [], + "override_delay": 30, + "meta": {}, + }, + }, ], "blutrv:200": { "id": 0, @@ -272,6 +299,17 @@ MOCK_BLU_TRV_REMOTE_CONFIG = { "name": "TRV-Name", "local_name": "SBTR-001AEU", }, + "blutrv:201": { + "id": 1, + "enable": True, + "min_valve_position": 0, + "default_boost_duration": 1800, + "default_override_duration": 2147483647, + "default_override_target_C": 8, + "addr": "f8:44:77:25:f0:de", + "name": "TRV-201", + "local_name": "SBTR-001AEU", + }, } @@ -287,6 +325,17 @@ MOCK_BLU_TRV_REMOTE_STATUS = { "battery": 100, "errors": [], }, + "blutrv:201": { + "id": 0, + "pos": 0, + "steps": 0, + "current_C": 15.2, + "target_C": 17.1, + "schedule_rev": 0, + "rssi": -60, + "battery": 100, + "errors": [], + }, } diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 283de897d8d..703df09bb61 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, call, patch from aioshelly.block_device import COAP from aioshelly.common import ConnectionOptions -from aioshelly.const import MODEL_PLUS_2PM +from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM from aioshelly.exceptions import ( DeviceConnectionError, InvalidAuthError, @@ -38,6 +38,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import DeviceRegistry, format_mac +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from . import MOCK_MAC, init_integration, mutate_rpc_device_status @@ -606,3 +607,49 @@ async def test_ble_scanner_unsupported_firmware_fixed( assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 + + +async def test_blu_trv_stale_device_removal( + hass: HomeAssistant, + mock_blu_trv: Mock, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test BLU TRV removal of stale a device after un-pairing.""" + trv_200_entity_id = "climate.trv_name" + trv_201_entity_id = "climate.trv_201" + + monkeypatch.setattr(mock_blu_trv, "model", MODEL_BLU_GATEWAY_G3) + gw_entry = await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3) + + # verify that both trv devices are present + assert hass.states.get(trv_200_entity_id) is not None + trv_200_entry = entity_registry.async_get(trv_200_entity_id) + assert trv_200_entry + + trv_200_device_entry = device_registry.async_get(trv_200_entry.device_id) + assert trv_200_device_entry + assert trv_200_device_entry.name == "TRV-Name" + + assert hass.states.get(trv_201_entity_id) is not None + trv_201_entry = entity_registry.async_get(trv_201_entity_id) + assert trv_201_entry + + trv_201_device_entry = device_registry.async_get(trv_201_entry.device_id) + assert trv_201_device_entry + assert trv_201_device_entry.name == "TRV-201" + + # simulate un-pairing of trv 201 device + monkeypatch.delitem(mock_blu_trv.config, "blutrv:201") + monkeypatch.delitem(mock_blu_trv.status, "blutrv:201") + + await hass.config_entries.async_reload(gw_entry.entry_id) + await hass.async_block_till_done() + + # verify that trv 201 is removed + assert hass.states.get(trv_200_entity_id) is not None + assert device_registry.async_get(trv_200_entry.device_id) is not None + + assert hass.states.get(trv_201_entity_id) is None + assert device_registry.async_get(trv_201_entry.device_id) is None From af72d1854f3a8763a1b65656c6a5841396c4ffe8 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 11 Jun 2025 15:24:37 +0100 Subject: [PATCH 1150/1175] Add guide for Honeywell Lyric application credentials setup (#146281) * Add guide for Honeywell Lyric application credentials setup * Fix --------- Co-authored-by: Joostlek --- homeassistant/components/lyric/application_credentials.py | 8 ++++++++ homeassistant/components/lyric/strings.json | 5 ++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/application_credentials.py b/homeassistant/components/lyric/application_credentials.py index 2ccdca72bb6..9c53395bb6d 100644 --- a/homeassistant/components/lyric/application_credentials.py +++ b/homeassistant/components/lyric/application_credentials.py @@ -24,3 +24,11 @@ async def async_get_auth_implementation( token_url=OAUTH2_TOKEN, ), ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "developer_dashboard_url": "https://developer.honeywellhome.com", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 41598dfbdd0..786f49e5300 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "To be able to log in to Honeywell Lyric the integration requires a client ID and secret. To acquire those, please follow the following steps.\n\n1. Go to the [Honeywell Lyric Developer Apps Dashboard]({developer_dashboard_url}).\n1. Sign up for a developer account if you don't have one yet. This is a separate account from your Honeywell account.\n1. Log in with your Honeywell Lyric developer account.\n1. Go to the **My Apps** section.\n1. Press the **CREATE NEW APP** button.\n1. Give the application a name of your choice.\n1. Set the **Callback URL** to `{redirect_url}`.\n1. Save your changes.\\n1. Copy the **Consumer Key** and paste it here as the **Client ID**, then copy the **Consumer Secret** and paste it here as the **Client Secret**." + }, "config": { "step": { "pick_implementation": { @@ -9,7 +12,7 @@ "description": "The Lyric integration needs to re-authenticate your account." }, "oauth_discovery": { - "description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric." + "description": "Home Assistant has found a Honeywell Lyric device on your network. Be aware that the setup of the Lyric integration is more complicated than other integrations. Press **Submit** to continue setting up Honeywell Lyric." } }, "abort": { From 82de2ed8e184ea89bca1d99e22fef29a9f6b7d60 Mon Sep 17 00:00:00 2001 From: tronikos Date: Wed, 11 Jun 2025 09:35:26 -0700 Subject: [PATCH 1151/1175] Rename Amazon Devices to Alexa Devices (#146362) Co-authored-by: Simone Chemelli Co-authored-by: Joostlek --- .strict-typing | 2 +- CODEOWNERS | 4 ++-- homeassistant/brands/amazon.json | 2 +- .../{amazon_devices => alexa_devices}/__init__.py | 4 ++-- .../binary_sensor.py | 4 ++-- .../{amazon_devices => alexa_devices}/config_flow.py | 4 ++-- .../{amazon_devices => alexa_devices}/const.py | 4 ++-- .../{amazon_devices => alexa_devices}/coordinator.py | 4 ++-- .../{amazon_devices => alexa_devices}/diagnostics.py | 2 +- .../{amazon_devices => alexa_devices}/entity.py | 4 ++-- .../{amazon_devices => alexa_devices}/icons.json | 0 .../{amazon_devices => alexa_devices}/manifest.json | 6 +++--- .../{amazon_devices => alexa_devices}/notify.py | 4 ++-- .../quality_scale.yaml | 0 .../{amazon_devices => alexa_devices}/strings.json | 12 ++++++------ .../{amazon_devices => alexa_devices}/switch.py | 4 ++-- homeassistant/generated/config_flows.py | 2 +- homeassistant/generated/integrations.json | 4 ++-- mypy.ini | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../{amazon_devices => alexa_devices}/__init__.py | 2 +- .../{amazon_devices => alexa_devices}/conftest.py | 12 ++++++------ .../{amazon_devices => alexa_devices}/const.py | 2 +- .../snapshots/test_binary_sensor.ambr | 4 ++-- .../snapshots/test_diagnostics.ambr | 2 +- .../snapshots/test_init.ambr | 2 +- .../snapshots/test_notify.ambr | 4 ++-- .../snapshots/test_switch.ambr | 2 +- .../test_binary_sensor.py | 6 +++--- .../test_config_flow.py | 4 ++-- .../test_diagnostics.py | 4 ++-- .../{amazon_devices => alexa_devices}/test_init.py | 4 ++-- .../{amazon_devices => alexa_devices}/test_notify.py | 6 +++--- .../{amazon_devices => alexa_devices}/test_switch.py | 6 +++--- 35 files changed, 67 insertions(+), 67 deletions(-) rename homeassistant/components/{amazon_devices => alexa_devices}/__init__.py (91%) rename homeassistant/components/{amazon_devices => alexa_devices}/binary_sensor.py (93%) rename homeassistant/components/{amazon_devices => alexa_devices}/config_flow.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/const.py (60%) rename homeassistant/components/{amazon_devices => alexa_devices}/coordinator.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/diagnostics.py (97%) rename homeassistant/components/{amazon_devices => alexa_devices}/entity.py (95%) rename homeassistant/components/{amazon_devices => alexa_devices}/icons.json (100%) rename homeassistant/components/{amazon_devices => alexa_devices}/manifest.json (79%) rename homeassistant/components/{amazon_devices => alexa_devices}/notify.py (94%) rename homeassistant/components/{amazon_devices => alexa_devices}/quality_scale.yaml (100%) rename homeassistant/components/{amazon_devices => alexa_devices}/strings.json (75%) rename homeassistant/components/{amazon_devices => alexa_devices}/switch.py (95%) rename tests/components/{amazon_devices => alexa_devices}/__init__.py (88%) rename tests/components/{amazon_devices => alexa_devices}/conftest.py (84%) rename tests/components/{amazon_devices => alexa_devices}/const.py (82%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_binary_sensor.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_diagnostics.ambr (98%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_init.ambr (96%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_notify.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/snapshots/test_switch.ambr (97%) rename tests/components/{amazon_devices => alexa_devices}/test_binary_sensor.py (92%) rename tests/components/{amazon_devices => alexa_devices}/test_config_flow.py (96%) rename tests/components/{amazon_devices => alexa_devices}/test_diagnostics.py (93%) rename tests/components/{amazon_devices => alexa_devices}/test_init.py (87%) rename tests/components/{amazon_devices => alexa_devices}/test_notify.py (92%) rename tests/components/{amazon_devices => alexa_devices}/test_switch.py (94%) diff --git a/.strict-typing b/.strict-typing index 4febfd68486..b34cbfa5fca 100644 --- a/.strict-typing +++ b/.strict-typing @@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.alert.* homeassistant.components.alexa.* +homeassistant.components.alexa_devices.* homeassistant.components.alpha_vantage.* -homeassistant.components.amazon_devices.* homeassistant.components.amazon_polly.* homeassistant.components.amberelectric.* homeassistant.components.ambient_network.* diff --git a/CODEOWNERS b/CODEOWNERS index 3f3ce07ce84..b447c878128 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,8 +89,8 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh -/homeassistant/components/amazon_devices/ @chemelli74 -/tests/components/amazon_devices/ @chemelli74 +/homeassistant/components/alexa_devices/ @chemelli74 +/tests/components/alexa_devices/ @chemelli74 /homeassistant/components/amazon_polly/ @jschlyter /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index d2e25468388..126b69c848d 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -3,7 +3,7 @@ "name": "Amazon", "integrations": [ "alexa", - "amazon_devices", + "alexa_devices", "amazon_polly", "aws", "aws_s3", diff --git a/homeassistant/components/amazon_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py similarity index 91% rename from homeassistant/components/amazon_devices/__init__.py rename to homeassistant/components/alexa_devices/__init__.py index 1db41d335ef..7a4139a65da 100644 --- a/homeassistant/components/amazon_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -1,4 +1,4 @@ -"""Amazon Devices integration.""" +"""Alexa Devices integration.""" from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -13,7 +13,7 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool: - """Set up Amazon Devices platform.""" + """Set up Alexa Devices platform.""" coordinator = AmazonDevicesCoordinator(hass, entry) diff --git a/homeassistant/components/amazon_devices/binary_sensor.py b/homeassistant/components/alexa_devices/binary_sensor.py similarity index 93% rename from homeassistant/components/amazon_devices/binary_sensor.py rename to homeassistant/components/alexa_devices/binary_sensor.py index ab1fadc7548..16cf73aee9f 100644 --- a/homeassistant/components/amazon_devices/binary_sensor.py +++ b/homeassistant/components/alexa_devices/binary_sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription): - """Amazon Devices binary sensor entity description.""" + """Alexa Devices binary sensor entity description.""" is_on_fn: Callable[[AmazonDevice], bool] @@ -52,7 +52,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices binary sensors based on a config entry.""" + """Set up Alexa Devices binary sensors based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/components/amazon_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py similarity index 95% rename from homeassistant/components/amazon_devices/config_flow.py rename to homeassistant/components/alexa_devices/config_flow.py index d0c3d067cee..5add7ceb711 100644 --- a/homeassistant/components/amazon_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Amazon Devices integration.""" +"""Config flow for Alexa Devices integration.""" from __future__ import annotations @@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Amazon Devices.""" + """Handle a config flow for Alexa Devices.""" async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/amazon_devices/const.py b/homeassistant/components/alexa_devices/const.py similarity index 60% rename from homeassistant/components/amazon_devices/const.py rename to homeassistant/components/alexa_devices/const.py index b8cf2c264b1..ca0290a10bc 100644 --- a/homeassistant/components/amazon_devices/const.py +++ b/homeassistant/components/alexa_devices/const.py @@ -1,8 +1,8 @@ -"""Amazon Devices constants.""" +"""Alexa Devices constants.""" import logging _LOGGER = logging.getLogger(__package__) -DOMAIN = "amazon_devices" +DOMAIN = "alexa_devices" CONF_LOGIN_DATA = "login_data" diff --git a/homeassistant/components/amazon_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py similarity index 95% rename from homeassistant/components/amazon_devices/coordinator.py rename to homeassistant/components/alexa_devices/coordinator.py index 48e31cb3f94..8e58441d46c 100644 --- a/homeassistant/components/amazon_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -1,4 +1,4 @@ -"""Support for Amazon Devices.""" +"""Support for Alexa Devices.""" from datetime import timedelta @@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator] class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): - """Base coordinator for Amazon Devices.""" + """Base coordinator for Alexa Devices.""" config_entry: AmazonConfigEntry diff --git a/homeassistant/components/amazon_devices/diagnostics.py b/homeassistant/components/alexa_devices/diagnostics.py similarity index 97% rename from homeassistant/components/amazon_devices/diagnostics.py rename to homeassistant/components/alexa_devices/diagnostics.py index e9a0773cd3f..0c4cb794416 100644 --- a/homeassistant/components/amazon_devices/diagnostics.py +++ b/homeassistant/components/alexa_devices/diagnostics.py @@ -1,4 +1,4 @@ -"""Diagnostics support for Amazon Devices integration.""" +"""Diagnostics support for Alexa Devices integration.""" from __future__ import annotations diff --git a/homeassistant/components/amazon_devices/entity.py b/homeassistant/components/alexa_devices/entity.py similarity index 95% rename from homeassistant/components/amazon_devices/entity.py rename to homeassistant/components/alexa_devices/entity.py index 962e2f55ae6..f539079602f 100644 --- a/homeassistant/components/amazon_devices/entity.py +++ b/homeassistant/components/alexa_devices/entity.py @@ -1,4 +1,4 @@ -"""Defines a base Amazon Devices entity.""" +"""Defines a base Alexa Devices entity.""" from aioamazondevices.api import AmazonDevice from aioamazondevices.const import SPEAKER_GROUP_MODEL @@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]): - """Defines a base Amazon Devices entity.""" + """Defines a base Alexa Devices entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/amazon_devices/icons.json b/homeassistant/components/alexa_devices/icons.json similarity index 100% rename from homeassistant/components/amazon_devices/icons.json rename to homeassistant/components/alexa_devices/icons.json diff --git a/homeassistant/components/amazon_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json similarity index 79% rename from homeassistant/components/amazon_devices/manifest.json rename to homeassistant/components/alexa_devices/manifest.json index f63893c1598..2a9e88cfd85 100644 --- a/homeassistant/components/amazon_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -1,9 +1,9 @@ { - "domain": "amazon_devices", - "name": "Amazon Devices", + "domain": "alexa_devices", + "name": "Alexa Devices", "codeowners": ["@chemelli74"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/amazon_devices", + "documentation": "https://www.home-assistant.io/integrations/alexa_devices", "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], diff --git a/homeassistant/components/amazon_devices/notify.py b/homeassistant/components/alexa_devices/notify.py similarity index 94% rename from homeassistant/components/amazon_devices/notify.py rename to homeassistant/components/alexa_devices/notify.py index 3762a7a3264..ff0cd4e59ea 100644 --- a/homeassistant/components/amazon_devices/notify.py +++ b/homeassistant/components/alexa_devices/notify.py @@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) class AmazonNotifyEntityDescription(NotifyEntityDescription): - """Amazon Devices notify entity description.""" + """Alexa Devices notify entity description.""" method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]] subkey: str @@ -49,7 +49,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices notification entity based on a config entry.""" + """Set up Alexa Devices notification entity based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/components/amazon_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml similarity index 100% rename from homeassistant/components/amazon_devices/quality_scale.yaml rename to homeassistant/components/alexa_devices/quality_scale.yaml diff --git a/homeassistant/components/amazon_devices/strings.json b/homeassistant/components/alexa_devices/strings.json similarity index 75% rename from homeassistant/components/amazon_devices/strings.json rename to homeassistant/components/alexa_devices/strings.json index 47e6234cd9c..9d615b248ed 100644 --- a/homeassistant/components/amazon_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -12,16 +12,16 @@ "step": { "user": { "data": { - "country": "[%key:component::amazon_devices::common::data_country%]", + "country": "[%key:component::alexa_devices::common::data_country%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "code": "[%key:component::amazon_devices::common::data_description_code%]" + "code": "[%key:component::alexa_devices::common::data_description_code%]" }, "data_description": { - "country": "[%key:component::amazon_devices::common::data_description_country%]", - "username": "[%key:component::amazon_devices::common::data_description_username%]", - "password": "[%key:component::amazon_devices::common::data_description_password%]", - "code": "[%key:component::amazon_devices::common::data_description_code%]" + "country": "[%key:component::alexa_devices::common::data_description_country%]", + "username": "[%key:component::alexa_devices::common::data_description_username%]", + "password": "[%key:component::alexa_devices::common::data_description_password%]", + "code": "[%key:component::alexa_devices::common::data_description_code%]" } } }, diff --git a/homeassistant/components/amazon_devices/switch.py b/homeassistant/components/alexa_devices/switch.py similarity index 95% rename from homeassistant/components/amazon_devices/switch.py rename to homeassistant/components/alexa_devices/switch.py index 428ef3e3b45..b8f78134feb 100644 --- a/homeassistant/components/amazon_devices/switch.py +++ b/homeassistant/components/alexa_devices/switch.py @@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1 @dataclass(frozen=True, kw_only=True) class AmazonSwitchEntityDescription(SwitchEntityDescription): - """Amazon Devices switch entity description.""" + """Alexa Devices switch entity description.""" is_on_fn: Callable[[AmazonDevice], bool] subkey: str @@ -43,7 +43,7 @@ async def async_setup_entry( entry: AmazonConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up Amazon Devices switches based on a config entry.""" + """Set up Alexa Devices switches based on a config entry.""" coordinator = entry.runtime_data diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 44a9b19e8c2..2d246f53ca3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,7 +47,7 @@ FLOWS = { "airzone", "airzone_cloud", "alarmdecoder", - "amazon_devices", + "alexa_devices", "amberelectric", "ambient_network", "ambient_station", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 775272f77c4..846a5c74ddb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -207,11 +207,11 @@ "amazon": { "name": "Amazon", "integrations": { - "amazon_devices": { + "alexa_devices": { "integration_type": "hub", "config_flow": true, "iot_class": "cloud_polling", - "name": "Amazon Devices" + "name": "Alexa Devices" }, "amazon_polly": { "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index da76e4ae2cd..1fdab75663e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -405,7 +405,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.alpha_vantage.*] +[mypy-homeassistant.components.alexa_devices.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -415,7 +415,7 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.amazon_devices.*] +[mypy-homeassistant.components.alpha_vantage.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/requirements_all.txt b/requirements_all.txt index 95a5f919add..d9a9014a6b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 -# homeassistant.components.amazon_devices +# homeassistant.components.alexa_devices aioamazondevices==3.0.6 # homeassistant.components.ambient_network diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ed51e54164..66f34dd6d69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -169,7 +169,7 @@ aioairzone-cloud==0.6.12 # homeassistant.components.airzone aioairzone==1.0.0 -# homeassistant.components.amazon_devices +# homeassistant.components.alexa_devices aioamazondevices==3.0.6 # homeassistant.components.ambient_network diff --git a/tests/components/amazon_devices/__init__.py b/tests/components/alexa_devices/__init__.py similarity index 88% rename from tests/components/amazon_devices/__init__.py rename to tests/components/alexa_devices/__init__.py index 47ee520b124..24348248e0c 100644 --- a/tests/components/amazon_devices/__init__.py +++ b/tests/components/alexa_devices/__init__.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices integration.""" +"""Tests for the Alexa Devices integration.""" from homeassistant.core import HomeAssistant diff --git a/tests/components/amazon_devices/conftest.py b/tests/components/alexa_devices/conftest.py similarity index 84% rename from tests/components/amazon_devices/conftest.py rename to tests/components/alexa_devices/conftest.py index f0ee29d44e5..4ce2eb743ea 100644 --- a/tests/components/amazon_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -1,4 +1,4 @@ -"""Amazon Devices tests configuration.""" +"""Alexa Devices tests configuration.""" from collections.abc import Generator from unittest.mock import AsyncMock, patch @@ -7,7 +7,7 @@ from aioamazondevices.api import AmazonDevice from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest -from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" with patch( - "homeassistant.components.amazon_devices.async_setup_entry", + "homeassistant.components.alexa_devices.async_setup_entry", return_value=True, ) as mock_setup_entry: yield mock_setup_entry @@ -27,14 +27,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def mock_amazon_devices_client() -> Generator[AsyncMock]: - """Mock an Amazon Devices client.""" + """Mock an Alexa Devices client.""" with ( patch( - "homeassistant.components.amazon_devices.coordinator.AmazonEchoApi", + "homeassistant.components.alexa_devices.coordinator.AmazonEchoApi", autospec=True, ) as mock_client, patch( - "homeassistant.components.amazon_devices.config_flow.AmazonEchoApi", + "homeassistant.components.alexa_devices.config_flow.AmazonEchoApi", new=mock_client, ), ): diff --git a/tests/components/amazon_devices/const.py b/tests/components/alexa_devices/const.py similarity index 82% rename from tests/components/amazon_devices/const.py rename to tests/components/alexa_devices/const.py index a2600ba98a6..8a2f5b6b158 100644 --- a/tests/components/amazon_devices/const.py +++ b/tests/components/alexa_devices/const.py @@ -1,4 +1,4 @@ -"""Amazon Devices tests const.""" +"""Alexa Devices tests const.""" TEST_CODE = "023123" TEST_COUNTRY = "IT" diff --git a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr b/tests/components/alexa_devices/snapshots/test_binary_sensor.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_binary_sensor.ambr rename to tests/components/alexa_devices/snapshots/test_binary_sensor.ambr index e914541d19c..16f9eeaedae 100644 --- a/tests/components/amazon_devices/snapshots/test_binary_sensor.ambr +++ b/tests/components/alexa_devices/snapshots/test_binary_sensor.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Bluetooth', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, @@ -73,7 +73,7 @@ 'original_device_class': , 'original_icon': None, 'original_name': 'Connectivity', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr similarity index 98% rename from tests/components/amazon_devices/snapshots/test_diagnostics.ambr rename to tests/components/alexa_devices/snapshots/test_diagnostics.ambr index 0b5164418aa..95798fca817 100644 --- a/tests/components/amazon_devices/snapshots/test_diagnostics.ambr +++ b/tests/components/alexa_devices/snapshots/test_diagnostics.ambr @@ -57,7 +57,7 @@ 'disabled_by': None, 'discovery_keys': dict({ }), - 'domain': 'amazon_devices', + 'domain': 'alexa_devices', 'minor_version': 1, 'options': dict({ }), diff --git a/tests/components/amazon_devices/snapshots/test_init.ambr b/tests/components/alexa_devices/snapshots/test_init.ambr similarity index 96% rename from tests/components/amazon_devices/snapshots/test_init.ambr rename to tests/components/alexa_devices/snapshots/test_init.ambr index be0a5894eea..e0460c4c173 100644 --- a/tests/components/amazon_devices/snapshots/test_init.ambr +++ b/tests/components/alexa_devices/snapshots/test_init.ambr @@ -13,7 +13,7 @@ 'id': , 'identifiers': set({ tuple( - 'amazon_devices', + 'alexa_devices', 'echo_test_serial_number', ), }), diff --git a/tests/components/amazon_devices/snapshots/test_notify.ambr b/tests/components/alexa_devices/snapshots/test_notify.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_notify.ambr rename to tests/components/alexa_devices/snapshots/test_notify.ambr index a47bf7a63ae..64776c14420 100644 --- a/tests/components/amazon_devices/snapshots/test_notify.ambr +++ b/tests/components/alexa_devices/snapshots/test_notify.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Announce', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, @@ -74,7 +74,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Speak', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/snapshots/test_switch.ambr b/tests/components/alexa_devices/snapshots/test_switch.ambr similarity index 97% rename from tests/components/amazon_devices/snapshots/test_switch.ambr rename to tests/components/alexa_devices/snapshots/test_switch.ambr index 8a2ce8d529a..c622cc67ea7 100644 --- a/tests/components/amazon_devices/snapshots/test_switch.ambr +++ b/tests/components/alexa_devices/snapshots/test_switch.ambr @@ -25,7 +25,7 @@ 'original_device_class': None, 'original_icon': None, 'original_name': 'Do not disturb', - 'platform': 'amazon_devices', + 'platform': 'alexa_devices', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, diff --git a/tests/components/amazon_devices/test_binary_sensor.py b/tests/components/alexa_devices/test_binary_sensor.py similarity index 92% rename from tests/components/amazon_devices/test_binary_sensor.py rename to tests/components/alexa_devices/test_binary_sensor.py index b31d85e06aa..a2e38b3459b 100644 --- a/tests/components/amazon_devices/test_binary_sensor.py +++ b/tests/components/alexa_devices/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices binary sensor platform.""" +"""Tests for the Alexa Devices binary sensor platform.""" from unittest.mock import AsyncMock, patch @@ -11,7 +11,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -31,7 +31,7 @@ async def test_all_entities( ) -> None: """Test all entities.""" with patch( - "homeassistant.components.amazon_devices.PLATFORMS", [Platform.BINARY_SENSOR] + "homeassistant.components.alexa_devices.PLATFORMS", [Platform.BINARY_SENSOR] ): await setup_integration(hass, mock_config_entry) diff --git a/tests/components/amazon_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py similarity index 96% rename from tests/components/amazon_devices/test_config_flow.py rename to tests/components/alexa_devices/test_config_flow.py index ce1ac44d102..9bf174c5955 100644 --- a/tests/components/amazon_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -1,11 +1,11 @@ -"""Tests for the Amazon Devices config flow.""" +"""Tests for the Alexa Devices config flow.""" from unittest.mock import AsyncMock from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect import pytest -from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN +from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant diff --git a/tests/components/amazon_devices/test_diagnostics.py b/tests/components/alexa_devices/test_diagnostics.py similarity index 93% rename from tests/components/amazon_devices/test_diagnostics.py rename to tests/components/alexa_devices/test_diagnostics.py index e548702650b..3c18d432543 100644 --- a/tests/components/amazon_devices/test_diagnostics.py +++ b/tests/components/alexa_devices/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for Amazon Devices diagnostics platform.""" +"""Tests for Alexa Devices diagnostics platform.""" from __future__ import annotations @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion from syrupy.filters import props -from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr diff --git a/tests/components/amazon_devices/test_init.py b/tests/components/alexa_devices/test_init.py similarity index 87% rename from tests/components/amazon_devices/test_init.py rename to tests/components/alexa_devices/test_init.py index 489952dbd4c..3100cfe5fa9 100644 --- a/tests/components/amazon_devices/test_init.py +++ b/tests/components/alexa_devices/test_init.py @@ -1,10 +1,10 @@ -"""Tests for the Amazon Devices integration.""" +"""Tests for the Alexa Devices integration.""" from unittest.mock import AsyncMock from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.const import DOMAIN +from homeassistant.components.alexa_devices.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr diff --git a/tests/components/amazon_devices/test_notify.py b/tests/components/alexa_devices/test_notify.py similarity index 92% rename from tests/components/amazon_devices/test_notify.py rename to tests/components/alexa_devices/test_notify.py index b486380fd07..6067874e370 100644 --- a/tests/components/amazon_devices/test_notify.py +++ b/tests/components/alexa_devices/test_notify.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices notify platform.""" +"""Tests for the Alexa Devices notify platform.""" from unittest.mock import AsyncMock, patch @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.components.notify import ( ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, @@ -32,7 +32,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.NOTIFY]): + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.NOTIFY]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/amazon_devices/test_switch.py b/tests/components/alexa_devices/test_switch.py similarity index 94% rename from tests/components/amazon_devices/test_switch.py rename to tests/components/alexa_devices/test_switch.py index 24af96db280..26a18fb731a 100644 --- a/tests/components/amazon_devices/test_switch.py +++ b/tests/components/alexa_devices/test_switch.py @@ -1,4 +1,4 @@ -"""Tests for the Amazon Devices switch platform.""" +"""Tests for the Alexa Devices switch platform.""" from unittest.mock import AsyncMock, patch @@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -37,7 +37,7 @@ async def test_all_entities( entity_registry: er.EntityRegistry, ) -> None: """Test all entities.""" - with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.SWITCH]): + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SWITCH]): await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) From 6384c800c313f27f8a02d6b487156c3c1a9e033d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:46:40 +1200 Subject: [PATCH 1152/1175] Fix solax state class of `Today's Generated Energy` (#146492) --- homeassistant/components/solax/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 1cdec0389fe..61420c152a5 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -42,7 +42,7 @@ SENSOR_DESCRIPTIONS: dict[tuple[Units, bool], SensorEntityDescription] = { key=f"{Units.KWH}_{False}", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), (Units.KWH, True): SensorEntityDescription( key=f"{Units.KWH}_{True}", From e0f32cfd54a068820c6158e5e62337812fe39fa7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 12:42:09 +0200 Subject: [PATCH 1153/1175] Allow removing entity registry items twice (#146519) --- homeassistant/helpers/entity_registry.py | 8 ++++++++ tests/helpers/test_entity_registry.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b503ba5f787..72689bc4997 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -980,6 +980,14 @@ class EntityRegistry(BaseRegistry): def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" self.hass.verify_event_loop_thread("entity_registry.async_remove") + if entity_id not in self.entities: + # Allow attempts to remove an entity which does not exist. If this is + # not allowed, there will be races during cleanup where we iterate over + # lists of entities to remove, but there are listeners for entity + # registry events which delete entities at the same time. + # For example, if we clean up entities A and B, there might be a listener + # which deletes entity B when entity A is being removed. + return entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cef52810fa0..554adff3700 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -289,6 +289,24 @@ def test_get_or_create_suggested_object_id_conflict_existing( assert entry.entity_id == "light.hue_1234_2" +def test_remove(entity_registry: er.EntityRegistry) -> None: + """Test that we can remove an item.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + + assert not entity_registry.deleted_entities + assert list(entity_registry.entities) == [entry.entity_id] + + # Remove the item + entity_registry.async_remove(entry.entity_id) + assert list(entity_registry.deleted_entities) == [("light", "hue", "1234")] + assert not entity_registry.entities + + # Remove the item again + entity_registry.async_remove(entry.entity_id) + assert list(entity_registry.deleted_entities) == [("light", "hue", "1234")] + assert not entity_registry.entities + + def test_create_triggers_save(entity_registry: er.EntityRegistry) -> None: """Test that registering entry triggers a save.""" with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save: From cc972d20f64a44045c2127cd1d948fc9755d8241 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 11 Jun 2025 12:31:07 +0200 Subject: [PATCH 1154/1175] Remove Z-Wave useless reconfigure options (#146520) * Remove emulate hardware option * Remove log level option --- .../components/zwave_js/config_flow.py | 24 -------- homeassistant/components/zwave_js/const.py | 2 - .../components/zwave_js/strings.json | 2 - tests/components/zwave_js/test_config_flow.py | 58 +------------------ 4 files changed, 1 insertion(+), 85 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 08c9ec2e2b2..5e8e7022839 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -46,8 +46,6 @@ from .addon import get_addon_manager from .const import ( ADDON_SLUG, CONF_ADDON_DEVICE, - CONF_ADDON_EMULATE_HARDWARE, - CONF_ADDON_LOG_LEVEL, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY, CONF_ADDON_LR_S2_AUTHENTICATED_KEY, CONF_ADDON_NETWORK_KEY, @@ -78,17 +76,7 @@ TITLE = "Z-Wave JS" ADDON_SETUP_TIMEOUT = 5 ADDON_SETUP_TIMEOUT_ROUNDS = 40 -CONF_EMULATE_HARDWARE = "emulate_hardware" -CONF_LOG_LEVEL = "log_level" -ADDON_LOG_LEVELS = { - "error": "Error", - "warn": "Warn", - "info": "Info", - "verbose": "Verbose", - "debug": "Debug", - "silly": "Silly", -} ADDON_USER_INPUT_MAP = { CONF_ADDON_DEVICE: CONF_USB_PATH, CONF_ADDON_S0_LEGACY_KEY: CONF_S0_LEGACY_KEY, @@ -97,8 +85,6 @@ ADDON_USER_INPUT_MAP = { CONF_ADDON_S2_UNAUTHENTICATED_KEY: CONF_S2_UNAUTHENTICATED_KEY, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: CONF_LR_S2_ACCESS_CONTROL_KEY, CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY, - CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL, - CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE, } ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) @@ -1097,10 +1083,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_S2_UNAUTHENTICATED_KEY: self.s2_unauthenticated_key, CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: self.lr_s2_access_control_key, CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, - CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL], - CONF_ADDON_EMULATE_HARDWARE: user_input.get( - CONF_EMULATE_HARDWARE, False - ), } await self._async_set_addon_config(addon_config_updates) @@ -1135,8 +1117,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): lr_s2_authenticated_key = addon_config.get( CONF_ADDON_LR_S2_AUTHENTICATED_KEY, self.lr_s2_authenticated_key or "" ) - log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") - emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) try: ports = await async_get_usb_ports(self.hass) @@ -1163,10 +1143,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): vol.Optional( CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key ): str, - vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In( - ADDON_LOG_LEVELS - ), - vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool, } ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6d5cbb98902..3d626710d52 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -16,8 +16,6 @@ LR_ADDON_VERSION = AwesomeVersion("0.5.0") USER_AGENT = {APPLICATION_NAME: HA_VERSION} CONF_ADDON_DEVICE = "device" -CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" -CONF_ADDON_LOG_LEVEL = "log_level" CONF_ADDON_NETWORK_KEY = "network_key" CONF_ADDON_S0_LEGACY_KEY = "s0_legacy_key" CONF_ADDON_S2_ACCESS_CONTROL_KEY = "s2_access_control_key" diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 439fc7b1aad..d1d4cc94346 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -52,8 +52,6 @@ }, "configure_addon_reconfigure": { "data": { - "emulate_hardware": "Emulate Hardware", - "log_level": "Log level", "lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]", "lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]", "s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]", diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index fc01c9b29b1..dd8838e0775 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -2566,8 +2566,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, ), @@ -2591,8 +2589,6 @@ async def test_reconfigure_not_addon_with_addon_stop_fail( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 1, ), @@ -2706,8 +2702,6 @@ async def test_reconfigure_addon_running( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/test", @@ -2717,8 +2711,6 @@ async def test_reconfigure_addon_running( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, ), ], @@ -2836,8 +2828,6 @@ async def different_device_server_version(*args): "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -2847,35 +2837,6 @@ async def different_device_server_version(*args): "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, - }, - 0, - different_device_server_version, - ), - ( - {}, - { - "device": "/test", - "network_key": "old123", - "s0_legacy_key": "old123", - "s2_access_control_key": "old456", - "s2_authenticated_key": "old789", - "s2_unauthenticated_key": "old987", - "lr_s2_access_control_key": "old654", - "lr_s2_authenticated_key": "old321", - "log_level": "info", - }, - { - "usb_path": "/new", - "s0_legacy_key": "new123", - "s2_access_control_key": "new456", - "s2_authenticated_key": "new789", - "s2_unauthenticated_key": "new987", - "lr_s2_access_control_key": "new654", - "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, different_device_server_version, @@ -2946,8 +2907,7 @@ async def test_reconfigure_different_device( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - # Default emulate_hardware is False. - addon_options = {"emulate_hardware": False} | old_addon_options + addon_options = {} | old_addon_options # Legacy network key is not reset. addon_options.pop("network_key") @@ -2994,8 +2954,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -3005,8 +2963,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, [SupervisorError(), None], @@ -3022,8 +2978,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, }, { "usb_path": "/new", @@ -3033,8 +2987,6 @@ async def test_reconfigure_different_device( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, [ @@ -3151,8 +3103,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, } new_addon_options = { "usb_path": "/test", @@ -3162,8 +3112,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "old987", "lr_s2_access_control_key": "old654", "lr_s2_authenticated_key": "old321", - "log_level": "info", - "emulate_hardware": False, } addon_options.update(old_addon_options) entry = integration @@ -3236,8 +3184,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 0, ), @@ -3261,8 +3207,6 @@ async def test_reconfigure_addon_running_server_info_failure( "s2_unauthenticated_key": "new987", "lr_s2_access_control_key": "new654", "lr_s2_authenticated_key": "new321", - "log_level": "info", - "emulate_hardware": False, }, 1, ), From 5ee39df3305ee9ecceb1d43e2e444182c98d6a1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:16:51 +0200 Subject: [PATCH 1155/1175] Handle changes to source entity in history_stats helper (#146521) --- .../components/history_stats/__init__.py | 26 ++ .../components/history_stats/config_flow.py | 2 +- tests/components/history_stats/test_init.py | 294 +++++++++++++++++- 3 files changed, 314 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/history_stats/__init__.py b/homeassistant/components/history_stats/__init__.py index 63f32138dba..a3565f9ed77 100644 --- a/homeassistant/components/history_stats/__init__.py +++ b/homeassistant/components/history_stats/__init__.py @@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, CONF_STATE from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.template import Template from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS @@ -51,6 +53,30 @@ async def async_setup_entry( entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # history_stats does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index 96c8f319fbc..ca3d5229b6b 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -107,7 +107,7 @@ OPTIONS_FLOW = { } -class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): +class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config flow for History stats.""" config_flow = CONFIG_FLOW diff --git a/tests/components/history_stats/test_init.py b/tests/components/history_stats/test_init.py index 4cd999ba31c..37b5416fdbb 100644 --- a/tests/components/history_stats/test_init.py +++ b/tests/components/history_stats/test_init.py @@ -2,24 +2,107 @@ from __future__ import annotations +from unittest.mock import patch + +import pytest + +from homeassistant.components import history_stats +from homeassistant.components.history_stats.config_flow import ( + HistoryStatsConfigFlowHandler, +) from homeassistant.components.history_stats.const import ( CONF_END, CONF_START, DEFAULT_NAME, DOMAIN as HISTORY_STATS_DOMAIN, ) -from homeassistant.components.recorder import Recorder -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry -async def test_unload_entry( - recorder_mock: Recorder, hass: HomeAssistant, loaded_entry: MockConfigEntry -) -> None: +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def history_stats_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a history_stats config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_NAME: DEFAULT_NAME, + CONF_ENTITY_ID: sensor_entity_entry.entity_id, + CONF_STATE: ["on"], + CONF_TYPE: "count", + CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}", + CONF_END: "{{ utcnow() }}", + }, + title="My history stats", + version=HistoryStatsConfigFlowHandler.VERSION, + minor_version=HistoryStatsConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + +@pytest.mark.usefixtures("recorder_mock") +async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" assert loaded_entry.state is ConfigEntryState.LOADED @@ -28,8 +111,8 @@ async def test_unload_entry( assert loaded_entry.state is ConfigEntryState.NOT_LOADED +@pytest.mark.usefixtures("recorder_mock") async def test_device_cleaning( - recorder_mock: Recorder, hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, @@ -116,3 +199,200 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the history_stats config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + + # Check that the history_stats config entry is removed + assert ( + history_stats_config_entry.entry_id not in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == ["remove"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert history_stats_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert history_stats_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +@pytest.mark.usefixtures("recorder_mock") +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + history_stats_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(history_stats_config_entry.entry_id) + await hass.async_block_till_done() + + history_stats_entity_entry = entity_registry.async_get("sensor.my_history_stats") + assert history_stats_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, history_stats_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.history_stats.async_unload_entry", + wraps=history_stats.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the history_stats config entry is updated with the new entity ID + assert history_stats_config_entry.options[CONF_ENTITY_ID] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert history_stats_config_entry.entry_id in sensor_device.config_entries + + # Check that the history_stats config entry is not removed + assert history_stats_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 0cf1fd1d41bee8170e57d5b201d7299103cda910 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:08 +0200 Subject: [PATCH 1156/1175] Handle changes to source entity in integration helper (#146522) --- .../components/integration/__init__.py | 25 ++ tests/components/integration/test_init.py | 276 +++++++++++++++++- 2 files changed, 300 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 4ccf0dec258..0a64ce7140f 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from .const import CONF_SOURCE_SENSOR @@ -21,6 +23,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_SOURCE_SENSOR], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE_SENSOR] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py index 9fee54f4500..0ce3297a2ff 100644 --- a/tests/components/integration/test_init.py +++ b/tests/components/integration/test_init.py @@ -1,14 +1,97 @@ """Test the Integration - Riemann sum integral integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import integration +from homeassistant.components.integration.config_flow import ConfigFlowHandler from homeassistant.components.integration.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def integration_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create an integration config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "trapezoidal", + "name": "My integration", + "round": 1.0, + "source": sensor_entity_entry.entity_id, + "unit_prefix": "k", + "unit_time": "min", + "max_sub_interval": {"minutes": 1}, + }, + title="My integration", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + @pytest.mark.parametrize("platform", ["sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, @@ -209,3 +292,194 @@ async def test_device_cleaning( integration_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the integration config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the integration config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert integration_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert integration_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + integration_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(integration_config_entry.entry_id) + await hass.async_block_till_done() + + integration_entity_entry = entity_registry.async_get("sensor.my_integration") + assert integration_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, integration_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.integration.async_unload_entry", + wraps=integration.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the integration config entry is updated with the new entity ID + assert integration_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert integration_config_entry.entry_id in sensor_device.config_entries + + # Check that the integration config entry is not removed + assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From caaa4d5f3516038a9bcd6dc87d959e403e1640f1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:34 +0200 Subject: [PATCH 1157/1175] Handle changes to source entity in threshold helper (#146524) --- .../components/threshold/__init__.py | 25 ++ tests/components/threshold/test_init.py | 274 +++++++++++++++++- 2 files changed, 298 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index ea8b469fd32..9460a50db80 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -4,8 +4,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +19,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups( entry, (Platform.BINARY_SENSOR,) ) diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py index 6e85d659922..599612ce0b7 100644 --- a/tests/components/threshold/test_init.py +++ b/tests/components/threshold/test_init.py @@ -1,14 +1,95 @@ """Test the Min/Max integration.""" +from unittest.mock import patch + import pytest +from homeassistant.components import threshold +from homeassistant.components.threshold.config_flow import ConfigFlowHandler from homeassistant.components.threshold.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def threshold_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a threshold config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": sensor_entity_entry.entity_id, + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold", + "upper": None, + }, + title="My threshold", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + @pytest.mark.parametrize("platform", ["binary_sensor"]) async def test_setup_and_remove_config_entry( hass: HomeAssistant, @@ -208,3 +289,194 @@ async def test_device_cleaning( threshold_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the threshold config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the threshold config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert threshold_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert threshold_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + threshold_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(threshold_config_entry.entry_id) + await hass.async_block_till_done() + + threshold_entity_entry = entity_registry.async_get("binary_sensor.my_threshold") + assert threshold_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, threshold_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.threshold.async_unload_entry", + wraps=threshold.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the threshold config entry is updated with the new entity ID + assert threshold_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert threshold_config_entry.entry_id in sensor_device.config_entries + + # Check that the threshold config entry is not removed + assert threshold_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 273ccb3929c921dbe3f53f15474182852aa96f06 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:42 +0200 Subject: [PATCH 1158/1175] Handle changes to source entity in trend helper (#146525) --- homeassistant/components/trend/__init__.py | 26 ++ tests/components/trend/test_init.py | 275 ++++++++++++++++++++- 2 files changed, 299 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index c38730e7591..086ac818c8e 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes PLATFORMS = [Platform.BINARY_SENSOR] @@ -21,6 +23,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # trend does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/tests/components/trend/test_init.py b/tests/components/trend/test_init.py index 7ffb18de297..4ff6213d082 100644 --- a/tests/components/trend/test_init.py +++ b/tests/components/trend/test_init.py @@ -1,15 +1,95 @@ """Test the Trend integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import trend +from homeassistant.components.trend.config_flow import ConfigFlowHandler from homeassistant.components.trend.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from .conftest import ComponentSetup from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def trend_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a trend config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My trend", + "entity_id": sensor_entity_entry.entity_id, + "invert": False, + }, + title="My trend", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_setup_and_remove_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, @@ -135,3 +215,194 @@ async def test_device_cleaning( trend_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the trend config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + + # Check that the trend config entry is removed + assert trend_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert trend_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert trend_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + trend_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(trend_config_entry.entry_id) + await hass.async_block_till_done() + + trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend") + assert trend_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, trend_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.trend.async_unload_entry", + wraps=trend.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the trend config entry is updated with the new entity ID + assert trend_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert trend_config_entry.entry_id in sensor_device.config_entries + + # Check that the trend config entry is not removed + assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From 2ab32220ed2ad926272f87b34b5a02737760de73 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:52 +0200 Subject: [PATCH 1159/1175] Handle changes to source entity in utility_meter (#146526) --- .../components/utility_meter/__init__.py | 25 ++ tests/components/utility_meter/test_init.py | 369 +++++++++++++++++- 2 files changed, 393 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index e2b3411c193..64fa3342c08 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -17,9 +17,11 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.typing import ConfigType from .const import ( @@ -217,6 +219,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_SOURCE_SENSOR] + ), + source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR], + source_entity_removed=source_entity_removed, + ) + ) + if not entry.options.get(CONF_TARIFFS): # Only a single meter sensor is required hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index eba7cf913db..ea4af741e19 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -3,10 +3,12 @@ from __future__ import annotations from datetime import timedelta +from unittest.mock import patch from freezegun import freeze_time import pytest +from homeassistant.components import utility_meter from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, @@ -16,7 +18,9 @@ from homeassistant.components.utility_meter import ( select as um_select, sensor as um_sensor, ) +from homeassistant.components.utility_meter.config_flow import ConfigFlowHandler from homeassistant.components.utility_meter.const import DOMAIN, SERVICE_RESET +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, @@ -25,14 +29,94 @@ from homeassistant.const import ( Platform, UnitOfEnergy, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, mock_restore_cache +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def utility_meter_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + tariffs: list[str], +) -> MockConfigEntry: + """Fixture to create a utility_meter config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "cycle": "monthly", + "delta_values": False, + "name": "My utility meter", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": sensor_entity_entry.entity_id, + "tariffs": tariffs, + }, + title="My utility meter", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_restore_state(hass: HomeAssistant) -> None: """Test utility sensor restore state.""" config = { @@ -533,3 +617,286 @@ async def test_device_cleaning( utility_meter_config_entry.entry_id ) assert len(devices_after_reload) == 1 + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the utility_meter config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check that the utility_meter config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Remove the source sensor from the device + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert utility_meter_config_entry.entry_id not in sensor_device_2.config_entries + + # Move the source sensor to another device + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert utility_meter_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == ["update"] + + +@pytest.mark.parametrize( + ("tariffs", "expected_entities"), + [ + ([], {"sensor.my_utility_meter"}), + ( + ["peak", "offpeak"], + { + "select.my_utility_meter", + "sensor.my_utility_meter_offpeak", + "sensor.my_utility_meter_peak", + }, + ), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + utility_meter_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, + expected_entities: set[str], +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(utility_meter_config_entry.entry_id) + await hass.async_block_till_done() + + events = {} + for ( + utility_meter_entity + ) in entity_registry.entities.get_entries_for_config_entry_id( + utility_meter_config_entry.entry_id + ): + assert utility_meter_entity.device_id == sensor_entity_entry.device_id + events[utility_meter_entity.entity_id] = track_entity_registry_actions( + hass, utility_meter_entity.entity_id + ) + assert set(events) == expected_entities + + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Change the source entity's entity ID + with patch( + "homeassistant.components.utility_meter.async_unload_entry", + wraps=utility_meter.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the utility_meter config entry is updated with the new entity ID + assert utility_meter_config_entry.options["source"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert utility_meter_config_entry.entry_id in sensor_device.config_entries + + # Check that the utility_meter config entry is not removed + assert utility_meter_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + for entity_events in events.values(): + assert entity_events == [] From bcedb06862e6ac57e9c1891e385a2060fb9b2134 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:10:00 +0200 Subject: [PATCH 1160/1175] Bump linkplay to v0.2.11 (#146530) --- homeassistant/components/linkplay/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/linkplay/manifest.json b/homeassistant/components/linkplay/manifest.json index 1bbf70ed3ac..d6319c7a506 100644 --- a/homeassistant/components/linkplay/manifest.json +++ b/homeassistant/components/linkplay/manifest.json @@ -7,6 +7,6 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["linkplay"], - "requirements": ["python-linkplay==0.2.10"], + "requirements": ["python-linkplay==0.2.11"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index d9a9014a6b9..500fa0676ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2452,7 +2452,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.10 +python-linkplay==0.2.11 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66f34dd6d69..1136433d059 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2022,7 +2022,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.10.2 # homeassistant.components.linkplay -python-linkplay==0.2.10 +python-linkplay==0.2.11 # homeassistant.components.lirc # python-lirc==1.2.3 From 91e296a0c81ad41879949943817ad978d3db7e31 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 11 Jun 2025 16:46:52 +0300 Subject: [PATCH 1161/1175] Bump hdate to 1.1.1 (#146536) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index c93844dd559..550a6514593 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate[astral]==1.1.0"], + "requirements": ["hdate[astral]==1.1.1"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 500fa0676ab..c98f22f9d91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ hass-splunk==0.1.1 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.0 +hdate[astral]==1.1.1 # homeassistant.components.heatmiser heatmiserV3==2.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1136433d059..d1265ec260d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -988,7 +988,7 @@ hass-nabucasa==0.101.0 hassil==2.2.3 # homeassistant.components.jewish_calendar -hdate[astral]==1.1.0 +hdate[astral]==1.1.1 # homeassistant.components.here_travel_time here-routing==1.0.1 From 232f853d6804d717671cf3c929a7450e12a117a2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 12:27:51 +0200 Subject: [PATCH 1162/1175] Simplify helper_integration.async_handle_source_entity_changes (#146516) --- homeassistant/components/derivative/__init__.py | 8 -------- homeassistant/components/switch_as_x/__init__.py | 8 +------- homeassistant/helpers/helper_integration.py | 14 ++++++-------- tests/helpers/test_helper_integration.py | 7 ------- 4 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 6d539817875..0806a8f824d 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -2,19 +2,15 @@ from __future__ import annotations -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SOURCE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import DOMAIN - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" @@ -33,14 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # The source entity has been removed, we need to clean the device links. async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) - entity_registry = er.async_get(hass) entry.async_on_unload( async_handle_source_entity_changes( hass, helper_config_entry_id=entry.entry_id, - get_helper_entity_id=lambda: entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, entry.entry_id - ), set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_entity_id_to_device_id( hass, entry.options[CONF_SOURCE] diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 7d12ae4aec2..c77eda9b294 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -13,10 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN -from .light import LightSwitch - -__all__ = ["LightSwitch"] +from .const import CONF_INVERT, CONF_TARGET_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -72,9 +69,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_handle_source_entity_changes( hass, helper_config_entry_id=entry.entry_id, - get_helper_entity_id=lambda: entity_registry.async_get_entity_id( - entry.options[CONF_TARGET_DOMAIN], DOMAIN, entry.entry_id - ), set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=async_add_to_device(hass, entry, entity_id), source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 37aa246178e..61bb0bcd45d 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -15,7 +15,6 @@ def async_handle_source_entity_changes( hass: HomeAssistant, *, helper_config_entry_id: str, - get_helper_entity_id: Callable[[], str | None], set_source_entity_id_or_uuid: Callable[[str], None], source_device_id: str | None, source_entity_id_or_uuid: str, @@ -37,8 +36,6 @@ def async_handle_source_entity_changes( to no device, and the helper config entry removed from the old device. Then the helper config entry is reloaded. - :param get_helper_entity: A function which returns the helper entity's entity ID, - or None if the helper entity does not exist. :param set_source_entity_id_or_uuid: A function which updates the source entity ID or UUID, e.g., in the helper config entry options. :param source_entity_removed: A function which is called when the source entity @@ -81,13 +78,14 @@ def async_handle_source_entity_changes( return # The source entity has been moved to a different device, update the helper - # helper entity to link to the new device and the helper device to include - # the helper config entry - helper_entity_id = get_helper_entity_id() - if helper_entity_id: + # entities to link to the new device and the helper device to include the + # helper config entry + for helper_entity in entity_registry.entities.get_entries_for_config_entry_id( + helper_config_entry_id + ): # Update the helper entity to link to the new device (or no device) entity_registry.async_update_entity( - helper_entity_id, device_id=source_entity_entry.device_id + helper_entity.entity_id, device_id=source_entity_entry.device_id ) if source_entity_entry.device_id is not None: diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index 12433894dc7..47f1b62feb7 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -156,18 +156,11 @@ def mock_helper_integration( ) -> None: """Mock the helper integration.""" - def get_helper_entity_id() -> str | None: - """Get the helper entity ID.""" - return entity_registry.async_get_entity_id( - "sensor", HELPER_DOMAIN, helper_config_entry.entry_id - ) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Mock setup entry.""" async_handle_source_entity_changes( hass, helper_config_entry_id=helper_config_entry.entry_id, - get_helper_entity_id=get_helper_entity_id, set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, source_device_id=source_entity_entry.device_id, source_entity_id_or_uuid=helper_config_entry.options["source"], From c02707a90f09e30ba1eab5a7c8c990f19de4569b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:17:19 +0200 Subject: [PATCH 1163/1175] Handle changes to source entity in statistics helper (#146523) --- .../components/statistics/__init__.py | 26 ++ tests/components/statistics/test_init.py | 283 +++++++++++++++++- 2 files changed, 305 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index f71274e0ee7..f800c82f1f9 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -4,8 +4,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes DOMAIN = "statistics" PLATFORMS = [Platform.SENSOR] @@ -20,6 +22,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_ENTITY_ID], ) + def set_source_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_ENTITY_ID: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we remove the config entry because + # statistics does not allow replacing the input entity. + await hass.config_entries.async_remove(entry.entry_id) + + entry.async_on_unload( + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_source_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_ENTITY_ID] + ), + source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID], + source_entity_removed=source_entity_removed, + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/tests/components/statistics/test_init.py b/tests/components/statistics/test_init.py index 64829ea7d66..c11045a2eb2 100644 --- a/tests/components/statistics/test_init.py +++ b/tests/components/statistics/test_init.py @@ -2,14 +2,98 @@ from __future__ import annotations -from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from unittest.mock import patch + +import pytest + +from homeassistant.components import statistics +from homeassistant.components.statistics import DOMAIN +from homeassistant.components.statistics.config_flow import StatisticsConfigFlowHandler +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def statistics_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a statistics config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My statistics", + "entity_id": sensor_entity_entry.entity_id, + "state_characteristic": "mean", + "keep_last_sample": False, + "percentile": 50.0, + "precision": 2.0, + "sampling_size": 20.0, + }, + title="My statistics", + version=StatisticsConfigFlowHandler.VERSION, + minor_version=StatisticsConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None: """Test unload an entry.""" @@ -51,7 +135,7 @@ async def test_device_cleaning( # Configure the configuration entry for Statistics statistics_config_entry = MockConfigEntry( data={}, - domain=STATISTICS_DOMAIN, + domain=DOMAIN, options={ "name": "Statistics", "entity_id": "sensor.test_source", @@ -107,3 +191,194 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the statistics config entry is removed when the source entity is removed.""" + # Add another config entry to the sensor device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + sensor_device.id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Remove the source sensor's config entry from the device, this removes the + # source sensor + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + + # Check that the statistics config entry is removed + assert statistics_config_entry.entry_id not in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["remove"] + + +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity removed from the source device.""" + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Remove the source sensor from the device + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is removed from the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity is moved to another device.""" + sensor_device_2 = device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert statistics_config_entry.entry_id not in sensor_device_2.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Move the source sensor to another device + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, device_id=sensor_device_2.id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is moved to the other device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id not in sensor_device.config_entries + sensor_device_2 = device_registry.async_get(sensor_device_2.id) + assert statistics_config_entry.entry_id in sensor_device_2.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == ["update"] + + +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + statistics_config_entry: MockConfigEntry, + sensor_device: dr.DeviceEntry, + sensor_entity_entry: er.RegistryEntry, +) -> None: + """Test the source entity's entity ID is changed.""" + assert await hass.config_entries.async_setup(statistics_config_entry.entry_id) + await hass.async_block_till_done() + + statistics_entity_entry = entity_registry.async_get("sensor.my_statistics") + assert statistics_entity_entry.device_id == sensor_entity_entry.device_id + + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.statistics.async_unload_entry", + wraps=statistics.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id" + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the statistics config entry is updated with the new entity ID + assert statistics_config_entry.options["entity_id"] == "sensor.new_entity_id" + + # Check that the helper config is still in the device + sensor_device = device_registry.async_get(sensor_device.id) + assert statistics_config_entry.entry_id in sensor_device.config_entries + + # Check that the statistics config entry is not removed + assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids() + + # Check we got the expected events + assert events == [] From e73bcc73b58360377dd04b839987c0d0cd3d3016 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 17:26:20 +0000 Subject: [PATCH 1164/1175] Bump version to 2025.6.0b8 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6cbe42d6dbe..8c4fd0bf774 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index f0aa8169054..21717881d35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b7" +version = "2025.6.0b8" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From fd605e0abe97d7176aa1f1e376ec2f0dc5e2e1a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:18:04 +0200 Subject: [PATCH 1165/1175] Handle changes to source entities in generic_hygrostat helper (#146538) --- .../components/generic_hygrostat/__init__.py | 59 ++- .../components/generic_hygrostat/test_init.py | 428 +++++++++++++++++- 2 files changed, 480 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index b4a6014c5a4..a12994c1a75 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -5,11 +5,18 @@ import voluptuous as vol from homeassistant.components.humidifier import HumidifierDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import ( + config_validation as cv, + discovery, + entity_registry as er, +) from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes from homeassistant.helpers.typing import ConfigType DOMAIN = "generic_hygrostat" @@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.options[CONF_HUMIDIFIER], ) + def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_HUMIDIFIER: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + # We use async_handle_source_entity_changes to track changes to the humidifer, + # but not the humidity sensor because the generic_hygrostat adds itself to the + # humidifier's device. + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_HUMIDIFIER] + ), + source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER], + source_entity_removed=source_entity_removed, + ) + ) + + async def async_sensor_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] != "update": + return + if "entity_id" not in data["changes"]: + return + + # Entity_id changed, update the config entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SENSOR: data["entity_id"]}, + ) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entry.options[CONF_SENSOR], async_sensor_updated + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/generic_hygrostat/test_init.py b/tests/components/generic_hygrostat/test_init.py index bd4792f939d..254d4da5806 100644 --- a/tests/components/generic_hygrostat/test_init.py +++ b/tests/components/generic_hygrostat/test_init.py @@ -2,17 +2,136 @@ from __future__ import annotations -from homeassistant.components.generic_hygrostat import ( - DOMAIN as GENERIC_HYDROSTAT_DOMAIN, -) -from homeassistant.core import HomeAssistant +from unittest.mock import patch + +import pytest + +from homeassistant.components import generic_hygrostat +from homeassistant.components.generic_hygrostat import DOMAIN +from homeassistant.components.generic_hygrostat.config_flow import ConfigFlowHandler +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from .test_humidifier import ENT_SENSOR from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EE")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def switch_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a switch config entry.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + return switch_config_entry + + +@pytest.fixture +def switch_device( + device_registry: dr.DeviceRegistry, switch_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a switch device.""" + return device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def switch_entity_entry( + entity_registry: er.EntityRegistry, + switch_config_entry: ConfigEntry, + switch_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=switch_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def generic_hygrostat_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + switch_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a generic_hygrostat config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "device_class": "humidifier", + "dry_tolerance": 2.0, + "humidifier": switch_entity_entry.entity_id, + "name": "My generic hygrostat", + "target_sensor": sensor_entity_entry.entity_id, + "wet_tolerance": 4.0, + }, + title="My generic hygrostat", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_device_cleaning( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -45,7 +164,7 @@ async def test_device_cleaning( # Configure the configuration entry for helper helper_config_entry = MockConfigEntry( data={}, - domain=GENERIC_HYDROSTAT_DOMAIN, + domain=DOMAIN, options={ "device_class": "humidifier", "dry_tolerance": 2.0, @@ -100,3 +219,302 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "expected_events"), + [("switch.test_unique", True, ["update"]), ("sensor.test_unique", False, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + expected_events: list[str], +) -> None: + """Test the generic_hygrostat config entry is removed when the source entity is removed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_entity_entry.device_id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Remove the source entity's config entry from the device, this removes the + # source entity + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_entity_entry.config_entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check if the generic_hygrostat config entry is not in the device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity removed from the source device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Remove the source entity from the device + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_hygrostat config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity is moved to another device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_entity_entry.config_entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + source_device_2 = device_registry.async_get(source_device_2.id) + assert generic_hygrostat_config_entry.entry_id not in source_device_2.config_entries + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Move the source entity to another device + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_hygrostat config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert generic_hygrostat_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device_2.config_entries + ) == helper_in_device + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "new_entity_id", "helper_in_device", "config_key"), + [ + ("switch.test_unique", "switch.new_entity_id", True, "humidifier"), + ("sensor.test_unique", "sensor.new_entity_id", False, "target_sensor"), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_hygrostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + new_entity_id: str, + helper_in_device: bool, + config_key: str, +) -> None: + """Test the source entity's entity ID is changed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_hygrostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_hygrostat_entity_entry = entity_registry.async_get( + "humidifier.my_generic_hygrostat" + ) + assert generic_hygrostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_hygrostat_entity_entry.entity_id + ) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.generic_hygrostat.async_unload_entry", + wraps=generic_hygrostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id=new_entity_id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the generic_hygrostat config entry is updated with the new entity ID + assert generic_hygrostat_config_entry.options[config_key] == new_entity_id + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert ( + generic_hygrostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + # Check that the generic_hygrostat config entry is not removed + assert ( + generic_hygrostat_config_entry.entry_id in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == [] From 89637a618eb82626c2b70ac902418e7e85efac71 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 Jun 2025 15:26:52 +0200 Subject: [PATCH 1166/1175] Handle changes to source entities in generic_thermostat helper (#146541) --- .../components/generic_thermostat/__init__.py | 57 ++- .../generic_thermostat/test_init.py | 428 +++++++++++++++++- 2 files changed, 482 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index dc43049a262..3e2af8598de 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -1,12 +1,16 @@ """The generic_thermostat component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device import ( + async_entity_id_to_device_id, async_remove_stale_devices_links_keep_entity_device, ) +from homeassistant.helpers.event import async_track_entity_registry_updated_event +from homeassistant.helpers.helper_integration import async_handle_source_entity_changes -from .const import CONF_HEATER, PLATFORMS +from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.entry_id, entry.options[CONF_HEATER], ) + + def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_HEATER: source_entity_id}, + ) + + async def source_entity_removed() -> None: + # The source entity has been removed, we need to clean the device links. + async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None) + + entry.async_on_unload( + # We use async_handle_source_entity_changes to track changes to the heater, but + # not the temperature sensor because the generic_hygrostat adds itself to the + # heater's device. + async_handle_source_entity_changes( + hass, + helper_config_entry_id=entry.entry_id, + set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid, + source_device_id=async_entity_id_to_device_id( + hass, entry.options[CONF_HEATER] + ), + source_entity_id_or_uuid=entry.options[CONF_HEATER], + source_entity_removed=source_entity_removed, + ) + ) + + async def async_sensor_updated( + event: Event[er.EventEntityRegistryUpdatedData], + ) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] != "update": + return + if "entity_id" not in data["changes"]: + return + + # Entity_id changed, update the config entry + hass.config_entries.async_update_entry( + entry, + options={**entry.options, CONF_SENSOR: data["entity_id"]}, + ) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entry.options[CONF_SENSOR], async_sensor_updated + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/tests/components/generic_thermostat/test_init.py b/tests/components/generic_thermostat/test_init.py index addae2f684e..9131e3ffdd4 100644 --- a/tests/components/generic_thermostat/test_init.py +++ b/tests/components/generic_thermostat/test_init.py @@ -2,13 +2,134 @@ from __future__ import annotations +from unittest.mock import patch + +import pytest + +from homeassistant.components import generic_thermostat +from homeassistant.components.generic_thermostat.config_flow import ConfigFlowHandler from homeassistant.components.generic_thermostat.const import DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from tests.common import MockConfigEntry +@pytest.fixture +def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a sensor config entry.""" + sensor_config_entry = MockConfigEntry() + sensor_config_entry.add_to_hass(hass) + return sensor_config_entry + + +@pytest.fixture +def sensor_device( + device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a sensor device.""" + return device_registry.async_get_or_create( + config_entry_id=sensor_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EE")}, + ) + + +@pytest.fixture +def sensor_entity_entry( + entity_registry: er.EntityRegistry, + sensor_config_entry: ConfigEntry, + sensor_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a sensor entity entry.""" + return entity_registry.async_get_or_create( + "sensor", + "test", + "unique", + config_entry=sensor_config_entry, + device_id=sensor_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def switch_config_entry(hass: HomeAssistant) -> er.RegistryEntry: + """Fixture to create a switch config entry.""" + switch_config_entry = MockConfigEntry() + switch_config_entry.add_to_hass(hass) + return switch_config_entry + + +@pytest.fixture +def switch_device( + device_registry: dr.DeviceRegistry, switch_config_entry: ConfigEntry +) -> dr.DeviceEntry: + """Fixture to create a switch device.""" + return device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + +@pytest.fixture +def switch_entity_entry( + entity_registry: er.EntityRegistry, + switch_config_entry: ConfigEntry, + switch_device: dr.DeviceEntry, +) -> er.RegistryEntry: + """Fixture to create a switch entity entry.""" + return entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=switch_device.id, + original_name="ABC", + ) + + +@pytest.fixture +def generic_thermostat_config_entry( + hass: HomeAssistant, + sensor_entity_entry: er.RegistryEntry, + switch_entity_entry: er.RegistryEntry, +) -> MockConfigEntry: + """Fixture to create a generic_thermostat config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My generic thermostat", + "heater": switch_entity_entry.entity_id, + "target_sensor": sensor_entity_entry.entity_id, + "ac_mode": False, + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + }, + title="My generic thermostat", + version=ConfigFlowHandler.VERSION, + minor_version=ConfigFlowHandler.MINOR_VERSION, + ) + + config_entry.add_to_hass(hass) + + return config_entry + + +def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]: + """Track entity registry actions for an entity.""" + events = [] + + def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None: + """Add entity registry updated event to the list.""" + events.append(event.data["action"]) + + async_track_entity_registry_updated_event(hass, entity_id, add_event) + + return events + + async def test_device_cleaning( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -96,3 +217,308 @@ async def test_device_cleaning( assert len(devices_after_reload) == 1 assert devices_after_reload[0].id == source_device1_entry.id + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "expected_events"), + [("switch.test_unique", True, ["update"]), ("sensor.test_unique", False, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + expected_events: list[str], +) -> None: + """Test the generic_thermostat config entry is removed when the source entity is removed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + # Add another config entry to the source device + other_config_entry = MockConfigEntry() + other_config_entry.add_to_hass(hass) + device_registry.async_update_device( + source_entity_entry.device_id, add_config_entry_id=other_config_entry.entry_id + ) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Remove the source entity's config entry from the device, this removes the + # source entity + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + device_registry.async_update_device( + source_device.id, remove_config_entry_id=source_entity_entry.config_entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + mock_unload_entry.assert_not_called() + + # Check if the generic_thermostat config entry is not in the device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_removed_from_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity removed from the source device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Remove the source entity from the device + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=None + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_thermostat config entry is removed from the device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "helper_in_device", "unload_entry_calls", "expected_events"), + [("switch.test_unique", True, 1, ["update"]), ("sensor.test_unique", False, 0, [])], +) +async def test_async_handle_source_entity_changes_source_entity_moved_other_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + helper_in_device: bool, + unload_entry_calls: int, + expected_events: list[str], +) -> None: + """Test the source entity is moved to another device.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + source_device_2 = device_registry.async_get_or_create( + config_entry_id=source_entity_entry.config_entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")}, + ) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_thermostat_config_entry.entry_id not in source_device_2.config_entries + ) + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Move the source entity to another device + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, device_id=source_device_2.id + ) + await hass.async_block_till_done() + assert len(mock_unload_entry.mock_calls) == unload_entry_calls + + # Check that the generic_thermostat config entry is moved to the other device + source_device = device_registry.async_get(source_device.id) + assert generic_thermostat_config_entry.entry_id not in source_device.config_entries + source_device_2 = device_registry.async_get(source_device_2.id) + assert ( + generic_thermostat_config_entry.entry_id in source_device_2.config_entries + ) == helper_in_device + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == expected_events + + +@pytest.mark.usefixtures( + "sensor_config_entry", + "sensor_device", + "sensor_entity_entry", + "switch_config_entry", + "switch_device", +) +@pytest.mark.parametrize( + ("source_entity_id", "new_entity_id", "helper_in_device", "config_key"), + [ + ("switch.test_unique", "switch.new_entity_id", True, "heater"), + ("sensor.test_unique", "sensor.new_entity_id", False, "target_sensor"), + ], +) +async def test_async_handle_source_entity_new_entity_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + generic_thermostat_config_entry: MockConfigEntry, + switch_entity_entry: er.RegistryEntry, + source_entity_id: str, + new_entity_id: str, + helper_in_device: bool, + config_key: str, +) -> None: + """Test the source entity's entity ID is changed.""" + source_entity_entry = entity_registry.async_get(source_entity_id) + + assert await hass.config_entries.async_setup( + generic_thermostat_config_entry.entry_id + ) + await hass.async_block_till_done() + + generic_thermostat_entity_entry = entity_registry.async_get( + "climate.my_generic_thermostat" + ) + assert generic_thermostat_entity_entry.device_id == switch_entity_entry.device_id + + source_device = device_registry.async_get(source_entity_entry.device_id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + events = track_entity_registry_actions( + hass, generic_thermostat_entity_entry.entity_id + ) + + # Change the source entity's entity ID + with patch( + "homeassistant.components.generic_thermostat.async_unload_entry", + wraps=generic_thermostat.async_unload_entry, + ) as mock_unload_entry: + entity_registry.async_update_entity( + source_entity_entry.entity_id, new_entity_id=new_entity_id + ) + await hass.async_block_till_done() + mock_unload_entry.assert_called_once() + + # Check that the generic_thermostat config entry is updated with the new entity ID + assert generic_thermostat_config_entry.options[config_key] == new_entity_id + + # Check that the helper config is still in the device + source_device = device_registry.async_get(source_device.id) + assert ( + generic_thermostat_config_entry.entry_id in source_device.config_entries + ) == helper_in_device + + # Check that the generic_thermostat config entry is not removed + assert ( + generic_thermostat_config_entry.entry_id + in hass.config_entries.async_entry_ids() + ) + + # Check we got the expected events + assert events == [] From 43797c03cccf42f0df991607e099edea05cd1552 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 15:46:19 +0200 Subject: [PATCH 1167/1175] Update frontend to 20250531.1 (#146542) --- 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 7282482f329..5c3b8ed2264 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==20250531.0"] + "requirements": ["home-assistant-frontend==20250531.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b5d1af412f9..921de18a732 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c98f22f9d91..91268677b43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1265ec260d..a9ac282ad4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.0 +home-assistant-frontend==20250531.1 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 1f221712a25635d68f975d3b38e0378ab36d1f7f Mon Sep 17 00:00:00 2001 From: Petar Petrov Date: Wed, 11 Jun 2025 16:39:02 +0300 Subject: [PATCH 1168/1175] Remove the Delete button on the ZwaveJS device page (#146544) --- homeassistant/components/zwave_js/__init__.py | 32 ------------------- tests/components/zwave_js/test_init.py | 21 ------------ 2 files changed, 53 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index abbf10fb494..e8f2bf6f2d4 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -1119,38 +1119,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: LOGGER.error(err) -async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry -) -> bool: - """Remove a config entry from a device.""" - client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT] - - # Driver may not be ready yet so we can't allow users to remove a device since - # we need to check if the device is still known to the controller - if (driver := client.driver) is None: - LOGGER.error("Driver for %s is not ready", config_entry.title) - return False - - # If a node is found on the controller that matches the hardware based identifier - # on the device, prevent the device from being removed. - if next( - ( - node - for node in driver.controller.nodes.values() - if get_device_id_ext(driver, node) in device_entry.identifiers - ), - None, - ): - return False - - controller_events: ControllerEvents = config_entry.runtime_data[ - DATA_DRIVER_EVENTS - ].controller_events - controller_events.registered_unique_ids.pop(device_entry.id, None) - controller_events.discovered_value_ids.pop(device_entry.id, None) - return True - - async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> None: """Ensure that Z-Wave JS add-on is installed and running.""" addon_manager = _get_addon_manager(hass) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a0423efdf52..ef74373ad9e 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -1692,27 +1692,6 @@ async def test_replace_different_node( (DOMAIN, multisensor_6_device_id_ext), } - ws_client = await hass_ws_client(hass) - - # Simulate the driver not being ready to ensure that the device removal handler - # does not crash - driver = client.driver - client.driver = None - - response = await ws_client.remove_device(hank_device.id, integration.entry_id) - assert not response["success"] - - client.driver = driver - - # Attempting to remove the hank device should pass, but removing the multisensor should not - response = await ws_client.remove_device(hank_device.id, integration.entry_id) - assert response["success"] - - response = await ws_client.remove_device( - multisensor_6_device.id, integration.entry_id - ) - assert not response["success"] - async def test_node_model_change( hass: HomeAssistant, From 75e6f23a82bf40f41913a4bf6d9991a4444068e5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 11 Jun 2025 17:12:34 +0200 Subject: [PATCH 1169/1175] Update frontend to 20250531.2 (#146551) --- 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 5c3b8ed2264..4299d2b7503 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==20250531.1"] + "requirements": ["home-assistant-frontend==20250531.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 921de18a732..d0904cd12b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ habluetooth==3.49.0 hass-nabucasa==0.101.0 hassil==2.2.3 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 home-assistant-intents==2025.6.10 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 91268677b43..c73829cf379 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 # homeassistant.components.conversation home-assistant-intents==2025.6.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9ac282ad4e..6af2c4ecc6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1010,7 +1010,7 @@ hole==0.8.0 holidays==0.74 # homeassistant.components.frontend -home-assistant-frontend==20250531.1 +home-assistant-frontend==20250531.2 # homeassistant.components.conversation home-assistant-intents==2025.6.10 From 60b8230eccb213b16faf92a9c7e117247f020906 Mon Sep 17 00:00:00 2001 From: andreimoraru Date: Wed, 11 Jun 2025 18:53:25 +0300 Subject: [PATCH 1170/1175] Bump yt-dlp to 2025.06.09 (#146553) * Bumped yt-dlp to 2025.06.09 * fix --------- Co-authored-by: Joostlek --- 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 3ce80f497ef..20068efccef 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2025.05.22"], + "requirements": ["yt-dlp[default]==2025.06.09"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index c73829cf379..b70c806c1be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3162,7 +3162,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.05.22 +yt-dlp[default]==2025.06.09 # homeassistant.components.zabbix zabbix-utils==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6af2c4ecc6e..39fe3779466 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2606,7 +2606,7 @@ youless-api==2.2.0 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2025.05.22 +yt-dlp[default]==2025.06.09 # homeassistant.components.zamg zamg==0.3.6 From 02524b8b9b287556c65c0d361422ab536cefdd83 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Jun 2025 18:39:46 +0200 Subject: [PATCH 1171/1175] Make issue creation check architecture instead of uname (#146537) --- homeassistant/components/hassio/__init__.py | 35 ++++-- .../components/homeassistant/__init__.py | 30 +++-- .../components/homeassistant/strings.json | 4 +- tests/components/hassio/conftest.py | 13 ++ tests/components/hassio/test_init.py | 113 ++++++++++++++---- tests/components/homeassistant/test_init.py | 110 +++++++++-------- 6 files changed, 202 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 041877e3944..6772034e53f 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -9,8 +9,10 @@ from functools import partial import logging import os import re +import struct from typing import Any, NamedTuple +import aiofiles from aiohasupervisor import SupervisorError import voluptuous as vol @@ -56,7 +58,6 @@ from homeassistant.helpers.issue_registry import IssueSeverity from homeassistant.helpers.service_info.hassio import ( HassioServiceInfo as _HassioServiceInfo, ) -from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.async_ import create_eager_task @@ -233,6 +234,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( ) +def _is_32_bit() -> bool: + size = struct.calcsize("P") + return size * 8 == 32 + + +async def _get_arch() -> str: + async with aiofiles.open("/etc/apk/arch") as arch_file: + raw_arch = await arch_file.read() + return {"x86": "i386"}.get(raw_arch, raw_arch) + + class APIEndpointSettings(NamedTuple): """Settings for API endpoint.""" @@ -554,7 +566,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[ADDONS_COORDINATOR] = coordinator - system_info = await async_get_system_info(hass) + arch = await _get_arch() def deprecated_setup_issue() -> None: os_info = get_os_info(hass) @@ -562,20 +574,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if os_info is None or info is None: return is_haos = info.get("hassos") is not None - arch = system_info["arch"] board = os_info.get("board") - supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"} - if is_haos and arch == "armv7" and supported_board: + unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"} + unsupported_os_on_board = board in {"rpi3", "rpi4"} + if is_haos and (unsupported_board or unsupported_os_on_board): issue_id = "deprecated_os_" - if board in {"rpi3", "rpi4"}: + if unsupported_os_on_board: issue_id += "aarch64" - elif board in {"tinker", "odroid-xu4", "rpi2"}: + elif unsupported_board: issue_id += "armv7" ir.async_create_issue( hass, "homeassistant", issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, @@ -584,9 +595,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "installation_guide": "https://www.home-assistant.io/installation/", }, ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board): - deprecated_architecture = True + bit32 = _is_32_bit() + deprecated_architecture = bit32 and not ( + unsupported_board or unsupported_os_on_board + ) if not is_haos or deprecated_architecture: issue_id = "deprecated" if not is_haos: @@ -597,7 +609,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, "homeassistant", issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 1433358b568..4360fa9c16e 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -4,8 +4,10 @@ import asyncio from collections.abc import Callable, Coroutine import itertools as it import logging +import struct from typing import Any +import aiofiles import voluptuous as vol from homeassistant import config as conf_util, core_config @@ -94,6 +96,17 @@ DEPRECATION_URL = ( ) +def _is_32_bit() -> bool: + size = struct.calcsize("P") + return size * 8 == 32 + + +async def _get_arch() -> str: + async with aiofiles.open("/etc/apk/arch") as arch_file: + raw_arch = (await arch_file.read()).strip() + return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up general services related to Home Assistant.""" @@ -403,23 +416,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: installation_type = info["installation_type"][15:] if installation_type in {"Core", "Container"}: deprecated_method = installation_type == "Core" + bit32 = _is_32_bit() arch = info["arch"] - if arch == "armv7" and installation_type == "Container": + if bit32 and installation_type == "Container": + arch = await _get_arch() ir.async_create_issue( hass, DOMAIN, - "deprecated_container_armv7", - breaks_in_ha_version="2025.12.0", + "deprecated_container", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, - translation_key="deprecated_container_armv7", + translation_key="deprecated_container", + translation_placeholders={"arch": arch}, ) - deprecated_architecture = False - if arch in {"i386", "armhf"} or ( - arch == "armv7" and installation_type != "Container" - ): - deprecated_architecture = True + deprecated_architecture = bit32 and installation_type != "Container" if deprecated_method or deprecated_architecture: issue_id = "deprecated" if deprecated_method: @@ -430,7 +441,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hass, DOMAIN, issue_id, - breaks_in_ha_version="2025.12.0", learn_more_url=DEPRECATION_URL, is_fixable=False, severity=IssueSeverity.WARNING, diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 93b4105c702..940af999c4d 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -107,9 +107,9 @@ "title": "Deprecation notice: 32-bit architecture", "description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware." }, - "deprecated_container_armv7": { + "deprecated_container": { "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", - "description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." + "description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware." }, "deprecated_os_aarch64": { "title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]", diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index a71ee370b32..56f7ffaa5b9 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -260,3 +260,16 @@ def all_setup_requests( }, }, ) + + +@pytest.fixture +def arch() -> str: + """Arch found in apk file.""" + return "amd64" + + +@pytest.fixture(autouse=True) +def mock_arch_file(arch: str) -> Generator[None]: + """Mock arch file.""" + with patch("homeassistant.components.hassio._get_arch", return_value=arch): + yield diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f74ed852a49..f424beedc85 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1156,7 +1156,11 @@ def test_deprecated_constants( ("rpi2", "deprecated_os_armv7"), ], ) -async def test_deprecated_installation_issue_aarch64( +@pytest.mark.parametrize( + "arch", + ["armv7"], +) +async def test_deprecated_installation_issue_os_armv7( hass: HomeAssistant, issue_registry: ir.IssueRegistry, freezer: FrozenDateTimeFactory, @@ -1167,18 +1171,15 @@ async def test_deprecated_installation_issue_aarch64( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": "armv7", }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "armv7", - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", return_value={"board": board} @@ -1228,7 +1229,7 @@ async def test_deprecated_installation_issue_aarch64( "armv7", ], ) -async def test_deprecated_installation_issue_32bit_method( +async def test_deprecated_installation_issue_32bit_os( hass: HomeAssistant, issue_registry: ir.IssueRegistry, freezer: FrozenDateTimeFactory, @@ -1238,18 +1239,15 @@ async def test_deprecated_installation_issue_32bit_method( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": arch, }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": arch, - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", @@ -1308,18 +1306,15 @@ async def test_deprecated_installation_issue_32bit_supervised( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant Supervised", "arch": arch, }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Supervised", - "arch": arch, - }, + "homeassistant.components.hassio._is_32_bit", + return_value=True, ), patch( "homeassistant.components.hassio.get_os_info", @@ -1365,6 +1360,75 @@ async def test_deprecated_installation_issue_32bit_supervised( } +@pytest.mark.parametrize( + "arch", + [ + "amd64", + "aarch64", + ], +) +async def test_deprecated_installation_issue_64bit_supervised( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + freezer: FrozenDateTimeFactory, + arch: str, +) -> None: + """Test deprecated architecture issue.""" + with ( + patch.dict(os.environ, MOCK_ENVIRON), + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Supervised", + "arch": arch, + }, + ), + patch( + "homeassistant.components.hassio._is_32_bit", + return_value=False, + ), + patch( + "homeassistant.components.hassio.get_os_info", + return_value={"board": "generic-x86-64"}, + ), + patch( + "homeassistant.components.hassio.get_info", return_value={"hassos": None} + ), + patch("homeassistant.components.hardware.async_setup", return_value=True), + ): + assert await async_setup_component(hass, "homeassistant", {}) + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + freezer.tick(REQUEST_REFRESH_DELAY) + async_fire_time_changed(hass) + await hass.async_block_till_done() + await hass.services.async_call( + "homeassistant", + "update_entity", + { + "entity_id": [ + "update.home_assistant_core_update", + "update.home_assistant_supervisor_update", + ] + }, + blocking=True, + ) + freezer.tick(HASSIO_UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue("homeassistant", "deprecated_method") + assert issue.domain == "homeassistant" + assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == { + "installation_type": "Supervised", + "arch": arch, + } + + @pytest.mark.parametrize( ("board", "issue_id"), [ @@ -1382,18 +1446,15 @@ async def test_deprecated_installation_issue_supported_board( with ( patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.async_get_system_info", + "homeassistant.components.homeassistant.async_get_system_info", return_value={ "installation_type": "Home Assistant OS", "arch": "aarch64", }, ), patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant OS", - "arch": "aarch64", - }, + "homeassistant.components.hassio._is_32_bit", + return_value=False, ), patch( "homeassistant.components.hassio.get_os_info", return_value={"board": board} diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 0010422cd28..0646b4dcfa6 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -648,18 +648,24 @@ async def test_reload_all( "armv7", ], ) -async def test_deprecated_installation_issue_32bit_method( +async def test_deprecated_installation_issue_32bit_core( hass: HomeAssistant, issue_registry: ir.IssueRegistry, arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Core", - "arch": arch, - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Core", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=True, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() @@ -679,48 +685,28 @@ async def test_deprecated_installation_issue_32bit_method( @pytest.mark.parametrize( "arch", [ - "i386", - "armhf", + "aarch64", + "generic-x86-64", ], ) -async def test_deprecated_installation_issue_32bit( +async def test_deprecated_installation_issue_64bit_core( hass: HomeAssistant, issue_registry: ir.IssueRegistry, arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Container", - "arch": arch, - }, - ): - assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) - await hass.async_block_till_done() - - assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_architecture" - ) - assert issue.domain == HOMEASSISTANT_DOMAIN - assert issue.severity == ir.IssueSeverity.WARNING - assert issue.translation_placeholders == { - "installation_type": "Container", - "arch": arch, - } - - -async def test_deprecated_installation_issue_method( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Core", - "arch": "generic-x86-64", - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Core", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=False, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() @@ -731,28 +717,46 @@ async def test_deprecated_installation_issue_method( assert issue.severity == ir.IssueSeverity.WARNING assert issue.translation_placeholders == { "installation_type": "Core", - "arch": "generic-x86-64", + "arch": arch, } -async def test_deprecated_installation_issue_armv7_container( +@pytest.mark.parametrize( + "arch", + [ + "i386", + "armv7", + "armhf", + ], +) +async def test_deprecated_installation_issue_32bit( hass: HomeAssistant, issue_registry: ir.IssueRegistry, + arch: str, ) -> None: """Test deprecated installation issue.""" - with patch( - "homeassistant.components.homeassistant.async_get_system_info", - return_value={ - "installation_type": "Home Assistant Container", - "arch": "armv7", - }, + with ( + patch( + "homeassistant.components.homeassistant.async_get_system_info", + return_value={ + "installation_type": "Home Assistant Container", + "arch": arch, + }, + ), + patch( + "homeassistant.components.homeassistant._is_32_bit", + return_value=True, + ), + patch( + "homeassistant.components.homeassistant._get_arch", + return_value=arch, + ), ): assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {}) await hass.async_block_till_done() assert len(issue_registry.issues) == 1 - issue = issue_registry.async_get_issue( - HOMEASSISTANT_DOMAIN, "deprecated_container_armv7" - ) + issue = issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_container") assert issue.domain == HOMEASSISTANT_DOMAIN assert issue.severity == ir.IssueSeverity.WARNING + assert issue.translation_placeholders == {"arch": arch} From dc4627f4136647503f70b4422f9b16d4202d00d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 18:07:37 +0000 Subject: [PATCH 1172/1175] Bump version to 2025.6.0b9 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8c4fd0bf774..3992fc93730 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 21717881d35..aa4b2a9c176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b8" +version = "2025.6.0b9" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3." From cada2f84a9e719122ee0ab9893e07ca07ed84e2f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 18:13:03 +0000 Subject: [PATCH 1173/1175] Hotfix ruff warnings --- tests/components/history_stats/test_init.py | 2 +- tests/components/homematicip_cloud/test_hap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/history_stats/test_init.py b/tests/components/history_stats/test_init.py index 37b5416fdbb..f418b1f7ef1 100644 --- a/tests/components/history_stats/test_init.py +++ b/tests/components/history_stats/test_init.py @@ -69,7 +69,7 @@ def history_stats_config_entry( """Fixture to create a history_stats config entry.""" config_entry = MockConfigEntry( data={}, - domain=DOMAIN, + domain=HISTORY_STATS_DOMAIN, options={ CONF_NAME: DEFAULT_NAME, CONF_ENTITY_ID: sensor_entity_entry.entity_id, diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 94d6f9d5dd6..c258c85ac93 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -237,7 +237,7 @@ async def test_get_state_after_disconnect( hass: HomeAssistant, hmip_config_entry: MockConfigEntry, simple_mock_home ) -> None: """Test get state after disconnect.""" - hass.config.components.add(DOMAIN) + hass.config.components.add(HMIPC_DOMAIN) hap = HomematicipHAP(hass, hmip_config_entry) assert hap From fb4c77d43b3e45e22cbd075d894459e9970e0587 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 11 Jun 2025 19:32:38 +0100 Subject: [PATCH 1174/1175] Add aiofiles to pyproject.toml (#146561) --- homeassistant/package_constraints.txt | 9 +-------- pyproject.toml | 1 + requirements.txt | 1 + script/gen_requirements_all.py | 8 -------- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d0904cd12b3..57a037f0fb7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,6 +3,7 @@ aiodhcpwatcher==1.2.0 aiodiscover==2.7.0 aiodns==3.4.0 +aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 @@ -201,14 +202,6 @@ tenacity!=8.4.0 # TypeError: 'Timeout' object does not support the context manager protocol async-timeout==4.0.3 -# aiofiles keeps getting downgraded by custom components -# causing newer methods to not be available and breaking -# some integrations at startup -# https://github.com/home-assistant/core/issues/127529 -# https://github.com/home-assistant/core/issues/122508 -# https://github.com/home-assistant/core/issues/118004 -aiofiles>=24.1.0 - # multidict < 6.4.0 has memory leaks # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 diff --git a/pyproject.toml b/pyproject.toml index aa4b2a9c176..88bd59a95dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ requires-python = ">=3.13.2" dependencies = [ "aiodns==3.4.0", + "aiofiles==24.1.0", # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 diff --git a/requirements.txt b/requirements.txt index e353adac9d3..6dc604d877b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ # Home Assistant Core aiodns==3.4.0 +aiofiles==24.1.0 aiohasupervisor==0.3.1 aiohttp==3.12.12 aiohttp_cors==0.7.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0ea69b365a2..8d1ce521b28 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -228,14 +228,6 @@ tenacity!=8.4.0 # TypeError: 'Timeout' object does not support the context manager protocol async-timeout==4.0.3 -# aiofiles keeps getting downgraded by custom components -# causing newer methods to not be available and breaking -# some integrations at startup -# https://github.com/home-assistant/core/issues/127529 -# https://github.com/home-assistant/core/issues/122508 -# https://github.com/home-assistant/core/issues/118004 -aiofiles>=24.1.0 - # multidict < 6.4.0 has memory leaks # https://github.com/aio-libs/multidict/issues/1134 # https://github.com/aio-libs/multidict/issues/1131 From 54d8d71de5c2d198ff5a71ba60c467a51e3259cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 11 Jun 2025 19:14:05 +0000 Subject: [PATCH 1175/1175] Bump version to 2025.6.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 3992fc93730..c006cd9dbed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2025 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b9" +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, 13, 2) diff --git a/pyproject.toml b/pyproject.toml index 88bd59a95dc..07f19628d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2025.6.0b9" +version = "2025.6.0" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."